Spaces:
Sleeping
Sleeping
| # 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 λ‘μ§ ν΅ν© --- | |
| # --- μ΄λ―Έμ§ λ‘λ ν¨μ --- | |
| 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) λ°μ΄ν° λ‘λ ν¨μ μμ --- | |
| 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 | |
| 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() | |