import os import streamlit as st import httpx import uuid import asyncio from datetime import datetime from config import API_BASE_URL, APP_TITLE, APP_SUBTITLE, THEME, EXAMPLE_QUERIES, INTENT_METADATA # ══════════════════════════════════════════════════════════════ # 1. PAGE CONFIGURATION # ══════════════════════════════════════════════════════════════ st.set_page_config( page_title=APP_TITLE, page_icon="🛡️", layout="wide", initial_sidebar_state="expanded" ) # ══════════════════════════════════════════════════════════════ # 2. GLOBAL CSS — Premium dark glassmorphism design # ══════════════════════════════════════════════════════════════ st.markdown(f""" """, unsafe_allow_html=True) # ══════════════════════════════════════════════════════════════ # 3. SESSION STATE # ══════════════════════════════════════════════════════════════ if "session_id" not in st.session_state: st.session_state.session_id = str(uuid.uuid4()) if "messages" not in st.session_state: st.session_state.messages = [] if "is_loading" not in st.session_state: st.session_state.is_loading = False if "total_queries" not in st.session_state: st.session_state.total_queries = 0 # ══════════════════════════════════════════════════════════════ # 4. API HELPERS # ══════════════════════════════════════════════════════════════ async def call_chat_api(query: str): async with httpx.AsyncClient(timeout=120.0) as client: try: payload = {"session_id": st.session_state.session_id, "query": query} if "plan_tier" in st.session_state: payload["plan_tier"] = st.session_state.plan_tier resp = await client.post(f"{API_BASE_URL}/chat", json=payload) resp.raise_for_status() return resp.json() except Exception as e: st.error(f"Backend error: {e}") return None async def get_health(): async with httpx.AsyncClient(timeout=5.0) as client: try: r = await client.get(f"{API_BASE_URL}/health") return r.json() except: return None # ══════════════════════════════════════════════════════════════ # 5. SIDEBAR # ══════════════════════════════════════════════════════════════ with st.sidebar: # Brand st.markdown(f"""
{APP_TITLE}
{APP_SUBTITLE}
""", unsafe_allow_html=True) # Dev Console shortcut console_url = "/dev-console" if ( os.getenv("RUN_MONOLITH","false").lower() == "true" or "localhost:8000" not in API_BASE_URL ) else "http://localhost:8000/dev-console" st.markdown(f""" 🚀  Open Developer Console """, unsafe_allow_html=True) st.markdown('', unsafe_allow_html=True) st.markdown(f"""
ID  {st.session_state.session_id[:8]}…{st.session_state.session_id[-4:]}
""", unsafe_allow_html=True) st.selectbox( "Plan Tier", ["Unknown", "Bronze", "Silver", "Gold"], index=0, key="plan_tier" ) col_a, col_b = st.columns(2) with col_a: if st.button("🆕 New Session", use_container_width=True): st.session_state.session_id = str(uuid.uuid4()) st.session_state.messages = [] st.session_state.total_queries = 0 st.rerun() with col_b: if st.button("🗑️ Clear Chat", use_container_width=True): st.session_state.messages = [] st.rerun() st.markdown('', unsafe_allow_html=True) for q in EXAMPLE_QUERIES: if st.button(q, key=f"eq_{q}", use_container_width=True): st.session_state.pending_query = q st.rerun() st.markdown('', unsafe_allow_html=True) if st.button("🔍 Fetch Last Trace", use_container_width=True): with st.spinner("Loading trace…"): async def _get_trace(): async with httpx.AsyncClient() as c: try: r = await c.get(f"{API_BASE_URL}/session/{st.session_state.session_id}/trace") return r.json() except: return None td = asyncio.run(_get_trace()) if td and "detail" not in td: st.json(td) else: st.info("Ask a question first.") # Stats st.markdown('', unsafe_allow_html=True) st.metric("Queries this session", st.session_state.total_queries) st.markdown("""
v1.0.0 · LangGraph · GPT-4o · LangSmith
""", unsafe_allow_html=True) # ══════════════════════════════════════════════════════════════ # 6. MAIN CHAT AREA # ══════════════════════════════════════════════════════════════ # Hero header — shown only when no messages if not st.session_state.messages: st.markdown(f"""
🛡️
{APP_TITLE}
Ask about coverage, providers, pharmacy benefits, and plan comparisons.
""", unsafe_allow_html=True) # Inline quick-start cards below hero st.markdown("
Try asking:
", unsafe_allow_html=True) cols = st.columns(2) for i, q in enumerate(EXAMPLE_QUERIES[:4]): with cols[i % 2]: if st.button(q, key=f"hero_{q}", use_container_width=True): st.session_state.pending_query = q st.rerun() else: # Compact status strip when conversation is active st.markdown(f"""
Backend connected  ·  {st.session_state.total_queries} message{"s" if st.session_state.total_queries != 1 else ""} this session
""", unsafe_allow_html=True) # ── Render Chat History ──────────────────────────────────────── for msg in st.session_state.messages: if msg["role"] == "user": st.markdown(f"""
{msg["content"]}
👤
""", unsafe_allow_html=True) else: intent = msg.get("intent", "POLICY_QUESTION") meta = INTENT_METADATA.get(intent, INTENT_METADATA["POLICY_QUESTION"]) ts = msg.get("timestamp", "")[:16].replace("T", " ") if msg.get("timestamp") else "" run_id = msg.get("run_id") memories = msg.get("memories_used", []) n_mems = len(memories) if memories else 0 # Build the badge row langsmith_html = "" if run_id: langsmith_html = f'📊 LangSmith Trace' mem_html = f'🧠 {n_mems} memory fact{"s" if n_mems!=1 else ""}' if n_mems else "" # Escape HTML in the answer for safe rendering inside the bubble answer_safe = msg["content"].replace("<", "<").replace(">", ">") # Build a single flat HTML string with no newlines or indentations to prevent markdown parsing issues badge_row = f'
' badge_row += f'{meta["icon"]} {intent.replace("_"," ")}' if ts: badge_row += f'{ts}' if langsmith_html: badge_row += langsmith_html if mem_html: badge_row += mem_html badge_row += '
' html_content = ( f'
' f'
🛡️
' f'
' f'{badge_row}' f'
{answer_safe}
' f'
' f'
' ) st.markdown(html_content, unsafe_allow_html=True) # Pipeline trace — collapsible steps = msg.get("steps_log", []) if steps: with st.expander(f"⛓️ Pipeline trace · {len(steps)} steps"): st.markdown("""
""", unsafe_allow_html=True) for step in steps: st.markdown(f"
{step}
", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # ── Chat Input ────────────────────────────────────────────── query = st.chat_input("Ask about your health insurance coverage…") if "pending_query" in st.session_state: query = st.session_state.pending_query del st.session_state.pending_query if query: # Append user message st.session_state.messages.append({"role": "user", "content": query}) with st.status("🧠 Processing your query…", expanded=True) as status: status.write("📡 Sending to orchestrator…") result = asyncio.run(call_chat_api(query)) if result: status.write(f"✅ Intent → **{result.get('intent','—')}**") steps = result.get("steps_log", []) if steps: status.write(f"⛓️ {len(steps)} pipeline steps completed") status.update(label="✅ Answer ready!", state="complete", expanded=False) st.session_state.messages.append({ "role": "ai", "content": result["answer"], "intent": result.get("intent", "POLICY_QUESTION"), "steps_log": result.get("steps_log", []), "memories_used": result.get("memories_used", []), "run_id": result.get("run_id"), "timestamp": result.get("timestamp", datetime.now().isoformat()), }) st.session_state.total_queries += 1 st.rerun() else: status.update(label="❌ Backend error", state="error")