MarketSync / streamlit_app.py
hyeonjoo's picture
Initial project commit with LFS
9b1e3db
# streamlit_app.py (FastAPI 톡합 버전)
import streamlit as st
import os
import pandas as pd
import numpy as np # api/server.pyμ—μ„œ ν•„μš”
import math # api/server.pyμ—μ„œ ν•„μš”
import json
import traceback
# import requests # 더 이상 API ν˜ΈμΆœμ— ν•„μš”ν•˜μ§€ μ•ŠμŒ
from PIL import Image
from pathlib import Path
from langchain_core.messages import HumanMessage, AIMessage
import config
from orchestrator import AgentOrchestrator
from modules.visualization import display_merchant_profile
from modules.knowledge_base import load_marketing_vectorstore, load_festival_vectorstore
logger = config.get_logger(__name__)
# --- νŽ˜μ΄μ§€ μ„€μ • ---
st.set_page_config(
page_title="MarketSync(λ§ˆμΌ“μ‹±ν¬)",
page_icon="πŸŽ‰",
layout="wide",
initial_sidebar_state="expanded"
)
# --- (1) api/data_loader.pyμ—μ„œ κ°€μ Έμ˜¨ ν•¨μˆ˜ ---
# config.pyλ₯Ό 직접 μž„ν¬νŠΈν•˜λ―€λ‘œ sys.path μ‘°μž‘ ν•„μš” μ—†μŒ
def load_and_preprocess_data():
"""
미리 κ°€κ³΅λœ final_df.csv νŒŒμΌμ„ μ•ˆμ „ν•˜κ²Œ μ°Ύμ•„ λ‘œλ“œν•˜κ³ ,
데이터λ₯Ό μ²˜λ¦¬ν•˜λŠ” κ³Όμ •μ—μ„œ λ°œμƒν•  수 μžˆλŠ” λͺ¨λ“  였λ₯˜λ₯Ό λ°©μ–΄ν•©λ‹ˆλ‹€.
(api/data_loader.py의 원본 ν•¨μˆ˜)
"""
try:
file_path = config.PATH_FINAL_DF
if not file_path.exists():
logger.critical(f"--- [CRITICAL DATA ERROR] 데이터 νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. μ˜ˆμƒ 경둜: {file_path}")
logger.critical(f"--- ν˜„μž¬ μž‘μ—… 경둜: {Path.cwd()} ---")
return None
df = pd.read_csv(file_path)
except Exception as e:
logger.critical(f"--- [CRITICAL DATA ERROR] 데이터 파일 λ‘œλ”© 쀑 μ˜ˆμΈ‘ν•˜μ§€ λͺ»ν•œ 였λ₯˜ λ°œμƒ: {e} ---", exc_info=True)
return None
logger.info("--- [Preprocess] Streamlit Arrow λ³€ν™˜ 였λ₯˜ λ°©μ§€μš© 데이터 클리닝 μ‹œμž‘ ---")
for col in df.select_dtypes(include='object').columns:
temp_series = (
df[col]
.astype(str)
.str.replace('%', '', regex=False)
.str.replace(',', '', regex=False)
.str.strip()
)
numeric_series = pd.to_numeric(temp_series, errors='coerce')
df[col] = numeric_series.fillna(temp_series)
logger.info("--- [Preprocess] 데이터 클리닝 μ™„λ£Œ ---")
cols_to_process = ['μ›”λ§€μΆœκΈˆμ•‘_ꡬ간', 'μ›”λ§€μΆœκ±΄μˆ˜_ꡬ간', 'μ›”μœ λ‹ˆν¬κ³ κ°μˆ˜_ꡬ간', '월객단가_ꡬ간']
for col in cols_to_process:
if col in df.columns:
try:
series_str = df[col].astype(str).fillna('')
series_split = series_str.str.split('_').str[0]
series_numeric = pd.to_numeric(series_split, errors='coerce')
df[col] = series_numeric.fillna(0).astype(int)
except Exception as e:
logger.warning(f"--- [DATA WARNING] '{col}' 컬럼 처리 쀑 였λ₯˜ λ°œμƒ: {e}. ν•΄λ‹Ή μ»¬λŸΌμ„ κ±΄λ„ˆλœλ‹ˆλ‹€. ---", exc_info=True)
continue
logger.info(f"--- [Preprocess] 데이터 λ‘œλ“œ 및 μ „μ²˜λ¦¬ μ΅œμ’… μ™„λ£Œ. (Shape: {df.shape}) ---")
return df
# --- (2) api/server.pyμ—μ„œ κ°€μ Έμ˜¨ 헬퍼 ν•¨μˆ˜ ---
def replace_nan_with_none(data):
"""
λ”•μ…”μ…”λ„ˆλ¦¬λ‚˜ 리슀트 λ‚΄μ˜ λͺ¨λ“  NaN 값을 None으둜 μž¬κ·€μ μœΌλ‘œ λ³€ν™˜ν•©λ‹ˆλ‹€.
(api/server.py의 원본 ν•¨μˆ˜)
"""
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
# --- (3) api/server.py의 POST /profile λ‘œμ§μ„ λ³€ν™˜ν•œ ν•¨μˆ˜ ---
def get_merchant_profile_logic(merchant_id: str, df_merchant: pd.DataFrame):
"""
가맹점 ID와 λ§ˆμŠ€ν„° λ°μ΄ν„°ν”„λ ˆμž„μ„ λ°›μ•„ ν”„λ‘œνŒŒμΌλ§λœ 데이터λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
(api/server.py의 POST /profile μ—”λ“œν¬μΈνŠΈ 둜직)
"""
logger.info(f"βœ… [Local Logic] 가맹점 ID '{merchant_id}' ν”„λ‘œνŒŒμΌλ§ μš”μ²­ μˆ˜μ‹ ")
try:
store_df_multiple = df_merchant[df_merchant['가맹점ID'] == merchant_id]
if store_df_multiple.empty:
logger.warning(f"⚠️ [Local Logic] 404 - '{merchant_id}' 가맹점 IDλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
raise ValueError(f"'{merchant_id}' 가맹점 IDλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
if len(store_df_multiple) > 1:
logger.info(f" [INFO] '{merchant_id}'에 λŒ€ν•΄ {len(store_df_multiple)}개의 데이터 발견. μ΅œμ‹  λ°μ΄ν„°λ‘œ ν•„ν„°λ§ν•©λ‹ˆλ‹€.")
temp_df = store_df_multiple.copy()
temp_df['κΈ°μ€€λ…„μ›”_dt'] = pd.to_datetime(temp_df['κΈ°μ€€λ…„μ›”'])
latest_store_df = temp_df.sort_values(by='κΈ°μ€€λ…„μ›”_dt', ascending=False).iloc[[0]]
else:
latest_store_df = store_df_multiple
store_data = latest_store_df.iloc[0].to_dict()
# (고객 λΉ„μœ¨ 및 μžλ™μΆ”μΆœνŠΉμ§• 계산 λ‘œμ§μ€ 원본과 동일)
# 4-1. 고객 성별 λΉ„μœ¨ 계산 및 μ €μž₯
store_data['λ‚¨μ„±κ³ κ°λΉ„μœ¨'] = (
store_data.get('남성20λŒ€μ΄ν•˜λΉ„μœ¨', 0) + store_data.get('남성30λŒ€λΉ„μœ¨', 0) +
store_data.get('남성40λŒ€λΉ„μœ¨', 0) + store_data.get('남성50λŒ€λΉ„μœ¨', 0) +
store_data.get('남성60λŒ€μ΄μƒλΉ„μœ¨', 0)
)
store_data['μ—¬μ„±κ³ κ°λΉ„μœ¨'] = (
store_data.get('μ—¬μ„±20λŒ€μ΄ν•˜λΉ„μœ¨', 0) + store_data.get('μ—¬μ„±30λŒ€λΉ„μœ¨', 0) +
store_data.get('μ—¬μ„±40λŒ€λΉ„μœ¨', 0) + store_data.get('μ—¬μ„±50λŒ€λΉ„μœ¨', 0) +
store_data.get('μ—¬μ„±60λŒ€μ΄μƒλΉ„μœ¨', 0)
)
# 4-2. μ—°λ ΉλŒ€λ³„ λΉ„μœ¨ 계산 (20λŒ€μ΄ν•˜, 30λŒ€, 40λŒ€, 50λŒ€μ΄μƒ)
store_data['μ—°λ ΉλŒ€20λŒ€μ΄ν•˜κ³ κ°λΉ„μœ¨'] = store_data.get('남성20λŒ€μ΄ν•˜λΉ„μœ¨', 0) + store_data.get('μ—¬μ„±20λŒ€μ΄ν•˜λΉ„μœ¨', 0)
store_data['μ—°λ ΉλŒ€30λŒ€κ³ κ°λΉ„μœ¨'] = store_data.get('남성30λŒ€λΉ„μœ¨', 0) + store_data.get('μ—¬μ„±30λŒ€λΉ„μœ¨', 0)
store_data['μ—°λ ΉλŒ€40λŒ€κ³ κ°λΉ„μœ¨'] = store_data.get('남성40λŒ€λΉ„μœ¨', 0) + store_data.get('μ—¬μ„±40λŒ€λΉ„μœ¨', 0)
store_data['μ—°λ ΉλŒ€50λŒ€κ³ κ°λΉ„μœ¨'] = (
store_data.get('남성50λŒ€λΉ„μœ¨', 0) + store_data.get('μ—¬μ„±50λŒ€λΉ„μœ¨', 0) +
store_data.get('남성60λŒ€μ΄μƒλΉ„μœ¨', 0) + store_data.get('μ—¬μ„±60λŒ€μ΄μƒλΉ„μœ¨', 0)
)
male_ratio = store_data.get('λ‚¨μ„±κ³ κ°λΉ„μœ¨', 0)
female_ratio = store_data.get('μ—¬μ„±κ³ κ°λΉ„μœ¨', 0)
핡심고객_성별 = '남성 쀑심' if male_ratio > female_ratio else 'μ—¬μ„± 쀑심'
age_ratios = {
'20λŒ€μ΄ν•˜': store_data.get('μ—°λ ΉλŒ€20λŒ€μ΄ν•˜κ³ κ°λΉ„μœ¨', 0),
'30λŒ€': store_data.get('μ—°λ ΉλŒ€30λŒ€κ³ κ°λΉ„μœ¨', 0),
'40λŒ€': store_data.get('μ—°λ ΉλŒ€40λŒ€κ³ κ°λΉ„μœ¨', 0),
'50λŒ€μ΄μƒ': store_data.get('μ—°λ ΉλŒ€50λŒ€κ³ κ°λΉ„μœ¨', 0),
}
ν•΅μ‹¬μ—°λ ΉλŒ€_κ²°κ³Ό = max(age_ratios, key=age_ratios.get)
store_data['μžλ™μΆ”μΆœνŠΉμ§•'] = {
"핡심고객": 핡심고객_성별,
"ν•΅μ‹¬μ—°λ ΉλŒ€": ν•΅μ‹¬μ—°λ ΉλŒ€_κ²°κ³Ό,
"λ§€μΆœμˆœμœ„": f"μƒκΆŒ λ‚΄ μƒμœ„ {store_data.get('λ™μΌμƒκΆŒλ‚΄λ§€μΆœμˆœμœ„λΉ„μœ¨', 0):.1f}%, μ—…μ’… λ‚΄ μƒμœ„ {store_data.get('λ™μΌμ—…μ’…λ‚΄λ§€μΆœμˆœμœ„λΉ„μœ¨', 0):.1f}%"
}
area = store_data.get('μƒκΆŒ')
category = store_data.get('μ—…μ’…')
average_df = df_merchant[(df_merchant['μƒκΆŒ'] == area) & (df_merchant['μ—…μ’…'] == category)]
if average_df.empty:
average_data = {}
else:
numeric_cols = average_df.select_dtypes(include=np.number).columns
average_data = average_df[numeric_cols].mean().to_dict()
average_data['가맹점λͺ…'] = f"{area} {category} μ—…μ’… 평균"
final_result = {
"store_profile": store_data,
"average_profile": average_data
}
clean_result = replace_nan_with_none(final_result)
logger.info(f"βœ… [Local Logic] '{store_data.get('가맹점λͺ…')}({merchant_id})' ν”„λ‘œνŒŒμΌλ§ 성곡 (κΈ°μ€€λ…„μ›”: {store_data.get('κΈ°μ€€λ…„μ›”')})")
return clean_result
except ValueError as e: # HTTPException을 ValueError둜 λ³€κ²½
logger.error(f"❌ [Local Logic ERROR] 처리 쀑 였λ₯˜: {e}", exc_info=True)
raise e
except Exception as e:
logger.critical(f"❌ [Local Logic CRITICAL] μ˜ˆμΈ‘ν•˜μ§€ λͺ»ν•œ 였λ₯˜: {e}\n{traceback.format_exc()}", exc_info=True)
raise Exception(f"μ„œλ²„ λ‚΄λΆ€ 였λ₯˜ λ°œμƒ: {e}")
# --- (끝) API 둜직 톡합 ---
# --- 이미지 λ‘œλ“œ ν•¨μˆ˜ ---
@st.cache_data
def load_image(image_name: str) -> Image.Image | None:
"""assets ν΄λ”μ—μ„œ 이미지λ₯Ό λ‘œλ“œν•˜κ³  μΊμ‹œν•©λ‹ˆλ‹€."""
try:
image_path = config.ASSETS / image_name
if not image_path.is_file():
logger.error(f"이미지 νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {image_path}")
# ... (였λ₯˜ λ‘œκΉ…) ...
return None
return Image.open(image_path)
except Exception as e:
logger.error(f"이미지 λ‘œλ”© 쀑 였λ₯˜ λ°œμƒ ({image_name}): {e}", exc_info=True)
return None
# --- (4) 데이터 λ‘œλ“œ ν•¨μˆ˜ μˆ˜μ • ---
@st.cache_data
def load_master_dataframe():
"""
(μˆ˜μ •) FastAPI μ„œλ²„μ˜ 역할을 λŒ€μ‹ ν•˜μ—¬,
μ•± μ‹œμž‘ μ‹œ 'final_df.csv' λ§ˆμŠ€ν„° 데이터 전체λ₯Ό λ‘œλ“œν•˜κ³  μ „μ²˜λ¦¬ν•©λ‹ˆλ‹€.
"""
logger.info("λ§ˆμŠ€ν„° λ°μ΄ν„°ν”„λ ˆμž„ λ‘œλ“œ μ‹œλ„...")
df = load_and_preprocess_data() # (1)μ—μ„œ λ³΅μ‚¬ν•œ ν•¨μˆ˜ 호좜
if df is None:
logger.critical("--- [Streamlit Error] λ§ˆμŠ€ν„° 데이터 λ‘œλ”© μ‹€νŒ¨! ---")
return None
logger.info("--- [Streamlit] λ§ˆμŠ€ν„° λ°μ΄ν„°ν”„λ ˆμž„ λ‘œλ“œ 및 μΊμ‹œ μ™„λ£Œ ---")
return df
@st.cache_data
def load_merchant_list_for_ui(_df_master: pd.DataFrame):
"""
(μˆ˜μ •) λ§ˆμŠ€ν„° λ°μ΄ν„°ν”„λ ˆμž„μ—μ„œ UI κ²€μƒ‰μš© (ID, 이름) λͺ©λ‘λ§Œ μΆ”μΆœν•©λ‹ˆλ‹€.
(api/server.py의 GET /merchants μ—”λ“œν¬μΈνŠΈ 둜직)
"""
try:
if _df_master is None:
return None
logger.info(f"βœ… [Local Logic] '/merchants' 가맹점 λͺ©λ‘ μš”μ²­ μˆ˜μ‹ ")
merchant_list = _df_master[['가맹점ID', '가맹점λͺ…']].drop_duplicates().to_dict('records')
logger.info(f"βœ… [Local Logic] 가맹점 λͺ©λ‘ {len(merchant_list)}개 λ°˜ν™˜ μ™„λ£Œ")
return pd.DataFrame(merchant_list)
except Exception as e:
st.error(f"κ°€κ²Œ λͺ©λ‘μ„ λΆˆλŸ¬μ˜€λŠ” 데 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: {e}")
logger.critical(f"κ°€κ²Œ λͺ©λ‘ λ‘œλ”© μ‹€νŒ¨: {e}", exc_info=True)
return None
# --- 데이터 λ‘œλ“œ μ‹€ν–‰ (μˆ˜μ •) ---
# λ§ˆμŠ€ν„° λ°μ΄ν„°ν”„λ ˆμž„μ„ λ¨Όμ € λ‘œλ“œν•©λ‹ˆλ‹€.
MASTER_DF = load_master_dataframe()
if MASTER_DF is None:
st.error("🚨 데이터 λ‘œλ”© μ‹€νŒ¨! data/final_df.csv νŒŒμΌμ„ ν™•μΈν•΄μ£Όμ„Έμš”.")
st.stop()
# UI용 가맹점 λͺ©λ‘μ„ λ§ˆμŠ€ν„°μ—μ„œ μΆ”μΆœν•©λ‹ˆλ‹€.
merchant_df = load_merchant_list_for_ui(MASTER_DF)
if merchant_df is None:
st.error("🚨 가맹점 λͺ©λ‘ μΆ”μΆœ μ‹€νŒ¨!")
st.stop()
# --- μ„Έμ…˜ μ΄ˆκΈ°ν™” ν•¨μˆ˜ ---
def initialize_session():
""" μ„Έμ…˜ μ΄ˆκΈ°ν™” 및 AI λͺ¨λ“ˆ λ‘œλ“œ """
if "orchestrator" not in st.session_state:
google_api_key = os.environ.get("GOOGLE_API_KEY")
if not google_api_key:
st.error("πŸ”‘ GOOGLE_API_KEY ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€!")
st.stop()
with st.spinner("🧠 AI λͺ¨λΈκ³Ό 빅데이터λ₯Ό λ‘œλ”©ν•˜κ³  μžˆμ–΄μš”... μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”!"):
try:
# LLM μΊμ‹œ μ„€μ •
try:
from langchain.cache import InMemoryCache
from langchain.globals import set_llm_cache
set_llm_cache(InMemoryCache())
logger.info("--- [Streamlit] μ „μ—­ LLM μΊμ‹œ(InMemoryCache) ν™œμ„±ν™” ---")
except ImportError:
logger.warning("--- [Streamlit] langchain.cache μž„ν¬νŠΈ μ‹€νŒ¨. LLM μΊμ‹œ λΉ„ν™œμ„±ν™” ---")
load_marketing_vectorstore()
db = load_festival_vectorstore()
if db is None:
st.error("πŸ’Ύ μΆ•μ œ 벑터 DB λ‘œλ”© μ‹€νŒ¨! 'build_vector_store.py' μ‹€ν–‰ μ—¬λΆ€λ₯Ό ν™•μΈν•˜μ„Έμš”.")
st.stop()
logger.info("--- [Streamlit] λͺ¨λ“  AI λͺ¨λ“ˆ λ‘œλ”© μ™„λ£Œ ---")
except Exception as e:
st.error(f"🀯 AI λͺ¨λ“ˆ μ΄ˆκΈ°ν™” 쀑 였λ₯˜ λ°œμƒ: {e}")
logger.critical(f"AI λͺ¨λ“ˆ μ΄ˆκΈ°ν™” μ‹€νŒ¨: {e}", exc_info=True)
st.stop()
st.session_state.orchestrator = AgentOrchestrator(google_api_key)
# μ„Έμ…˜ μƒνƒœ λ³€μˆ˜ μ΄ˆκΈ°ν™”
if "step" not in st.session_state:
st.session_state.step = "get_merchant_name"
st.session_state.messages = []
st.session_state.merchant_id = None
st.session_state.merchant_name = None
st.session_state.profile_data = None
st.session_state.consultation_result = None
if "last_recommended_festivals" not in st.session_state:
st.session_state.last_recommended_festivals = []
# --- 처음으둜 λŒμ•„κ°€κΈ° ν•¨μˆ˜ ---
def restart_consultation():
""" μ„Έμ…˜ μƒνƒœ μ΄ˆκΈ°ν™” """
keys_to_reset = ["step", "merchant_name", "merchant_id", "profile_data", "messages", "consultation_result", "last_recommended_festivals"]
for key in keys_to_reset:
if key in st.session_state:
del st.session_state[key]
# --- μ‚¬μ΄λ“œλ°” λ Œλ”λ§ ν•¨μˆ˜ ---
def render_sidebar():
""" μ‚¬μ΄λ“œλ°” λ Œλ”λ§ (Synapse 둜고 κ°•μ‘° 및 간격 μ‘°μ •) """
with st.sidebar:
# 둜고 이미지 λ‘œλ“œ
synapse_logo = load_image("Synapse.png")
shinhancard_logo = load_image("ShinhanCard_Logo.png")
col1, col2, col3 = st.columns([1, 5, 1]) # κ°€μš΄λ° 컬럼 λ„ˆλΉ„ μ‘°μ •
with col2:
if synapse_logo:
st.image(synapse_logo, use_container_width=True)
st.write("")
st.markdown(" ")
col_sh1, col_sh2, col_sh3 = st.columns([1, 5, 1])
with col_sh2:
if shinhancard_logo:
st.image(shinhancard_logo, use_container_width=True) # 컬럼 λ„ˆλΉ„μ— 맞좀
st.markdown("<p style='text-align: center; color: grey; margin-top: 20px;'>2025 Big Contest</p>", unsafe_allow_html=True) # μœ„μͺ½ λ§ˆμ§„ 살짝 늘림
st.markdown("<p style='text-align: center; color: grey;'>AI DATA ν™œμš©λΆ„μ•Ό</p>", unsafe_allow_html=True)
st.markdown("---")
if st.button('처음으둜 λŒμ•„κ°€κΈ°', key='restart_button_styled', use_container_width=True): # λ²„νŠΌ μ•„μ΄μ½˜ μΆ”κ°€
restart_consultation()
st.rerun()
# --- κ°€κ²Œ 검색 UI ν•¨μˆ˜ (μˆ˜μ •) ---
def render_get_merchant_name_step():
""" UI 1단계: 가맹점 검색 및 선택 (API 호좜 둜직 μˆ˜μ •) """
st.subheader("πŸ” μ»¨μ„€νŒ… 받을 κ°€κ²Œλ₯Ό κ²€μƒ‰ν•΄μ£Όμ„Έμš”")
st.caption("κ°€κ²Œ 이름 λ˜λŠ” 가맹점 ID의 일뢀λ₯Ό μž…λ ₯ν•˜μ—¬ 검색할 수 μžˆμŠ΅λ‹ˆλ‹€.")
search_query = st.text_input(
"κ°€κ²Œ 이름 λ˜λŠ” 가맹점 ID 검색",
placeholder="예: 메가컀피, μŠ€νƒ€λ²…μŠ€, 003AC99735 λ“±",
label_visibility="collapsed"
)
if search_query:
mask = (
merchant_df['가맹점λͺ…'].str.contains(search_query, case=False, na=False, regex=False) |
merchant_df['가맹점ID'].str.contains(search_query, case=False, na=False, regex=False)
)
search_results = merchant_df[mask].copy()
if not search_results.empty:
search_results['display'] = search_results['가맹점λͺ…'] + " (" + search_results['가맹점ID'] + ")"
options = ["⬇ μ•„λž˜ λͺ©λ‘μ—μ„œ κ°€κ²Œλ₯Ό μ„ νƒν•΄μ£Όμ„Έμš”..."] + search_results['display'].tolist()
selected_display_name = st.selectbox(
"κ°€κ²Œ 선택:",
options,
label_visibility="collapsed"
)
if selected_display_name != "⬇️ μ•„λž˜ λͺ©λ‘μ—μ„œ κ°€κ²Œλ₯Ό μ„ νƒν•΄μ£Όμ„Έμš”...":
try:
selected_row = search_results[search_results['display'] == selected_display_name].iloc[0]
selected_merchant_id = selected_row['가맹점ID']
selected_merchant_name = selected_row['가맹점λͺ…']
button_label = f"πŸš€ '{selected_merchant_name}' 뢄석 μ‹œμž‘ν•˜κΈ°"
is_selection_valid = True
except (IndexError, KeyError):
button_label = "뢄석 μ‹œμž‘ν•˜κΈ°"
is_selection_valid = False
if st.button(button_label, disabled=not is_selection_valid, type="primary", use_container_width=True):
with st.spinner(f"πŸ“ˆ '{selected_merchant_name}' κ°€κ²Œ 정보λ₯Ό 뢄석 μ€‘μž…λ‹ˆλ‹€... μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”!"):
profile_data = None
try:
# --- (μˆ˜μ •) API POST μš”μ²­ λŒ€μ‹  (3)μ—μ„œ λ§Œλ“  둜컬 ν•¨μˆ˜ 호좜 ---
profile_data = get_merchant_profile_logic(selected_merchant_id, MASTER_DF)
# --------------------------------------------------------
if "store_profile" not in profile_data or "average_profile" not in profile_data:
st.error("ν”„λ‘œν•„ 생성 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
profile_data = None
except ValueError as e: # 404 였λ₯˜
st.error(f"κ°€κ²Œ ν”„λ‘œν•„ λ‘œλ”© μ‹€νŒ¨: {e}")
except Exception as e:
st.error(f"κ°€κ²Œ ν”„λ‘œν•„ λ‘œλ”© 쀑 μ˜ˆμƒμΉ˜ λͺ»ν•œ 였λ₯˜ λ°œμƒ: {e}")
logger.critical(f"κ°€κ²Œ ν”„λ‘œν•„ 둜컬 둜직 μ‹€νŒ¨: {e}", exc_info=True)
if profile_data:
st.session_state.merchant_name = selected_merchant_name
st.session_state.merchant_id = selected_merchant_id
st.session_state.profile_data = profile_data
st.session_state.step = "show_profile_and_chat"
st.success(f"βœ… '{selected_merchant_name}' 뢄석 μ™„λ£Œ!")
st.rerun()
else:
st.info("πŸ’‘ 검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€. λ‹€λ₯Έ 검색어λ₯Ό μ‹œλ„ν•΄λ³΄μ„Έμš”.")
# --- ν”„λ‘œν•„ 및 μ±„νŒ… UI ν•¨μˆ˜ ---
def render_show_profile_and_chat_step():
"""UI 2단계: ν”„λ‘œν•„ 확인 및 AI μ±„νŒ…"""
st.subheader(f"✨ '{st.session_state.merchant_name}' κ°€κ²Œ 뢄석 μ™„λ£Œ")
with st.expander("πŸ“Š 상세 데이터 뢄석 리포트 보기", expanded=True):
try:
display_merchant_profile(st.session_state.profile_data)
except Exception as e:
st.error(f"ν”„λ‘œν•„ μ‹œκ°ν™” 쀑 였λ₯˜ λ°œμƒ: {e}")
logger.error(f"--- [Visualize ERROR]: {e}\n{traceback.format_exc()}", exc_info=True)
st.divider()
st.subheader("πŸ’¬ AI μ»¨μ„€ν„΄νŠΈμ™€ 상담을 μ‹œμž‘ν•˜μ„Έμš”.")
st.info("κ°€κ²Œ 뢄석 정보λ₯Ό λ°”νƒ•μœΌλ‘œ κΆκΈˆν•œ 점을 μ§ˆλ¬Έν•΄λ³΄μ„Έμš”. (예: '20λŒ€ μ—¬μ„± 고객을 늘리고 μ‹Άμ–΄μš”')")
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if prompt := st.chat_input("μš”μ²­μ‚¬ν•­μ„ μž…λ ₯ν•˜μ„Έμš”..."):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
with st.spinner("AI μ»¨μ„€ν„΄νŠΈκ°€ 닡변을 생성 μ€‘μž…λ‹ˆλ‹€...(μ΅œλŒ€ 1~2λΆ„)"):
orchestrator = st.session_state.orchestrator
if "store_profile" not in st.session_state.profile_data:
st.error("μ„Έμ…˜μ— 'store_profile' 데이터가 μ—†μŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œμž‘ν•΄μ£Όμ„Έμš”.")
st.stop()
agent_history = []
history_to_convert = st.session_state.messages[:-1][-10:]
for msg in history_to_convert:
if msg["role"] == "user":
agent_history.append(HumanMessage(content=msg["content"]))
elif msg["role"] == "assistant":
agent_history.append(AIMessage(content=msg["content"]))
result = orchestrator.invoke_agent(
user_query=prompt,
store_profile_dict=st.session_state.profile_data["store_profile"],
chat_history=agent_history,
last_recommended_festivals=st.session_state.last_recommended_festivals,
)
response_text = ""
st.session_state.last_recommended_festivals = []
if "error" in result:
response_text = f"였λ₯˜ λ°œμƒ: {result['error']}"
elif "final_response" in result:
response_text = result.get("final_response", "응닡을 μƒμ„±ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.")
intermediate_steps = result.get("intermediate_steps", [])
try:
for step in intermediate_steps:
action = step[0]
tool_output = step[1]
if hasattr(action, 'tool') and action.tool == "recommend_festivals":
if tool_output and isinstance(tool_output, list) and isinstance(tool_output[0], dict):
recommended_list = [
f.get("μΆ•μ œλͺ…") for f in tool_output if f.get("μΆ•μ œλͺ…")
]
st.session_state.last_recommended_festivals = recommended_list
logger.info(f"--- [Streamlit] μΆ”μ²œ μΆ•μ œ μ €μž₯됨 (Intermediate Steps): {recommended_list} ---")
break
except Exception as e:
logger.critical(f"--- [Streamlit CRITICAL] Intermediate steps 처리 쀑 μ˜ˆμ™Έ λ°œμƒ: {e} ---", exc_info=True)
else:
response_text = "μ•Œ 수 μ—†λŠ” 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."
st.markdown(response_text)
st.session_state.messages.append({"role": "assistant", "content": response_text})
# --- 메인 μ‹€ν–‰ ν•¨μˆ˜ ---
def main():
st.title("πŸŽ‰ MarketSync (λ§ˆμΌ“μ‹±ν¬)")
st.subheader("μ†Œμƒκ³΅μΈ λ§žμΆ€ν˜• μΆ•μ œ μΆ”μ²œ & λ§ˆμΌ€νŒ… AI μ»¨μ„€ν„΄νŠΈ")
st.caption("μ‹ ν•œμΉ΄λ“œ 빅데이터와 AI μ—μ΄μ „νŠΈλ₯Ό ν™œμš©ν•˜μ—¬, 사μž₯λ‹˜ κ°€κ²Œμ— κΌ­ λ§žλŠ” μ§€μ—­ μΆ•μ œμ™€ λ§ˆμΌ€νŒ… μ „λž΅μ„ μ°Ύμ•„λ“œλ¦½λ‹ˆλ‹€.")
st.divider()
initialize_session()
render_sidebar()
if st.session_state.step == "get_merchant_name":
render_get_merchant_name_step()
elif st.session_state.step == "show_profile_and_chat":
render_show_profile_and_chat_step()
# --- μ•± μ‹€ν–‰ ---
if __name__ == "__main__":
main()