Area51base/cc-chatbot
0
1"""2Hugging Face Spaces entry point — app.py3Runs as a native Streamlit app (sdk: streamlit).4Ingestion runs once at startup via st.cache_resource.5All queries go directly to the Python backend (no HTTP server needed).6"""7import os8import sys9import uuid10import json11import re12import html13from typing import Optional14 15import streamlit as st16import httpx17 18# ── Path setup ────────────────────────────────────────────────────────────────19sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))20 21os.environ.setdefault("CHROMA_PERSIST_DIR", "/tmp/chroma_db")22os.environ.setdefault("APP_ENV", "production")23os.environ.setdefault("CORS_ORIGINS", '["*"]')24 25API_BASE = os.getenv("CC_API_URL", "http://localhost:8000")26 27# ── One-time ingestion ────────────────────────────────────────────────────────28@st.cache_resource(show_spinner="⏳ Loading credit card data (first launch ~2 min)…")29def _bootstrap():30 """Run ingestion (if needed) then return the ask() function. Cached — runs only once."""31 chroma_dir = os.getenv("CHROMA_PERSIST_DIR", "/tmp/chroma_db")32 db_file = os.path.join(chroma_dir, "chroma.sqlite3")33 if not os.path.exists(db_file):34 import subprocess35 subprocess.run([sys.executable, "scripts/ingest.py"], check=False)36 # Verify DB actually has data — re-run if empty37 try:38 import chromadb39 from chromadb.config import Settings as ChromaSettings40 client = chromadb.PersistentClient(41 path=chroma_dir,42 settings=ChromaSettings(anonymized_telemetry=False),43 )44 col = client.get_or_create_collection("cc_cards")45 if col.count() == 0:46 import subprocess47 subprocess.run([sys.executable, "scripts/ingest.py"], check=False)48 except Exception:49 pass50 from cc_chatbot.services.orchestrator import ask51 return ask52 53_ask = _bootstrap()54 55# ── Page config ───────────────────────────────────────────────────────────────56st.set_page_config(57 page_title="CC AI Assistant",58 page_icon="💳",59 layout="centered",60 initial_sidebar_state="collapsed",61)62 63# ── Custom CSS ────────────────────────────────────────────────────────────────64st.markdown("""65<style>66.user-bubble {67 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);68 color: white;69 padding: 12px 16px;70 border-radius: 18px 18px 4px 18px;71 margin: 8px 0 8px 20%;72 max-width: 80%;73 float: right;74 clear: both;75 font-size: 0.95rem;76 box-shadow: 0 2px 8px rgba(0,0,0,0.15);77}78.assistant-bubble {79 background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf0 100%);80 color: #1a1a2e;81 padding: 14px 18px;82 border-radius: 18px 18px 18px 4px;83 margin: 8px 20% 8px 0;84 max-width: 80%;85 float: left;86 clear: both;87 font-size: 0.95rem;88 box-shadow: 0 2px 8px rgba(0,0,0,0.1);89 border-left: 3px solid #667eea;90}91.meta-badge {92 display: inline-block;93 background: #e8f4fd;94 color: #0366d6;95 padding: 2px 8px;96 border-radius: 10px;97 font-size: 0.75rem;98 margin: 2px;99}100.header-gradient {101 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);102 color: white;103 padding: 20px;104 border-radius: 12px;105 text-align: center;106 margin-bottom: 20px;107}108.clearfix { clear: both; }109[data-testid="stSidebar"] { display: none !important; }110[data-testid="collapsedControl"] { display: none !important; }111</style>112""", unsafe_allow_html=True)113 114# ── Session state ─────────────────────────────────────────────────────────────115if "session_id" not in st.session_state:116 st.session_state.session_id = str(uuid.uuid4())117if "messages" not in st.session_state:118 st.session_state.messages = []119if "suggestions" not in st.session_state:120 st.session_state.suggestions = []121 122# ── Header ────────────────────────────────────────────────────────────────────123col_h, col_btn = st.columns([5, 1])124with col_h:125 st.markdown("""126<div class="header-gradient">127 <h2 style="margin:0">💳 Credit Card AI Assistant</h2>128 <p style="margin:4px 0 0 0; opacity:0.9; font-size:0.9rem">129 Find the best cards · Compare rewards · Plan redemptions130 </p>131</div>132""", unsafe_allow_html=True)133with col_btn:134 st.markdown("<div style='padding-top:18px'></div>", unsafe_allow_html=True)135 if st.button("🔄 New", use_container_width=True):136 st.session_state.session_id = str(uuid.uuid4())137 st.session_state.messages = []138 st.session_state.suggestions = []139 st.rerun()140 141# ── Example questions ─────────────────────────────────────────────────────────142if not st.session_state.messages:143 st.markdown("**💡 Try asking:**")144 examples = [145 "Best card for Swiggy food orders?",146 "Which card for international travel?",147 "I have 20,000 HDFC Infinia points, want to go to Spain",148 "Compare HDFC Infinia vs Diners Club Black",149 "Best no-fee credit card with good rewards",150 "Which card gives unlimited lounge access?",151 "Best cashback card for Amazon shopping",152 "HDFC Regalia rewards and benefits",153 ]154 cols = st.columns(2)155 for i, eq in enumerate(examples):156 if cols[i % 2].button(eq, key=f"ex_{i}", use_container_width=True):157 st.session_state.pending_question = eq158 159 160# ── Helpers ───────────────────────────────────────────────────────────────────161def _format_answer(text: str) -> str:162 safe = html.escape(text)163 safe = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", safe)164 safe = re.sub(r"^(\d+)\.\s+", r"<br><strong>\1.</strong> ", safe, flags=re.MULTILINE)165 safe = re.sub(r"^✅\s+", r"<br>✅ ", safe, flags=re.MULTILINE)166 safe = re.sub(r"^[-•]\s+", r"<br>• ", safe, flags=re.MULTILINE)167 safe = re.sub(r"^(──.+)$", r"<br><strong>\1</strong>", safe, flags=re.MULTILINE)168 safe = re.sub(r"^(Verdict:)", r"<br><strong>Verdict:</strong>", safe, flags=re.MULTILINE)169 safe = re.sub(r"^(Pro tip:)", r"<br><strong>💡 Pro tip:</strong>", safe, flags=re.MULTILINE)170 safe = safe.replace("\n", "<br>")171 return safe172 173 174def render_message(role: str, content: str, meta: Optional[dict] = None):175 if role == "user":176 st.markdown(177 f'<div class="user-bubble">{content}</div><div class="clearfix"></div>',178 unsafe_allow_html=True,179 )180 else:181 badges = ""182 if meta:183 if meta.get("intent"):184 badges += f'<span class="meta-badge">🎯 {meta["intent"]}</span>'185 if meta.get("cache_hit"):186 badges += f'<span class="meta-badge">⚡ Cached ({meta.get("similarity", 0):.2f})</span>'187 st.markdown(188 f'<div class="assistant-bubble">{badges}<br>{_format_answer(content)}</div><div class="clearfix"></div>',189 unsafe_allow_html=True,190 )191 192 193# ── Render chat history ───────────────────────────────────────────────────────194for msg in st.session_state.messages:195 render_message(msg["role"], msg["content"], msg.get("meta"))196 197# ── Suggestions ───────────────────────────────────────────────────────────────198if st.session_state.suggestions:199 st.markdown("**💬 You might also ask:**")200 cols = st.columns(len(st.session_state.suggestions[:3]))201 for i, s in enumerate(st.session_state.suggestions[:3]):202 if cols[i].button(s, key=f"sug_{i}_{s[:20]}"):203 st.session_state.pending_question = s204 205if "pending_question" in st.session_state:206 st.session_state.pending_send = st.session_state.pop("pending_question")207 208# ── Chat input ────────────────────────────────────────────────────────────────209user_input = st.chat_input("Ask about credit cards — rewards, travel, dining, comparisons…")210question = user_input or st.session_state.pop("pending_send", None)211 212 213def _send(q: str):214 st.session_state.messages.append({"role": "user", "content": q})215 st.session_state.suggestions = []216 with st.spinner("Thinking…"):217 try:218 # Try HTTP API first (if local backend is running)219 resp = httpx.post(220 f"{API_BASE}/api/chat",221 json={"question": q, "session_id": st.session_state.session_id},222 timeout=60,223 )224 resp.raise_for_status()225 data = resp.json()226 except Exception:227 # Direct Python call (works on HF Spaces without a separate server)228 try:229 data = _ask(question=q, session_id=st.session_state.session_id)230 except Exception as e:231 err = str(e)232 if "list index out of range" in err or "empty" in err.lower():233 answer = (234 "⏳ **Still loading data** — the credit card database is being built "235 "in the background (takes ~2 minutes on first launch). "236 "Please try again in a moment!"237 )238 else:239 answer = f"❌ Error: {err}"240 st.session_state.messages.append({"role": "assistant", "content": answer, "meta": {}})241 return242 243 answer = data.get("answer", "Sorry, I couldn't generate an answer.")244 meta = {245 "intent": data.get("intent"),246 "cache_hit": data.get("cache_hit", False),247 "similarity": data.get("similarity", 0),248 }249 st.session_state.session_id = data.get("session_id", st.session_state.session_id)250 st.session_state.suggestions = data.get("suggestions", [])251 st.session_state.messages.append({"role": "assistant", "content": answer, "meta": meta})252 253 254if question:255 _send(question)256 st.rerun()257 258# ── Footer ────────────────────────────────────────────────────────────────────259st.markdown(260 "<hr><p style='text-align:center;color:#888;font-size:0.8rem'>"261 "💳 CC AI Assistant • Data from Livemint • Powered by Gemini</p>",262 unsafe_allow_html=True,263)264 