MBG0903 commited on
Commit
88fd50b
Β·
verified Β·
1 Parent(s): 4065ea5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +158 -125
app.py CHANGED
@@ -1,33 +1,17 @@
1
- # app.py β€” Streamlit Hugging Face Space (Standalone Prototype)
2
- # Features:
3
- # - Auto-creates dummy supplier dataset (~50 suppliers)
4
- # - Parses inquiry (qty/brand/wattage/category/location)
5
- # - Agent-style steps + supplier ranking
6
- # - 3 pricing modes (Competitive/Balanced/High Margin)
7
- # - New-product fallback flow
8
- #
9
- # requirements.txt:
10
- # streamlit
11
- # pandas
12
-
13
  import os
14
  import re
15
  import random
16
  from datetime import datetime, timedelta
17
 
18
  import pandas as pd
19
- import streamlit as st
20
-
21
- st.set_page_config(page_title="AI Lighting Quotation Agent", layout="wide")
22
- st.title("πŸ’‘ AI Lighting Quotation Agent (Prototype)")
23
- st.caption("Paste inquiry β†’ AI extracts specs β†’ ranks suppliers β†’ recommends pricing β†’ generates quote draft")
24
-
25
- DATA_PATH = "supplier_db.csv"
26
- random.seed(42)
27
 
28
  # ----------------------------
29
- # Dummy supplier data
30
  # ----------------------------
 
 
 
31
  PRODUCT_CATALOG = [
32
  ("LED Panel", ["panel", "led panel", "ceiling panel"], (14, 28)),
33
  ("Downlight", ["downlight", "spot", "spotlight"], (6, 18)),
@@ -42,11 +26,16 @@ PRODUCT_CATALOG = [
42
  BRANDS = ["Philips", "Osram", "Panasonic", "Schneider", "Opple", "NVC", "Crompton", "Wipro", "Havells", "Generic"]
43
  REGIONS = ["SG Central", "SG East", "SG West", "SG North", "Johor", "KL", "Batam"]
44
 
 
 
 
 
45
  def make_supplier_name(i: int) -> str:
46
  prefixes = ["Bright", "Nova", "Apex", "Luma", "Spark", "Prime", "Zen", "Vertex", "Delta", "Orion"]
47
  suffixes = ["Lighting", "Electrics", "Solutions", "Supply", "Traders", "Distributors", "Imports", "Wholesale", "Mart", "Hub"]
48
  return f"{random.choice(prefixes)} {random.choice(suffixes)} Pte Ltd #{i:02d}"
49
 
 
50
  def generate_supplier_db(n_suppliers: int = 50) -> pd.DataFrame:
51
  rows = []
52
  for i in range(1, n_suppliers + 1):
@@ -58,7 +47,6 @@ def generate_supplier_db(n_suppliers: int = 50) -> pd.DataFrame:
58
  moq = random.choice([1, 5, 10, 20, 30, 50])
59
  competitiveness = round(random.uniform(0.85, 1.20), 2)
60
  brands_supported = random.sample(BRANDS, k=random.randint(2, 5))
61
-
62
  rows.append({
63
  "supplier_id": f"SUP-{1000+i}",
64
  "supplier_name": supplier,
@@ -74,21 +62,35 @@ def generate_supplier_db(n_suppliers: int = 50) -> pd.DataFrame:
74
  })
75
  return pd.DataFrame(rows)
76
 
 
77
  def load_or_create_db() -> pd.DataFrame:
78
  if os.path.exists(DATA_PATH):
79
- return pd.read_csv(DATA_PATH)
 
 
 
80
  df = generate_supplier_db(50)
81
  df.to_csv(DATA_PATH, index=False)
82
  return df
83
 
84
- df_suppliers = load_or_create_db()
 
 
 
 
 
 
 
 
 
85
 
86
  # ----------------------------
87
- # Parser (no LLM)
88
  # ----------------------------
89
  def normalize_text(t: str) -> str:
90
  return re.sub(r"\s+", " ", (t or "").strip().lower())
91
 
 
92
  def detect_quantity(text: str):
93
  patterns = [
94
  r"\bqty[:\s]*([0-9]{1,5})\b",
@@ -101,17 +103,20 @@ def detect_quantity(text: str):
101
  return int(m.group(1))
102
  return None
103
 
 
104
  def detect_wattage(text: str):
105
  m = re.search(r"\b([0-9]{1,4})\s*(w|watt|watts)\b", text, flags=re.IGNORECASE)
106
  return int(m.group(1)) if m else None
107
 
 
108
  def detect_brand(text: str):
109
- t = text.lower()
110
  for b in BRANDS:
111
  if b.lower() in t:
112
  return b
113
  return None
114
 
 
115
  def detect_category(text: str):
116
  t = normalize_text(text)
117
  for category, keywords, _rng in PRODUCT_CATALOG:
@@ -120,6 +125,7 @@ def detect_category(text: str):
120
  return category
121
  return None
122
 
 
123
  def detect_location(text: str):
124
  t = normalize_text(text)
125
  loc_map = {
@@ -138,9 +144,9 @@ def detect_location(text: str):
138
  return v
139
  return None
140
 
 
141
  def parse_inquiry(text: str) -> dict:
142
  return {
143
- "raw_text": (text or "").strip(),
144
  "quantity": detect_quantity(text) or 10,
145
  "wattage": detect_wattage(text),
146
  "brand": detect_brand(text),
@@ -148,6 +154,7 @@ def parse_inquiry(text: str) -> dict:
148
  "location": detect_location(text),
149
  }
150
 
 
151
  def estimate_market_range(category: str | None, wattage: int | None):
152
  if not category:
153
  return (10.0, 40.0)
@@ -166,12 +173,14 @@ def estimate_market_range(category: str | None, wattage: int | None):
166
  hi = hi * (0.85 + 0.20 * scale)
167
  return (round(lo, 2), round(hi, 2))
168
 
 
169
  def pick_margin(pricing_mode: str, base_margin: float):
170
  if pricing_mode == "Competitive":
171
  return max(5, base_margin - 6)
172
  if pricing_mode == "High Margin":
173
  return min(40, base_margin + 8)
174
- return base_margin # Balanced
 
175
 
176
  def compute_offers(req: dict, suppliers: pd.DataFrame, margin_pct: float):
177
  category = req.get("category")
@@ -205,13 +214,10 @@ def compute_offers(req: dict, suppliers: pd.DataFrame, margin_pct: float):
205
  supplier_cost *= 1.05
206
 
207
  supplier_cost = round(supplier_cost, 2)
208
- sell_price = supplier_cost / (1 - margin_pct / 100.0)
209
- sell_price = round(sell_price, 2)
210
 
211
  reliability = float(s["reliability_score"])
212
  lead = int(s["lead_time_days"])
213
-
214
- # Score: cheaper + reliable + fast lead
215
  score = (1 / max(sell_price, 0.01)) * 100 + reliability * 10 + (1 / max(lead, 1)) * 5
216
 
217
  rows.append({
@@ -230,97 +236,29 @@ def compute_offers(req: dict, suppliers: pd.DataFrame, margin_pct: float):
230
  offers = pd.DataFrame(rows).sort_values("score", ascending=False).head(10).reset_index(drop=True)
231
  return offers, (market_lo, market_hi)
232
 
233
- # ----------------------------
234
- # Sidebar
235
- # ----------------------------
236
- st.sidebar.header("βš™οΈ Prototype Controls")
237
- base_margin = st.sidebar.slider("Base Margin (%)", 5, 40, 20, 1)
238
- pricing_mode = st.sidebar.radio("Pricing Mode", ["Balanced", "Competitive", "High Margin"], index=0)
239
- top_n = st.sidebar.slider("Top offers to show", 3, 10, 5, 1)
240
-
241
- with st.sidebar.expander("πŸ“¦ Supplier Database", expanded=False):
242
- st.write(f"Suppliers loaded: **{len(df_suppliers)}**")
243
- st.download_button(
244
- "Download supplier_db.csv",
245
- data=open(DATA_PATH, "rb").read(),
246
- file_name="supplier_db.csv",
247
- mime="text/csv",
248
- use_container_width=True,
249
- )
250
- if st.button("Regenerate dummy DB (50 suppliers)"):
251
- df_suppliers = generate_supplier_db(50)
252
- df_suppliers.to_csv(DATA_PATH, index=False)
253
- st.success("supplier_db.csv regenerated")
254
- st.rerun()
255
-
256
- # ----------------------------
257
- # Main
258
- # ----------------------------
259
- left, right = st.columns([1.2, 1.0], gap="large")
260
-
261
- with left:
262
- st.subheader("1) Paste Customer Inquiry")
263
- sample = "Hi, please quote best price for 50 pcs Philips 18W LED panel light. Delivery to Singapore in 2 weeks."
264
- inquiry = st.text_area("Inquiry", value=sample, height=150)
265
-
266
- req = parse_inquiry(inquiry)
267
-
268
- st.subheader("2) Agent Step: Requirement Extraction")
269
- st.json(req)
270
-
271
- with right:
272
- st.subheader("3) Agent Step: Market Intelligence (Demo)")
273
- market_lo, market_hi = estimate_market_range(req.get("category"), req.get("wattage"))
274
- st.metric("Estimated market low (SGD/unit)", f"{market_lo:.2f}")
275
- st.metric("Estimated market high (SGD/unit)", f"{market_hi:.2f}")
276
- st.caption("Heuristic estimate for demo. Later replace with real web/catalog research agent.")
277
-
278
- st.divider()
279
-
280
- margin_to_use = pick_margin(pricing_mode, base_margin)
281
- st.subheader("4) Agent Step: Supplier Shortlist + Pricing Recommendation")
282
- st.caption(f"Pricing mode: **{pricing_mode}** β†’ Margin applied: **{margin_to_use:.0f}%**")
283
-
284
- offers_df, market_rng = compute_offers(req, df_suppliers, margin_to_use)
285
-
286
- if offers_df.empty:
287
- st.error("No matching suppliers found in internal database.")
288
- st.markdown("### πŸ†• New Product / No-Match Mode (Prototype)")
289
- st.write("**What the agent would do next:**")
290
- st.write("1) Search market range online (by category + specs).")
291
- st.write("2) Identify supplier categories that can support this product.")
292
- st.write("3) Prepare a supplier outreach list and request quotes automatically.")
293
- st.write("4) Update internal catalog once confirmed.")
294
- else:
295
- st.dataframe(offers_df.head(top_n), use_container_width=True)
296
-
297
- best = offers_df.iloc[0].to_dict()
298
- st.success(
299
- f"Recommended supplier: **{best['supplier_name']}** | "
300
- f"Cost **SGD {best['est_supplier_cost_sgd']:.2f}** β†’ Sell **SGD {best['recommended_sell_price_sgd']:.2f}** | "
301
- f"Lead **{best['lead_time_days']}d** | Reliability **{best['reliability_score']:.2f}**"
302
- )
303
 
304
- st.divider()
 
 
 
 
 
 
 
 
 
305
 
306
- st.subheader("5) Quote Draft (Copy/Paste Demo)")
307
- company_name = st.text_input("Your company name", value="Delight Lighting (Demo)")
308
- customer_name = st.text_input("Customer name", value="Customer")
309
- quote_valid_days = st.number_input("Quote validity (days)", min_value=1, max_value=30, value=7)
310
-
311
- if offers_df.empty:
312
- st.info("Once the agent finds matching suppliers, the quote draft will appear here.")
313
- else:
314
  qty = int(req.get("quantity") or 10)
315
  category = req.get("category") or "Lighting Product"
316
  brand = req.get("brand") or "Brand-agnostic"
317
  wattage = f"{req.get('wattage')}W" if req.get("wattage") else ""
318
  unit_price = float(offers_df.iloc[0]["recommended_sell_price_sgd"])
319
  total = round(unit_price * qty, 2)
 
320
 
321
- valid_until = (datetime.today() + timedelta(days=int(quote_valid_days))).strftime("%Y-%m-%d")
322
 
323
- quote_text = f"""Subject: Quotation - {brand} {wattage} {category} (Qty: {qty})
324
 
325
  Hi {customer_name},
326
 
@@ -331,23 +269,118 @@ Quantity: {qty}
331
  Unit Price: SGD {unit_price:.2f}
332
  Total: SGD {total:.2f}
333
 
334
- Estimated Lead Time: {int(offers_df.iloc[0]["lead_time_days"])} days
335
  Validity: Until {valid_until}
336
- Terms: 50% advance, balance before delivery (demo terms)
337
-
338
- If you’d like, we can share alternate pricing options as well.
339
 
340
  Regards,
341
  Sales Team
342
  {company_name}
343
  """
344
- st.text_area("Generated Quote Draft", value=quote_text, height=240)
345
- st.download_button(
346
- "Download quote draft (.txt)",
347
- data=quote_text.encode("utf-8"),
348
- file_name="quote_draft.txt",
349
- mime="text/plain",
350
- use_container_width=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  )
352
 
353
- st.caption("Next upgrade: real LLM extraction + auto supplier outreach + PDF quote + CRM integration once Zoho usage is confirmed.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import re
3
  import random
4
  from datetime import datetime, timedelta
5
 
6
  import pandas as pd
7
+ import gradio as gr
 
 
 
 
 
 
 
8
 
9
  # ----------------------------
10
+ # Config
11
  # ----------------------------
12
+ random.seed(42)
13
+ DATA_PATH = os.environ.get("SUPPLIER_DB_PATH", "/tmp/supplier_db.csv")
14
+
15
  PRODUCT_CATALOG = [
16
  ("LED Panel", ["panel", "led panel", "ceiling panel"], (14, 28)),
17
  ("Downlight", ["downlight", "spot", "spotlight"], (6, 18)),
 
26
  BRANDS = ["Philips", "Osram", "Panasonic", "Schneider", "Opple", "NVC", "Crompton", "Wipro", "Havells", "Generic"]
27
  REGIONS = ["SG Central", "SG East", "SG West", "SG North", "Johor", "KL", "Batam"]
28
 
29
+
30
+ # ----------------------------
31
+ # Dummy Supplier DB
32
+ # ----------------------------
33
  def make_supplier_name(i: int) -> str:
34
  prefixes = ["Bright", "Nova", "Apex", "Luma", "Spark", "Prime", "Zen", "Vertex", "Delta", "Orion"]
35
  suffixes = ["Lighting", "Electrics", "Solutions", "Supply", "Traders", "Distributors", "Imports", "Wholesale", "Mart", "Hub"]
36
  return f"{random.choice(prefixes)} {random.choice(suffixes)} Pte Ltd #{i:02d}"
37
 
38
+
39
  def generate_supplier_db(n_suppliers: int = 50) -> pd.DataFrame:
40
  rows = []
41
  for i in range(1, n_suppliers + 1):
 
47
  moq = random.choice([1, 5, 10, 20, 30, 50])
48
  competitiveness = round(random.uniform(0.85, 1.20), 2)
49
  brands_supported = random.sample(BRANDS, k=random.randint(2, 5))
 
50
  rows.append({
51
  "supplier_id": f"SUP-{1000+i}",
52
  "supplier_name": supplier,
 
62
  })
63
  return pd.DataFrame(rows)
64
 
65
+
66
  def load_or_create_db() -> pd.DataFrame:
67
  if os.path.exists(DATA_PATH):
68
+ try:
69
+ return pd.read_csv(DATA_PATH)
70
+ except Exception:
71
+ pass
72
  df = generate_supplier_db(50)
73
  df.to_csv(DATA_PATH, index=False)
74
  return df
75
 
76
+
77
+ SUPPLIERS_DF = load_or_create_db()
78
+
79
+
80
+ def regenerate_db():
81
+ global SUPPLIERS_DF
82
+ SUPPLIERS_DF = generate_supplier_db(50)
83
+ SUPPLIERS_DF.to_csv(DATA_PATH, index=False)
84
+ return SUPPLIERS_DF.head(10), DATA_PATH
85
+
86
 
87
  # ----------------------------
88
+ # Lightweight inquiry parsing (no LLM)
89
  # ----------------------------
90
  def normalize_text(t: str) -> str:
91
  return re.sub(r"\s+", " ", (t or "").strip().lower())
92
 
93
+
94
  def detect_quantity(text: str):
95
  patterns = [
96
  r"\bqty[:\s]*([0-9]{1,5})\b",
 
103
  return int(m.group(1))
104
  return None
105
 
106
+
107
  def detect_wattage(text: str):
108
  m = re.search(r"\b([0-9]{1,4})\s*(w|watt|watts)\b", text, flags=re.IGNORECASE)
109
  return int(m.group(1)) if m else None
110
 
111
+
112
  def detect_brand(text: str):
113
+ t = (text or "").lower()
114
  for b in BRANDS:
115
  if b.lower() in t:
116
  return b
117
  return None
118
 
119
+
120
  def detect_category(text: str):
121
  t = normalize_text(text)
122
  for category, keywords, _rng in PRODUCT_CATALOG:
 
125
  return category
126
  return None
127
 
128
+
129
  def detect_location(text: str):
130
  t = normalize_text(text)
131
  loc_map = {
 
144
  return v
145
  return None
146
 
147
+
148
  def parse_inquiry(text: str) -> dict:
149
  return {
 
150
  "quantity": detect_quantity(text) or 10,
151
  "wattage": detect_wattage(text),
152
  "brand": detect_brand(text),
 
154
  "location": detect_location(text),
155
  }
156
 
157
+
158
  def estimate_market_range(category: str | None, wattage: int | None):
159
  if not category:
160
  return (10.0, 40.0)
 
173
  hi = hi * (0.85 + 0.20 * scale)
174
  return (round(lo, 2), round(hi, 2))
175
 
176
+
177
  def pick_margin(pricing_mode: str, base_margin: float):
178
  if pricing_mode == "Competitive":
179
  return max(5, base_margin - 6)
180
  if pricing_mode == "High Margin":
181
  return min(40, base_margin + 8)
182
+ return base_margin
183
+
184
 
185
  def compute_offers(req: dict, suppliers: pd.DataFrame, margin_pct: float):
186
  category = req.get("category")
 
214
  supplier_cost *= 1.05
215
 
216
  supplier_cost = round(supplier_cost, 2)
217
+ sell_price = round(supplier_cost / (1 - margin_pct / 100.0), 2)
 
218
 
219
  reliability = float(s["reliability_score"])
220
  lead = int(s["lead_time_days"])
 
 
221
  score = (1 / max(sell_price, 0.01)) * 100 + reliability * 10 + (1 / max(lead, 1)) * 5
222
 
223
  rows.append({
 
236
  offers = pd.DataFrame(rows).sort_values("score", ascending=False).head(10).reset_index(drop=True)
237
  return offers, (market_lo, market_hi)
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
+ def build_quote_text(req, offers_df, pricing_mode, margin_used, company_name, customer_name, valid_days):
241
+ if offers_df.empty:
242
+ return (
243
+ "No matching suppliers found.\n\n"
244
+ "New Product Mode:\n"
245
+ "1) Search market range online\n"
246
+ "2) Identify supplier categories\n"
247
+ "3) Send RFQs to shortlisted suppliers\n"
248
+ "4) Update internal catalog once confirmed\n"
249
+ )
250
 
 
 
 
 
 
 
 
 
251
  qty = int(req.get("quantity") or 10)
252
  category = req.get("category") or "Lighting Product"
253
  brand = req.get("brand") or "Brand-agnostic"
254
  wattage = f"{req.get('wattage')}W" if req.get("wattage") else ""
255
  unit_price = float(offers_df.iloc[0]["recommended_sell_price_sgd"])
256
  total = round(unit_price * qty, 2)
257
+ valid_until = (datetime.today() + timedelta(days=int(valid_days))).strftime("%Y-%m-%d")
258
 
259
+ best = offers_df.iloc[0]
260
 
261
+ return f"""Subject: Quotation - {brand} {wattage} {category} (Qty: {qty})
262
 
263
  Hi {customer_name},
264
 
 
269
  Unit Price: SGD {unit_price:.2f}
270
  Total: SGD {total:.2f}
271
 
272
+ Estimated Lead Time: {int(best["lead_time_days"])} days
273
  Validity: Until {valid_until}
274
+ Pricing Mode: {pricing_mode} (Margin applied: {margin_used:.0f}%)
 
 
275
 
276
  Regards,
277
  Sales Team
278
  {company_name}
279
  """
280
+
281
+
282
+ def run_agent(inquiry_text, base_margin, pricing_mode, top_n, company_name, customer_name, valid_days):
283
+ req = parse_inquiry(inquiry_text)
284
+ margin_used = pick_margin(pricing_mode, float(base_margin))
285
+
286
+ offers_df, market_rng = compute_offers(req, SUPPLIERS_DF, margin_used)
287
+
288
+ market_text = "Estimated market range: "
289
+ if market_rng:
290
+ market_text += f"SGD {market_rng[0]:.2f} – {market_rng[1]:.2f} per unit"
291
+ else:
292
+ lo, hi = estimate_market_range(req.get("category"), req.get("wattage"))
293
+ market_text += f"SGD {lo:.2f} – {hi:.2f} per unit"
294
+
295
+ steps = []
296
+ steps.append(f"Step 1 β€” Extracted requirement: {req}")
297
+ steps.append(f"Step 2 β€” Market intelligence: {market_text}")
298
+ steps.append(f"Step 3 β€” Pricing mode: {pricing_mode} | Margin applied: {margin_used:.0f}%")
299
+ if offers_df.empty:
300
+ steps.append("Step 4 β€” No internal matches found β†’ New Product Mode triggered.")
301
+ else:
302
+ steps.append(f"Step 4 β€” Shortlisted {min(len(offers_df), int(top_n))} suppliers; top recommendation: {offers_df.iloc[0]['supplier_name']}")
303
+
304
+ offers_view = offers_df.head(int(top_n)) if not offers_df.empty else pd.DataFrame(
305
+ columns=["supplier_id","supplier_name","region","reliability_score","lead_time_days","moq","est_supplier_cost_sgd","recommended_sell_price_sgd","score","contact_email"]
306
+ )
307
+
308
+ quote_text = build_quote_text(req, offers_df, pricing_mode, margin_used, company_name, customer_name, valid_days)
309
+
310
+ # Write quote text to a file for download
311
+ quote_path = "/tmp/quote_draft.txt"
312
+ with open(quote_path, "w", encoding="utf-8") as f:
313
+ f.write(quote_text)
314
+
315
+ # Return: parsed req (json-like), agent steps, market text, offers table, quote text, downloadable file, downloadable csv
316
+ return req, "\n".join(steps), market_text, offers_view, quote_text, quote_path, DATA_PATH
317
+
318
+
319
+ # ----------------------------
320
+ # UI (Gradio)
321
+ # ----------------------------
322
+ with gr.Blocks(title="Delight AI Agent (Prototype)") as demo:
323
+ gr.Markdown(
324
+ """
325
+ # πŸ’‘ Delight AI Agent (Prototype)
326
+ Paste a customer inquiry β†’ agent extracts requirement β†’ ranks suppliers β†’ recommends pricing β†’ generates quotation draft.
327
+ """
328
+ )
329
+
330
+ with gr.Row():
331
+ inquiry = gr.Textbox(
332
+ label="Customer Inquiry",
333
+ lines=6,
334
+ value="Hi, please quote best price for 50 pcs Philips 18W LED panel light. Delivery to Singapore in 2 weeks."
335
+ )
336
+
337
+ with gr.Row():
338
+ base_margin = gr.Slider(5, 40, value=20, step=1, label="Base Margin (%)")
339
+ pricing_mode = gr.Radio(["Balanced", "Competitive", "High Margin"], value="Balanced", label="Pricing Mode")
340
+ top_n = gr.Slider(3, 10, value=5, step=1, label="Top offers to show")
341
+
342
+ with gr.Row():
343
+ company_name = gr.Textbox(label="Your company name", value="Delight Lighting (Demo)")
344
+ customer_name = gr.Textbox(label="Customer name", value="Customer")
345
+ valid_days = gr.Slider(1, 30, value=7, step=1, label="Quote validity (days)")
346
+
347
+ run_btn = gr.Button("πŸš€ Run Agent")
348
+
349
+ with gr.Row():
350
+ parsed_req = gr.JSON(label="Extracted Requirement")
351
+ agent_steps = gr.Textbox(label="Agent Steps", lines=10)
352
+
353
+ market_info = gr.Textbox(label="Market Intelligence", lines=2)
354
+
355
+ offers_table = gr.Dataframe(
356
+ label="Recommended Supplier Options (Top N)",
357
+ interactive=False,
358
+ wrap=True
359
+ )
360
+
361
+ quote_text = gr.Textbox(label="Generated Quote Draft", lines=14)
362
+
363
+ with gr.Row():
364
+ quote_file = gr.File(label="Download Quote Draft (.txt)")
365
+ supplier_csv = gr.File(label="Download Supplier DB (.csv)")
366
+
367
+ with gr.Accordion("βš™οΈ Admin: Regenerate Dummy Supplier DB", open=False):
368
+ regen_btn = gr.Button("Regenerate DB (50 suppliers)")
369
+ db_preview = gr.Dataframe(label="DB Preview (Top 10)", interactive=False)
370
+ db_file = gr.File(label="Download Fresh DB (.csv)")
371
+
372
+ run_btn.click(
373
+ fn=run_agent,
374
+ inputs=[inquiry, base_margin, pricing_mode, top_n, company_name, customer_name, valid_days],
375
+ outputs=[parsed_req, agent_steps, market_info, offers_table, quote_text, quote_file, supplier_csv],
376
+ )
377
+
378
+ regen_btn.click(
379
+ fn=regenerate_db,
380
+ inputs=[],
381
+ outputs=[db_preview, db_file],
382
  )
383
 
384
+ if __name__ == "__main__":
385
+ # Hugging Face uses 7860 by default; this makes it explicit and stable.
386
+ demo.launch(server_name="0.0.0.0", server_port=7860)