CoderHassan commited on
Commit
2942022
Β·
verified Β·
1 Parent(s): 940f39c

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +400 -0
app.py ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ KhidmatAI β€” AI Service Orchestrator for Informal Economy
3
+ Streamlit interface for HuggingFace Spaces (free tier)
4
+ """
5
+
6
+ import streamlit as st
7
+ import json, time, os, sys
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).parent))
12
+ from agents import intent as intent_agent
13
+ from agents import discovery as discovery_agent
14
+ from agents import recommendation as recommendation_agent
15
+ from agents import booking as booking_agent
16
+
17
+ # ════════════════════════════════════════════════════════════════════
18
+ # PAGE CONFIG
19
+ # ════════════════════════════════════════════════════════════════════
20
+ st.set_page_config(page_title="KhidmatAI", page_icon="πŸ”§", layout="centered")
21
+
22
+ st.markdown("""
23
+ <style>
24
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
25
+ html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
26
+
27
+ .hero {
28
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
29
+ border-radius: 16px; padding: 32px 28px 24px;
30
+ text-align: center; margin-bottom: 24px;
31
+ border: 1px solid rgba(255,255,255,0.08);
32
+ }
33
+ .hero h1 { color:#fff; font-size:2rem; font-weight:700; margin:0; }
34
+ .hero p { color:rgba(255,255,255,0.65); font-size:0.95rem; margin:8px 0 0; }
35
+ .hero-badge {
36
+ display:inline-block; background:rgba(99,179,237,0.15);
37
+ border:1px solid rgba(99,179,237,0.3); color:#90cdf4;
38
+ font-size:0.75rem; padding:3px 10px; border-radius:20px;
39
+ margin-bottom:12px; font-weight:500;
40
+ }
41
+ .key-box {
42
+ background:#fffbeb; border:1.5px solid #f6ad55;
43
+ border-radius:12px; padding:16px 18px; margin-bottom:18px;
44
+ }
45
+ .key-box h4 { color:#c05621; margin:0 0 8px; font-size:0.95rem; }
46
+ .key-box p { color:#744210; font-size:0.83rem; margin:4px 0; line-height:1.6; }
47
+ .key-box code { background:#feebc8; padding:1px 5px; border-radius:4px; font-size:0.82rem; }
48
+ .mode-pill {
49
+ display:inline-block; font-size:0.72rem; font-weight:600;
50
+ padding:2px 9px; border-radius:10px; margin-left:6px;
51
+ }
52
+ .mode-gemini { background:#ebf8ff; color:#2b6cb0; }
53
+ .mode-rules { background:#fefcbf; color:#744210; }
54
+ .agent-card {
55
+ background:#f8fafc; border:1px solid #e2e8f0;
56
+ border-left:4px solid #4299e1; border-radius:10px;
57
+ padding:14px 16px; margin-bottom:10px;
58
+ }
59
+ .agent-card.done { border-left-color:#48bb78; background:#f0fff4; }
60
+ .agent-card.error { border-left-color:#fc8181; background:#fff5f5; }
61
+ .agent-title { font-weight:600; font-size:0.9rem; color:#2d3748; margin-bottom:4px; }
62
+ .agent-body { font-size:0.82rem; color:#4a5568; line-height:1.5; }
63
+ .provider-card {
64
+ background:#fff; border:1.5px solid #e2e8f0;
65
+ border-radius:12px; padding:16px 18px; margin-bottom:10px;
66
+ }
67
+ .provider-card.best { border-color:#48bb78; background:#f0fff4; }
68
+ .provider-name { font-size:1rem; font-weight:600; color:#1a202c; }
69
+ .provider-meta { font-size:0.8rem; color:#718096; margin-top:4px; }
70
+ .badge-best {
71
+ background:#c6f6d5; color:#276749; font-size:0.72rem;
72
+ padding:2px 8px; border-radius:8px; font-weight:600; margin-left:8px;
73
+ }
74
+ .score-bar { height:5px; background:#e2e8f0; border-radius:3px; margin-top:8px; }
75
+ .score-fill { height:5px; background:#48bb78; border-radius:3px; }
76
+ .receipt {
77
+ background:linear-gradient(135deg,#f0fff4,#e6fffa);
78
+ border:2px solid #9ae6b4; border-radius:16px;
79
+ padding:24px; text-align:center;
80
+ }
81
+ .receipt-id { font-size:0.78rem; color:#68d391; letter-spacing:2px; font-weight:600; }
82
+ .receipt-name { font-size:1.4rem; font-weight:700; color:#1a202c; margin:8px 0 4px; }
83
+ .receipt-row {
84
+ display:flex; justify-content:space-between; padding:8px 0;
85
+ border-bottom:1px solid rgba(0,0,0,0.06); font-size:0.88rem;
86
+ }
87
+ .receipt-row:last-child { border-bottom:none; }
88
+ .receipt-label { color:#718096; }
89
+ .receipt-value { color:#1a202c; font-weight:500; }
90
+ .reminder-box {
91
+ background:#ebf8ff; border:1px solid #90cdf4;
92
+ border-radius:10px; padding:12px 16px; margin-top:14px;
93
+ font-size:0.85rem; color:#2b6cb0;
94
+ }
95
+ .trace-header {
96
+ font-size:0.75rem; font-weight:600; color:#a0aec0;
97
+ text-transform:uppercase; letter-spacing:1px; margin-bottom:6px;
98
+ }
99
+ </style>
100
+ """, unsafe_allow_html=True)
101
+
102
+ # ════════════════════════════════════════════════════════════════════
103
+ # SESSION STATE
104
+ # ════════════════════════════════════════════════════════════════════
105
+ for k, v in [("result", None), ("trace_log", None)]:
106
+ if k not in st.session_state:
107
+ st.session_state[k] = v
108
+
109
+ # ═══���════════════════════════════════════════════════════════════════
110
+ # HERO
111
+ # ════════════════════════════════════════════════════════════════════
112
+ st.markdown("""
113
+ <div class="hero">
114
+ <div class="hero-badge">πŸ€– Google Antigravity Β· Gemini API Β· Streamlit</div>
115
+ <h1>πŸ”§ KhidmatAI</h1>
116
+ <p>AI Service Orchestrator for Pakistan's Informal Economy<br>
117
+ Type your request in <strong>Urdu Β· Roman Urdu Β· English</strong></p>
118
+ </div>
119
+ """, unsafe_allow_html=True)
120
+
121
+ # ════════════════════════════════════════════════════════════════════
122
+ # API KEY STATUS BANNER
123
+ # ════════════════════════════════════════════════════════════════════
124
+ api_key = os.environ.get("GEMINI_API_KEY", "").strip()
125
+ key_valid = bool(api_key and api_key != "your_gemini_api_key_here" and len(api_key) > 20)
126
+
127
+ if not key_valid:
128
+ st.markdown("""
129
+ <div class="key-box">
130
+ <h4>⚠️ Gemini API Key Not Set β€” Running in Demo Mode</h4>
131
+ <p>The app still works using a <strong>rule-based parser</strong>, but Gemini AI gives better results.</p>
132
+ <p>To enable full AI mode:</p>
133
+ <p>
134
+ 1️⃣ Get a <strong>free</strong> key at <code>aistudio.google.com/apikey</code><br>
135
+ 2️⃣ In your HuggingFace Space β†’ <strong>Settings β†’ Variables and Secrets</strong><br>
136
+ 3️⃣ Click <strong>New secret</strong> β†’ Name: <code>GEMINI_API_KEY</code> β†’ paste your key β†’ Save<br>
137
+ 4️⃣ Restart the Space (Settings β†’ Factory reboot)
138
+ </p>
139
+ </div>
140
+ """, unsafe_allow_html=True)
141
+ else:
142
+ st.success("βœ… Gemini API key detected β€” AI mode active")
143
+
144
+ # ════════════════════════════════════════════════════════════════════
145
+ # EXAMPLE BUTTONS
146
+ # ════════════════════════════════════════════════════════════════════
147
+ st.markdown("**Try an example:**")
148
+ examples = [
149
+ "Mujhe kal subah G-13 mein AC technician chahiye",
150
+ "I need a plumber in F-10 today",
151
+ "G-9 mein electrician chahiye aaj evening",
152
+ "Maths tutor chahiye G-11 mein kal",
153
+ "Home maid service chahiye F-7 mein",
154
+ "Driver chahiye kal airport ke liye G-10 se",
155
+ ]
156
+ cols = st.columns(3)
157
+ selected_example = None
158
+ for i, ex in enumerate(examples):
159
+ if cols[i % 3].button(f"πŸ’¬ {ex[:28]}…", key=f"ex_{i}", use_container_width=True):
160
+ selected_example = ex
161
+
162
+ # ════════════════════════════════════════════════════════════════════
163
+ # INPUT
164
+ # ════════════════════════════════════════════════════════════════════
165
+ user_input = st.text_area(
166
+ "Request:",
167
+ value=selected_example or st.session_state.get("last_input", ""),
168
+ height=90,
169
+ placeholder="e.g. Mujhe kal subah G-13 mein AC technician chahiye",
170
+ label_visibility="collapsed",
171
+ )
172
+ c1, c2 = st.columns([3, 1])
173
+ run_btn = c1.button("πŸš€ Find & Book Service", type="primary", use_container_width=True)
174
+ clear_btn = c2.button("πŸ”„ Clear", use_container_width=True)
175
+
176
+ if clear_btn:
177
+ st.session_state.result = None
178
+ st.session_state.trace_log = None
179
+ st.rerun()
180
+
181
+ # ════════════════════════════════════════════════════════════════════
182
+ # PIPELINE
183
+ # ════════════════════════════════════════════════════════════════════
184
+ def agent_card_running(title, desc):
185
+ st.markdown(f"""<div class="agent-card">
186
+ <div class="agent-title">⏳ {title}</div>
187
+ <div class="agent-body">{desc}</div>
188
+ </div>""", unsafe_allow_html=True)
189
+
190
+ def agent_card_done(title, body, ms, mode=None):
191
+ mode_pill = ""
192
+ if mode == "gemini":
193
+ mode_pill = '<span class="mode-pill mode-gemini">Gemini AI</span>'
194
+ elif mode in ("rule_based_fallback", "rules"):
195
+ mode_pill = '<span class="mode-pill mode-rules">Rule-based</span>'
196
+ st.markdown(f"""<div class="agent-card done">
197
+ <div class="agent-title">βœ… {title}{mode_pill} <small style="color:#718096">({ms}ms)</small></div>
198
+ <div class="agent-body">{body}</div>
199
+ </div>""", unsafe_allow_html=True)
200
+
201
+ def agent_card_error(title, err):
202
+ st.markdown(f"""<div class="agent-card error">
203
+ <div class="agent-title">❌ {title} Failed</div>
204
+ <div class="agent-body"><code>{err}</code></div>
205
+ </div>""", unsafe_allow_html=True)
206
+
207
+ def run_pipeline(message: str):
208
+ trace = []
209
+ st.markdown("---")
210
+ st.markdown("### πŸ€– Agent Pipeline")
211
+
212
+ # Agent 1 β€” Intent
213
+ ph1 = st.empty()
214
+ with ph1:
215
+ agent_card_running("Agent 1 β€” Intent Agent", "Parsing your request...")
216
+ t0 = time.time()
217
+ try:
218
+ intent = intent_agent.run(message)
219
+ ms = int((time.time()-t0)*1000)
220
+ warning = intent.pop("_warning", None)
221
+ mode = intent.get("mode", "rules")
222
+ trace.append({"agent":"IntentAgent","step":1,
223
+ "input":{"message":message},"output":intent,
224
+ "duration_ms":ms,
225
+ "reasoning":f"Language={intent.get('language')}, service={intent.get('service_type')}, "
226
+ f"location={intent.get('location')}, time={intent.get('time')} [mode={mode}]"})
227
+ ph1.empty()
228
+ agent_card_done(
229
+ "Agent 1 β€” Intent Agent",
230
+ f"🏷️ <b>{intent.get('service_type','?')}</b> &nbsp;|&nbsp; "
231
+ f"πŸ“ <b>{intent.get('location','?')}</b> &nbsp;|&nbsp; "
232
+ f"πŸ• <b>{intent.get('time','?')}</b> &nbsp;|&nbsp; "
233
+ f"🌐 <b>{intent.get('language','?')}</b>",
234
+ ms, mode
235
+ )
236
+ if warning:
237
+ st.caption(f"ℹ️ {warning}")
238
+ except Exception as e:
239
+ ph1.empty()
240
+ agent_card_error("Agent 1 β€” Intent Agent", str(e))
241
+ return None, trace
242
+
243
+ # Agent 2 β€” Discovery
244
+ ph2 = st.empty()
245
+ with ph2:
246
+ agent_card_running("Agent 2 β€” Discovery Agent", "Searching nearby providers...")
247
+ t0 = time.time()
248
+ try:
249
+ disc = discovery_agent.run(intent)
250
+ ms = int((time.time()-t0)*1000)
251
+ trace.append({"agent":"DiscoveryAgent","step":2,
252
+ "input":intent,"output":{"total_found":disc["total_found"]},
253
+ "duration_ms":ms,
254
+ "reasoning":f"Filtered providers.json for '{intent.get('service_type')}' near {disc['user_location']}. "
255
+ f"Found {disc['total_found']} provider(s). Distance via haversine formula."})
256
+ ph2.empty()
257
+ agent_card_done(
258
+ "Agent 2 β€” Discovery Agent",
259
+ f"Found <b>{disc['total_found']}</b> provider(s) for "
260
+ f"<b>{intent.get('service_type')}</b> near <b>{disc['user_location']}</b>.",
261
+ ms
262
+ )
263
+ except Exception as e:
264
+ ph2.empty()
265
+ agent_card_error("Agent 2 β€” Discovery Agent", str(e))
266
+ return None, trace
267
+
268
+ if disc["total_found"] == 0:
269
+ st.warning("πŸ˜• No providers found. Try a different service or location.")
270
+ return None, trace
271
+
272
+ # Agent 3 β€” Recommendation
273
+ ph3 = st.empty()
274
+ with ph3:
275
+ agent_card_running("Agent 3 β€” Recommendation Agent", "Ranking providers...")
276
+ t0 = time.time()
277
+ try:
278
+ rec = recommendation_agent.run(disc)
279
+ ms = int((time.time()-t0)*1000)
280
+ trace.append({"agent":"RecommendationAgent","step":3,
281
+ "input":{"providers_found":disc["total_found"]},
282
+ "output":{"selected":rec["best_provider"]["name"],"score":rec["best_provider"]["score"]},
283
+ "duration_ms":ms, "reasoning":rec["reasoning"]})
284
+ ph3.empty()
285
+ agent_card_done("Agent 3 β€” Recommendation Agent", rec["reasoning"], ms)
286
+ except Exception as e:
287
+ ph3.empty()
288
+ agent_card_error("Agent 3 β€” Recommendation Agent", str(e))
289
+ return None, trace
290
+
291
+ # Agent 4 β€” Booking
292
+ ph4 = st.empty()
293
+ with ph4:
294
+ agent_card_running("Agent 4 β€” Booking Agent", "Confirming your booking...")
295
+ t0 = time.time()
296
+ try:
297
+ bk = booking_agent.run(rec, intent)
298
+ ms = int((time.time()-t0)*1000)
299
+ trace.append({"agent":"BookingAgent","step":4,
300
+ "input":{"provider":rec["best_provider"]["name"]},
301
+ "output":{"booking_id":bk["booking_id"],"slot":bk["confirmed_slot"]},
302
+ "duration_ms":ms,
303
+ "reasoning":f"Slot {bk['confirmed_slot']} selected. Booking ID: {bk['booking_id']}. Reminder: {bk['reminder_time']}."})
304
+ ph4.empty()
305
+ agent_card_done(
306
+ "Agent 4 β€” Booking Agent",
307
+ f"Booking <b>{bk['booking_id']}</b> confirmed Β· Slot <b>{bk['confirmed_slot']}</b> Β· "
308
+ f"Reminder at <b>{bk['reminder_time']}</b>.",
309
+ ms
310
+ )
311
+ except Exception as e:
312
+ ph4.empty()
313
+ agent_card_error("Agent 4 β€” Booking Agent", str(e))
314
+ return None, trace
315
+
316
+ return {"intent":intent, "discovery":disc, "recommendation":rec, "booking":bk}, trace
317
+
318
+ # ════════════════════════════════════════════════════════════════════
319
+ # TRIGGER
320
+ # ════════════════════════════════════════════════════════════════════
321
+ if run_btn:
322
+ if not user_input.strip():
323
+ st.warning("Please type a service request first.")
324
+ else:
325
+ st.session_state["last_input"] = user_input.strip()
326
+ st.session_state.result = None
327
+ st.session_state.trace_log = None
328
+ result, trace = run_pipeline(user_input.strip())
329
+ st.session_state.result = result
330
+ st.session_state.trace_log = trace
331
+
332
+ # ════════════════════════════════════════════════════════════════════
333
+ # RESULTS
334
+ # ════════════════════════════════════════════════════════════════════
335
+ if st.session_state.result:
336
+ result = st.session_state.result
337
+ rec = result["recommendation"]
338
+ bk = result["booking"]
339
+ disc = result["discovery"]
340
+
341
+ st.markdown("---")
342
+ tab1, tab2, tab3 = st.tabs(["πŸ“‹ Providers", "βœ… Booking Receipt", "πŸ” Agent Trace"])
343
+
344
+ with tab1:
345
+ st.markdown(f"**{disc['total_found']} provider(s) found** β€” ranked by distance Β· rating Β· availability")
346
+ for i, p in enumerate(rec["all_ranked"]):
347
+ is_best = i == 0
348
+ badge = '<span class="badge-best">⭐ TOP PICK</span>' if is_best else ""
349
+ avail = "🟒 Available" if p.get("available") else "πŸ”΄ Unavailable"
350
+ sw = int(p["score"]*100)
351
+ st.markdown(f"""
352
+ <div class="provider-card {'best' if is_best else ''}">
353
+ <div class="provider-name">#{i+1} {p['name']}{badge}</div>
354
+ <div class="provider-meta">
355
+ πŸ“ {p['location']['area']} &nbsp;Β·&nbsp;
356
+ πŸ“ {p['distance_km']} km &nbsp;Β·&nbsp;
357
+ ⭐ {p['rating']} ({p['total_reviews']} reviews) &nbsp;·&nbsp;
358
+ {avail} &nbsp;Β·&nbsp;
359
+ πŸ’° {p.get('price_range','N/A')}
360
+ </div>
361
+ <div class="score-bar"><div class="score-fill" style="width:{sw}%"></div></div>
362
+ <div style="font-size:0.73rem;color:#a0aec0;margin-top:3px;">Score: {p['score']}</div>
363
+ </div>""", unsafe_allow_html=True)
364
+
365
+ with tab2:
366
+ st.markdown(f"""
367
+ <div class="receipt">
368
+ <div class="receipt-id">BOOKING ID: {bk['booking_id']}</div>
369
+ <div class="receipt-name">πŸŽ‰ {bk['provider_name']}</div>
370
+ <div style="color:#48bb78;font-size:0.85rem;margin-bottom:16px;">Booking Confirmed βœ“</div>
371
+ <div class="receipt-row"><span class="receipt-label">πŸ”§ Service</span><span class="receipt-value">{bk['service']}</span></div>
372
+ <div class="receipt-row"><span class="receipt-label">πŸ“ Location</span><span class="receipt-value">{bk['location']}</span></div>
373
+ <div class="receipt-row"><span class="receipt-label">πŸ“… Date</span><span class="receipt-value">{bk['booked_date']}</span></div>
374
+ <div class="receipt-row"><span class="receipt-label">⏰ Slot</span><span class="receipt-value">{bk['confirmed_slot']}</span></div>
375
+ <div class="receipt-row"><span class="receipt-label">πŸ“ž Contact</span><span class="receipt-value">{bk['provider_phone']}</span></div>
376
+ <div class="reminder-box">πŸ”” <b>Reminder</b> set for <b>{bk['reminder_time']}</b> β€” 1 hour before appointment</div>
377
+ </div>""", unsafe_allow_html=True)
378
+
379
+ with tab3:
380
+ st.markdown('<div class="trace-header">Full Agent Decision Log</div>', unsafe_allow_html=True)
381
+ for step in st.session_state.trace_log:
382
+ with st.expander(f"Step {step['step']} β€” {step['agent']} ({step['duration_ms']}ms)"):
383
+ st.markdown(f"**Reasoning:** {step['reasoning']}")
384
+ ca, cb = st.columns(2)
385
+ with ca:
386
+ st.markdown("**Input**"); st.json(step["input"])
387
+ with cb:
388
+ st.markdown("**Output**"); st.json(step["output"])
389
+ st.markdown("**Full JSON:**")
390
+ st.json(st.session_state.trace_log)
391
+
392
+ # ════════════════════════════════════════════════════════════════════
393
+ # FOOTER
394
+ # ═══════════════════════════════════════��════════════════════════════
395
+ st.markdown("---")
396
+ st.markdown(
397
+ "<div style='text-align:center;color:#a0aec0;font-size:0.78rem'>"
398
+ "KhidmatAI Β· Google Antigravity Β· Gemini API Β· Streamlit Β· Hackathon 2025"
399
+ "</div>", unsafe_allow_html=True
400
+ )