MOAI / app.py
wenbemi
Update app.py
2b87337 verified
raw
history blame
63.4 kB
# -*- coding: utf-8 -*-
# ──────────────────────────────── BOOTSTRAP (must be first) ────────────────────────────────
import os, pathlib, io, json, random
APP_DIR = pathlib.Path(__file__).parent.resolve()
# Streamlit ν™ˆ/μ„€μ •
os.environ["HOME"] = str(APP_DIR)
CONFIG_DIR = APP_DIR / ".streamlit"
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
os.environ["STREAMLIT_HOME"] = str(CONFIG_DIR)
os.environ["STREAMLIT_SERVER_HEADLESS"] = "true"
os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
CACHE_ROOT = os.environ.get("HF_CACHE_ROOT") or f"/tmp/hf-cache/u{os.getuid()}"
HF_HOME = pathlib.Path(CACHE_ROOT) / "hf-home"
TRANSFORMERS_CACHE = pathlib.Path(CACHE_ROOT) / "hf-cache"
HUB_CACHE = pathlib.Path(CACHE_ROOT) / "hf-cache"
TORCH_HOME = pathlib.Path(CACHE_ROOT) / "torch-cache"
XDG_CACHE_HOME = pathlib.Path(CACHE_ROOT) / "xdg-cache"
for p in [HF_HOME, TRANSFORMERS_CACHE, HUB_CACHE, TORCH_HOME, XDG_CACHE_HOME]:
p.mkdir(parents=True, exist_ok=True)
os.environ["HF_HOME"] = str(HF_HOME)
os.environ["TRANSFORMERS_CACHE"] = str(TRANSFORMERS_CACHE)
os.environ["HUGGINGFACE_HUB_CACHE"] = str(HUB_CACHE)
os.environ["TORCH_HOME"] = str(TORCH_HOME)
os.environ["XDG_CACHE_HOME"] = str(XDG_CACHE_HOME)
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
from huggingface_hub import hf_hub_download
import pandas as pd
import streamlit as st
from streamlit.components.v1 import html
from css import render_message, render_chip_buttons, log_and_render, replay_log
st.success("πŸŽ‰ 앱이 μ„±κ³΅μ μœΌλ‘œ μ‹œμž‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€! 라이브러리 μ„€μΉ˜ 성곡!")
# ──────────────────────────────── Dataset Repo μ„€μ • ────────────────────────────────
HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "emisdfde/moai-travel-data")
HF_DATASET_REV = os.getenv("HF_DATASET_REV", "main")
def _is_pointer_bytes(b: bytes) -> bool:
head = b[:2048].decode(errors="ignore").lower()
return (
"version https://git-lfs.github.com/spec/v1" in head
or "git-lfs" in head
or "xet" in head # e.g. xet 포인터
or "pointer size" in head
)
def _read_csv_bytes(b: bytes) -> pd.DataFrame:
try:
return pd.read_csv(io.BytesIO(b), encoding="utf-8")
except UnicodeDecodeError:
return pd.read_csv(io.BytesIO(b), encoding="cp949")
def load_csv_smart(local_path: str,
hub_filename: str | None = None,
repo_id: str = HF_DATASET_REPO,
repo_type: str = "dataset",
revision: str = HF_DATASET_REV) -> pd.DataFrame:
# hub_filename μƒλž΅ μ‹œ 둜컬 파일λͺ… μ‚¬μš©
if hub_filename is None:
hub_filename = os.path.basename(local_path)
# 1) 둜컬 μš°μ„ 
if os.path.exists(local_path):
with open(local_path, "rb") as f:
data = f.read()
if not _is_pointer_bytes(data):
return _read_csv_bytes(data)
# 2) ν—ˆλΈŒ λ‹€μš΄λ‘œλ“œ
cached = hf_hub_download(repo_id=repo_id, filename=hub_filename,
repo_type=repo_type, revision=revision)
try:
return pd.read_csv(cached, encoding="utf-8")
except UnicodeDecodeError:
return pd.read_csv(cached, encoding="cp949")
def load_json_smart(local_path: str,
hub_filename: str | None = None,
repo_id: str = HF_DATASET_REPO,
repo_type: str = "dataset",
revision: str = HF_DATASET_REV):
if hub_filename is None:
hub_filename = os.path.basename(local_path)
if os.path.exists(local_path):
with open(local_path, "rb") as f:
data = f.read()
if not _is_pointer_bytes(data):
return json.loads(data.decode("utf-8"))
cached = hf_hub_download(repo_id=repo_id, filename=hub_filename,
repo_type=repo_type, revision=revision)
with open(cached, "r", encoding="utf-8") as f:
return json.load(f)
# ──────────────────────────────── 데이터 λ‘œλ“œ ────────────────────────────────
travel_df = load_csv_smart("trip_emotions.csv", "trip_emotions.csv")
external_score_df = load_csv_smart("external_scores.csv", "external_scores.csv")
festival_df = load_csv_smart("festivals.csv", "festivals.csv")
weather_df = load_csv_smart("weather.csv", "weather.csv")
package_df = load_csv_smart("packages.csv", "packages.csv")
master_df = load_csv_smart("countries_cities.csv", "countries_cities.csv")
theme_title_phrases = load_json_smart("theme_title_phrases.json", "theme_title_phrases.json")
# ν•„μˆ˜ 컬럼 κ°€λ“œ
for col in ("μ—¬ν–‰λ‚˜λΌ", "μ—¬ν–‰λ„μ‹œ", "μ—¬ν–‰μ§€"):
if col not in travel_df.columns:
st.error(f"'travel_df'에 '{col}' 컬럼이 μ—†μŠ΅λ‹ˆλ‹€. μ‹€μ œ 컬럼: {travel_df.columns.tolist()}")
st.stop()
# ──────────────────────────────── chat_a import & μ΄ˆκΈ°ν™” ────────────────────────────────
from chat_a import (
init_datasets, # ⬅️ μƒˆλ‘œ μΆ”κ°€λœ μ§€μ—° μ΄ˆκΈ°ν™” ν•¨μˆ˜
analyze_emotion,
detect_intent,
extract_themes,
recommend_places_by_theme,
detect_location_filter,
generate_intro_message,
theme_ui_map,
ui_to_theme_map,
theme_opening_lines,
intent_opening_lines,
apply_weighted_score_filter,
get_highlight_message,
get_weather_message,
get_intent_intro_message,
recommend_packages,
handle_selected_place,
generate_region_intro,
parse_companion_and_age,
filter_packages_by_companion_age,
make_top2_description_custom,
format_summary_tags_custom,
make_companion_age_message
)
# μ§€μ—° μ΄ˆκΈ°ν™”: import μ‹œμ μ—λŠ” 데이터 μ ‘κ·Ό κΈˆμ§€, μ—¬κΈ°μ„œ ν•œ 번만 μ£Όμž…
init_datasets(
travel_df=travel_df,
festival_df=festival_df,
external_score_df=external_score_df,
weather_df=weather_df,
package_df=package_df,
master_df=master_df,
theme_title_phrases=theme_title_phrases,
)
# ───────────────────────────────────── streamlit용 ν•¨μˆ˜
def init_session():
if "chat_log" not in st.session_state:
st.session_state.chat_log = []
if "mode" not in st.session_state:
st.session_state.mode = None
if "user_input" not in st.session_state:
st.session_state.user_input = ""
if "selected_theme" not in st.session_state:
st.session_state.selected_theme = None
def make_key(row) -> tuple[str, str]:
"""prev 에 λ„£κ³  κΊΌλ‚Ό λ•Œ μ“°λŠ” κ³ μœ ν‚€(μ—¬ν–‰μ§€, μ—¬ν–‰λ„μ‹œ)"""
return (row["μ—¬ν–‰μ§€"], row["μ—¬ν–‰λ„μ‹œ"])
# ── P κΈ€κΌ΄ 크기 14 px ───────────────────────────────────
st.markdown("""
<style>
/* κΈ°λ³Έ p νƒœκ·Έ κΈ€κΌ΄ 크기 */
html, body, p {
font-size: 14px !important; /* ← 14 px κ³ μ • */
line-height: 1.5; /* (선택) 가독성을 μœ„ν•œ 쀄간격 */
}
/* Streamlit κΈ°λ³Έ λ§ˆμ§„ 제거둜 λΆˆν•„μš”ν•œ μ—¬λ°± λ°©μ§€ (선택) */
p {
margin-top: 0;
margin-bottom: 0.5rem;
}
</style>
""", unsafe_allow_html=True)
# ───────────────────────────────────── region mode
def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
country_filter, city_filter, chat_container, log_and_render):
"""region λͺ¨λ“œ(νŠΉμ • λ‚˜λΌ, λ„μ‹œλ₯Ό 직접 μ–ΈκΈ‰ν–ˆμ„ 경우) μ „μš© UI & 둜직"""
# ────────────────── μ„Έμ…˜ ν‚€ μ •μ˜
region_key = "region_chip_selected"
prev_key = "region_prev_recommended"
step_key = "region_step"
sample_key = "region_sample_df"
# ────────────────── 0) μ΄ˆκΈ°ν™”
if step_key not in st.session_state:
st.session_state[step_key] = "recommend"
st.session_state[prev_key] = set()
st.session_state.pop(sample_key, None)
# ────────────────── 1) restart μƒνƒœλ©΄ 인트둜만 좜λ ₯ν•˜κ³  μ’…λ£Œ
if st.session_state[step_key] == "restart":
log_and_render(
"λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!<br>μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?",
sender="bot",
chat_container=chat_container,
key="region_restart_intro"
)
return
# ────────────────── 2) μΆ”μ²œ 단계
if st.session_state[step_key] == "recommend":
# 2.1) μΆ”μ²œ 문ꡬ 좜λ ₯ (λ„μ‹œ λ˜λŠ” κ΅­κ°€ κΈ°μ€€)
city_exists = bool(city_filter) and city_filter in travel_df["μ—¬ν–‰λ„μ‹œ"].values
country_exists = bool(country_filter) and country_filter in travel_df["μ—¬ν–‰λ‚˜λΌ"].values
# μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” λ„μ‹œμΈ 경우
if city_filter and not city_exists:
intro = generate_region_intro('', country_filter)
log_and_render(
f"μ£„μ†‘ν•΄μš”. {city_filter}의 μ—¬ν–‰μ§€λŠ” 아직 λ―Έμ •μ΄μ—μš”.<br>ν•˜μ§€λ§Œ, {intro}",
sender="bot",
chat_container=chat_container,
key="region_intro_invalid"
)
else:
# 정상적인 λ„μ‹œ/ꡭ가일 경우
intro = generate_region_intro(city_filter, country_filter)
log_and_render(intro,
sender="bot",
chat_container=chat_container,
key="region_intro")
# 2.2) μ—¬ν–‰μ§€ 후보 λͺ©λ‘ 필터링
df = travel_df.drop_duplicates(subset=["μ—¬ν–‰μ§€"])
if city_exists:
df = df[df["μ—¬ν–‰λ„μ‹œ"].str.contains(city_filter, na=False)]
elif country_exists:
df = df[df["μ—¬ν–‰λ‚˜λΌ"].str.contains(country_filter, na=False)]
# 2.3) 이전 μΆ”μ²œ λͺ©λ‘κ³Ό κ²ΉμΉ˜μ§€ μ•ŠλŠ” μ—¬ν–‰μ§€λ§Œ 남김
prev = st.session_state.setdefault(prev_key, set())
remaining = df[~df.apply(lambda r: make_key(r) in prev, axis=1)]
# μΆ”μ²œ κ°€λŠ₯ν•œ μ—¬ν–‰μ§€κ°€ μ—†λ‹€λ©΄ μ’…λ£Œ λ‹¨κ³„λ‘œ μ „ν™˜
if remaining.empty and sample_key not in st.session_state:
st.session_state[step_key] = "recommand_end"
st.rerun()
return
# 2.4) μƒ˜ν”Œλ§ (이전 μƒ˜ν”Œμ΄ μ—†κ±°λ‚˜ λΉ„μ–΄ 있으면 μƒˆλ‘œ μΆ”μΆœ)
if sample_key not in st.session_state or st.session_state[sample_key].empty:
sampled = remaining.sample(
n=min(3, len(remaining)), #μ΅œλŒ€ 3개
random_state=random.randint(1, 9999)
)
st.session_state[sample_key] = sampled
# tuple ν˜•νƒœλ‘œ ν•œκΊΌλ²ˆμ— μΆ”κ°€
prev.update([make_key(r) for _, r in sampled.iterrows()])
st.session_state[prev_key] = prev
else:
sampled = st.session_state[sample_key]
loc_df = st.session_state[sample_key]
# 2.5) μΆ”μ²œ 리슀트 좜λ ₯ & μΉ© UI
message = (
"πŸ“Œ μΆ”μ²œ μ—¬ν–‰μ§€ λͺ©λ‘<br>κ°€μž₯ κ°€κ³  싢은 곳을 κ³¨λΌμ£Όμ„Έμš”!<br><br>" +
"<br>".join([
f"{i+1}. <strong>{row.μ—¬ν–‰μ§€}</strong> "
f"({row.μ—¬ν–‰λ‚˜λΌ}, {row.μ—¬ν–‰λ„μ‹œ}) "
f"{getattr(row, 'ν•œμ€„μ„€λͺ…', 'μ„€λͺ…이 μ—†μŠ΅λ‹ˆλ‹€')}"
for i, row in enumerate(loc_df.itertuples())
])
)
with chat_container:
log_and_render(message,
sender="bot",
chat_container=chat_container,
key=f"region_recommendation_{random.randint(1,999999)}"
)
# μΉ© λ²„νŠΌμœΌλ‘œ μΆ”μ²œμ§€ 쀑 선택받기
prev_choice = st.session_state.get(region_key, None)
choice = render_chip_buttons(
loc_df["μ—¬ν–‰μ§€"].tolist() + ["λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„"],
key_prefix="region_chip",
selected_value=prev_choice
)
# 2.7) 선택 κ²°κ³Ό 처리
if not choice or choice == prev_choice:
return
if choice == "λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„":
log_and_render("λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„",
sender="user",
chat_container=chat_container,
key=f"user_place_refresh_{random.randint(1,999999)}")
st.session_state.pop(sample_key, None)
st.rerun()
return
# 2.8) μ—¬ν–‰μ§€ 선택 μ™„λ£Œ
st.session_state[region_key] = choice
st.session_state[step_key] = "detail"
st.session_state.chat_log.append(("user", choice))
# μ‹€μ œλ‘œ μ„ νƒλœ μ—¬ν–‰μ§€λ§Œ prev에 기둝
match = sampled[sampled["μ—¬ν–‰μ§€"] == choice]
if not match.empty:
prev.add(make_key(match.iloc[0]))
st.session_state[prev_key] = prev
# μƒ˜ν”Œ 폐기
st.session_state.pop(sample_key, None)
st.rerun()
return
# ────────────────── 3) μΆ”μ²œ μ’…λ£Œ 단계: 더 이상 μΆ”μ²œν•  μ—¬ν–‰μ§€κ°€ 없을 λ•Œ
elif st.session_state[step_key] == "recommand_end":
with chat_container:
# 3.1) λ©”μ‹œμ§€ 좜λ ₯
log_and_render(
"⚠️ 더 이상 μƒˆλ‘œμš΄ μ—¬ν–‰μ§€κ°€ μ—†μ–΄μš”.<br>λ‹€μ‹œ μ§ˆλ¬Έν•˜μ‹œκ² μ–΄μš”?",
sender="bot",
chat_container=chat_container,
key="region_empty"
)
# 3.2) μž¬μ‹œμž‘ μ—¬λΆ€ μΉ© λ²„νŠΌ 좜λ ₯
restart_done_key = "region_restart_done"
chip_ph = st.empty()
if not st.session_state.get(restart_done_key, False):
with chip_ph:
choice = render_chip_buttons(
["예 πŸ”„", "μ•„λ‹ˆμ˜€ ❌"],
key_prefix="region_restart"
)
else:
choice = None
# 3.3) 아직 아무것도 μ„ νƒν•˜μ§€ μ•Šμ€ 경우
if choice is None:
return
chip_ph.empty()
st.session_state[restart_done_key] = True
# 3.4) μ‚¬μš©μž 선택값 좜λ ₯
log_and_render(
choice,
sender="user",
chat_container=chat_container,
key=f"user_restart_choice_{choice}"
)
# 3.5) μ‚¬μš©μžκ°€ μž¬μΆ”μ²œμ„ μ›ν•˜λŠ” 경우
if choice == "예 πŸ”„":
# μ—¬ν–‰ μΆ”μ²œ μƒνƒœ μ΄ˆκΈ°ν™”
for k in [region_key, prev_key, sample_key, restart_done_key]:
st.session_state.pop(k, None)
chip_ph.empty()
# λ‹€μŒ μΆ”μ²œ λ‹¨κ³„λ‘œ μ΄ˆκΈ°ν™”
st.session_state["user_input_rendered"] = False
st.session_state["region_step"] = "restart"
log_and_render(
"λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!<br>μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?",
sender="bot",
chat_container=chat_container,
key="region_restart_intro"
)
return
# 3.6) μ‚¬μš©μžκ°€ μ’…λ£Œλ₯Ό μ„ νƒν•œ 경우
else:
log_and_render("μ—¬ν–‰ μΆ”μ²œμ„ μ’…λ£Œν• κ²Œμš”. ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️",
sender="bot",
chat_container=chat_container,
key="region_exit")
st.stop()
return
# ────────────────── 4) μ—¬ν–‰μ§€ 상세 단계
if st.session_state[step_key] == "detail":
chosen = st.session_state[region_key]
# city 이름 λ½‘μ•„μ„œ μ„Έμ…˜μ— μ €μž₯
row = travel_df[travel_df["μ—¬ν–‰μ§€"] == chosen].iloc[0]
st.session_state["selected_city"] = row["μ—¬ν–‰λ„μ‹œ"]
st.session_state["selected_place"] = chosen
log_and_render(chosen,
sender="user",
chat_container=chat_container,
key=f"user_place_{chosen}")
handle_selected_place(
chosen,
travel_df,
external_score_df,
festival_df,
weather_df,
chat_container=chat_container
)
st.session_state[step_key] = "companion"
st.rerun()
return
# ────────────────── 5) 동행·연령 λ°›κΈ° 단계
elif st.session_state[step_key] == "companion":
with chat_container:
# 5.1) μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯
log_and_render(
"ν•¨κ»˜ κ°€λŠ” λΆ„μ΄λ‚˜ μ—°λ ΉλŒ€λ₯Ό μ•Œλ €μ£Όμ‹œλ©΄ 더 λ”± λ§žλŠ” μƒν’ˆμ„ κ³¨λΌλ“œλ¦΄κ²Œμš”!<br>"
"1️⃣ 동행 μ—¬λΆ€ (혼자 / 친ꡬ / μ»€ν”Œ / κ°€μ‘± / 단체)<br>"
"2️⃣ μ—°λ ΉλŒ€ (20λŒ€ / 30λŒ€ / 40λŒ€ / 50λŒ€ / 60λŒ€ 이상)",
sender="bot",
chat_container=chat_container,
key="ask_companion_age"
)
# 5.1.1) 동행 μ²΄ν¬λ°•μŠ€
st.markdown(
'<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">πŸ‘« 동행 선택</div>',
unsafe_allow_html=True
)
c_cols = st.columns(5)
comp_flags = {
"혼자": c_cols[0].checkbox("혼자"),
"친ꡬ": c_cols[1].checkbox("친ꡬ"),
"μ»€ν”Œ": c_cols[2].checkbox("μ»€ν”Œ"),
"κ°€μ‘±": c_cols[3].checkbox("κ°€μ‘±"),
"단체": c_cols[4].checkbox("단체"),
}
companions = [k for k, v in comp_flags.items() if v]
# 5.1.2) μ—°λ Ή μ²΄ν¬λ°•μŠ€
st.markdown(
'<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">πŸŽ‚ μ—°λ Ή 선택</div>',
unsafe_allow_html=True
)
a_cols = st.columns(5)
age_flags = {
"20λŒ€": a_cols[0].checkbox("20λŒ€"),
"30λŒ€": a_cols[1].checkbox("30λŒ€"),
"40λŒ€": a_cols[2].checkbox("40λŒ€"),
"50λŒ€": a_cols[3].checkbox("50λŒ€"),
"60λŒ€ 이상": a_cols[4].checkbox("60λŒ€ 이상"),
}
age_group = [k for k, v in age_flags.items() if v]
# 5.1.3) 확인 λ²„νŠΌ
confirm = st.button(
"μΆ”μ²œ λ°›κΈ°",
key="btn_confirm_companion",
disabled=not (companions or age_group),
)
# 5.2) λ©”μ‹œμ§€ 좜λ ₯
if confirm:
# μ‚¬μš©μž 버블 좜λ ₯
user_msg = " / ".join(companions + age_group)
log_and_render(
user_msg if user_msg else "선택 μ•ˆ 함",
sender="user",
chat_container=chat_container,
key=f"user_comp_age_{random.randint(1,999999)}"
)
# μ„Έμ…˜ μ €μž₯
st.session_state["companions"] = companions or None
st.session_state["age_group"] = age_group or None
# λ‹€μŒ μŠ€ν…
st.session_state[step_key] = "package"
st.rerun()
return
# ────────────────── 6) 동행·연령 필터링· νŒ¨ν‚€μ§€ 좜λ ₯ 단계
elif st.session_state[step_key] == "package":
# νŒ¨ν‚€μ§€ 버블을 이미 λ§Œλ“€μ—ˆμœΌλ©΄ κ±΄λ„ˆλœ€
if st.session_state.get("package_rendered", False):
st.session_state[step_key] = "package_end"
return
companions = st.session_state.get("companions")
age_group = st.session_state.get("age_group")
city = st.session_state.get("selected_city")
place = st.session_state.get("selected_place")
filtered = filter_packages_by_companion_age(
package_df, companions, age_group, city=city, top_n=2
)
if filtered.empty:
log_and_render(
"⚠️ μ•„μ‰½μ§€λ§Œ μ§€κΈˆ 쑰건에 λ§žλŠ” νŒ¨ν‚€μ§€κ°€ μ—†μ–΄μš”.<br>"
"λ‹€λ₯Έ 쑰건으둜 λ‹€μ‹œ μ°Ύμ•„λ³ΌκΉŒμš”?",
sender="bot", chat_container=chat_container,
key="no_package"
)
st.session_state[step_key] = "companion" # λ‹€μ‹œ μž…λ ₯ λ‹¨κ³„λ‘œ
st.rerun()
return
combo_msg = make_companion_age_message(companions, age_group)
header = f"{combo_msg}"
# νŒ¨ν‚€μ§€ μΉ΄λ“œ 좜λ ₯
used_phrases = set()
theme_row = travel_df[travel_df["μ—¬ν–‰μ§€"] == place]
raw_theme = theme_row["ν†΅ν•©ν…Œλ§ˆλͺ…"].iloc[0] if not theme_row.empty else None
selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0]
title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆ”μ²œ"])
sampled_titles = random.sample(title_candidates,
k=min(2, len(title_candidates)))
# λ©”μ‹œμ§€ 생성
pkg_msgs = [header]
for i, (_, row) in enumerate(filtered.iterrows(), 1):
desc, used_phrases = make_top2_description_custom(
row.to_dict(), used_phrases
)
tags = format_summary_tags_custom(row["μš”μ•½μ •λ³΄"])
title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles)
else random.choice(title_candidates))
title = f"{city} {title_phrase} νŒ¨ν‚€μ§€"
url = row.URL
pkg_msgs.append(
f"{i}. <strong>{title}</strong><br>"
f"πŸ…Ό {desc}<br>{tags}<br>"
f'<a href="{url}" target="_blank" rel="noopener noreferrer" '
'style="text-decoration:none;font-weight:600;color:#009c75;">'
'πŸ’š λ°”λ‘œκ°€κΈ°&nbsp;β†—</a>'
)
# λ©”μ‹œμ§€ 좜λ ₯
log_and_render(
"<br><br>".join(pkg_msgs),
sender="bot",
chat_container=chat_container,
key=f"pkg_bundle_{random.randint(1,999999)}"
)
# μ„Έμ…˜ 정리
st.session_state["package_rendered"] = True
st.session_state[step_key] = "package_end"
return
# ────────────────── 7) μ’…λ£Œ 단계
elif st.session_state[step_key] == "package_end":
log_and_render("ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️",
sender="bot", chat_container=chat_container,
key="goodbye")
# ───────────────────────────────────── intent λͺ¨λ“œ
def intent_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
country_filter, city_filter, chat_container, intent, log_and_render):
"""intent(μ˜λ„λ₯Ό μž…λ ₯ν–ˆμ„ 경우) λͺ¨λ“œ μ „μš© UI & 둜직"""
# ────────────────── μ„Έμ…˜ ν‚€ μ •μ˜
sample_key = "intent_sample_df"
step_key = "intent_step"
prev_key = "intent_prev_places"
intent_key = "intent_chip_selected"
# ────────────────── 0) μ΄ˆκΈ°ν™”
if step_key not in st.session_state:
st.session_state[step_key] = "recommend_places"
st.session_state[prev_key] = set()
st.session_state.pop(sample_key, None)
# ────────────────── 1) restart μƒνƒœλ©΄ 인트둜만 좜λ ₯ν•˜κ³  μ’…λ£Œ
if st.session_state[step_key] == "restart":
log_and_render(
"λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!<br>μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?",
sender="bot",
chat_container=chat_container,
key="region_restart_intro"
)
return
# ────────────────── 2) μ—¬ν–‰μ§€ μΆ”μ²œ 단계
if st.session_state[step_key] == "recommend_places":
selected_theme = intent
theme_df = recommend_places_by_theme(selected_theme, country_filter, city_filter)
theme_df = theme_df.drop_duplicates(subset=["μ—¬ν–‰λ„μ‹œ"])
theme_df = theme_df.drop_duplicates(subset=["μ—¬ν–‰μ§€"])
# 2.1) 이전 μΆ”μ²œ 기둝 μ„ΈνŒ…
prev = st.session_state.setdefault(prev_key, set())
# 2.2) 이미 μƒ˜ν”Œμ΄ μžˆλ‹€λ©΄ result_df μž¬μ‚¬μš©
if sample_key in st.session_state and not st.session_state[sample_key].empty:
result_df = st.session_state[sample_key]
else:
# 2.3) μƒˆλ‘œμš΄ μΆ”μ²œ λŒ€μƒ 필터링
candidates = theme_df[~theme_df["μ—¬ν–‰μ§€"].isin(prev)]
# 2.4) 후보가 μ—†λ‹€λ©΄ μ’…λ£Œ
if candidates.empty:
st.session_state[step_key] = "recommend_places_end"
st.rerun()
return
# 2.5) μƒˆλ‘œμš΄ μΆ”μ²œ μΆ”μΆœ 및 μ €μž₯
result_df = apply_weighted_score_filter(candidates)
st.session_state[sample_key] = result_df
# prev에 λ“±λ‘ν•˜μ—¬ 쀑볡 μΆ”μ²œ λ°©μ§€
prev.update(result_df["μ—¬ν–‰μ§€"])
st.session_state[prev_key] = prev
# 2.6) μ˜€ν”„λ‹ λ¬Έμž₯ 생성
opening_line = intent_opening_lines.get(selected_theme, f"'{selected_theme}' μ—¬ν–‰μ§€λ₯Ό μ†Œκ°œν• κ²Œμš”.")
opening_line = opening_line.format(len(result_df))
# 2.7) μΆ”μ²œ λ©”μ‹œμ§€ ꡬ성
message = "<br>".join([
f"{i+1}. <strong>{row.μ—¬ν–‰μ§€}</strong> "
f"({row.μ—¬ν–‰λ‚˜λΌ}, {row.μ—¬ν–‰λ„μ‹œ}) "
f"{getattr(row, 'ν•œμ€„μ„€λͺ…', 'μ„€λͺ…이 μ—†μŠ΅λ‹ˆλ‹€')}"
for i, row in enumerate(result_df.itertuples())
])
# 2.8) 챗봇 좜λ ₯ + μΉ© λ²„νŠΌ λ Œλ”λ§
with chat_container:
log_and_render(f"{opening_line}<br><br>{message}",
sender="bot",
chat_container=chat_container,
key=f"intent_recommendation_{random.randint(1,999999)}")
recommend_names = result_df["μ—¬ν–‰μ§€"].tolist()
prev_choice = st.session_state.get(intent_key, None)
choice = render_chip_buttons(
recommend_names + ["λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„"],
key_prefix="intent_chip",
selected_value=prev_choice
)
# 2.9) 선택 μ—†κ±°λ‚˜ 쀑볡 선택이면 λŒ€κΈ°
if not choice or choice == prev_choice:
return
# 선택 κ²°κ³Ό 처리
if choice:
if choice == "λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„":
log_and_render("λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„",
sender="user",
chat_container=chat_container,
key=f"user_place_refresh_{random.randint(1,999999)}")
st.session_state.pop(sample_key, None)
st.rerun()
return
# 정상 μ„ νƒλœ 경우
st.session_state[intent_key] = choice
st.session_state[step_key] = "detail"
st.session_state.chat_log.append(("user", choice))
# μ‹€μ œλ‘œ μ„ νƒλœ μ—¬ν–‰μ§€λ§Œ prev에 기둝
match = result_df[result_df["μ—¬ν–‰μ§€"] == choice]
if not match.empty:
prev.add(choice)
st.session_state[prev_key] = prev
# μƒ˜ν”Œ 폐기
st.session_state.pop(sample_key, None)
st.rerun()
return
# ────────────────── 3) μΆ”μ²œ μ’…λ£Œ 단계
elif st.session_state[step_key] == "recommend_places_end":
# 3.1) λ©”μ‹œμ§€ 좜λ ₯
with chat_container:
log_and_render(
"⚠️ 더 이상 μƒˆλ‘œμš΄ μ—¬ν–‰μ§€κ°€ μ—†μ–΄μš”.<br>λ‹€μ‹œ μ§ˆλ¬Έν•˜μ‹œκ² μ–΄μš”?",
sender="bot",
chat_container=chat_container,
key="intent_empty"
)
# 3.2) μž¬μ‹œμž‘ μ—¬λΆ€ μΉ© λ²„νŠΌ 좜λ ₯
restart_done_key = "intent_restart_done"
chip_ph = st.empty()
if not st.session_state.get(restart_done_key, False):
with chip_ph:
choice = render_chip_buttons(
["예 πŸ”„", "μ•„λ‹ˆμ˜€ ❌"],
key_prefix="intent_restart")
else:
choice = None
# 3.3) 아직 아무것도 μ„ νƒν•˜μ§€ μ•Šμ€ 경우
if choice is None:
return
chip_ph.empty()
st.session_state[restart_done_key] = True
# 3.4) μ‚¬μš©μž 선택값 좜λ ₯
log_and_render(choice,
sender="user",
chat_container=chat_container
)
# 3.5) μ‚¬μš©μžκ°€ μž¬μΆ”μ²œμ„ μ›ν•˜λŠ” 경우
if choice == "예 πŸ”„":
for k in [sample_key, prev_key, intent_key, restart_done_key]:
st.session_state.pop(k, None)
chip_ph.empty()
# λ‹€μŒ μΆ”μ²œ λ‹¨κ³„λ‘œ μ΄ˆκΈ°ν™”
st.session_state["user_input_rendered"] = False
st.session_state["intent_step"] = "restart"
log_and_render(
"λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!<br>μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?",
sender="bot",
chat_container=chat_container,
key="intent_restart_intro"
)
return
# 3.6) μ‚¬μš©μžκ°€ μ’…λ£Œλ₯Ό μ„ νƒν•œ 경우
else:
log_and_render("μ—¬ν–‰ μΆ”μ²œμ„ μ’…λ£Œν• κ²Œμš”. ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️",
sender="bot",
chat_container=chat_container,
key="intent_exit")
st.stop()
return
# ────────────────── 4) μ—¬ν–‰μ§€ 상세 단계
if st.session_state[step_key] == "detail":
chosen = st.session_state[intent_key]
# city 이름 λ½‘μ•„μ„œ μ„Έμ…˜μ— μ €μž₯
row = travel_df[travel_df["μ—¬ν–‰μ§€"] == chosen].iloc[0]
st.session_state["selected_city"] = row["μ—¬ν–‰λ„μ‹œ"]
st.session_state["selected_place"] = chosen
log_and_render(chosen,
sender="user",
chat_container=chat_container,
key=f"user_place_{chosen}")
handle_selected_place(
chosen,
travel_df,
external_score_df,
festival_df,
weather_df,
chat_container=chat_container
)
st.session_state[step_key] = "companion"
st.rerun()
return
# ────────────────── 5) 동행·연령 λ°›κΈ° 단계
elif st.session_state[step_key] == "companion":
with chat_container:
# 5.1) μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯
log_and_render(
"ν•¨κ»˜ κ°€λŠ” λΆ„μ΄λ‚˜ μ—°λ ΉλŒ€λ₯Ό μ•Œλ €μ£Όμ‹œλ©΄ 더 λ”± λ§žλŠ” μƒν’ˆμ„ κ³¨λΌλ“œλ¦΄κ²Œμš”!<br>"
"1️⃣ 동행 μ—¬λΆ€ (혼자 / 친ꡬ / μ»€ν”Œ / κ°€μ‘± / 단체)<br>"
"2️⃣ μ—°λ ΉλŒ€ (20λŒ€ / 30λŒ€ / 40λŒ€ / 50λŒ€ / 60λŒ€ 이상)",
sender="bot",
chat_container=chat_container,
key="ask_companion_age"
)
# 5.1.1) 동행 μ²΄ν¬λ°•μŠ€
st.markdown(
'<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">πŸ‘« 동행 선택</div>',
unsafe_allow_html=True
)
c_cols = st.columns(5)
comp_flags = {
"혼자": c_cols[0].checkbox("혼자"),
"친ꡬ": c_cols[1].checkbox("친ꡬ"),
"μ»€ν”Œ": c_cols[2].checkbox("μ»€ν”Œ"),
"κ°€μ‘±": c_cols[3].checkbox("κ°€μ‘±"),
"단체": c_cols[4].checkbox("단체"),
}
companions = [k for k, v in comp_flags.items() if v]
# 5.1.2) μ—°λ Ή μ²΄ν¬λ°•μŠ€
st.markdown(
'<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">πŸŽ‚ μ—°λ Ή 선택</div>',
unsafe_allow_html=True
)
a_cols = st.columns(5)
age_flags = {
"20λŒ€": a_cols[0].checkbox("20λŒ€"),
"30λŒ€": a_cols[1].checkbox("30λŒ€"),
"40λŒ€": a_cols[2].checkbox("40λŒ€"),
"50λŒ€": a_cols[3].checkbox("50λŒ€"),
"60λŒ€ 이상": a_cols[4].checkbox("60λŒ€ 이상"),
}
age_group = [k for k, v in age_flags.items() if v]
# 5.1.3) 확인 λ²„νŠΌ
confirm = st.button(
"μΆ”μ²œ λ°›κΈ°",
key="btn_confirm_companion",
disabled=not (companions or age_group),
)
# 5.2) λ©”μ‹œμ§€ 좜λ ₯
if confirm:
# μ‚¬μš©μž 버블 좜λ ₯
user_msg = " / ".join(companions + age_group)
log_and_render(
user_msg if user_msg else "선택 μ•ˆ 함",
sender="user",
chat_container=chat_container,
key=f"user_comp_age_{random.randint(1,999999)}"
)
# μ„Έμ…˜ μ €μž₯
st.session_state["companions"] = companions or None
st.session_state["age_group"] = age_group or None
# λ‹€μŒ μŠ€ν…
st.session_state[step_key] = "package"
st.rerun()
return
# ────────────────── 6) 동행·연령 필터링· νŒ¨ν‚€μ§€ 좜λ ₯ 단계
elif st.session_state[step_key] == "package":
# νŒ¨ν‚€μ§€ 버블을 이미 λ§Œλ“€μ—ˆμœΌλ©΄ κ±΄λ„ˆλœ€
if st.session_state.get("package_rendered", False):
st.session_state[step_key] = "package_end"
return
companions = st.session_state.get("companions")
age_group = st.session_state.get("age_group")
city = st.session_state.get("selected_city")
place = st.session_state.get("selected_place")
filtered = filter_packages_by_companion_age(
package_df, companions, age_group, city=city, top_n=2
)
if filtered.empty:
log_and_render(
"⚠️ μ•„μ‰½μ§€λ§Œ μ§€κΈˆ 쑰건에 λ§žλŠ” νŒ¨ν‚€μ§€κ°€ μ—†μ–΄μš”.<br>"
"λ‹€λ₯Έ 쑰건으둜 λ‹€μ‹œ μ°Ύμ•„λ³ΌκΉŒμš”?",
sender="bot", chat_container=chat_container,
key="no_package"
)
st.session_state[step_key] = "companion" # λ‹€μ‹œ μž…λ ₯ λ‹¨κ³„λ‘œ
st.rerun()
return
combo_msg = make_companion_age_message(companions, age_group)
header = f"{combo_msg}"
# νŒ¨ν‚€μ§€ μΉ΄λ“œ 좜λ ₯
used_phrases = set()
theme_row = travel_df[travel_df["μ—¬ν–‰μ§€"] == place]
raw_theme = theme_row["ν†΅ν•©ν…Œλ§ˆλͺ…"].iloc[0] if not theme_row.empty else None
selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0]
title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆ”μ²œ"])
sampled_titles = random.sample(title_candidates,
k=min(2, len(title_candidates)))
# λ©”μ‹œμ§€ 생성
pkg_msgs = [header]
for i, (_, row) in enumerate(filtered.iterrows(), 1):
desc, used_phrases = make_top2_description_custom(
row.to_dict(), used_phrases
)
tags = format_summary_tags_custom(row["μš”μ•½μ •λ³΄"])
title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles)
else random.choice(title_candidates))
title = f"{city} {title_phrase} νŒ¨ν‚€μ§€"
url = row.URL
pkg_msgs.append(
f"{i}. <strong>{title}</strong><br>"
f"πŸ…Ό {desc}<br>{tags}<br>"
f'<a href="{url}" target="_blank" rel="noopener noreferrer" '
'style="text-decoration:none;font-weight:600;color:#009c75;">'
'πŸ’š λ°”λ‘œκ°€κΈ°&nbsp;β†—</a>'
)
# λ©”μ‹œμ§€ 좜λ ₯
log_and_render(
"<br><br>".join(pkg_msgs),
sender="bot",
chat_container=chat_container,
key=f"pkg_bundle_{random.randint(1,999999)}"
)
# μ„Έμ…˜ 정리
st.session_state["package_rendered"] = True
st.session_state[step_key] = "package_end"
return
# ────────────────── 7) μ’…λ£Œ 단계
elif st.session_state[step_key] == "package_end":
log_and_render("ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️",
sender="bot", chat_container=chat_container,
key="goodbye")
# ───────────────────────────────────── emotion λͺ¨λ“œ
def emotion_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
country_filter, city_filter, chat_container, candidate_themes,
intent, emotion_groups, top_emotions, log_and_render):
"""emotion(감정을 μž…λ ₯ν–ˆμ„ 경우) λͺ¨λ“œ μ „μš© UI & 둜직"""
# ────────────────── μ„Έμ…˜ ν‚€ μ •μ˜
sample_key = "emotion_sample_df"
step_key = "emotion_step"
theme_key = "selected_theme"
emotion_key = "emotion_chip_selected"
prev_key = "emotion_prev_places"
# ────────────────── 0) μ΄ˆκΈ°ν™”
if step_key not in st.session_state:
st.session_state[step_key] = "theme_selection"
st.session_state[prev_key] = set()
st.session_state.pop(sample_key, None)
# ────────────────── 1) restart μƒνƒœλ©΄ 인트둜만 좜λ ₯ν•˜κ³  μ’…λ£Œ
if st.session_state[step_key] == "restart":
log_and_render(
"λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!<br>μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?",
sender="bot",
chat_container=chat_container,
key="region_restart_intro"
)
return
# ────────────────── 2) ν…Œλ§ˆ μΆ”μ²œ 단계
if st.session_state[step_key] == "theme_selection":
# μΆ”μ²œ ν…Œλ§ˆ 1개일 경우
if len(candidate_themes) == 1:
selected_theme = candidate_themes[0]
st.session_state[theme_key] = selected_theme
log_and_render(f"μΆ”μ²œ κ°€λŠ₯ν•œ ν…Œλ§ˆκ°€ 1κ°œμ΄λ―€λ‘œ '{selected_theme}'을 μ„ νƒν• κ²Œμš”.", sender="bot", chat_container=chat_container)
st.session_state[step_key] = "recommend_places"
st.rerun()
# ν…Œλ§ˆκ°€ μ—¬λŸ¬ 개일 경우
else:
# 인트둜 λ©”μ‹œμ§€
intro_msg = generate_intro_message(intent=intent, emotion_groups=emotion_groups, emotion_scores=top_emotions)
log_and_render(f"{intro_msg}<br>μ•„λž˜ 쀑 마음이 λŒλ¦¬λŠ” μ—¬ν–‰ μŠ€νƒ€μΌμ„ κ³¨λΌμ£Όμ„Έμš” πŸ’«", sender="bot", chat_container=chat_container)
# 후보 ν…Œλ§ˆ μ€€λΉ„
dfs = [recommend_places_by_theme(t, country_filter, city_filter) for t in candidate_themes]
dfs = [df for df in dfs if not df.empty]
all_theme_df = pd.concat(dfs) if dfs else pd.DataFrame(columns=travel_df.columns)
all_theme_df = all_theme_df.drop_duplicates(subset=["μ—¬ν–‰μ§€"])
all_theme_names = all_theme_df["ν†΅ν•©ν…Œλ§ˆλͺ…"].dropna().tolist()
available_themes = []
for t in candidate_themes:
if t in all_theme_names and t not in available_themes:
available_themes.append(t)
for t in all_theme_names:
if t not in available_themes:
available_themes.append(t)
available_themes = available_themes[:3] # μ΅œλŒ€ 3개
# μΉ© UI 좜λ ₯
with chat_container:
chip = render_chip_buttons(
[theme_ui_map.get(t, (t, ""))[0] for t in available_themes],
key_prefix="theme_chip"
)
# 선택이 μ™„λ£Œλ˜λ©΄ λ‹€μŒ λ‹¨κ³„λ‘œ 이동
if chip:
selected_theme = ui_to_theme_map.get(chip, chip)
st.session_state[theme_key] = selected_theme
st.session_state[step_key] = "recommend_places"
st.session_state["emotion_all_theme_df"] = all_theme_df
log_and_render(f"{chip}", sender="user",
chat_container=chat_container)
st.rerun()
# ────────────────── 3) μ—¬ν–‰μ§€ μΆ”μ²œ 단계
if st.session_state[step_key] == "recommend_places":
all_theme_df = st.session_state.get("emotion_all_theme_df", pd.DataFrame())
selected_theme = st.session_state.get(theme_key, "")
prev_key = "emotion_prev_places"
prev = st.session_state.setdefault(prev_key, set())
# μ˜ˆμ™Έ 처리: 데이터 없을 경우
if all_theme_df.empty or not selected_theme:
log_and_render("μΆ”μ²œ 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 데 λ¬Έμ œκ°€ λ°œμƒν–ˆμ–΄μš”. <br>λ‹€μ‹œ μž…λ ₯ν•΄ μ£Όμ„Έμš”.", sender="bot", chat_container=chat_container)
return
if sample_key not in st.session_state:
theme_df = all_theme_df[all_theme_df["ν†΅ν•©ν…Œλ§ˆλͺ…"] == selected_theme]
theme_df = theme_df.drop_duplicates(subset=["μ—¬ν–‰λ„μ‹œ"])
theme_df = theme_df.drop_duplicates(subset=["μ—¬ν–‰μ§€"])
remaining = theme_df[~theme_df["μ—¬ν–‰μ§€"].isin(prev)]
if remaining.empty:
st.session_state[step_key] = "recommend_places_end"
st.rerun()
return
result_df = apply_weighted_score_filter(remaining)
st.session_state[sample_key] = result_df
else:
result_df = st.session_state[sample_key]
# μΆ”μ²œ 수 λΆ€μ‘±ν•  경우 Fallback 보완
if len(result_df) < 3:
fallback = travel_df[
(travel_df["ν†΅ν•©ν…Œλ§ˆλͺ…"] == selected_theme) &
(~travel_df["μ—¬ν–‰μ§€"].isin(result_df["μ—¬ν–‰μ§€"]))
].drop_duplicates(subset=["μ—¬ν–‰μ§€"])
if not fallback.empty:
fill_count = min(3 - len(result_df), len(fallback))
fill = fallback.sample(n=fill_count, random_state=random.randint(1, 9999))
result_df = pd.concat([result_df, fill], ignore_index=True)
# μƒ˜ν”Œ μ €μž₯
st.session_state[sample_key] = result_df
# 2.1)첫 λ¬Έμž₯ 좜λ ₯
ui_name = theme_ui_map.get(selected_theme, (selected_theme,))[0]
opening_line_template = theme_opening_lines.get(ui_name)
opening_line = opening_line_template.format(len(result_df)) if opening_line_template else ""
message = (
"<br>".join([
f"{i+1}. <strong>{row.μ—¬ν–‰μ§€}</strong> "
f"({row.μ—¬ν–‰λ‚˜λΌ}, {row.μ—¬ν–‰λ„μ‹œ}) "
f"{getattr(row, 'ν•œμ€„μ„€λͺ…', 'μ„€λͺ…이 μ—†μŠ΅λ‹ˆλ‹€')}"
for i, row in enumerate(result_df.itertuples())
])
)
if opening_line_template:
message_combined = f"{opening_line}<br><br>{message}"
with chat_container:
log_and_render(message_combined,
sender="bot",
chat_container=chat_container,
key=f"emotion_recommendation_{random.randint(1,999999)}"
)
# 2.2) μΉ© λ²„νŠΌμœΌλ‘œ μΆ”μ²œμ§€ 쀑 선택받기
recommend_names = result_df["μ—¬ν–‰μ§€"].tolist()
prev_choice = st.session_state.get(emotion_key, None)
choice = render_chip_buttons(
recommend_names + ["λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„"],
key_prefix="emotion_chip",
selected_value=prev_choice
)
# 2.3) 선택 κ²°κ³Ό 처리
if not choice or choice == prev_choice:
return
if choice == "λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„":
log_and_render("λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„",
sender="user",
chat_container=chat_container,
key=f"user_place_refresh_{random.randint(1,999999)}")
st.session_state.pop(sample_key, None)
st.rerun()
return
# μ‹€μ œ μ„ νƒν•œ μ—¬ν–‰μ§€ 처리
st.session_state[emotion_key] = choice
st.session_state[step_key] = "detail"
st.session_state.chat_log.append(("user", choice))
# μ„ νƒν•œ μ—¬ν–‰μ§€λ₯Ό prev 기둝에 μΆ”κ°€
match = result_df[result_df["μ—¬ν–‰μ§€"] == choice]
if not match.empty:
prev.add(choice)
st.session_state[prev_key] = prev
# μƒ˜ν”Œ 폐기
st.session_state.pop(sample_key, None)
st.rerun()
return
# ────────────────── 3) μΆ”μ²œ μ’…λ£Œ 단계: 더 이상 μΆ”μ²œν•  μ—¬ν–‰μ§€κ°€ 없을 λ•Œ
elif st.session_state[step_key] == "recommend_place_end":
with chat_container:
# 3.1) λ©”μ‹œμ§€ 좜λ ₯
log_and_render(
"⚠️ 더 이상 μƒˆλ‘œμš΄ μ—¬ν–‰μ§€κ°€ μ—†μ–΄μš”.<br>λ‹€μ‹œ μ§ˆλ¬Έν•˜μ‹œκ² μ–΄μš”?",
sender="bot",
chat_container=chat_container,
key="emotion_empty"
)
# 3.2) μž¬μ‹œμž‘ μ—¬λΆ€ μΉ© λ²„νŠΌ 좜λ ₯
restart_done_key = "emotion_restart_done"
chip_ph = st.empty()
if not st.session_state.get(restart_done_key, False):
with chip_ph:
choice = render_chip_buttons(
["예 πŸ”„", "μ•„λ‹ˆμ˜€ ❌"],
key_prefix="emotion_restart"
)
else:
choice = None
# 3.3) 아직 아무것도 μ„ νƒν•˜μ§€ μ•Šμ€ 경우
if choice is None:
return
chip_ph.empty()
st.session_state[restart_done_key] = True
# 3.4) μ‚¬μš©μž 선택값 좜λ ₯
log_and_render(
choice,
sender="user",
chat_container=chat_container,
key=f"user_restart_choice_{choice}"
)
# 3.5) μ‚¬μš©μžκ°€ μž¬μΆ”μ²œμ„ μ›ν•˜λŠ” 경우
if choice == "예 πŸ”„":
# μ—¬ν–‰ μΆ”μ²œ μƒνƒœ μ΄ˆκΈ°ν™”
for k in [emotion_key, prev_key, sample_key, restart_done_key]:
st.session_state.pop(k, None)
chip_ph.empty()
# λ‹€μŒ μΆ”μ²œ λ‹¨κ³„λ‘œ μ΄ˆκΈ°ν™”
st.session_state["user_input_rendered"] = False
st.session_state["emotion_step"] = "restart"
log_and_render(
"λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!<br>μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?",
sender="bot",
chat_container=chat_container,
key="emotion_restart_intro"
)
return
# 3.6) μ‚¬μš©μžκ°€ μ’…λ£Œλ₯Ό μ„ νƒν•œ 경우
else:
log_and_render("μ—¬ν–‰ μΆ”μ²œμ„ μ’…λ£Œν• κ²Œμš”. ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️",
sender="bot",
chat_container=chat_container,
key="emotion_exit")
st.stop()
return
# ────────────────── 4) μ—¬ν–‰μ§€ 상세 단계
if st.session_state[step_key] == "detail":
chosen = st.session_state[emotion_key]
# city 이름 λ½‘μ•„μ„œ μ„Έμ…˜μ— μ €μž₯
row = travel_df[travel_df["μ—¬ν–‰μ§€"] == chosen].iloc[0]
st.session_state["selected_city"] = row["μ—¬ν–‰λ„μ‹œ"]
st.session_state["selected_place"] = chosen
log_and_render(chosen,
sender="user",
chat_container=chat_container,
key=f"user_place_{chosen}")
handle_selected_place(
chosen,
travel_df,
external_score_df,
festival_df,
weather_df,
chat_container=chat_container
)
st.session_state[step_key] = "companion"
st.rerun()
return
# ────────────────── 5) 동행·연령 λ°›κΈ° 단계
elif st.session_state[step_key] == "companion":
with chat_container:
# 5.1) μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯
log_and_render(
"ν•¨κ»˜ κ°€λŠ” λΆ„μ΄λ‚˜ μ—°λ ΉλŒ€λ₯Ό μ•Œλ €μ£Όμ‹œλ©΄ 더 λ”± λ§žλŠ” μƒν’ˆμ„ κ³¨λΌλ“œλ¦΄κ²Œμš”!<br>"
"1️⃣ 동행 μ—¬λΆ€ (혼자 / 친ꡬ / μ»€ν”Œ / κ°€μ‘± / 단체)<br>"
"2️⃣ μ—°λ ΉλŒ€ (20λŒ€ / 30λŒ€ / 40λŒ€ / 50λŒ€ / 60λŒ€ 이상)",
sender="bot",
chat_container=chat_container,
key="ask_companion_age"
)
# 5.1.1) 동행 μ²΄ν¬λ°•μŠ€
st.markdown(
'<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">πŸ‘« 동행 선택</div>',
unsafe_allow_html=True
)
c_cols = st.columns(5)
comp_flags = {
"혼자": c_cols[0].checkbox("혼자"),
"친ꡬ": c_cols[1].checkbox("친ꡬ"),
"μ»€ν”Œ": c_cols[2].checkbox("μ»€ν”Œ"),
"κ°€μ‘±": c_cols[3].checkbox("κ°€μ‘±"),
"단체": c_cols[4].checkbox("단체"),
}
companions = [k for k, v in comp_flags.items() if v]
# 5.1.2) μ—°λ Ή μ²΄ν¬λ°•μŠ€
st.markdown(
'<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">πŸŽ‚ μ—°λ Ή 선택</div>',
unsafe_allow_html=True
)
a_cols = st.columns(5)
age_flags = {
"20λŒ€": a_cols[0].checkbox("20λŒ€"),
"30λŒ€": a_cols[1].checkbox("30λŒ€"),
"40λŒ€": a_cols[2].checkbox("40λŒ€"),
"50λŒ€": a_cols[3].checkbox("50λŒ€"),
"60λŒ€ 이상": a_cols[4].checkbox("60λŒ€ 이상"),
}
age_group = [k for k, v in age_flags.items() if v]
# 5.1.3) 확인 λ²„νŠΌ
confirm = st.button(
"μΆ”μ²œ λ°›κΈ°",
key="btn_confirm_companion",
disabled=not (companions or age_group),
)
# 5.2) λ©”μ‹œμ§€ 좜λ ₯
if confirm:
# μ‚¬μš©μž 버블 좜λ ₯
user_msg = " / ".join(companions + age_group)
log_and_render(
user_msg if user_msg else "선택 μ•ˆ 함",
sender="user",
chat_container=chat_container,
key=f"user_comp_age_{random.randint(1,999999)}"
)
# μ„Έμ…˜ μ €μž₯
st.session_state["companions"] = companions or None
st.session_state["age_group"] = age_group or None
# λ‹€μŒ μŠ€ν…
st.session_state[step_key] = "package"
st.rerun()
return
# ────────────────── 6) 동행·연령 필터링· νŒ¨ν‚€μ§€ 좜λ ₯ 단계
elif st.session_state[step_key] == "package":
# νŒ¨ν‚€μ§€ 버블을 이미 λ§Œλ“€μ—ˆμœΌλ©΄ κ±΄λ„ˆλœ€
if st.session_state.get("package_rendered", False):
st.session_state[step_key] = "package_end"
return
companions = st.session_state.get("companions")
age_group = st.session_state.get("age_group")
city = st.session_state.get("selected_city")
place = st.session_state.get("selected_place")
filtered = filter_packages_by_companion_age(
package_df, companions, age_group, city=city, top_n=2
)
if filtered.empty:
log_and_render(
"⚠️ μ•„μ‰½μ§€λ§Œ μ§€κΈˆ 쑰건에 λ§žλŠ” νŒ¨ν‚€μ§€κ°€ μ—†μ–΄μš”.<br>"
"λ‹€λ₯Έ 쑰건으둜 λ‹€μ‹œ μ°Ύμ•„λ³ΌκΉŒμš”?",
sender="bot", chat_container=chat_container,
key="no_package"
)
st.session_state[step_key] = "companion"
st.rerun()
return
combo_msg = make_companion_age_message(companions, age_group)
header = f"{combo_msg}"
# νŒ¨ν‚€μ§€ μΉ΄λ“œ 좜λ ₯
used_phrases = set()
theme_row = travel_df[travel_df["μ—¬ν–‰μ§€"] == place]
raw_theme = theme_row["ν†΅ν•©ν…Œλ§ˆλͺ…"].iloc[0] if not theme_row.empty else None
selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0]
title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆ”μ²œ"])
sampled_titles = random.sample(title_candidates,
k=min(2, len(title_candidates)))
# λ©”μ‹œμ§€ 생성
pkg_msgs = [header]
for i, (_, row) in enumerate(filtered.iterrows(), 1):
desc, used_phrases = make_top2_description_custom(
row.to_dict(), used_phrases
)
tags = format_summary_tags_custom(row["μš”μ•½μ •λ³΄"])
title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles)
else random.choice(title_candidates))
title = f"{city} {title_phrase} νŒ¨ν‚€μ§€"
url = row.URL
pkg_msgs.append(
f"{i}. <strong>{title}</strong><br>"
f"πŸ…Ό {desc}<br>{tags}<br>"
f'<a href="{url}" target="_blank" rel="noopener noreferrer" '
'style="text-decoration:none;font-weight:600;color:#009c75;">'
'πŸ’š λ°”λ‘œκ°€κΈ°&nbsp;β†—</a>'
)
# λ©”μ‹œμ§€ 좜λ ₯
log_and_render(
"<br><br>".join(pkg_msgs),
sender="bot",
chat_container=chat_container,
key=f"pkg_bundle_{random.randint(1,999999)}"
)
# μ„Έμ…˜ 정리
st.session_state["package_rendered"] = True
st.session_state[step_key] = "package_end"
return
# ────────────────── 7) μ’…λ£Œ 단계
elif st.session_state[step_key] == "package_end":
log_and_render("ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️",
sender="bot", chat_container=chat_container,
key="goodbye")
# ───────────────────────────────────── unknown λͺ¨λ“œ
def unknown_ui(country, city, chat_container, log_and_render):
"""unknown λͺ¨λ“œ(아직 DB에 μ—†λŠ” λ‚˜λΌΒ·λ„μ‹œμΌ λ•Œ μ•ˆλ‚΄) μ „μš© UI & 둜직"""
# μ•ˆλ‚΄ λ©”μ‹œμ§€
if city:
msg = (f"πŸ” μ£„μ†‘ν•΄μš”. ν•΄λ‹Ή <strong>{city}</strong>의 μ—¬ν–‰μ§€λŠ” "
"아직 μ€€λΉ„ μ€‘μ΄μ—μš”.<br> λΉ λ₯Έ μ‹œμΌ μ•ˆμ— μ—…λ°μ΄νŠΈν• κ²Œμš”!")
elif country:
msg = (f"πŸ” μ£„μ†‘ν•΄μš”. ν•΄λ‹Ή <strong>{country}</strong>의 μ—¬ν–‰μ§€λŠ” "
"아직 μ€€λΉ„ μ€‘μ΄μ—μš”.<br> λΉ λ₯Έ μ‹œμΌ μ•ˆμ— μ—…λ°μ΄νŠΈν• κ²Œμš”!")
else:
msg = "πŸ” μ£„μ†‘ν•΄μš”. ν•΄λ‹Ή μ—¬ν–‰μ§€λŠ” 아직 μ€€λΉ„ μ€‘μ΄μ—μš”."
with chat_container:
log_and_render(
f"{msg}",
sender="bot",
chat_container=chat_container,
key="unknown_dest"
)
# ───────────────────────────────────── 챗봇 호좜
def main():
init_session()
chat_container = st.container()
if "chat_log" in st.session_state and st.session_state.chat_log:
replay_log(chat_container)
# ───── greeting λ©”μ‹œμ§€ 좜λ ₯
if not st.session_state.get("greeting_rendered", False):
greeting_message = (
"μ•ˆλ…•ν•˜μ„Έμš”. <strong>λͺ¨μ•„(MoAi)</strong>μž…λ‹ˆλ‹€.πŸ€–<br><br>"
"μš”μ¦˜ μ–΄λ–€ 여행이 λ– μ˜€λ₯΄μ„Έμš”?<br>""λͺ¨μ•„κ°€ λ”± λ§žλŠ” μ—¬ν–‰μ§€λ₯Ό μ°Ύμ•„λ“œλ¦΄κ²Œμš”."
)
log_and_render(
greeting_message,
sender="bot",
chat_container=chat_container,
key="greeting"
)
st.session_state["greeting_rendered"] = True
# ───── μ‚¬μš©μž μž…λ ₯ & μΆ”μ²œ μ‹œμž‘
# 1) μ‚¬μš©μž μž…λ ₯
user_input = st.text_input(
"μž…λ ₯μ°½", # λΉ„μ–΄μžˆμ§€ μ•Šμ€ 라벨(μ ‘κ·Όμ„± 확보)
placeholder="ex)'μš”μ¦˜ 힐링이 ν•„μš”ν•΄μš”', 'κ°€μ‘± μ—¬ν–‰ μ–΄λ””κ°€ μ’‹μ„κΉŒμš”?'",
key="user_input",
label_visibility="collapsed", # 화면에선 μˆ¨κΉ€
)
user_input_key = "last_user_input"
select_keys = ["intent_chip_selected", "region_chip_selected", "emotion_chip_selected", "theme_chip_selected"]
# 1-1) β€œμ§„μ§œ μƒˆλ‘œ μž…λ ₯” 감지
prev = st.session_state.get(user_input_key, "")
if user_input and user_input != prev:
for k in select_keys:
st.session_state.pop(k, None)
st.session_state[user_input_key] = user_input
st.session_state["user_input_rendered"] = False
# step μ΄ˆκΈ°ν™”
st.session_state["region_step"] = "recommend"
st.rerun()
# 1-2) μ‚¬μš©μž λ©”μ‹œμ§€ ν•œ 번만 λ Œλ”λ§
if user_input and not st.session_state.get("user_input_rendered", False):
log_and_render(
user_input,
sender="user",
chat_container = chat_container,
key=f"user_input_{user_input}"
)
st.session_state["user_input_rendered"] = True
if user_input:
# 2) mode 탐지
_, _, mode = detect_location_filter(user_input)
top_emotions, emotion_groups = analyze_emotion(user_input)
intent, intent_score = detect_intent(user_input)
country_filter, city_filter, _ = detect_location_filter(user_input)
candidate_themes = extract_themes(
emotion_groups,
intent,
force_mode=(intent_score >= 0.70)
)
if intent_score >= 0.70:
mode = "intent"
# 🌟 DEBUG ────────────────────────────────
# with st.expander("πŸ” DEBUG - λͺ¨λ“œ νŒμ •", expanded=True):
# st.markdown(f"""
# **μž…λ ₯ λ¬Έμž₯**: `{user_input}`
# **detect_location_filter** πŸ‘‰
# β€’ country&nbsp;β†’ `{country_filter}`
# β€’ city&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;β†’ `{city_filter}`
# β€’ mode&nbsp;&nbsp;β†’ `{mode}`
# **intent_score**: `{intent_score:.3f}`
# **top_emotions**: `{top_emotions}`
# """)
# ────────────────────────────────────────
# 3) λͺ¨λ“œλ³„ λΆ„κΈ°
if mode == "region":
region_ui(
travel_df,
external_score_df,
festival_df,
weather_df,
package_df,
country_filter,
city_filter,
chat_container,
log_and_render
)
return
elif mode == "intent":
intent_ui(
travel_df,
external_score_df,
festival_df,
weather_df,
package_df,
country_filter,
city_filter,
chat_container,
intent,
log_and_render)
return
elif mode == "unknown":
unknown_ui(
country_filter,
city_filter,
chat_container,
log_and_render)
return
else:
emotion_ui(
travel_df,
external_score_df,
festival_df,
weather_df,
package_df,
country_filter,
city_filter,
chat_container,
candidate_themes,
intent,
emotion_groups,
top_emotions,
log_and_render
)
if __name__ == "__main__":
main()
#cmd μž…λ ₯-> cd "파일 μœ„μΉ˜ 경둜 볡뢙"
#ex(C:\Users\gayoung\Desktop\multi\0514 - project\06 - streamlit ν…ŒμŠ€νŠΈ\test)
#cmd μž…λ ₯ -> streamlit run app.py