MarketSync / tools /profile_analyzer.py
hyeonjoo's picture
Initial project commit with LFS
9b1e3db
# tools/profile_analyzer.py
import json
import traceback
import pandas as pd
import math
import streamlit as st
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
import config
from modules.llm_provider import get_llm
# filtering ๋ชจ๋“ˆ์—์„œ ๋‚ ์งœ ์˜ˆ์ธก ํ•จ์ˆ˜ ๊ฐ€์ ธ์˜ค๊ธฐ
from modules.filtering import FestivalRecommender
logger = config.get_logger(__name__)
# nan ๊ฐ’ ์ฒ˜๋ฆฌ๊ธฐ
def replace_nan_with_none(data):
if isinstance(data, dict):
return {k: replace_nan_with_none(v) for k, v in data.items()}
elif isinstance(data, list):
return [replace_nan_with_none(i) for i in data]
elif isinstance(data, float) and math.isnan(data):
return None
return data
# ์ถ•์ œ ๋ฐ์ดํ„ฐ ๋กœ๋”
@st.cache_data
def _load_festival_data():
try:
file_path = config.PATH_FESTIVAL_DF
if not file_path.exists():
logger.error(f"--- [Tool Definition ERROR] '{config.PATH_FESTIVAL_DF}' ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
return None
df = pd.read_csv(file_path)
if '์ถ•์ œ๋ช…' not in df.columns:
logger.error("--- [Tool Definition ERROR] '์ถ•์ œ๋ช…' ์ปฌ๋Ÿผ์ด df์— ์—†์Šต๋‹ˆ๋‹ค.")
return None
df_dict = df.set_index('์ถ•์ œ๋ช…').to_dict(orient='index')
logger.info(f"--- [Cache] ์ถ•์ œ ์›๋ณธ CSV ๋กœ๋“œ ๋ฐ ๋”•์…”๋„ˆ๋ฆฌ ๋ณ€ํ™˜ ์™„๋ฃŒ (์ด {len(df_dict)}๊ฐœ) ---")
return df_dict
except Exception as e:
logger.critical(f"--- [Tool Definition CRITICAL ERROR] ์ถ•์ œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ: {e} ---", exc_info=True)
return None
# ----------------------------
# Tool 1: ํŠน์ • ์ถ•์ œ ์ •๋ณด ์กฐํšŒ
@tool
def get_festival_profile_by_name(festival_name: str) -> str:
"""
์ถ•์ œ ์ด๋ฆ„์„ ์ž…๋ ฅ๋ฐ›์•„, ํ•ด๋‹น ์ถ•์ œ์˜ ์ƒ์„ธ ํ”„๋กœํ•„(์†Œ๊ฐœ, ์ง€์—ญ, ํ‚ค์›Œ๋“œ, ๊ธฐ๊ฐ„, ๊ณ ๊ฐ์ธต ๋“ฑ)์„
JSON ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ •ํ™•ํ•œ ์ด๋ฆ„์„ ์ฐพ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.
(์˜ˆ: "๋ณด๋ น๋จธ๋“œ์ถ•์ œ ์ƒ์„ธ ์ •๋ณด ์•Œ๋ ค์ค˜")
"""
logger.info(f"--- [Tool] 'ํŠน์ • ์ถ•์ œ ์ •๋ณด ์กฐํšŒ' ๋„๊ตฌ ํ˜ธ์ถœ (๋Œ€์ƒ: {festival_name}) ---")
try:
festival_db = _load_festival_data()
if festival_db is None:
return json.dumps({"error": "์ถ•์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋กœ๋“œํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."})
profile_dict = festival_db.get(festival_name)
if profile_dict:
profile_dict = replace_nan_with_none(profile_dict)
profile_dict['์ถ•์ œ๋ช…'] = festival_name
return json.dumps(profile_dict, ensure_ascii=False)
else:
return json.dumps({"error": f"'{festival_name}' ์ถ•์ œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ฒ ์ž๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”."})
except Exception as e:
logger.critical(f"--- [Tool CRITICAL] 'ํŠน์ • ์ถ•์ œ ์ •๋ณด ์กฐํšŒ' ์ค‘ ์˜ค๋ฅ˜: {e} ---", exc_info=True)
return json.dumps({"error": f"'{festival_name}' ์ถ•์ œ ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}"})
# ----------------------------
# Tool 2: ๊ฐ€๋งน์  ํ”„๋กœํ•„ ๋ถ„์„ (LLM)
@tool
def analyze_merchant_profile(store_profile: str) -> str:
"""
๊ฐ€๋งน์ (๊ฐ€๊ฒŒ)์˜ ํ”„๋กœํ•„ ๋ฐ์ดํ„ฐ(JSON ๋ฌธ์ž์—ด)๋ฅผ ์ž…๋ ฅ๋ฐ›์•„, LLM์„ ์‚ฌ์šฉํ•˜์—ฌ
[๊ฐ•์ , ์•ฝ์ , ๊ธฐํšŒ ์š”์ธ]์„ ๋ถ„์„ํ•˜๋Š” ์ปจ์„คํŒ… ๋ฆฌํฌํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
์ด ๋„๊ตฌ๋Š” ๊ฐ€๊ฒŒ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์ง„๋‹จํ•˜๊ณ  ๋งˆ์ผ€ํŒ… ์ „๋žต์„ ์ œ์•ˆํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
"""
logger.info("--- [Tool] '๊ฐ€๋งน์  ํ”„๋กœํ•„ ๋ถ„์„' ๋„๊ตฌ ํ˜ธ์ถœ ---")
try:
llm = get_llm(temperature=0.3)
prompt = f"""
๋‹น์‹ ์€ ์ตœ๊ณ ์˜ ์ƒ๊ถŒ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
์•„๋ž˜ [๊ฐ€๊ฒŒ ํ”„๋กœํ•„] ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ, ์ด ๊ฐ€๊ฒŒ์˜ [๊ฐ•์ ], [์•ฝ์ ], [๊ธฐํšŒ ์š”์ธ]์„
์‚ฌ์žฅ๋‹˜์ด ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์ปจ์„คํŒ… ๋ฆฌํฌํŠธ ํ˜•์‹์œผ๋กœ ์š”์•ฝํ•ด์ฃผ์„ธ์š”.
[๊ฐ€๊ฒŒ ํ”„๋กœํ•„]
{store_profile}
[๋ถ„์„ ๊ฐ€์ด๋“œ๋ผ์ธ]
1. **๊ฐ•์  (Strengths)**: '๋™์ผ ์ƒ๊ถŒ/์—…์ข… ๋Œ€๋น„' ๋†’์€ ์ˆ˜์น˜(๋งค์ถœ, ๋ฐฉ๋ฌธ๊ฐ, ๊ฐ๋‹จ๊ฐ€ ๋“ฑ)๋‚˜ '์žฌ๋ฐฉ๋ฌธ์œจ' ๋“ฑ์„ ์ฐพ์•„ **๊ฒฝ์Ÿ ์šฐ์œ„**๊ฐ€ ๋˜๋Š” ํ•ต์‹ฌ ์š”์†Œ ๊ฐ•์กฐํ•˜์„ธ์š”.
2. **์•ฝ์  (Weaknesses)**: '๋™์ผ ์ƒ๊ถŒ/์—…์ข… ๋Œ€๋น„' ๋‚ฎ์€ ์ˆ˜์น˜๋‚˜ '์‹ ๊ทœ ๊ณ ๊ฐ ๋น„์œจ' ๋“ฑ์„ ์ฐพ์•„ **๊ฐœ์„ ์ด ์‹œ๊ธ‰ํ•œ ์˜์—ญ**์„ ์–ธ๊ธ‰ํ•˜์„ธ์š”.
3. **๊ธฐํšŒ (Opportunities)**: ๊ฐ€๊ฒŒ์˜ ํ˜„์žฌ ๊ฐ•์ ๊ณผ '์ฃผ์š” ๊ณ ๊ฐ์ธต'์ด๋‚˜ '์ƒ๊ถŒ' ํŠน์„ฑ์„ ๋ฐ”ํƒ•์œผ๋กœ, **๊ฐ€๊ฒŒ๊ฐ€ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋งˆ์ผ€ํŒ…(์˜ˆ: ํŠน์ • ์—ฐ๋ น๋Œ€ ํƒ€๊ฒŸ, ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜)์ด ํšจ๊ณผ์ ์ผ์ง€ ์ œ์•ˆํ•˜๊ณ  ์ด๋ฅผ ๋‹ฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉํ–ฅ์„ฑ์„ ์ œ์‹œํ•˜์„ธ์š”.
4. **ํ˜•์‹**: ๋งˆํฌ๋‹ค์šด์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ช…ํ™•ํ•˜๊ณ  ๊ฐ€๋…์„ฑ ์ข‹๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”.
5. **์ „๋ฌธ์„ฑ/์นœ์ ˆํ•จ**: ์ „๋ฌธ์ ์ธ ๋ถ„์„ ์šฉ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋˜, ์‚ฌ์žฅ๋‹˜์ด ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก ์นœ์ ˆํ•˜๊ณ  ๋ช…ํ™•ํ•˜๊ฒŒ ์„ค๋ช…ํ•˜์„ธ์š”.
6. **(์š”์ฒญ 4) ์ทจ์†Œ์„  ๊ธˆ์ง€**: ์ ˆ๋Œ€๋กœ `~~text~~`์™€ ๊ฐ™์€ ์ทจ์†Œ์„  ๋งˆํฌ๋‹ค์šด์„ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”.
[๋‹ต๋ณ€ ํ˜•์‹]
### ๐Ÿช ์‚ฌ์žฅ๋‹˜ ๊ฐ€๊ฒŒ ํ”„๋กœํ•„ ๋ถ„์„ ๋ฆฌํฌํŠธ
**1. ๊ฐ•์  (Strengths)**
* [๋ถ„์„๋œ ๊ฐ•์  1] (๋ถ„์„ ๊ทผ๊ฑฐ ๋ช…์‹œ)
* [๋ถ„์„๋œ ๊ฐ•์  2] (๋ถ„์„ ๊ทผ๊ฑฐ ๋ช…์‹œ)
* [ํ•„์š”์‹œ ์ถ”๊ฐ€ ๊ฐ•์ ]
**2. ์•ฝ์  (Weaknesses)**
* [๋ถ„์„๋œ ์•ฝ์  1] (๊ฐœ์„  ํ•„์š”์„ฑ ๋ช…์‹œ)
* [๋ถ„์„๋œ ์•ฝ์  2] (๊ฐœ์„  ํ•„์š”์„ฑ ๋ช…์‹œ)
* [ํ•„์š”์‹œ ์ถ”๊ฐ€ ์•ฝ์ ]
**3. ๊ธฐํšŒ (Opportunities)**
* [๋ถ„์„๋œ ๊ธฐํšŒ ์š”์ธ 1] (ํ™œ์šฉ ๋ฐฉ์•ˆ ์ œ์‹œ)
* [๋ถ„์„๋œ ๊ธฐํšŒ ์š”์ธ 2] (ํ™œ์šฉ ๋ฐฉ์•ˆ ์ œ์‹œ)
* [ํ•„์š”์‹œ ์ถ”๊ฐ€ ๊ธฐํšŒ ์š”์ธ]
"""
response = llm.invoke([HumanMessage(content=prompt)])
analysis_report = response.content.strip()
return analysis_report
except Exception as e:
logger.critical(f"--- [Tool CRITICAL] '๊ฐ€๋งน์  ํ”„๋กœํ•„ ๋ถ„์„' ์ค‘ ์˜ค๋ฅ˜: {e} ---", exc_info=True)
return f"๊ฐ€๊ฒŒ ํ”„๋กœํ•„์„ ๋ถ„์„ํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}"
# ----------------------------
# Tool 3: ์ถ•์ œ ํ”„๋กœํ•„ ๋ถ„์„ (LLM)
@tool
def analyze_festival_profile(festival_name: str) -> str:
"""
์ถ•์ œ ์ด๋ฆ„์„ ์ž…๋ ฅ๋ฐ›์•„, ํ•ด๋‹น ์ถ•์ œ์˜ ์ƒ์„ธ ํ”„๋กœํ•„์„ ์กฐํšŒํ•˜๊ณ ,
LLM์„ ์‚ฌ์šฉํ•˜์—ฌ [ํ•ต์‹ฌ ํŠน์ง•]๊ณผ [์ฃผ์š” ๋ฐฉ๋ฌธ๊ฐ ํŠน์„ฑ]์„ ์š”์•ฝ ๋ฆฌํฌํŠธ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
(์˜ˆ: "๋ณด๋ น๋จธ๋“œ์ถ•์ œ๋Š” ์–ด๋–ค ์ถ•์ œ์•ผ?")
"""
logger.info(f"--- [Tool] '์ถ•์ œ ํ”„๋กœํ•„ ๋ถ„์„' ๋„๊ตฌ ํ˜ธ์ถœ (๋Œ€์ƒ: {festival_name}) ---")
try:
# 1. Tool 1 ํ˜ธ์ถœ
profile_json = get_festival_profile_by_name.invoke(festival_name)
profile_dict = json.loads(profile_json)
if "error" in profile_dict:
return profile_json
# 2. LLM ์š”์•ฝ์„ ์œ„ํ•œ ์ •๋ณด ์ถ”์ถœ
summary = {
"์ถ•์ œ๋ช…": profile_dict.get('์ถ•์ œ๋ช…'),
"์†Œ๊ฐœ": profile_dict.get('์†Œ๊ฐœ'),
"์ง€์—ญ": profile_dict.get('์ง€์—ญ'),
"ํ‚ค์›Œ๋“œ": profile_dict.get('ํ‚ค์›Œ๋“œ'),
"2025_๊ธฐ๊ฐ„": profile_dict.get('2025_๊ธฐ๊ฐ„'),
"์ฃผ์š”_๊ณ ๊ฐ์ธต": profile_dict.get('์ฃผ์š”๊ณ ๊ฐ์ธต', 'N/A'),
"์ฃผ์š”_๋ฐฉ๋ฌธ์ž": profile_dict.get('์ฃผ์š”๋ฐฉ๋ฌธ์ž', 'N/A'),
"์ถ•์ œ_์ธ๊ธฐ๋„": profile_dict.get('์ถ•์ œ์ธ๊ธฐ', 'N/A'),
"์ธ๊ธฐ๋„_์ ์ˆ˜": profile_dict.get('์ธ๊ธฐ๋„_์ ์ˆ˜', 'N/A'),
"ํ™ˆํŽ˜์ด์ง€": profile_dict.get('ํ™ˆํŽ˜์ด์ง€')
}
# 2026๋…„ ๋‚ ์งœ ์˜ˆ์ธก ์ถ”๊ฐ€
temp_recommender = FestivalRecommender("", "")
predicted_2026_timing = temp_recommender._predict_next_year_date(summary["2025_๊ธฐ๊ฐ„"])
summary_str = json.dumps(summary, ensure_ascii=False, indent=2)
llm = get_llm(temperature=0.1)
# --- ํ”„๋กฌํ”„ํŠธ ์ˆ˜์ • ---
prompt = f"""
๋‹น์‹ ์€ ์ถ•์ œ ์ „๋ฌธ ๋ถ„์„๊ฐ€์ž…๋‹ˆ๋‹ค. ์•„๋ž˜ [์ถ•์ œ ํ”„๋กœํ•„ ์š”์•ฝ]์„ ๋ฐ”ํƒ•์œผ๋กœ,
์ด ์ถ•์ œ์˜ **ํ•ต์‹ฌ ํŠน์ง•**๊ณผ **์ฃผ์š” ๋ฐฉ๋ฌธ๊ฐ(ํƒ€๊ฒŸ ๊ณ ๊ฐ) ํŠน์„ฑ**์„
์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์š”์•ฝํ•ด์ฃผ์„ธ์š”.
[์ถ•์ œ ํ”„๋กœํ•„ ์š”์•ฝ]
{summary_str}
[๋ถ„์„ ๊ฐ€์ด๋“œ๋ผ์ธ]
1. **ํ•ต์‹ฌ ํŠน์ง•**: ์ž…๋ ฅ๋œ **'์†Œ๊ฐœ'** ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ถ•์ œ์˜ ์ฃผ์ œ์™€ ์ฃผ์š” ๋‚ด์šฉ์„ **2~3๋ฌธ์žฅ์œผ๋กœ ์ƒ์„ธํžˆ ์š”์•ฝ**ํ•˜๊ณ , 'ํ‚ค์›Œ๋“œ'์™€ '์ถ•์ œ_์ธ๊ธฐ๋„', '์ธ๊ธฐ๋„_์ ์ˆ˜'๋ฅผ ์–ธ๊ธ‰ํ•˜์—ฌ ๋ถ€์—ฐ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: "'{summary.get("์†Œ๊ฐœ", "์†Œ๊ฐœ ์ •๋ณด ์—†์Œ")[:50]}...'์„(๋ฅผ) ์ฃผ์ œ๋กœ ํ•˜๋Š” ์ถ•์ œ์ž…๋‹ˆ๋‹ค. ์ฃผ์š” ํ‚ค์›Œ๋“œ๋Š” '{summary.get("ํ‚ค์›Œ๋“œ", "N/A")}'์ด๋ฉฐ, ์ธ๊ธฐ๋„๋Š” '{summary.get("์ถ•์ œ_์ธ๊ธฐ๋„", "N/A")}' ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค.")
2. **์ฃผ์š” ๋ฐฉ๋ฌธ๊ฐ**: '์ฃผ์š”_๊ณ ๊ฐ์ธต'๊ณผ '์ฃผ์š”_๋ฐฉ๋ฌธ์ž' ์ปฌ๋Ÿผ์„ ์ง์ ‘ ์ธ์šฉํ•˜์—ฌ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.
(์˜ˆ: {summary.get("์ฃผ์š”_๊ณ ๊ฐ์ธต", "N/A")}์ด ์ฃผ๋กœ ๋ฐฉ๋ฌธํ•˜๋ฉฐ, {summary.get("์ฃผ์š”_๋ฐฉ๋ฌธ์ž", "N/A")} ๋น„์œจ์ด ๋†’์Šต๋‹ˆ๋‹ค.)
3. **ํ˜•์‹**: ์•„๋ž˜์™€ ๊ฐ™์€ ๋งˆํฌ๋‹ค์šด ํ˜•์‹์œผ๋กœ ๋‹ต๋ณ€์„ ์ž‘์„ฑํ•˜์„ธ์š”.
4. **์ทจ์†Œ์„  ๊ธˆ์ง€**: ์ ˆ๋Œ€๋กœ `~~text~~`์™€ ๊ฐ™์€ ์ทจ์†Œ์„  ๋งˆํฌ๋‹ค์šด์„ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”.
[๋‹ต๋ณ€ ํ˜•์‹]
### ๐ŸŽˆ ์ถ•์ œ ํ”„๋กœํ•„ ๋ถ„์„ ๋ฆฌํฌํŠธ: {summary.get("์ถ•์ œ๋ช…")}
**1. ์ถ•์ œ ํ•ต์‹ฌ ํŠน์ง•**
* [์ถ•์ œ ์†Œ๊ฐœ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ 2~3๋ฌธ์žฅ ์š”์•ฝ. ํ‚ค์›Œ๋“œ์™€ ์ธ๊ธฐ๋„ ํฌํ•จ]
**2. ์ฃผ์š” ๋ฐฉ๋ฌธ๊ฐ ํŠน์„ฑ**
* **์ฃผ์š” ๊ณ ๊ฐ์ธต:** {summary.get("์ฃผ์š”_๊ณ ๊ฐ์ธต")}
* **์ฃผ์š” ๋ฐฉ๋ฌธ์ž:** {summary.get("์ฃผ์š”_๋ฐฉ๋ฌธ์ž")}
**3. 2026๋…„ ๊ฐœ์ตœ ๊ธฐ๊ฐ„ (์˜ˆ์ƒ)**
* {predicted_2026_timing}
**4. ํ™ˆํŽ˜์ด์ง€**
* {summary.get("ํ™ˆํŽ˜์ด์ง€", "์ •๋ณด ์—†์Œ")}
"""
response = llm.invoke([HumanMessage(content=prompt)])
analysis_report = response.content.strip()
return analysis_report
except Exception as e:
logger.critical(f"--- [Tool CRITICAL] '์ถ•์ œ ํ”„๋กœํ•„ ๋ถ„์„' ์ค‘ ์˜ค๋ฅ˜: {e} ---", exc_info=True)
return f"'{festival_name}' ์ถ•์ œ ํ”„๋กœํ•„์„ ๋ถ„์„ํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}"