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"""
""", 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")