CoolFace
Apppublic

Rxrohans/PayLens-Dev

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
app.py386 linesDownload Raw Back to src
1"""2app.py — Phase 4 (v2): ChargeClarity Streamlit UI3---------------------------------------------------4Run: streamlit run src/app.py5"""6 7import sys8import re9from pathlib import Path10sys.path.insert(0, str(Path(__file__).parent))11from datetime_parser import parse_datetime_query12 13import streamlit as st14from dotenv import load_dotenv15load_dotenv()16 17# ── Page config ───────────────────────────────────────────18st.set_page_config(19    page_title="PayLens",20    page_icon="💳",21    layout="centered",22    initial_sidebar_state="collapsed"23)24 25# ── CSS ───────────────────────────────────────────────────26st.markdown("""27<style>28@import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=DM+Sans:wght@300;400;500&display=swap');29 30html, body, [class*="css"] { font-family: 'DM Sans', sans-serif; background:#0d0d14; color:#e2e2ee; }31.stApp { background:#0d0d14; }32#MainMenu, footer, header { visibility:hidden; }33.block-container { padding-top:2.5rem; padding-bottom:3rem; max-width:760px; }34 35/* hero */36.hero { text-align:center; padding:2.5rem 1rem 0.5rem; }37.hero-eyebrow {38    display:inline-block; background:rgba(139,92,246,0.12);39    border:1px solid rgba(139,92,246,0.25); color:#a78bfa;40    font-size:0.7rem; letter-spacing:0.14em; text-transform:uppercase;41    padding:0.28rem 0.9rem; border-radius:999px; margin-bottom:1.1rem;42}43.hero-title {44    font-family:'Syne',sans-serif; font-size:2.9rem; font-weight:800;45    background:linear-gradient(130deg,#fff 40%,#a78bfa);46    -webkit-background-clip:text; -webkit-text-fill-color:transparent;47    line-height:1.08; letter-spacing:-0.03em; margin-bottom:0.7rem;48}49.hero-sub { color:#52526e; font-size:0.95rem; font-weight:300; max-width:440px; margin:0 auto 1.8rem; line-height:1.65; }50 51/* chips */52.stButton>button {53    background:rgba(255,255,255,0.04) !important; border:1px solid rgba(255,255,255,0.09) !important;54    color:#7b7b9a !important; font-size:0.78rem !important; border-radius:999px !important;55    padding:0.3rem 0.9rem !important; transition:all 0.18s !important; font-family:'DM Sans' !important;56}57.stButton>button:hover { background:rgba(139,92,246,0.12) !important; color:#c4b5fd !important; border-color:rgba(139,92,246,0.35) !important; }58 59/* ask button override */60div[data-testid="column"]:last-child .stButton>button {61    background:linear-gradient(135deg,#6d28d9,#7c3aed) !important;62    color:white !important; border:none !important;63    border-radius:10px !important; font-weight:500 !important; font-size:0.95rem !important;64    padding:0.65rem 1.2rem !important;65}66 67/* input */68.stTextInput input {69    background:rgba(255,255,255,0.04) !important; border:1px solid rgba(255,255,255,0.1) !important;70    border-radius:10px !important; color:#e2e2ee !important; font-size:0.97rem !important;71    padding:0.8rem 1.1rem !important; font-family:'DM Sans' !important;72}73.stTextInput input:focus { border-color:rgba(139,92,246,0.55) !important; box-shadow:0 0 0 3px rgba(139,92,246,0.1) !important; }74.stTextInput input::placeholder { color:#3a3a52 !important; }75.stTextInput label { display:none !important; }76 77/* answer card */78.ans-card {79    background:rgba(255,255,255,0.025); border:1px solid rgba(255,255,255,0.07);80    border-radius:14px; padding:1.6rem 1.8rem; margin-top:1.4rem;81}82.ans-card p { margin:0 0 0.75rem; line-height:1.75; color:#c8c8e0; font-size:0.96rem; }83.ans-card p:last-child { margin-bottom:0; }84.ans-card ul { padding-left:1.2rem; margin:0.3rem 0 0.75rem; }85.ans-card li { color:#c8c8e0; font-size:0.96rem; line-height:1.75; margin-bottom:0.25rem; }86.ans-card strong { color:#e8e8f8; font-weight:500; }87 88/* badges */89.badge-row { display:flex; flex-wrap:wrap; gap:0.5rem; margin-top:1.1rem; align-items:center; }90.badge {91    font-size:0.72rem; font-weight:500; letter-spacing:0.04em;92    padding:0.22rem 0.7rem; border-radius:999px;93}94.b-high   { background:rgba(16,185,129,0.12); color:#6ee7b7; border:1px solid rgba(16,185,129,0.2); }95.b-medium { background:rgba(245,158,11,0.12);  color:#fcd34d; border:1px solid rgba(245,158,11,0.2); }96.b-low    { background:rgba(239,68,68,0.12);   color:#fca5a5; border:1px solid rgba(239,68,68,0.2); }97.b-none   { background:rgba(100,100,120,0.12); color:#9ca3af; border:1px solid rgba(100,100,120,0.2); }98.b-web    { background:rgba(59,130,246,0.12);  color:#93c5fd; border:1px solid rgba(59,130,246,0.2); }99.b-src    { background:rgba(139,92,246,0.1);   color:#c4b5fd; border:1px solid rgba(139,92,246,0.18); }100.b-time   { background:rgba(255,255,255,0.04); color:#52526e; border:1px solid rgba(255,255,255,0.07); }101 102/* links section */103.links-section { margin-top:1.1rem; }104.links-label { font-size:0.72rem; color:#3a3a52; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:0.5rem; }105.links-row { display:flex; flex-wrap:wrap; gap:0.45rem; }106.ext-link {107    font-size:0.78rem; color:#7c6faa; text-decoration:none;108    background:rgba(139,92,246,0.07); border:1px solid rgba(139,92,246,0.15);109    padding:0.22rem 0.75rem; border-radius:6px; transition:all 0.15s;110}111.ext-link:hover { background:rgba(139,92,246,0.15); color:#c4b5fd; }112 113/* blocked / warn */114.blocked { background:rgba(239,68,68,0.08); border:1px solid rgba(239,68,68,0.18); border-radius:10px; padding:1rem 1.2rem; color:#fca5a5; font-size:0.9rem; margin-top:1rem; }115.warn    { background:rgba(245,158,11,0.08); border:1px solid rgba(245,158,11,0.18); border-radius:10px; padding:0.7rem 1rem; color:#fcd34d; font-size:0.82rem; margin-top:0.7rem; }116 117/* history */118.hist-wrap { margin-top:2.5rem; }119.hist-label { font-size:0.7rem; color:#2e2e42; letter-spacing:0.1em; text-transform:uppercase; margin-bottom:0.9rem; }120.hist-item { border-left:2px solid rgba(139,92,246,0.2); padding:0.5rem 0.9rem; margin-bottom:0.7rem; }121.hist-q { font-size:0.86rem; color:#6b6b8a; font-weight:500; margin-bottom:0.2rem; }122.hist-a { font-size:0.82rem; color:#3a3a52; line-height:1.55; }123 124hr { border-color:rgba(255,255,255,0.05) !important; margin:2rem 0 !important; }125.stSpinner>div { border-top-color:#7c3aed !important; }126/* Hide "Press Enter to submit form" text */127.stForm small { display: none !important; }128.stForm [data-testid="InputInstructions"] { display: none !important; }129</style>130""", unsafe_allow_html=True)131 132 133# ── Cached resources ──────────────────────────────────────134@st.cache_resource(show_spinner=False)135def load_chain():136    from pathlib import Path137    import subprocess, sys138 139    # Build index if it doesn't exist (first run on HuggingFace)140    index_path = Path(__file__).parent.parent / "data" / "processed" / "faiss_index.bin"141 142    if not index_path.exists():143        with st.spinner("First run — building knowledge base index (2-3 mins)..."):144            subprocess.run([sys.executable, str(Path(__file__).parent / "chunker.py")], check=True)145            subprocess.run([sys.executable, str(Path(__file__).parent / "embedder.py")], check=True)146 147    from chain import ChargeChain148    return ChargeChain()149 150@st.cache_resource(show_spinner=False)151def get_guardrail_fn():152    from guardrails import run_with_guardrails153    return run_with_guardrails154 155 156# ── Session state ─────────────────────────────────────────157if "history" not in st.session_state:158    st.session_state.history = []159if "result" not in st.session_state:160    st.session_state.result = None161if "last_q" not in st.session_state:162    st.session_state.last_q = ""163 164# ── Tabs ──────────────────────────────────────────────────165tab_main, tab_eval = st.tabs(["💳 PayLens", "📊 Eval Dashboard"])166 167with tab_main:168    # ── Hero ──────────────────────────────────────────────────169    st.markdown("""170    <div class="hero">171        <div class="hero-eyebrow">AI · RAG · Fintech</div>172        <div class="hero-title">PayLens</div>173        <div class="hero-sub">Ask anything about payment fees, currency charges,174        UPI, taxes, or fintech — get a plain-English answer with sources.</div>175    </div>176    """, unsafe_allow_html=True)177 178    # ── Example chips ─────────────────────────────────────────179    EXAMPLES = [180        "Why did PayPal charge me so much?",181        "How does currency conversion work?",182        "Is UPI free for merchants?",183        "What is a chargeback?",184        "Do I pay GST on foreign income?",185    ]186 187    chip_cols = st.columns(len(EXAMPLES))188    for i, ex in enumerate(EXAMPLES):189        if chip_cols[i].button(ex, key=f"chip_{i}"):190            st.session_state["chip_query"] = ex191 192    st.markdown("<div style='height:0.8rem'></div>", unsafe_allow_html=True)193 194    # ── Input ─────────────────────────────────────────────────195    with st.form(key="query_form", clear_on_submit=False):196        col_in, col_btn = st.columns([5, 1])197        with col_in:198            # Use chip query if clicked, otherwise empty199            default_val = st.session_state.pop("chip_query", "")200            user_query = st.text_input(201                "q", label_visibility="collapsed",202                placeholder="e.g. Why did PayPal deduct 7% from my payment?",203                value=default_val, key="main_input"204            )205        with col_btn:206            ask_clicked = st.form_submit_button("Ask →", width='stretch')207 208 209    # ── Helper: render answer text as clean HTML ──────────────210    def render_answer(text: str) -> str:211        """212        Converts LLM markdown-ish output → clean HTML for the answer card.213        Handles: **bold**, bullet lists, numbered lists, paragraphs.214        """215        # Strip confidence tag216        text = re.sub(r'\s*\[(High|Medium|Low|None)\]\s*$', '', text, flags=re.IGNORECASE).strip()217 218        lines   = text.split("\n")219        html    = ""220        in_ul   = False221        in_ol   = False222 223        for line in lines:224            line = line.strip()225            if not line:226                if in_ul:  html += "</ul>"; in_ul = False227                if in_ol:  html += "</ol>"; in_ol = False228                continue229 230            # Bold231            line = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', line)232 233            # Bullet list234            if re.match(r'^[-•*] ', line):235                if in_ol:  html += "</ol>"; in_ol = False236                if not in_ul: html += "<ul>"; in_ul = True237                html += f"<li>{line[2:].strip()}</li>"238                continue239 240            # Numbered list241            if re.match(r'^\d+\. ', line):242                if in_ul:  html += "</ul>"; in_ul = False243                if not in_ol: html += "<ol>"; in_ol = True244                html += f"<li>{re.sub(r'^\d+\. ', '', line)}</li>"245                continue246 247            # Close open lists248            if in_ul:  html += "</ul>"; in_ul = False249            if in_ol:  html += "</ol>"; in_ol = False250 251            html += f"<p>{line}</p>"252 253        if in_ul: html += "</ul>"254        if in_ol: html += "</ol>"255        return html256 257 258    # ── Process query ─────────────────────────────────────────259   # ── Process query ─────────────────────────────────────────260    if ask_clicked and user_query.strip():261        # Parse datetime info BEFORE spinner (so it shows immediately)262        dt_info = parse_datetime_query(user_query.strip())263        264        # Show datetime detection if present265        if dt_info['has_temporal']:266            st.info(f"📅 **Temporal context detected:** {', '.join(dt_info['temporal_refs'])}")267            with st.expander("ℹ️ Date Context", expanded=False):268                st.caption(dt_info['date_context'])269        270        with st.spinner("Thinking..."):271            try:272                chain        = load_chain()273                guardrail_fn = get_guardrail_fn()274                result       = guardrail_fn(user_query.strip(), chain.ask)275                st.session_state.result = result276            except Exception as e:277                st.error(f"Error: {str(e)}")278                st.stop()279                st.session_state.last_q = user_query.strip()280 281            if not result["blocked"]:282                clean_a = re.sub(283                    r'\s*\[(High|Medium|Low|None)\]\s*$', '',284                    result["answer"], flags=re.IGNORECASE285                ).strip()286                st.session_state.history.insert(0, {287                    "q": user_query.strip(),288                    "a": clean_a[:160] + "..." if len(clean_a) > 160 else clean_a,289                    "confidence": result["confidence"]290                })291 292 293    # ── Render result ─────────────────────────────────────────294    result = st.session_state.result295 296    if result:297        if result["blocked"]:298            reason = result["guardrail_warnings"][0] if result["guardrail_warnings"] else "Request not supported."299            st.markdown(f'<div class="blocked">🚫 &nbsp;{reason}</div>', unsafe_allow_html=True)300 301        else:302            answer     = result["answer"]303            confidence = result.get("confidence", "medium")304            sources    = result.get("sources", [])305            latency    = result.get("latency_ms", 0)306            warnings   = result.get("guardrail_warnings", [])307            web_used   = result.get("web_search_used", False)308            links      = result.get("official_links", [])309 310            # ── Answer card ───────────────────────────────────311            rendered = render_answer(answer)312            st.markdown(f'<div class="ans-card">{rendered}</div>', unsafe_allow_html=True)313 314            # ── Badges ────────────────────────────────────────315            conf_map = {316                "high":   ("b-high",   "High Confidence"),317                "medium": ("b-medium", "Medium Confidence"),318                "low":    ("b-low",    "Low Confidence"),319                "none":   ("b-none",   "No Data"),320            }321            cls, label = conf_map.get(confidence.lower(), ("b-none", confidence))322 323            badges = f'<span class="badge {cls}">{label}</span>'324 325            if web_used:326                badges += ' <span class="badge b-web">🌐 Live Search</span>'327 328            clean_sources = [329                s for s in sources330                if s not in ("live_web_search",)331            ]332            for s in clean_sources:333                readable = s.replace("_", " ").replace("manual", "").strip().title()334                badges += f' <span class="badge b-src">📄 {readable}</span>'335 336            badges += f' <span class="badge b-time">⚡ {latency:.0f}ms</span>'337 338            st.markdown(f'<div class="badge-row">{badges}</div>', unsafe_allow_html=True)339 340            # ── Warnings ──────────────────────────────────────341            for w in warnings:342                st.markdown(f'<div class="warn">⚠️ {w}</div>', unsafe_allow_html=True)343 344            # ── Official links ────────────────────────────────345            if links:346                link_items = ""347                for url in links[:5]:348                    domain = url.replace("https://","").replace("http://","").split("/")[0]349                    link_items += f'<a class="ext-link" href="{url}" target="_blank">↗ {domain}</a>'350                st.markdown(f"""351                <div class="links-section">352                    <div class="links-label">Verify at official sources</div>353                    <div class="links-row">{link_items}</div>354                </div>355                """, unsafe_allow_html=True)356 357 358    # ── History ───────────────────────────────────────────────359    if st.session_state.history:360        st.markdown("<hr>", unsafe_allow_html=True)361        st.markdown('<div class="hist-label">Recent Questions</div>', unsafe_allow_html=True)362        for item in st.session_state.history[:5]:363            dot_color = {"high":"#6ee7b7","medium":"#fcd34d","low":"#fca5a5"}.get(item["confidence"],"#52526e")364            st.markdown(f"""365            <div class="hist-item">366                <div class="hist-q"><span style="color:{dot_color};margin-right:6px">●</span>{item['q']}</div>367                <div class="hist-a">{item['a']}</div>368            </div>""", unsafe_allow_html=True)369 370 371    # ── Footer ────────────────────────────────────────────────372    st.markdown("""373    <div style="text-align:center;margin-top:3rem;border-top:1px solid rgba(255,255,255,0.04);padding-top:1.5rem;">374        <p style="color:#2a2a3a;font-size:0.72rem;letter-spacing:0.06em;">375            PAYLENS &nbsp;·&nbsp; RAG + LIVE SEARCH &nbsp;·&nbsp;376            LLAMA 3.1 via GROQ &nbsp;·&nbsp; FAISS &nbsp;·&nbsp; LANGCHAIN377        </p>378    </div>379    """, unsafe_allow_html=True)380 381# end of tab_main content ↑382 383with tab_eval:384    sys.path.insert(0, str(Path(__file__).parent.parent / "eval"))385    from metrics_dashboard import render_dashboard386    render_dashboard()