Spaces:
Running
Running
| """ | |
| KhidmatAI β AI Service Orchestrator for Informal Economy | |
| """ | |
| import streamlit as st | |
| import json, time, os, sys | |
| from datetime import datetime | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from agents import intent as intent_agent | |
| from agents import discovery as discovery_agent | |
| from agents import recommendation as recommendation_agent | |
| from agents import booking as booking_agent | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PAGE CONFIG | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.set_page_config(page_title="KhidmatAI", page_icon="π§", layout="centered") | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); | |
| html, body, [class*="css"] { font-family: 'Inter', sans-serif; } | |
| .hero { | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); | |
| border-radius: 16px; padding: 32px 28px 24px; | |
| text-align: center; margin-bottom: 24px; | |
| border: 1px solid rgba(255,255,255,0.08); | |
| } | |
| .hero h1 { color:#fff; font-size:2rem; font-weight:700; margin:0; } | |
| .hero p { color:rgba(255,255,255,0.65); font-size:0.95rem; margin:8px 0 0; } | |
| .hero-badge { | |
| display:inline-block; background:rgba(99,179,237,0.15); | |
| border:1px solid rgba(99,179,237,0.3); color:#90cdf4; | |
| font-size:0.75rem; padding:3px 10px; border-radius:20px; | |
| margin-bottom:12px; font-weight:500; | |
| } | |
| .key-box { | |
| background:#fffbeb; border:1.5px solid #f6ad55; | |
| border-radius:12px; padding:16px 18px; margin-bottom:18px; | |
| } | |
| .key-box h4 { color:#c05621; margin:0 0 8px; font-size:0.95rem; } | |
| .key-box p { color:#744210; font-size:0.83rem; margin:4px 0; line-height:1.6; } | |
| .key-box code { background:#feebc8; padding:1px 5px; border-radius:4px; font-size:0.82rem; } | |
| .mode-pill { | |
| display:inline-block; font-size:0.72rem; font-weight:600; | |
| padding:2px 9px; border-radius:10px; margin-left:6px; | |
| } | |
| .mode-gemini { background:#ebf8ff; color:#2b6cb0; } | |
| .mode-rules { background:#fefcbf; color:#744210; } | |
| .agent-card { | |
| background:#f8fafc; border:1px solid #e2e8f0; | |
| border-left:4px solid #4299e1; border-radius:10px; | |
| padding:14px 16px; margin-bottom:10px; | |
| } | |
| .agent-card.done { border-left-color:#48bb78; background:#f0fff4; } | |
| .agent-card.error { border-left-color:#fc8181; background:#fff5f5; } | |
| .agent-title { font-weight:600; font-size:0.9rem; color:#2d3748; margin-bottom:4px; } | |
| .agent-body { font-size:0.82rem; color:#4a5568; line-height:1.5; } | |
| .provider-card { | |
| background:#fff; border:1.5px solid #e2e8f0; | |
| border-radius:12px; padding:16px 18px; margin-bottom:10px; | |
| } | |
| .provider-card.best { border-color:#48bb78; background:#f0fff4; } | |
| .provider-name { font-size:1rem; font-weight:600; color:#1a202c; } | |
| .provider-meta { font-size:0.8rem; color:#718096; margin-top:4px; } | |
| .badge-best { | |
| background:#c6f6d5; color:#276749; font-size:0.72rem; | |
| padding:2px 8px; border-radius:8px; font-weight:600; margin-left:8px; | |
| } | |
| .score-bar { height:5px; background:#e2e8f0; border-radius:3px; margin-top:8px; } | |
| .score-fill { height:5px; background:#48bb78; border-radius:3px; } | |
| .receipt { | |
| background:linear-gradient(135deg,#f0fff4,#e6fffa); | |
| border:2px solid #9ae6b4; border-radius:16px; | |
| padding:24px; text-align:center; | |
| } | |
| .receipt-id { font-size:0.78rem; color:#68d391; letter-spacing:2px; font-weight:600; } | |
| .receipt-name { font-size:1.4rem; font-weight:700; color:#1a202c; margin:8px 0 4px; } | |
| .receipt-row { | |
| display:flex; justify-content:space-between; padding:8px 0; | |
| border-bottom:1px solid rgba(0,0,0,0.06); font-size:0.88rem; | |
| } | |
| .receipt-row:last-child { border-bottom:none; } | |
| .receipt-label { color:#718096; } | |
| .receipt-value { color:#1a202c; font-weight:500; } | |
| .reminder-box { | |
| background:#ebf8ff; border:1px solid #90cdf4; | |
| border-radius:10px; padding:12px 16px; margin-top:14px; | |
| font-size:0.85rem; color:#2b6cb0; | |
| } | |
| .trace-header { | |
| font-size:0.75rem; font-weight:600; color:#a0aec0; | |
| text-transform:uppercase; letter-spacing:1px; margin-bottom:6px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SESSION STATE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| for k, v in [("result", None), ("trace_log", None)]: | |
| if k not in st.session_state: | |
| st.session_state[k] = v | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HERO | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <div class="hero"> | |
| <div class="hero-badge">π€ Google Antigravity Β· Gemini API Β· Streamlit</div> | |
| <h1>π§ KhidmatAI</h1> | |
| <p>AI Service Orchestrator for Pakistan's Informal Economy<br> | |
| Type your request in <strong>Urdu Β· Roman Urdu Β· English</strong></p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # API KEY STATUS BANNER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| api_key = os.environ.get("GEMINI_API_KEY", "GEMINI_API_KEY").strip() | |
| key_valid = bool(api_key and api_key != "YOUR_GEMINI_API_KEY_HERE" and len(api_key) > 20) | |
| if not key_valid: | |
| st.markdown(""" | |
| <div class="key-box"> | |
| <h4>β οΈ Gemini API Key Not Set β Running in Demo Mode</h4> | |
| <p>The app still works using a <strong>rule-based parser</strong>, but Gemini AI gives better results.</p> | |
| <p>To enable full AI mode:</p> | |
| <p> | |
| 1οΈβ£ Get a <strong>free</strong> key at <code>aistudio.google.com/apikey</code><br> | |
| 2οΈβ£ In your HuggingFace Space β <strong>Settings β Variables and Secrets</strong><br> | |
| 3οΈβ£ Click <strong>New secret</strong> β Name: <code>GEMINI_API_KEY</code> β paste your key β Save<br> | |
| 4οΈβ£ Restart the Space (Settings β Factory reboot) | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.success("β Gemini API key detected β AI mode active") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # EXAMPLE BUTTONS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown("**Try an example:**") | |
| examples = [ | |
| "Mujhe kal subah G-13 mein AC technician chahiye", | |
| "I need a plumber in F-10 today", | |
| "G-9 mein electrician chahiye aaj evening", | |
| "Maths tutor chahiye G-11 mein kal", | |
| "Home maid service chahiye F-7 mein", | |
| "Driver chahiye kal airport ke liye G-10 se", | |
| ] | |
| cols = st.columns(3) | |
| selected_example = None | |
| for i, ex in enumerate(examples): | |
| if cols[i % 3].button(f"π¬ {ex[:28]}β¦", key=f"ex_{i}", use_container_width=True): | |
| selected_example = ex | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # INPUT | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| user_input = st.text_area( | |
| "Request:", | |
| value=selected_example or st.session_state.get("last_input", ""), | |
| height=90, | |
| placeholder="e.g. Mujhe kal subah G-13 mein AC technician chahiye", | |
| label_visibility="collapsed", | |
| ) | |
| c1, c2 = st.columns([3, 1]) | |
| run_btn = c1.button("π Find & Book Service", type="primary", use_container_width=True) | |
| clear_btn = c2.button("π Clear", use_container_width=True) | |
| if clear_btn: | |
| st.session_state.result = None | |
| st.session_state.trace_log = None | |
| st.rerun() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PIPELINE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def agent_card_running(title, desc): | |
| st.markdown(f"""<div class="agent-card"> | |
| <div class="agent-title">β³ {title}</div> | |
| <div class="agent-body">{desc}</div> | |
| </div>""", unsafe_allow_html=True) | |
| def agent_card_done(title, body, ms, mode=None): | |
| mode_pill = "" | |
| if mode == "gemini": | |
| mode_pill = '<span class="mode-pill mode-gemini">Gemini AI</span>' | |
| elif mode in ("rule_based_fallback", "rules"): | |
| mode_pill = '<span class="mode-pill mode-rules">Rule-based</span>' | |
| st.markdown(f"""<div class="agent-card done"> | |
| <div class="agent-title">β {title}{mode_pill} <small style="color:#718096">({ms}ms)</small></div> | |
| <div class="agent-body">{body}</div> | |
| </div>""", unsafe_allow_html=True) | |
| def agent_card_error(title, err): | |
| st.markdown(f"""<div class="agent-card error"> | |
| <div class="agent-title">β {title} Failed</div> | |
| <div class="agent-body"><code>{err}</code></div> | |
| </div>""", unsafe_allow_html=True) | |
| def run_pipeline(message: str): | |
| trace = [] | |
| st.markdown("---") | |
| st.markdown("### π€ Agent Pipeline") | |
| # Agent 1 β Intent | |
| ph1 = st.empty() | |
| with ph1: | |
| agent_card_running("Agent 1 β Intent Agent", "Parsing your request...") | |
| t0 = time.time() | |
| try: | |
| intent = intent_agent.run(message) | |
| ms = int((time.time()-t0)*1000) | |
| warning = intent.pop("_warning", None) | |
| mode = intent.get("mode", "rules") | |
| trace.append({"agent":"IntentAgent","step":1, | |
| "input":{"message":message},"output":intent, | |
| "duration_ms":ms, | |
| "reasoning":f"Language={intent.get('language')}, service={intent.get('service_type')}, " | |
| f"location={intent.get('location')}, time={intent.get('time')} [mode={mode}]"}) | |
| ph1.empty() | |
| agent_card_done( | |
| "Agent 1 β Intent Agent", | |
| f"π·οΈ <b>{intent.get('service_type','?')}</b> | " | |
| f"π <b>{intent.get('location','?')}</b> | " | |
| f"π <b>{intent.get('time','?')}</b> | " | |
| f"π <b>{intent.get('language','?')}</b>", | |
| ms, mode | |
| ) | |
| if warning: | |
| st.caption(f"βΉοΈ {warning}") | |
| except Exception as e: | |
| ph1.empty() | |
| agent_card_error("Agent 1 β Intent Agent", str(e)) | |
| return None, trace | |
| # Agent 2 β Discovery | |
| ph2 = st.empty() | |
| with ph2: | |
| agent_card_running("Agent 2 β Discovery Agent", "Searching nearby providers...") | |
| t0 = time.time() | |
| try: | |
| disc = discovery_agent.run(intent) | |
| ms = int((time.time()-t0)*1000) | |
| trace.append({"agent":"DiscoveryAgent","step":2, | |
| "input":intent,"output":{"total_found":disc["total_found"]}, | |
| "duration_ms":ms, | |
| "reasoning":f"Filtered providers.json for '{intent.get('service_type')}' near {disc['user_location']}. " | |
| f"Found {disc['total_found']} provider(s). Distance via haversine formula."}) | |
| ph2.empty() | |
| agent_card_done( | |
| "Agent 2 β Discovery Agent", | |
| f"Found <b>{disc['total_found']}</b> provider(s) for " | |
| f"<b>{intent.get('service_type')}</b> near <b>{disc['user_location']}</b>.", | |
| ms | |
| ) | |
| except Exception as e: | |
| ph2.empty() | |
| agent_card_error("Agent 2 β Discovery Agent", str(e)) | |
| return None, trace | |
| if disc["total_found"] == 0: | |
| st.warning("π No providers found. Try a different service or location.") | |
| return None, trace | |
| # Agent 3 β Recommendation | |
| ph3 = st.empty() | |
| with ph3: | |
| agent_card_running("Agent 3 β Recommendation Agent", "Ranking providers...") | |
| t0 = time.time() | |
| try: | |
| rec = recommendation_agent.run(disc) | |
| ms = int((time.time()-t0)*1000) | |
| trace.append({"agent":"RecommendationAgent","step":3, | |
| "input":{"providers_found":disc["total_found"]}, | |
| "output":{"selected":rec["best_provider"]["name"],"score":rec["best_provider"]["score"]}, | |
| "duration_ms":ms, "reasoning":rec["reasoning"]}) | |
| ph3.empty() | |
| agent_card_done("Agent 3 β Recommendation Agent", rec["reasoning"], ms) | |
| except Exception as e: | |
| ph3.empty() | |
| agent_card_error("Agent 3 β Recommendation Agent", str(e)) | |
| return None, trace | |
| # Agent 4 β Booking | |
| ph4 = st.empty() | |
| with ph4: | |
| agent_card_running("Agent 4 β Booking Agent", "Confirming your booking...") | |
| t0 = time.time() | |
| try: | |
| bk = booking_agent.run(rec, intent) | |
| ms = int((time.time()-t0)*1000) | |
| trace.append({"agent":"BookingAgent","step":4, | |
| "input":{"provider":rec["best_provider"]["name"]}, | |
| "output":{"booking_id":bk["booking_id"],"slot":bk["confirmed_slot"]}, | |
| "duration_ms":ms, | |
| "reasoning":f"Slot {bk['confirmed_slot']} selected. Booking ID: {bk['booking_id']}. Reminder: {bk['reminder_time']}."}) | |
| ph4.empty() | |
| agent_card_done( | |
| "Agent 4 β Booking Agent", | |
| f"Booking <b>{bk['booking_id']}</b> confirmed Β· Slot <b>{bk['confirmed_slot']}</b> Β· " | |
| f"Reminder at <b>{bk['reminder_time']}</b>.", | |
| ms | |
| ) | |
| except Exception as e: | |
| ph4.empty() | |
| agent_card_error("Agent 4 β Booking Agent", str(e)) | |
| return None, trace | |
| return {"intent":intent, "discovery":disc, "recommendation":rec, "booking":bk}, trace | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TRIGGER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if run_btn: | |
| if not user_input.strip(): | |
| st.warning("Please type a service request first.") | |
| else: | |
| st.session_state["last_input"] = user_input.strip() | |
| st.session_state.result = None | |
| st.session_state.trace_log = None | |
| result, trace = run_pipeline(user_input.strip()) | |
| st.session_state.result = result | |
| st.session_state.trace_log = trace | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # RESULTS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if st.session_state.result: | |
| result = st.session_state.result | |
| rec = result["recommendation"] | |
| bk = result["booking"] | |
| disc = result["discovery"] | |
| st.markdown("---") | |
| tab1, tab2, tab3 = st.tabs(["π Providers", "β Booking Receipt", "π Agent Trace"]) | |
| with tab1: | |
| st.markdown(f"**{disc['total_found']} provider(s) found** β ranked by distance Β· rating Β· availability") | |
| for i, p in enumerate(rec["all_ranked"]): | |
| is_best = i == 0 | |
| badge = '<span class="badge-best">β TOP PICK</span>' if is_best else "" | |
| avail = "π’ Available" if p.get("available") else "π΄ Unavailable" | |
| sw = int(p["score"]*100) | |
| st.markdown(f""" | |
| <div class="provider-card {'best' if is_best else ''}"> | |
| <div class="provider-name">#{i+1} {p['name']}{badge}</div> | |
| <div class="provider-meta"> | |
| π {p['location']['area']} Β· | |
| π {p['distance_km']} km Β· | |
| β {p['rating']} ({p['total_reviews']} reviews) Β· | |
| {avail} Β· | |
| π° {p.get('price_range','N/A')} | |
| </div> | |
| <div class="score-bar"><div class="score-fill" style="width:{sw}%"></div></div> | |
| <div style="font-size:0.73rem;color:#a0aec0;margin-top:3px;">Score: {p['score']}</div> | |
| </div>""", unsafe_allow_html=True) | |
| with tab2: | |
| st.markdown(f""" | |
| <div class="receipt"> | |
| <div class="receipt-id">BOOKING ID: {bk['booking_id']}</div> | |
| <div class="receipt-name">π {bk['provider_name']}</div> | |
| <div style="color:#48bb78;font-size:0.85rem;margin-bottom:16px;">Booking Confirmed β</div> | |
| <div class="receipt-row"><span class="receipt-label">π§ Service</span><span class="receipt-value">{bk['service']}</span></div> | |
| <div class="receipt-row"><span class="receipt-label">π Location</span><span class="receipt-value">{bk['location']}</span></div> | |
| <div class="receipt-row"><span class="receipt-label">π Date</span><span class="receipt-value">{bk['booked_date']}</span></div> | |
| <div class="receipt-row"><span class="receipt-label">β° Slot</span><span class="receipt-value">{bk['confirmed_slot']}</span></div> | |
| <div class="receipt-row"><span class="receipt-label">π Contact</span><span class="receipt-value">{bk['provider_phone']}</span></div> | |
| <div class="reminder-box">π <b>Reminder</b> set for <b>{bk['reminder_time']}</b> β 1 hour before appointment</div> | |
| </div>""", unsafe_allow_html=True) | |
| with tab3: | |
| st.markdown('<div class="trace-header">Full Agent Decision Log</div>', unsafe_allow_html=True) | |
| for step in st.session_state.trace_log: | |
| with st.expander(f"Step {step['step']} β {step['agent']} ({step['duration_ms']}ms)"): | |
| st.markdown(f"**Reasoning:** {step['reasoning']}") | |
| ca, cb = st.columns(2) | |
| with ca: | |
| st.markdown("**Input**"); st.json(step["input"]) | |
| with cb: | |
| st.markdown("**Output**"); st.json(step["output"]) | |
| st.markdown("**Full JSON:**") | |
| st.json(st.session_state.trace_log) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FOOTER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown("---") | |
| st.markdown( | |
| "<div style='text-align:center;color:#a0aec0;font-size:0.78rem'>" | |
| "KhidmatAI Β· Google Antigravity Β· Gemini API Β· Streamlit Β· Hackathon 2025" | |
| "</div>", unsafe_allow_html=True | |
| ) | |