prabalGaur commited on
Commit
fdaeae0
·
verified ·
1 Parent(s): 0096402

Upload community_contributions/BernardUdo/week8_capstone_multi_agent.py with huggingface_hub

Browse files
community_contributions/BernardUdo/week8_capstone_multi_agent.py ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import math
6
+ import os
7
+ import random
8
+ import re
9
+ from dataclasses import asdict, dataclass
10
+ from pathlib import Path
11
+ from statistics import mean
12
+ from typing import List
13
+
14
+
15
+ ARTIFACTS_DIR = Path(__file__).resolve().parent / "artifacts"
16
+ MEMORY_PATH = ARTIFACTS_DIR / "memory.json"
17
+
18
+
19
+ @dataclass
20
+ class Deal:
21
+ title: str
22
+ summary: str
23
+ category: str
24
+ listed_price: float
25
+ url: str
26
+
27
+
28
+ @dataclass
29
+ class Opportunity:
30
+ deal: Deal
31
+ specialist_estimate: float
32
+ rag_estimate: float
33
+ ensemble_estimate: float
34
+
35
+ @property
36
+ def discount(self) -> float:
37
+ return self.ensemble_estimate - self.deal.listed_price
38
+
39
+
40
+ def ensure_artifacts() -> None:
41
+ ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
42
+ if not MEMORY_PATH.exists():
43
+ MEMORY_PATH.write_text("[]", encoding="utf-8")
44
+
45
+
46
+ def read_memory() -> list[dict]:
47
+ ensure_artifacts()
48
+ return json.loads(MEMORY_PATH.read_text(encoding="utf-8"))
49
+
50
+
51
+ def write_memory(rows: list[dict]) -> None:
52
+ ensure_artifacts()
53
+ MEMORY_PATH.write_text(json.dumps(rows, indent=2), encoding="utf-8")
54
+
55
+
56
+ class SpecialistAgent:
57
+ """
58
+ Day 1 style specialist pricer.
59
+ If MODAL_ENDPOINT is set, this class can be extended to call a deployed endpoint.
60
+ For this contribution we keep a deterministic local estimate so the project runs anywhere.
61
+ """
62
+
63
+ CATEGORY_BASE = {
64
+ "Electronics": 180.0,
65
+ "Home": 95.0,
66
+ "Tools": 120.0,
67
+ "Toys": 55.0,
68
+ }
69
+
70
+ def estimate(self, deal: Deal) -> float:
71
+ text = f"{deal.title} {deal.summary}".lower()
72
+ base = self.CATEGORY_BASE.get(deal.category, 100.0)
73
+ if re.search(r"\bpro\b", text) or re.search(r"\bpremium\b", text):
74
+ base += 70
75
+ if re.search(r"\bmini\b", text) or re.search(r"\bcompact\b", text):
76
+ base -= 25
77
+ if re.search(r"\bbundle\b", text):
78
+ base += 40
79
+ if re.search(r"\bwireless\b", text):
80
+ base += 20
81
+ return round(max(10.0, base), 2)
82
+
83
+
84
+ class RAGAgent:
85
+ """
86
+ Day 2 style local retrieval estimate using lexical overlap
87
+ against a small reference catalog.
88
+ """
89
+
90
+ def __init__(self) -> None:
91
+ self.reference_catalog = [
92
+ ("Wireless ANC headphones with travel case", 229.0, "Electronics"),
93
+ ("Compact mechanical keyboard with RGB lights", 119.0, "Electronics"),
94
+ ("Cordless drill kit with 2 batteries", 149.0, "Tools"),
95
+ ("Non-stick cookware set 10-piece", 89.0, "Home"),
96
+ ("Smart air fryer oven 6 quart", 129.0, "Home"),
97
+ ("STEM robotics kit for kids", 69.0, "Toys"),
98
+ ("Portable bluetooth speaker waterproof", 99.0, "Electronics"),
99
+ ("Ergonomic office chair with lumbar support", 199.0, "Home"),
100
+ ]
101
+
102
+ @staticmethod
103
+ def _tokens(text: str) -> set[str]:
104
+ return set(re.findall(r"[a-z0-9]+", text.lower()))
105
+
106
+ def estimate(self, deal: Deal) -> float:
107
+ deal_tokens = self._tokens(f"{deal.title} {deal.summary}")
108
+ scored = []
109
+ for description, price, category in self.reference_catalog:
110
+ cat_boost = 1.3 if category == deal.category else 1.0
111
+ overlap = len(deal_tokens.intersection(self._tokens(description)))
112
+ scored.append((overlap * cat_boost, price))
113
+ scored.sort(key=lambda x: x[0], reverse=True)
114
+ top = [price for score, price in scored[:3] if score > 0]
115
+ estimate = mean(top) if top else mean(price for _, price, _ in self.reference_catalog)
116
+ return round(float(estimate), 2)
117
+
118
+
119
+ class EnsembleAgent:
120
+ """
121
+ Day 2 ensemble.
122
+ """
123
+
124
+ def combine(self, specialist_estimate: float, rag_estimate: float) -> float:
125
+ return round(0.55 * specialist_estimate + 0.45 * rag_estimate, 2)
126
+
127
+
128
+ class ScannerAgent:
129
+ """
130
+ Day 3 scanner.
131
+ """
132
+
133
+ def scan(self, limit: int = 8) -> list[Deal]:
134
+ candidates = [
135
+ Deal(
136
+ title="Premium wireless ANC headphones bundle",
137
+ summary="Over-ear bluetooth headphones with ANC and hard case bundle.",
138
+ category="Electronics",
139
+ listed_price=129.99,
140
+ url="https://example.com/deal/headphones-bundle",
141
+ ),
142
+ Deal(
143
+ title="Compact cordless drill kit",
144
+ summary="Drill driver with charger and two lithium batteries.",
145
+ category="Tools",
146
+ listed_price=84.50,
147
+ url="https://example.com/deal/drill-kit",
148
+ ),
149
+ Deal(
150
+ title="Smart 6qt air fryer mini",
151
+ summary="Countertop air fryer with app support and presets.",
152
+ category="Home",
153
+ listed_price=79.99,
154
+ url="https://example.com/deal/air-fryer-mini",
155
+ ),
156
+ Deal(
157
+ title="Kids robotics starter kit",
158
+ summary="STEM learning kit with sensors and coding guide.",
159
+ category="Toys",
160
+ listed_price=39.00,
161
+ url="https://example.com/deal/robotics-kit",
162
+ ),
163
+ Deal(
164
+ title="Mechanical keyboard pro wireless",
165
+ summary="Low-profile wireless keyboard with tactile switches.",
166
+ category="Electronics",
167
+ listed_price=74.99,
168
+ url="https://example.com/deal/keyboard-pro",
169
+ ),
170
+ ]
171
+ random.seed(42)
172
+ random.shuffle(candidates)
173
+ return candidates[: max(1, min(limit, len(candidates)))]
174
+
175
+
176
+ class MessagingAgent:
177
+ """
178
+ Day 3 notification agent.
179
+ """
180
+
181
+ def alert(self, opp: Opportunity) -> str:
182
+ message = (
183
+ f"DEAL ALERT: {opp.deal.title} | listed=${opp.deal.listed_price:.2f} | "
184
+ f"estimated=${opp.ensemble_estimate:.2f} | potential value=${opp.discount:.2f} | {opp.deal.url}"
185
+ )
186
+ print(message)
187
+ return message
188
+
189
+
190
+ class PlanningAgent:
191
+ """
192
+ Day 4 planner.
193
+ """
194
+
195
+ def __init__(self, specialist: SpecialistAgent, rag: RAGAgent, ensemble: EnsembleAgent):
196
+ self.specialist = specialist
197
+ self.rag = rag
198
+ self.ensemble = ensemble
199
+ self.messenger = MessagingAgent()
200
+
201
+ def rank(self, deals: list[Deal]) -> list[Opportunity]:
202
+ opportunities: list[Opportunity] = []
203
+ for deal in deals:
204
+ spec = self.specialist.estimate(deal)
205
+ rag = self.rag.estimate(deal)
206
+ ens = self.ensemble.combine(spec, rag)
207
+ opportunities.append(
208
+ Opportunity(
209
+ deal=deal,
210
+ specialist_estimate=spec,
211
+ rag_estimate=rag,
212
+ ensemble_estimate=ens,
213
+ )
214
+ )
215
+ opportunities.sort(key=lambda opp: opp.discount, reverse=True)
216
+ return opportunities
217
+
218
+ def plan_and_notify(self, deals: list[Deal], top_k: int = 2) -> list[Opportunity]:
219
+ ranked = self.rank(deals)
220
+ for opp in ranked[:top_k]:
221
+ if opp.discount > 20:
222
+ self.messenger.alert(opp)
223
+ return ranked
224
+
225
+
226
+ class Week8CapstoneFramework:
227
+ """
228
+ Day 5 framework and run loop.
229
+ """
230
+
231
+ def __init__(self):
232
+ self.scanner = ScannerAgent()
233
+ self.planner = PlanningAgent(SpecialistAgent(), RAGAgent(), EnsembleAgent())
234
+
235
+ def run_once(self, limit: int = 5) -> list[Opportunity]:
236
+ deals = self.scanner.scan(limit=limit)
237
+ ranked = self.planner.plan_and_notify(deals, top_k=2)
238
+ rows = read_memory()
239
+ for opp in ranked:
240
+ rows.append(
241
+ {
242
+ "title": opp.deal.title,
243
+ "listed_price": opp.deal.listed_price,
244
+ "specialist_estimate": opp.specialist_estimate,
245
+ "rag_estimate": opp.rag_estimate,
246
+ "ensemble_estimate": opp.ensemble_estimate,
247
+ "discount": opp.discount,
248
+ "url": opp.deal.url,
249
+ }
250
+ )
251
+ write_memory(rows[-50:])
252
+ return ranked
253
+
254
+
255
+ def build_eval_set() -> list[tuple[Deal, float]]:
256
+ return [
257
+ (
258
+ Deal(
259
+ title="Wireless ANC headphones premium",
260
+ summary="Bluetooth headphones with active noise cancellation.",
261
+ category="Electronics",
262
+ listed_price=149.0,
263
+ url="https://example.com/eval/1",
264
+ ),
265
+ 225.0,
266
+ ),
267
+ (
268
+ Deal(
269
+ title="Compact cordless drill kit",
270
+ summary="Power drill with 2 batteries and charger.",
271
+ category="Tools",
272
+ listed_price=99.0,
273
+ url="https://example.com/eval/2",
274
+ ),
275
+ 145.0,
276
+ ),
277
+ (
278
+ Deal(
279
+ title="Smart air fryer mini",
280
+ summary="Countertop fryer with touch panel.",
281
+ category="Home",
282
+ listed_price=85.0,
283
+ url="https://example.com/eval/3",
284
+ ),
285
+ 130.0,
286
+ ),
287
+ (
288
+ Deal(
289
+ title="Kids robotics starter kit bundle",
290
+ summary="STEM education pack for beginners.",
291
+ category="Toys",
292
+ listed_price=49.0,
293
+ url="https://example.com/eval/4",
294
+ ),
295
+ 72.0,
296
+ ),
297
+ ]
298
+
299
+
300
+ def mae(y_true: list[float], y_pred: list[float]) -> float:
301
+ return sum(abs(a - b) for a, b in zip(y_true, y_pred)) / max(1, len(y_true))
302
+
303
+
304
+ def rmse(y_true: list[float], y_pred: list[float]) -> float:
305
+ return math.sqrt(sum((a - b) ** 2 for a, b in zip(y_true, y_pred)) / max(1, len(y_true)))
306
+
307
+
308
+ def command_plan(_args: argparse.Namespace) -> None:
309
+ plan = {
310
+ "day1": "Specialist pricing agent with optional remote deployment hook.",
311
+ "day2": "RAG estimate and ensemble combination.",
312
+ "day3": "Scanner and notifier for candidate deals.",
313
+ "day4": "Planning agent for ranking and autonomous notification decisions.",
314
+ "day5": "Framework orchestration and optional Gradio deployment.",
315
+ "deliverables": [
316
+ "week8_capstone_multi_agent.py",
317
+ "week8_capstone_multi_agent.ipynb",
318
+ "artifacts/memory.json",
319
+ ],
320
+ }
321
+ print(json.dumps(plan, indent=2))
322
+
323
+
324
+ def command_build(_args: argparse.Namespace) -> None:
325
+ ensure_artifacts()
326
+ print(f"Artifacts initialized at: {ARTIFACTS_DIR}")
327
+
328
+
329
+ def command_scan(args: argparse.Namespace) -> None:
330
+ framework = Week8CapstoneFramework()
331
+ deals = framework.scanner.scan(limit=args.limit)
332
+ print(json.dumps([asdict(d) for d in deals], indent=2))
333
+
334
+
335
+ def command_run_once(args: argparse.Namespace) -> None:
336
+ framework = Week8CapstoneFramework()
337
+ ranked = framework.run_once(limit=args.limit)
338
+ payload = [
339
+ {
340
+ "title": opp.deal.title,
341
+ "listed_price": opp.deal.listed_price,
342
+ "specialist_estimate": opp.specialist_estimate,
343
+ "rag_estimate": opp.rag_estimate,
344
+ "ensemble_estimate": opp.ensemble_estimate,
345
+ "discount": opp.discount,
346
+ "url": opp.deal.url,
347
+ }
348
+ for opp in ranked
349
+ ]
350
+ print(json.dumps(payload, indent=2))
351
+
352
+
353
+ def command_evaluate(_args: argparse.Namespace) -> None:
354
+ specialist = SpecialistAgent()
355
+ rag = RAGAgent()
356
+ ens = EnsembleAgent()
357
+ eval_data = build_eval_set()
358
+ y_true = [target for _, target in eval_data]
359
+ y_spec = [specialist.estimate(deal) for deal, _ in eval_data]
360
+ y_rag = [rag.estimate(deal) for deal, _ in eval_data]
361
+ y_ens = [ens.combine(s, r) for s, r in zip(y_spec, y_rag)]
362
+ result = {
363
+ "specialist_mae": round(mae(y_true, y_spec), 3),
364
+ "rag_mae": round(mae(y_true, y_rag), 3),
365
+ "ensemble_mae": round(mae(y_true, y_ens), 3),
366
+ "ensemble_rmse": round(rmse(y_true, y_ens), 3),
367
+ }
368
+ print(json.dumps(result, indent=2))
369
+
370
+
371
+ def command_deploy(_args: argparse.Namespace) -> None:
372
+ try:
373
+ import gradio as gr
374
+ except ModuleNotFoundError:
375
+ raise SystemExit("gradio is required for deploy. Install it in your environment first.")
376
+
377
+ specialist = SpecialistAgent()
378
+ rag = RAGAgent()
379
+ ens = EnsembleAgent()
380
+
381
+ def estimate_product(title: str, summary: str, category: str, listed_price: float):
382
+ deal = Deal(
383
+ title=title,
384
+ summary=summary,
385
+ category=category,
386
+ listed_price=float(listed_price),
387
+ url="https://local-ui",
388
+ )
389
+ spec = specialist.estimate(deal)
390
+ rag_est = rag.estimate(deal)
391
+ final = ens.combine(spec, rag_est)
392
+ discount = final - deal.listed_price
393
+ return (
394
+ f"${spec:.2f}",
395
+ f"${rag_est:.2f}",
396
+ f"${final:.2f}",
397
+ f"${discount:.2f}",
398
+ )
399
+
400
+ demo = gr.Interface(
401
+ fn=estimate_product,
402
+ inputs=[
403
+ gr.Textbox(label="Title", value="Wireless ANC headphones bundle"),
404
+ gr.Textbox(label="Summary", value="Bluetooth headphones with travel case and ANC."),
405
+ gr.Dropdown(
406
+ label="Category",
407
+ choices=["Electronics", "Home", "Tools", "Toys"],
408
+ value="Electronics",
409
+ ),
410
+ gr.Number(label="Listed price", value=129.99),
411
+ ],
412
+ outputs=[
413
+ gr.Textbox(label="Specialist estimate"),
414
+ gr.Textbox(label="RAG estimate"),
415
+ gr.Textbox(label="Ensemble estimate"),
416
+ gr.Textbox(label="Potential value"),
417
+ ],
418
+ title="Week 8 Capstone - BernardUdo",
419
+ description="Multi-agent price estimation demo (specialist + RAG + ensemble).",
420
+ )
421
+ preferred_port = int(os.getenv("WEEK8_GRADIO_PORT", "7868"))
422
+ candidate_ports = [preferred_port] + list(range(7869, 7881))
423
+ last_error: Exception | None = None
424
+ for port in candidate_ports:
425
+ try:
426
+ demo.launch(server_name="127.0.0.1", server_port=port, share=False)
427
+ return
428
+ except Exception as exc:
429
+ last_error = exc
430
+ raise SystemExit(f"Unable to launch Gradio on ports {candidate_ports}: {last_error}")
431
+
432
+
433
+ def build_parser() -> argparse.ArgumentParser:
434
+ parser = argparse.ArgumentParser(description="Week 8 multi-agent capstone contribution")
435
+ sub = parser.add_subparsers(dest="command", required=True)
436
+
437
+ p_plan = sub.add_parser("plan", help="Print day-by-day plan and deliverables")
438
+ p_plan.set_defaults(func=command_plan)
439
+
440
+ p_build = sub.add_parser("build", help="Initialize artifacts")
441
+ p_build.set_defaults(func=command_build)
442
+
443
+ p_scan = sub.add_parser("scan", help="Scan candidate deals")
444
+ p_scan.add_argument("--limit", type=int, default=5)
445
+ p_scan.set_defaults(func=command_scan)
446
+
447
+ p_run = sub.add_parser("run-once", help="Run one end-to-end cycle")
448
+ p_run.add_argument("--limit", type=int, default=5)
449
+ p_run.set_defaults(func=command_run_once)
450
+
451
+ p_eval = sub.add_parser("evaluate", help="Run quick offline metrics")
452
+ p_eval.set_defaults(func=command_evaluate)
453
+
454
+ p_deploy = sub.add_parser("deploy", help="Launch local Gradio UI")
455
+ p_deploy.set_defaults(func=command_deploy)
456
+
457
+ return parser
458
+
459
+
460
+ def main() -> None:
461
+ parser = build_parser()
462
+ args = parser.parse_args()
463
+ args.func(args)
464
+
465
+
466
+ if __name__ == "__main__":
467
+ main()