khidmatAI / app.py
CoderHassan's picture
Update app.py
32b5b6e verified
"""
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> &nbsp;|&nbsp; "
f"πŸ“ <b>{intent.get('location','?')}</b> &nbsp;|&nbsp; "
f"πŸ• <b>{intent.get('time','?')}</b> &nbsp;|&nbsp; "
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']} &nbsp;Β·&nbsp;
πŸ“ {p['distance_km']} km &nbsp;Β·&nbsp;
⭐ {p['rating']} ({p['total_reviews']} reviews) &nbsp;·&nbsp;
{avail} &nbsp;Β·&nbsp;
πŸ’° {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
)