MBG0903 commited on
Commit
ec4a82e
·
verified ·
1 Parent(s): 60cdb0f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +319 -0
app.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import random
4
+ from datetime import datetime, timedelta
5
+
6
+ import pandas as pd
7
+ import streamlit as st
8
+
9
+ st.set_page_config(page_title="AI Lighting Quotation Agent", layout="wide")
10
+ st.title("💡 AI Lighting Quotation Agent (Prototype)")
11
+ st.caption("Paste inquiry → extract specs → rank suppliers → recommend pricing → generate quote draft")
12
+
13
+ # ✅ Docker-safe writable location
14
+ DATA_PATH = os.environ.get("SUPPLIER_DB_PATH", "/tmp/supplier_db.csv")
15
+ random.seed(42)
16
+
17
+ PRODUCT_CATALOG = [
18
+ ("LED Panel", ["panel", "led panel", "ceiling panel"], (14, 28)),
19
+ ("Downlight", ["downlight", "spot", "spotlight"], (6, 18)),
20
+ ("Flood Light", ["flood", "floodlight"], (18, 55)),
21
+ ("High Bay", ["high bay", "warehouse bay"], (35, 120)),
22
+ ("Tube Light", ["tube", "t8", "batten"], (4, 12)),
23
+ ("Track Light", ["track", "rail light"], (10, 30)),
24
+ ("Street Light", ["street", "road light"], (40, 160)),
25
+ ("LED Strip", ["strip", "led strip", "tape"], (3, 15)),
26
+ ]
27
+
28
+ BRANDS = ["Philips", "Osram", "Panasonic", "Schneider", "Opple", "NVC", "Crompton", "Wipro", "Havells", "Generic"]
29
+ REGIONS = ["SG Central", "SG East", "SG West", "SG North", "Johor", "KL", "Batam"]
30
+
31
+ def make_supplier_name(i: int) -> str:
32
+ prefixes = ["Bright", "Nova", "Apex", "Luma", "Spark", "Prime", "Zen", "Vertex", "Delta", "Orion"]
33
+ suffixes = ["Lighting", "Electrics", "Solutions", "Supply", "Traders", "Distributors", "Imports", "Wholesale", "Mart", "Hub"]
34
+ return f"{random.choice(prefixes)} {random.choice(suffixes)} Pte Ltd #{i:02d}"
35
+
36
+ def generate_supplier_db(n_suppliers: int = 50) -> pd.DataFrame:
37
+ rows = []
38
+ for i in range(1, n_suppliers + 1):
39
+ supplier = make_supplier_name(i)
40
+ region = random.choice(REGIONS)
41
+ supported_categories = random.sample([c[0] for c in PRODUCT_CATALOG], k=random.randint(2, 4))
42
+ reliability = round(random.uniform(0.60, 0.98), 2)
43
+ lead_days = random.randint(2, 21)
44
+ moq = random.choice([1, 5, 10, 20, 30, 50])
45
+ competitiveness = round(random.uniform(0.85, 1.20), 2)
46
+ brands_supported = random.sample(BRANDS, k=random.randint(2, 5))
47
+ rows.append({
48
+ "supplier_id": f"SUP-{1000+i}",
49
+ "supplier_name": supplier,
50
+ "region": region,
51
+ "supported_categories": "|".join(supported_categories),
52
+ "brands_supported": "|".join(brands_supported),
53
+ "reliability_score": reliability,
54
+ "lead_time_days": lead_days,
55
+ "moq": moq,
56
+ "price_competitiveness_factor": competitiveness,
57
+ "contact_email": f"sales{i:02d}@example-supplier.com",
58
+ "last_updated": (datetime.today() - timedelta(days=random.randint(0, 60))).strftime("%Y-%m-%d"),
59
+ })
60
+ return pd.DataFrame(rows)
61
+
62
+ def load_or_create_db() -> pd.DataFrame:
63
+ if os.path.exists(DATA_PATH):
64
+ return pd.read_csv(DATA_PATH)
65
+ df = generate_supplier_db(50)
66
+ df.to_csv(DATA_PATH, index=False)
67
+ return df
68
+
69
+ df_suppliers = load_or_create_db()
70
+
71
+ def normalize_text(t: str) -> str:
72
+ return re.sub(r"\s+", " ", (t or "").strip().lower())
73
+
74
+ def detect_quantity(text: str):
75
+ patterns = [
76
+ r"\bqty[:\s]*([0-9]{1,5})\b",
77
+ r"\bquantity[:\s]*([0-9]{1,5})\b",
78
+ r"\b([0-9]{1,5})\s*(pcs|pc|pieces|nos|units)\b",
79
+ ]
80
+ for p in patterns:
81
+ m = re.search(p, text, flags=re.IGNORECASE)
82
+ if m:
83
+ return int(m.group(1))
84
+ return None
85
+
86
+ def detect_wattage(text: str):
87
+ m = re.search(r"\b([0-9]{1,4})\s*(w|watt|watts)\b", text, flags=re.IGNORECASE)
88
+ return int(m.group(1)) if m else None
89
+
90
+ def detect_brand(text: str):
91
+ t = (text or "").lower()
92
+ for b in BRANDS:
93
+ if b.lower() in t:
94
+ return b
95
+ return None
96
+
97
+ def detect_category(text: str):
98
+ t = normalize_text(text)
99
+ for category, keywords, _rng in PRODUCT_CATALOG:
100
+ for kw in keywords:
101
+ if kw in t:
102
+ return category
103
+ return None
104
+
105
+ def detect_location(text: str):
106
+ t = normalize_text(text)
107
+ loc_map = {
108
+ "singapore": "SG",
109
+ "sg": "SG",
110
+ "jurong": "SG West",
111
+ "tampines": "SG East",
112
+ "woodlands": "SG North",
113
+ "batam": "Batam",
114
+ "johor": "Johor",
115
+ "kuala lumpur": "KL",
116
+ "kl": "KL",
117
+ }
118
+ for k, v in loc_map.items():
119
+ if k in t:
120
+ return v
121
+ return None
122
+
123
+ def parse_inquiry(text: str) -> dict:
124
+ return {
125
+ "raw_text": (text or "").strip(),
126
+ "quantity": detect_quantity(text) or 10,
127
+ "wattage": detect_wattage(text),
128
+ "brand": detect_brand(text),
129
+ "category": detect_category(text),
130
+ "location": detect_location(text),
131
+ }
132
+
133
+ def estimate_market_range(category: str | None, wattage: int | None):
134
+ if not category:
135
+ return (10.0, 40.0)
136
+ base = None
137
+ for c, _kw, rng in PRODUCT_CATALOG:
138
+ if c == category:
139
+ base = rng
140
+ break
141
+ if not base:
142
+ return (10.0, 40.0)
143
+
144
+ lo, hi = base
145
+ if wattage:
146
+ scale = min(2.0, max(0.7, wattage / 18.0))
147
+ lo = lo * (0.85 + 0.15 * scale)
148
+ hi = hi * (0.85 + 0.20 * scale)
149
+ return (round(lo, 2), round(hi, 2))
150
+
151
+ def pick_margin(pricing_mode: str, base_margin: float):
152
+ if pricing_mode == "Competitive":
153
+ return max(5, base_margin - 6)
154
+ if pricing_mode == "High Margin":
155
+ return min(40, base_margin + 8)
156
+ return base_margin
157
+
158
+ def compute_offers(req: dict, suppliers: pd.DataFrame, margin_pct: float):
159
+ category = req.get("category")
160
+ brand = req.get("brand")
161
+ qty = int(req.get("quantity") or 10)
162
+
163
+ candidates = suppliers.copy()
164
+
165
+ if category:
166
+ candidates = candidates[candidates["supported_categories"].astype(str).str.contains(category, na=False)]
167
+
168
+ if brand:
169
+ bm = candidates["brands_supported"].astype(str).str.contains(brand, na=False)
170
+ if bm.sum() > 0:
171
+ candidates = candidates[bm]
172
+
173
+ candidates = candidates[candidates["moq"].fillna(1).astype(int) <= qty]
174
+ if candidates.empty:
175
+ return pd.DataFrame()
176
+
177
+ market_lo, market_hi = estimate_market_range(category, req.get("wattage"))
178
+ market_mid = (market_lo + market_hi) / 2
179
+
180
+ rows = []
181
+ for _, s in candidates.iterrows():
182
+ factor = float(s["price_competitiveness_factor"])
183
+ supplier_cost = market_mid * factor * random.uniform(0.92, 1.06)
184
+
185
+ region = str(s["region"])
186
+ if region in ["Johor", "KL", "Batam"]:
187
+ supplier_cost *= 1.05
188
+
189
+ supplier_cost = round(supplier_cost, 2)
190
+ sell_price = round(supplier_cost / (1 - margin_pct / 100.0), 2)
191
+
192
+ reliability = float(s["reliability_score"])
193
+ lead = int(s["lead_time_days"])
194
+ score = (1 / max(sell_price, 0.01)) * 100 + reliability * 10 + (1 / max(lead, 1)) * 5
195
+
196
+ rows.append({
197
+ "supplier_id": s["supplier_id"],
198
+ "supplier_name": s["supplier_name"],
199
+ "region": s["region"],
200
+ "reliability_score": reliability,
201
+ "lead_time_days": lead,
202
+ "moq": int(s["moq"]),
203
+ "est_supplier_cost_sgd": supplier_cost,
204
+ "recommended_sell_price_sgd": sell_price,
205
+ "score": round(score, 4),
206
+ "contact_email": s["contact_email"],
207
+ })
208
+
209
+ return pd.DataFrame(rows).sort_values("score", ascending=False).head(10).reset_index(drop=True)
210
+
211
+ # Sidebar
212
+ st.sidebar.header("⚙️ Controls")
213
+ base_margin = st.sidebar.slider("Base Margin (%)", 5, 40, 20, 1)
214
+ pricing_mode = st.sidebar.radio("Pricing Mode", ["Balanced", "Competitive", "High Margin"], index=0)
215
+ top_n = st.sidebar.slider("Top offers to show", 3, 10, 5, 1)
216
+
217
+ with st.sidebar.expander("📦 Supplier DB", expanded=False):
218
+ st.write(f"Loaded suppliers: **{len(df_suppliers)}**")
219
+ st.download_button(
220
+ "Download supplier_db.csv",
221
+ data=df_suppliers.to_csv(index=False).encode("utf-8"),
222
+ file_name="supplier_db.csv",
223
+ mime="text/csv",
224
+ use_container_width=True,
225
+ )
226
+ if st.button("Regenerate DB (50 suppliers)"):
227
+ df_suppliers = generate_supplier_db(50)
228
+ df_suppliers.to_csv(DATA_PATH, index=False)
229
+ st.success("Regenerated supplier database")
230
+ st.rerun()
231
+
232
+ # Main
233
+ left, right = st.columns([1.2, 1.0], gap="large")
234
+
235
+ with left:
236
+ st.subheader("1) Paste Customer Inquiry")
237
+ sample = "Hi, please quote best price for 50 pcs Philips 18W LED panel light. Delivery to Singapore in 2 weeks."
238
+ inquiry = st.text_area("Inquiry", value=sample, height=150)
239
+
240
+ req = parse_inquiry(inquiry)
241
+ st.subheader("2) Agent Step: Requirement Extraction")
242
+ st.json(req)
243
+
244
+ with right:
245
+ st.subheader("3) Agent Step: Market Intelligence (Demo)")
246
+ market_lo, market_hi = estimate_market_range(req.get("category"), req.get("wattage"))
247
+ st.metric("Estimated market low (SGD/unit)", f"{market_lo:.2f}")
248
+ st.metric("Estimated market high (SGD/unit)", f"{market_hi:.2f}")
249
+
250
+ st.divider()
251
+
252
+ margin_to_use = pick_margin(pricing_mode, base_margin)
253
+ st.subheader("4) Agent Step: Supplier Shortlist + Pricing Recommendation")
254
+ st.caption(f"Mode: **{pricing_mode}** → Margin applied: **{margin_to_use:.0f}%**")
255
+
256
+ offers_df = compute_offers(req, df_suppliers, margin_to_use)
257
+
258
+ if offers_df.empty:
259
+ st.error("No matching suppliers found (internal DB).")
260
+ st.markdown("### 🆕 New Product / No-Match Mode (Prototype)")
261
+ st.write("**Agent next actions:**")
262
+ st.write("1) Search online for market range and equivalent SKUs.")
263
+ st.write("2) Identify relevant supplier categories and shortlist outreach list.")
264
+ st.write("3) Auto-send RFQs (email/WhatsApp) and wait for quotes.")
265
+ st.write("4) Add new SKU to internal catalog once confirmed.")
266
+ else:
267
+ st.dataframe(offers_df.head(top_n), use_container_width=True)
268
+ best = offers_df.iloc[0].to_dict()
269
+ st.success(
270
+ f"Recommended: **{best['supplier_name']}** | "
271
+ f"Cost **SGD {best['est_supplier_cost_sgd']:.2f}** ��� Sell **SGD {best['recommended_sell_price_sgd']:.2f}** | "
272
+ f"Lead **{best['lead_time_days']}d** | Reliability **{best['reliability_score']:.2f}**"
273
+ )
274
+
275
+ st.divider()
276
+
277
+ st.subheader("5) Quote Draft (Copy/Paste Demo)")
278
+ company_name = st.text_input("Your company name", value="Delight Lighting (Demo)")
279
+ customer_name = st.text_input("Customer name", value="Customer")
280
+ quote_valid_days = st.number_input("Quote validity (days)", min_value=1, max_value=30, value=7)
281
+
282
+ if offers_df.empty:
283
+ st.info("Once suppliers match, the quote draft will be generated here.")
284
+ else:
285
+ qty = int(req.get("quantity") or 10)
286
+ category = req.get("category") or "Lighting Product"
287
+ brand = req.get("brand") or "Brand-agnostic"
288
+ wattage = f"{req.get('wattage')}W" if req.get("wattage") else ""
289
+ unit_price = float(offers_df.iloc[0]["recommended_sell_price_sgd"])
290
+ total = round(unit_price * qty, 2)
291
+ valid_until = (datetime.today() + timedelta(days=int(quote_valid_days))).strftime("%Y-%m-%d")
292
+
293
+ quote_text = f"""Subject: Quotation - {brand} {wattage} {category} (Qty: {qty})
294
+
295
+ Hi {customer_name},
296
+
297
+ Thanks for your inquiry. Please find our quotation below:
298
+
299
+ Item: {brand} {wattage} {category}
300
+ Quantity: {qty}
301
+ Unit Price: SGD {unit_price:.2f}
302
+ Total: SGD {total:.2f}
303
+
304
+ Estimated Lead Time: {int(offers_df.iloc[0]["lead_time_days"])} days
305
+ Validity: Until {valid_until}
306
+ Terms: 50% advance, balance before delivery (demo terms)
307
+
308
+ Regards,
309
+ Sales Team
310
+ {company_name}
311
+ """
312
+ st.text_area("Generated Quote Draft", value=quote_text, height=240)
313
+ st.download_button(
314
+ "Download quote draft (.txt)",
315
+ data=quote_text.encode("utf-8"),
316
+ file_name="quote_draft.txt",
317
+ mime="text/plain",
318
+ use_container_width=True,
319
+ )