CoderHassan commited on
Commit
59ce1e2
·
verified ·
1 Parent(s): 2c7942a

Upload 10 files

Browse files
README.md CHANGED
@@ -1,14 +1,19 @@
1
  ---
2
  title: KhidmatAI
3
- emoji: 😻
4
- colorFrom: yellow
5
- colorTo: blue
6
  sdk: streamlit
7
- sdk_version: 1.57.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
  ---
2
  title: KhidmatAI
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: streamlit
7
+ sdk_version: 1.35.0
 
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ short_description: AI Service Orchestrator for Pakistan's Informal Economy
12
  ---
13
 
14
+ # 🔧 KhidmatAI AI Service Orchestrator
15
+
16
+ AI-powered service booking for Pakistan's informal economy.
17
+ Type your request in **Urdu · Roman Urdu · English**.
18
+
19
+ Built with **Google Antigravity · Gemini API · Streamlit**.
agents/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # agents package
agents/booking.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent 4 — Booking Agent (writes to data/bookings.json)"""
2
+ import json, uuid, os
3
+ from datetime import datetime, timedelta
4
+ from pathlib import Path
5
+
6
+ BOOKINGS_FILE = Path(__file__).parent.parent / "data" / "bookings.json"
7
+
8
+ TIME_SLOT_MAP = {
9
+ "morning": ["09:00", "10:00", "11:00"],
10
+ "afternoon": ["13:00", "14:00", "15:00"],
11
+ "evening": ["17:00", "18:00", "19:00"],
12
+ "asap": ["09:00", "10:00", "11:00", "13:00", "14:00"],
13
+ }
14
+
15
+ def pick_slot(provider: dict, requested_time: str) -> str:
16
+ available = provider.get("available_slots", [])
17
+ rt = (requested_time or "").lower()
18
+ for key, slots in TIME_SLOT_MAP.items():
19
+ if key in rt:
20
+ for s in slots:
21
+ if s in available:
22
+ return s
23
+ return available[0] if available else "10:00"
24
+
25
+ def run(recommendation: dict, intent: dict) -> dict:
26
+ provider = recommendation.get("best_provider")
27
+ if not provider:
28
+ return {"error": "No provider available to book."}
29
+
30
+ slot = pick_slot(provider, intent.get("time", "morning"))
31
+ booking_id = f"BK-{datetime.now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:5].upper()}"
32
+ remind_time = f"{int(slot.split(':')[0]) - 1:02d}:{slot.split(':')[1]}"
33
+ booked_date = (datetime.now() + timedelta(days=1)).strftime("%A, %d %b %Y")
34
+
35
+ record = {
36
+ "booking_id": booking_id,
37
+ "provider_id": provider["provider_id"],
38
+ "provider_name": provider["name"],
39
+ "service": intent.get("service_type"),
40
+ "location": intent.get("location"),
41
+ "confirmed_slot": slot,
42
+ "booked_date": booked_date,
43
+ "provider_phone": provider.get("phone"),
44
+ "reminder_time": remind_time,
45
+ "created_at": datetime.now().isoformat(),
46
+ "status": "confirmed",
47
+ }
48
+
49
+ # Write to bookings.json
50
+ try:
51
+ existing = json.loads(BOOKINGS_FILE.read_text()) if BOOKINGS_FILE.exists() else []
52
+ existing.append(record)
53
+ BOOKINGS_FILE.write_text(json.dumps(existing, indent=2))
54
+ except Exception:
55
+ pass # HF free tier ephemeral filesystem — non-fatal
56
+
57
+ return record
agents/discovery.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent 2 — Discovery Agent (haversine distance, no Maps API needed on free tier)"""
2
+ import json, math, os
3
+ from pathlib import Path
4
+
5
+ PROVIDERS_FILE = Path(__file__).parent.parent / "data" / "providers.json"
6
+
7
+ AREA_COORDS = {
8
+ "G-13": (33.6844, 73.0479), "G-11": (33.6938, 73.0551),
9
+ "G-10": (33.7020, 73.0397), "G-9": (33.6995, 73.0440),
10
+ "G-6": (33.7290, 73.0935), "F-10": (33.7077, 73.0354),
11
+ "F-7": (33.7192, 73.0587), "E-11": (33.7215, 73.0194),
12
+ "I-8": (33.6741, 73.0632), "I-10": (33.6800, 73.0530),
13
+ "DEFAULT": (33.6844, 73.0479),
14
+ }
15
+
16
+ def haversine(lat1, lon1, lat2, lon2) -> float:
17
+ R = 6371
18
+ dlat = math.radians(lat2 - lat1)
19
+ dlon = math.radians(lon2 - lon1)
20
+ a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
21
+ return round(R * 2 * math.asin(math.sqrt(a)), 2)
22
+
23
+ def run(intent: dict) -> dict:
24
+ service_type = intent.get("service_type") or ""
25
+ location = (intent.get("location") or "DEFAULT").upper().strip()
26
+
27
+ with open(PROVIDERS_FILE) as f:
28
+ all_providers = json.load(f)
29
+
30
+ user_lat, user_lng = AREA_COORDS.get(location, AREA_COORDS["DEFAULT"])
31
+
32
+ matched = []
33
+ for p in all_providers:
34
+ cats = [c.lower() for c in p.get("service_categories", [])]
35
+ if service_type.lower() in cats:
36
+ dist = haversine(user_lat, user_lng, p["location"]["lat"], p["location"]["lng"])
37
+ matched.append({**p, "distance_km": dist})
38
+
39
+ matched.sort(key=lambda x: x["distance_km"])
40
+ return {"providers": matched, "user_location": location, "total_found": len(matched)}
agents/intent.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent 1 — Intent Agent (Gemini API, no paid Maps needed)"""
2
+ import os, json, re
3
+ import google.generativeai as genai
4
+
5
+ def run(message: str) -> dict:
6
+ api_key = os.environ.get("GEMINI_API_KEY", "")
7
+ if not api_key:
8
+ raise ValueError("GEMINI_API_KEY secret not set in HuggingFace Space settings.")
9
+
10
+ genai.configure(api_key=api_key)
11
+ model = genai.GenerativeModel("gemini-1.5-flash") # free-tier model
12
+
13
+ prompt = f"""You are a service request parser for Pakistan's informal economy.
14
+ Extract structured info from the user message. Respond ONLY with valid JSON, no markdown.
15
+
16
+ User message: "{message}"
17
+
18
+ Return exactly this JSON structure:
19
+ {{
20
+ "service_type": "<one of: AC Technician, Plumber, Electrician, Tutor, Beautician, Carpenter, Painter, Driver, Maid, Delivery Worker>",
21
+ "location": "<area name, e.g. G-13>",
22
+ "time": "<e.g. tomorrow morning, today evening, asap>",
23
+ "language": "<urdu | roman_urdu | english | mixed>",
24
+ "confidence": <0.0 to 1.0>
25
+ }}
26
+
27
+ If a field cannot be determined, use null."""
28
+
29
+ response = model.generate_content(prompt)
30
+ raw = response.text.strip()
31
+ raw = re.sub(r"```json|```", "", raw).strip()
32
+ return json.loads(raw)
agents/recommendation.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent 3 — Recommendation Agent"""
2
+
3
+ def compute_score(p: dict, max_dist: float) -> float:
4
+ proximity = 1 - (p["distance_km"] / max_dist) if max_dist > 0 else 1.0
5
+ rating = (p.get("rating", 3) - 1) / 4
6
+ availability = 1.0 if p.get("available") else 0.0
7
+ return round(0.40 * proximity + 0.40 * rating + 0.20 * availability, 4)
8
+
9
+ def run(discovery: dict) -> dict:
10
+ providers = discovery.get("providers", [])
11
+ if not providers:
12
+ return {"best_provider": None, "all_ranked": [], "reasoning": "No providers found."}
13
+
14
+ max_dist = max(p["distance_km"] for p in providers) or 1
15
+ scored = [{**p, "score": compute_score(p, max_dist)} for p in providers]
16
+ scored.sort(key=lambda x: x["score"], reverse=True)
17
+ best = scored[0]
18
+
19
+ reasoning = (
20
+ f"**{best['name']}** selected — "
21
+ f"{best['distance_km']} km away, "
22
+ f"⭐ {best['rating']} rating, "
23
+ f"{'available' if best['available'] else 'unavailable'}. "
24
+ f"Composite score: **{best['score']}**"
25
+ )
26
+ return {"best_provider": best, "all_ranked": scored, "reasoning": reasoning}
app.py ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # ── path so agents/ is importable ───────────────────────────────────
12
+ sys.path.insert(0, str(Path(__file__).parent))
13
+ from agents import intent as intent_agent
14
+ from agents import discovery as discovery_agent
15
+ from agents import recommendation as recommendation_agent
16
+ from agents import booking as booking_agent
17
+
18
+ # ════════════════════════════════════════════════════════════════════
19
+ # PAGE CONFIG
20
+ # ════════════════════════════════════════════════════════════════════
21
+ st.set_page_config(
22
+ page_title="KhidmatAI",
23
+ page_icon="🔧",
24
+ layout="centered",
25
+ )
26
+
27
+ # ════════════════════════════════════════════════════════════════════
28
+ # CUSTOM CSS
29
+ # ════════════════════════════════════════════════════════════════════
30
+ st.markdown("""
31
+ <style>
32
+ /* ── Global ─────────────────────────────────────────────── */
33
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
34
+ html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
35
+
36
+ /* ── Hero banner ────────────────────────────────────────── */
37
+ .hero {
38
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
39
+ border-radius: 16px;
40
+ padding: 32px 28px 24px;
41
+ text-align: center;
42
+ margin-bottom: 24px;
43
+ border: 1px solid rgba(255,255,255,0.08);
44
+ }
45
+ .hero h1 { color: #ffffff; font-size: 2rem; font-weight: 700; margin: 0; letter-spacing: -0.5px; }
46
+ .hero p { color: rgba(255,255,255,0.65); font-size: 0.95rem; margin: 8px 0 0; }
47
+ .hero-badge {
48
+ display: inline-block;
49
+ background: rgba(99,179,237,0.15);
50
+ border: 1px solid rgba(99,179,237,0.3);
51
+ color: #90cdf4;
52
+ font-size: 0.75rem;
53
+ padding: 3px 10px;
54
+ border-radius: 20px;
55
+ margin-bottom: 12px;
56
+ font-weight: 500;
57
+ }
58
+
59
+ /* ── Agent step card ────────────────────────────────────── */
60
+ .agent-card {
61
+ background: #f8fafc;
62
+ border: 1px solid #e2e8f0;
63
+ border-left: 4px solid #4299e1;
64
+ border-radius: 10px;
65
+ padding: 14px 16px;
66
+ margin-bottom: 10px;
67
+ }
68
+ .agent-card.done { border-left-color: #48bb78; background: #f0fff4; }
69
+ .agent-card.error { border-left-color: #fc8181; background: #fff5f5; }
70
+ .agent-title { font-weight: 600; font-size: 0.9rem; color: #2d3748; margin-bottom: 4px; }
71
+ .agent-body { font-size: 0.82rem; color: #4a5568; line-height: 1.5; }
72
+
73
+ /* ── Provider card ──────────────────────────────────────── */
74
+ .provider-card {
75
+ background: #ffffff;
76
+ border: 1.5px solid #e2e8f0;
77
+ border-radius: 12px;
78
+ padding: 16px 18px;
79
+ margin-bottom: 10px;
80
+ transition: border-color 0.2s;
81
+ }
82
+ .provider-card.best { border-color: #48bb78; background: #f0fff4; }
83
+ .provider-name { font-size: 1rem; font-weight: 600; color: #1a202c; }
84
+ .provider-meta { font-size: 0.8rem; color: #718096; margin-top: 4px; }
85
+ .badge-best { background: #c6f6d5; color: #276749; font-size: 0.72rem;
86
+ padding: 2px 8px; border-radius: 8px; font-weight: 600; margin-left: 8px; }
87
+ .score-bar { height: 5px; background: #e2e8f0; border-radius: 3px; margin-top: 8px; }
88
+ .score-fill { height: 5px; background: #48bb78; border-radius: 3px; }
89
+
90
+ /* ── Booking receipt ────────────────────────────────────── */
91
+ .receipt {
92
+ background: linear-gradient(135deg, #f0fff4, #e6fffa);
93
+ border: 2px solid #9ae6b4;
94
+ border-radius: 16px;
95
+ padding: 24px;
96
+ text-align: center;
97
+ }
98
+ .receipt-id { font-size: 0.78rem; color: #68d391; letter-spacing: 2px; font-weight: 600; }
99
+ .receipt-name { font-size: 1.4rem; font-weight: 700; color: #1a202c; margin: 8px 0 4px; }
100
+ .receipt-row { display: flex; justify-content: space-between; padding: 8px 0;
101
+ border-bottom: 1px solid rgba(0,0,0,0.06); font-size: 0.88rem; }
102
+ .receipt-row:last-child { border-bottom: none; }
103
+ .receipt-label { color: #718096; }
104
+ .receipt-value { color: #1a202c; font-weight: 500; }
105
+ .reminder-box {
106
+ background: #ebf8ff; border: 1px solid #90cdf4;
107
+ border-radius: 10px; padding: 12px 16px; margin-top: 14px;
108
+ font-size: 0.85rem; color: #2b6cb0;
109
+ }
110
+
111
+ /* ── Trace log ──────────────────────────────────────────── */
112
+ .trace-header { font-size: 0.75rem; font-weight: 600; color: #a0aec0;
113
+ text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
114
+
115
+ /* ── Input box ──────────────────────────────────────────── */
116
+ .stTextArea textarea {
117
+ font-size: 1rem !important;
118
+ border-radius: 10px !important;
119
+ border: 1.5px solid #cbd5e0 !important;
120
+ }
121
+
122
+ /* ── Example chips ──────────────────────────────────────── */
123
+ .chip {
124
+ display: inline-block;
125
+ background: #edf2f7;
126
+ border: 1px solid #e2e8f0;
127
+ border-radius: 20px;
128
+ padding: 4px 12px;
129
+ font-size: 0.78rem;
130
+ color: #4a5568;
131
+ margin: 3px;
132
+ cursor: pointer;
133
+ }
134
+ </style>
135
+ """, unsafe_allow_html=True)
136
+
137
+ # ════════════════════════════════════════════════════════════════════
138
+ # SESSION STATE
139
+ # ════════════════════════════════════════════════════════════════════
140
+ for key in ["result", "trace_log", "running"]:
141
+ if key not in st.session_state:
142
+ st.session_state[key] = None if key != "running" else False
143
+
144
+ # ════════════════════════════════════════════════════════════════════
145
+ # HERO
146
+ # ════════════════════════════════════════════════════════════════════
147
+ st.markdown("""
148
+ <div class="hero">
149
+ <div class="hero-badge">🤖 Powered by Google Antigravity + Gemini API</div>
150
+ <h1>🔧 KhidmatAI</h1>
151
+ <p>AI Service Orchestrator for Pakistan's Informal Economy<br>
152
+ Type your request in <strong>Urdu · Roman Urdu · English</strong></p>
153
+ </div>
154
+ """, unsafe_allow_html=True)
155
+
156
+ # ════════════════════════════════════════════════════════════════════
157
+ # CHECK API KEY
158
+ # ════════════════════════════════════════════════════════════════════
159
+ api_key = os.environ.get("GEMINI_API_KEY", "")
160
+ if not api_key:
161
+ st.error(
162
+ "⚠️ **GEMINI_API_KEY not set.**\n\n"
163
+ "Go to your HuggingFace Space → **Settings → Variables and Secrets** → "
164
+ "add a secret named `GEMINI_API_KEY` with your Gemini API key.\n\n"
165
+ "Get a free key at: https://aistudio.google.com/apikey"
166
+ )
167
+ st.stop()
168
+
169
+ # ════════════════════════════════════════════════════════════════════
170
+ # EXAMPLE INPUTS
171
+ # ════════════════════════════════════════════════════════════════════
172
+ st.markdown("**Try an example:**")
173
+ examples = [
174
+ "Mujhe kal subah G-13 mein AC technician chahiye",
175
+ "I need a plumber in F-10 today",
176
+ "G-9 mein electrician chahiye aaj evening",
177
+ "Maths tutor chahiye G-11 mein kal",
178
+ "Home maid service chahiye F-7 mein",
179
+ ]
180
+ cols = st.columns(3)
181
+ selected_example = None
182
+ for i, ex in enumerate(examples):
183
+ if cols[i % 3].button(f"💬 {ex[:30]}…", key=f"ex_{i}", use_container_width=True):
184
+ selected_example = ex
185
+
186
+ # ════════════════════════════════════════════════════════════════════
187
+ # INPUT
188
+ # ════════════════════════════════════════════════════════════════════
189
+ default_text = selected_example or ""
190
+ user_input = st.text_area(
191
+ "Your service request:",
192
+ value=default_text,
193
+ height=90,
194
+ placeholder="e.g. Mujhe kal subah G-13 mein AC technician chahiye",
195
+ label_visibility="collapsed",
196
+ )
197
+
198
+ col1, col2 = st.columns([3, 1])
199
+ with col1:
200
+ run_btn = st.button("🚀 Find & Book Service", type="primary", use_container_width=True)
201
+ with col2:
202
+ clear_btn = st.button("🔄 Clear", use_container_width=True)
203
+
204
+ if clear_btn:
205
+ st.session_state.result = None
206
+ st.session_state.trace_log = None
207
+ st.rerun()
208
+
209
+ # ════════════════════════════════════════════════════════════════════
210
+ # AGENT PIPELINE
211
+ # ════════════════════════════════════════════════════════════════════
212
+ def run_pipeline(message: str):
213
+ trace = []
214
+ st.markdown("---")
215
+ st.markdown("### 🤖 Agent Pipeline Running...")
216
+
217
+ # ── Agent 1: Intent ───────────────────────────────────────────
218
+ with st.container():
219
+ st.markdown("""<div class="agent-card">
220
+ <div class="agent-title">🧠 Agent 1 — Intent Agent</div>
221
+ <div class="agent-body">Parsing your request with Gemini API...</div>
222
+ </div>""", unsafe_allow_html=True)
223
+ t0 = time.time()
224
+ try:
225
+ intent = intent_agent.run(message)
226
+ ms = int((time.time() - t0) * 1000)
227
+ trace.append({
228
+ "agent": "IntentAgent", "step": 1,
229
+ "input": {"message": message},
230
+ "output": intent, "duration_ms": ms,
231
+ "reasoning": f"Detected language: {intent.get('language')}. "
232
+ f"Extracted service={intent.get('service_type')}, "
233
+ f"location={intent.get('location')}, time={intent.get('time')}."
234
+ })
235
+ st.markdown(f"""<div class="agent-card done">
236
+ <div class="agent-title">✅ Agent 1 — Intent Agent <small style="color:#718096">({ms}ms)</small></div>
237
+ <div class="agent-body">
238
+ 🏷️ <b>Service:</b> {intent.get('service_type') or '?'} &nbsp;|&nbsp;
239
+ 📍 <b>Location:</b> {intent.get('location') or '?'} &nbsp;|&nbsp;
240
+ 🕐 <b>Time:</b> {intent.get('time') or '?'} &nbsp;|&nbsp;
241
+ 🌐 <b>Language:</b> {intent.get('language') or '?'}
242
+ </div>
243
+ </div>""", unsafe_allow_html=True)
244
+ except Exception as e:
245
+ st.markdown(f"""<div class="agent-card error">
246
+ <div class="agent-title">❌ Agent 1 Failed</div>
247
+ <div class="agent-body">{e}</div>
248
+ </div>""", unsafe_allow_html=True)
249
+ return None, trace
250
+
251
+ # ── Agent 2: Discovery ────────────────────────────────────────
252
+ with st.container():
253
+ t0 = time.time()
254
+ try:
255
+ disc = discovery_agent.run(intent)
256
+ ms = int((time.time() - t0) * 1000)
257
+ trace.append({
258
+ "agent": "DiscoveryAgent", "step": 2,
259
+ "input": intent, "output": {"total_found": disc["total_found"]},
260
+ "duration_ms": ms,
261
+ "reasoning": f"Searched providers.json for '{intent.get('service_type')}' near {disc['user_location']}. "
262
+ f"Found {disc['total_found']} provider(s). Distances computed via haversine formula."
263
+ })
264
+ st.markdown(f"""<div class="agent-card done">
265
+ <div class="agent-title">✅ Agent 2 — Discovery Agent <small style="color:#718096">({ms}ms)</small></div>
266
+ <div class="agent-body">
267
+ Found <b>{disc['total_found']}</b> provider(s) for <b>{intent.get('service_type')}</b>
268
+ near <b>{disc['user_location']}</b>.
269
+ </div>
270
+ </div>""", unsafe_allow_html=True)
271
+ except Exception as e:
272
+ st.markdown(f"""<div class="agent-card error">
273
+ <div class="agent-title">❌ Agent 2 Failed</div>
274
+ <div class="agent-body">{e}</div>
275
+ </div>""", unsafe_allow_html=True)
276
+ return None, trace
277
+
278
+ if disc["total_found"] == 0:
279
+ st.warning("😕 No providers found for this service in your area. Try a different location or service.")
280
+ return None, trace
281
+
282
+ # ── Agent 3: Recommendation ───────────────────────────────────
283
+ with st.container():
284
+ t0 = time.time()
285
+ try:
286
+ rec = recommendation_agent.run(disc)
287
+ ms = int((time.time() - t0) * 1000)
288
+ trace.append({
289
+ "agent": "RecommendationAgent", "step": 3,
290
+ "input": {"providers_found": disc["total_found"]},
291
+ "output": {
292
+ "selected": rec["best_provider"]["name"],
293
+ "score": rec["best_provider"]["score"]
294
+ },
295
+ "duration_ms": ms,
296
+ "reasoning": rec["reasoning"]
297
+ })
298
+ st.markdown(f"""<div class="agent-card done">
299
+ <div class="agent-title">✅ Agent 3 — Recommendation Agent <small style="color:#718096">({ms}ms)</small></div>
300
+ <div class="agent-body">
301
+ {rec['reasoning']}
302
+ </div>
303
+ </div>""", unsafe_allow_html=True)
304
+ except Exception as e:
305
+ st.markdown(f"""<div class="agent-card error">
306
+ <div class="agent-title">❌ Agent 3 Failed</div>
307
+ <div class="agent-body">{e}</div>
308
+ </div>""", unsafe_allow_html=True)
309
+ return None, trace
310
+
311
+ # ── Agent 4: Booking ──────────────────────────────────────────
312
+ with st.container():
313
+ t0 = time.time()
314
+ try:
315
+ bk = booking_agent.run(rec, intent)
316
+ ms = int((time.time() - t0) * 1000)
317
+ trace.append({
318
+ "agent": "BookingAgent", "step": 4,
319
+ "input": {"provider": rec["best_provider"]["name"]},
320
+ "output": {"booking_id": bk["booking_id"], "slot": bk["confirmed_slot"]},
321
+ "duration_ms": ms,
322
+ "reasoning": f"Picked slot {bk['confirmed_slot']} from available morning slots. "
323
+ f"Wrote to bookings.json. Booking ID: {bk['booking_id']}."
324
+ })
325
+ st.markdown(f"""<div class="agent-card done">
326
+ <div class="agent-title">✅ Agent 4 — Booking Agent <small style="color:#718096">({ms}ms)</small></div>
327
+ <div class="agent-body">
328
+ Booking <b>{bk['booking_id']}</b> confirmed for <b>{bk['confirmed_slot']}</b>.
329
+ Reminder set for <b>{bk['reminder_time']}</b>.
330
+ </div>
331
+ </div>""", unsafe_allow_html=True)
332
+ except Exception as e:
333
+ st.markdown(f"""<div class="agent-card error">
334
+ <div class="agent-title">❌ Agent 4 Failed</div>
335
+ <div class="agent-body">{e}</div>
336
+ </div>""", unsafe_allow_html=True)
337
+ return None, trace
338
+
339
+ return {"intent": intent, "discovery": disc, "recommendation": rec, "booking": bk}, trace
340
+
341
+
342
+ # ════════════════════════════════════════════════════════════════════
343
+ # RUN
344
+ # ════════════════════════════════════════════════════════════════════
345
+ if run_btn and user_input.strip():
346
+ st.session_state.result = None
347
+ st.session_state.trace_log = None
348
+ result, trace = run_pipeline(user_input.strip())
349
+ st.session_state.result = result
350
+ st.session_state.trace_log = trace
351
+
352
+ elif run_btn and not user_input.strip():
353
+ st.warning("Please type a service request first.")
354
+
355
+ # ════════════════════════════════════════════════════════════════════
356
+ # RESULTS
357
+ # ════════════════════════════════════════════════════════════════════
358
+ if st.session_state.result:
359
+ result = st.session_state.result
360
+ rec = result["recommendation"]
361
+ bk = result["booking"]
362
+ disc = result["discovery"]
363
+
364
+ st.markdown("---")
365
+
366
+ # ── Tabs ────────────────────────────────────────────────────
367
+ tab1, tab2, tab3 = st.tabs(["📋 Providers Found", "✅ Booking Receipt", "🔍 Agent Trace"])
368
+
369
+ # ── Tab 1: Providers ─────────────────────────────────────────
370
+ with tab1:
371
+ st.markdown(f"**{disc['total_found']} provider(s) found** — ranked by distance · rating · availability")
372
+ for i, p in enumerate(rec["all_ranked"]):
373
+ is_best = i == 0
374
+ badge = '<span class="badge-best">⭐ TOP PICK</span>' if is_best else ""
375
+ avail = "🟢 Available" if p.get("available") else "🔴 Unavailable"
376
+ score_w = int(p["score"] * 100)
377
+ st.markdown(f"""
378
+ <div class="provider-card {'best' if is_best else ''}">
379
+ <div class="provider-name">#{i+1} {p['name']}{badge}</div>
380
+ <div class="provider-meta">
381
+ 📍 {p['location']['area']} ·
382
+ 📏 {p['distance_km']} km ·
383
+ ⭐ {p['rating']} ({p['total_reviews']} reviews) ·
384
+ {avail} ·
385
+ 💰 {p.get('price_range', 'N/A')}
386
+ </div>
387
+ <div class="score-bar"><div class="score-fill" style="width:{score_w}%"></div></div>
388
+ <div style="font-size:0.75rem;color:#a0aec0;margin-top:3px;">Score: {p['score']}</div>
389
+ </div>""", unsafe_allow_html=True)
390
+
391
+ # ── Tab 2: Receipt ────────────────────────────────────────────
392
+ with tab2:
393
+ st.markdown(f"""
394
+ <div class="receipt">
395
+ <div class="receipt-id">BOOKING ID: {bk['booking_id']}</div>
396
+ <div class="receipt-name">🎉 {bk['provider_name']}</div>
397
+ <div style="color:#48bb78;font-size:0.85rem;margin-bottom:16px;">Booking Confirmed</div>
398
+ <div class="receipt-row">
399
+ <span class="receipt-label">🔧 Service</span>
400
+ <span class="receipt-value">{bk['service']}</span>
401
+ </div>
402
+ <div class="receipt-row">
403
+ <span class="receipt-label">📍 Location</span>
404
+ <span class="receipt-value">{bk['location']}</span>
405
+ </div>
406
+ <div class="receipt-row">
407
+ <span class="receipt-label">📅 Date</span>
408
+ <span class="receipt-value">{bk['booked_date']}</span>
409
+ </div>
410
+ <div class="receipt-row">
411
+ <span class="receipt-label">⏰ Slot</span>
412
+ <span class="receipt-value">{bk['confirmed_slot']}</span>
413
+ </div>
414
+ <div class="receipt-row">
415
+ <span class="receipt-label">📞 Contact</span>
416
+ <span class="receipt-value">{bk['provider_phone']}</span>
417
+ </div>
418
+ <div class="reminder-box">
419
+ 🔔 <b>Reminder</b> set for <b>{bk['reminder_time']}</b> — 1 hour before your appointment.
420
+ </div>
421
+ </div>
422
+ """, unsafe_allow_html=True)
423
+
424
+ # ── Tab 3: Agent Trace ────────────────────────────────────────
425
+ with tab3:
426
+ st.markdown('<div class="trace-header">Full Agent Decision Log</div>', unsafe_allow_html=True)
427
+ for step in st.session_state.trace_log:
428
+ with st.expander(
429
+ f"Step {step['step']} — {step['agent']} ({step['duration_ms']}ms)",
430
+ expanded=False
431
+ ):
432
+ st.markdown(f"**Reasoning:** {step['reasoning']}")
433
+ col_a, col_b = st.columns(2)
434
+ with col_a:
435
+ st.markdown("**Input**")
436
+ st.json(step["input"])
437
+ with col_b:
438
+ st.markdown("**Output**")
439
+ st.json(step["output"])
440
+
441
+ st.markdown("**Full trace JSON:**")
442
+ st.json(st.session_state.trace_log)
443
+
444
+ # ════════════════════════════════════════════════════════════════════
445
+ # FOOTER
446
+ # ════════════════════════════════════════════════════════════════════
447
+ st.markdown("---")
448
+ st.markdown(
449
+ "<div style='text-align:center;color:#a0aec0;font-size:0.78rem'>"
450
+ "KhidmatAI · Built with Google Antigravity · Gemini API · Streamlit · "
451
+ "Hackathon Project 2025"
452
+ "</div>",
453
+ unsafe_allow_html=True
454
+ )
data/bookings.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
data/providers.json ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "provider_id": "PRV-001",
4
+ "name": "Ali AC Services",
5
+ "service_categories": ["AC Technician", "HVAC", "Refrigerator Repair"],
6
+ "location": { "area": "G-13", "city": "Islamabad", "lat": 33.6844, "lng": 73.0479 },
7
+ "rating": 4.8,
8
+ "total_reviews": 127,
9
+ "available": true,
10
+ "available_slots": ["09:00", "10:00", "14:00", "16:00"],
11
+ "price_range": "PKR 500-1500",
12
+ "phone": "+92-300-0000001",
13
+ "languages": ["Urdu", "English"]
14
+ },
15
+ {
16
+ "provider_id": "PRV-002",
17
+ "name": "Hassan Plumbing Works",
18
+ "service_categories": ["Plumber"],
19
+ "location": { "area": "G-11", "city": "Islamabad", "lat": 33.6938, "lng": 73.0551 },
20
+ "rating": 4.5,
21
+ "total_reviews": 89,
22
+ "available": true,
23
+ "available_slots": ["08:00", "11:00", "15:00"],
24
+ "price_range": "PKR 400-1200",
25
+ "phone": "+92-300-0000002",
26
+ "languages": ["Urdu"]
27
+ },
28
+ {
29
+ "provider_id": "PRV-003",
30
+ "name": "Bright Electricals",
31
+ "service_categories": ["Electrician"],
32
+ "location": { "area": "F-10", "city": "Islamabad", "lat": 33.7077, "lng": 73.0354 },
33
+ "rating": 4.7,
34
+ "total_reviews": 203,
35
+ "available": true,
36
+ "available_slots": ["09:00", "13:00", "17:00"],
37
+ "price_range": "PKR 600-2000",
38
+ "phone": "+92-300-0000003",
39
+ "languages": ["Urdu", "English"]
40
+ },
41
+ {
42
+ "provider_id": "PRV-004",
43
+ "name": "Sir Kamran Academy",
44
+ "service_categories": ["Tutor"],
45
+ "location": { "area": "G-9", "city": "Islamabad", "lat": 33.6995, "lng": 73.0440 },
46
+ "rating": 4.9,
47
+ "total_reviews": 312,
48
+ "available": true,
49
+ "available_slots": ["16:00", "17:00", "18:00", "19:00"],
50
+ "price_range": "PKR 800-2500 per session",
51
+ "phone": "+92-300-0000004",
52
+ "languages": ["Urdu", "English"]
53
+ },
54
+ {
55
+ "provider_id": "PRV-005",
56
+ "name": "Nadia Beauty Home Service",
57
+ "service_categories": ["Beautician"],
58
+ "location": { "area": "G-13", "city": "Islamabad", "lat": 33.6821, "lng": 73.0511 },
59
+ "rating": 4.6,
60
+ "total_reviews": 78,
61
+ "available": true,
62
+ "available_slots": ["10:00", "12:00", "14:00", "16:00"],
63
+ "price_range": "PKR 1000-5000",
64
+ "phone": "+92-300-0000005",
65
+ "languages": ["Urdu"]
66
+ },
67
+ {
68
+ "provider_id": "PRV-006",
69
+ "name": "Master Woodcraft",
70
+ "service_categories": ["Carpenter"],
71
+ "location": { "area": "I-8", "city": "Islamabad", "lat": 33.6741, "lng": 73.0632 },
72
+ "rating": 4.4,
73
+ "total_reviews": 55,
74
+ "available": true,
75
+ "available_slots": ["08:00", "10:00", "14:00"],
76
+ "price_range": "PKR 700-3000",
77
+ "phone": "+92-300-0000006",
78
+ "languages": ["Urdu"]
79
+ },
80
+ {
81
+ "provider_id": "PRV-007",
82
+ "name": "ColorPro Painters",
83
+ "service_categories": ["Painter"],
84
+ "location": { "area": "E-11", "city": "Islamabad", "lat": 33.7215, "lng": 73.0194 },
85
+ "rating": 4.3,
86
+ "total_reviews": 42,
87
+ "available": false,
88
+ "available_slots": [],
89
+ "price_range": "PKR 2000-8000 per room",
90
+ "phone": "+92-300-0000007",
91
+ "languages": ["Urdu", "Punjabi"]
92
+ },
93
+ {
94
+ "provider_id": "PRV-008",
95
+ "name": "Speedy Drivers Islamabad",
96
+ "service_categories": ["Driver"],
97
+ "location": { "area": "G-10", "city": "Islamabad", "lat": 33.7020, "lng": 73.0397 },
98
+ "rating": 4.7,
99
+ "total_reviews": 189,
100
+ "available": true,
101
+ "available_slots": ["06:00", "08:00", "10:00", "14:00", "18:00", "20:00"],
102
+ "price_range": "PKR 300-1500 per trip",
103
+ "phone": "+92-300-0000008",
104
+ "languages": ["Urdu", "English"]
105
+ },
106
+ {
107
+ "provider_id": "PRV-009",
108
+ "name": "CleanHome Maids",
109
+ "service_categories": ["Maid"],
110
+ "location": { "area": "F-7", "city": "Islamabad", "lat": 33.7192, "lng": 73.0587 },
111
+ "rating": 4.5,
112
+ "total_reviews": 134,
113
+ "available": true,
114
+ "available_slots": ["08:00", "09:00", "10:00"],
115
+ "price_range": "PKR 1200-3500 per visit",
116
+ "phone": "+92-300-0000009",
117
+ "languages": ["Urdu"]
118
+ },
119
+ {
120
+ "provider_id": "PRV-010",
121
+ "name": "FastDeliver Islamabad",
122
+ "service_categories": ["Delivery Worker"],
123
+ "location": { "area": "G-6", "city": "Islamabad", "lat": 33.7290, "lng": 73.0935 },
124
+ "rating": 4.2,
125
+ "total_reviews": 267,
126
+ "available": true,
127
+ "available_slots": ["09:00", "11:00", "13:00", "15:00", "17:00"],
128
+ "price_range": "PKR 150-500 per delivery",
129
+ "phone": "+92-300-0000010",
130
+ "languages": ["Urdu", "English"]
131
+ }
132
+ ]
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ streamlit==1.35.0
2
+ google-generativeai==0.7.2
3
+ python-dotenv==1.0.1