import os import re import random from datetime import datetime, timedelta import pandas as pd import streamlit as st st.set_page_config(page_title="AI Lighting Quotation Agent", layout="wide") st.title("💡 AI Lighting Quotation Agent (Prototype)") st.caption("Paste inquiry → extract specs → rank suppliers → recommend pricing → generate quote draft") # ✅ Docker-safe writable location DATA_PATH = os.environ.get("SUPPLIER_DB_PATH", "/tmp/supplier_db.csv") random.seed(42) PRODUCT_CATALOG = [ ("LED Panel", ["panel", "led panel", "ceiling panel"], (14, 28)), ("Downlight", ["downlight", "spot", "spotlight"], (6, 18)), ("Flood Light", ["flood", "floodlight"], (18, 55)), ("High Bay", ["high bay", "warehouse bay"], (35, 120)), ("Tube Light", ["tube", "t8", "batten"], (4, 12)), ("Track Light", ["track", "rail light"], (10, 30)), ("Street Light", ["street", "road light"], (40, 160)), ("LED Strip", ["strip", "led strip", "tape"], (3, 15)), ] BRANDS = ["Philips", "Osram", "Panasonic", "Schneider", "Opple", "NVC", "Crompton", "Wipro", "Havells", "Generic"] REGIONS = ["SG Central", "SG East", "SG West", "SG North", "Johor", "KL", "Batam"] def make_supplier_name(i: int) -> str: prefixes = ["Bright", "Nova", "Apex", "Luma", "Spark", "Prime", "Zen", "Vertex", "Delta", "Orion"] suffixes = ["Lighting", "Electrics", "Solutions", "Supply", "Traders", "Distributors", "Imports", "Wholesale", "Mart", "Hub"] return f"{random.choice(prefixes)} {random.choice(suffixes)} Pte Ltd #{i:02d}" def generate_supplier_db(n_suppliers: int = 50) -> pd.DataFrame: rows = [] for i in range(1, n_suppliers + 1): supplier = make_supplier_name(i) region = random.choice(REGIONS) supported_categories = random.sample([c[0] for c in PRODUCT_CATALOG], k=random.randint(2, 4)) reliability = round(random.uniform(0.60, 0.98), 2) lead_days = random.randint(2, 21) moq = random.choice([1, 5, 10, 20, 30, 50]) competitiveness = round(random.uniform(0.85, 1.20), 2) brands_supported = random.sample(BRANDS, k=random.randint(2, 5)) rows.append({ "supplier_id": f"SUP-{1000+i}", "supplier_name": supplier, "region": region, "supported_categories": "|".join(supported_categories), "brands_supported": "|".join(brands_supported), "reliability_score": reliability, "lead_time_days": lead_days, "moq": moq, "price_competitiveness_factor": competitiveness, "contact_email": f"sales{i:02d}@example-supplier.com", "last_updated": (datetime.today() - timedelta(days=random.randint(0, 60))).strftime("%Y-%m-%d"), }) return pd.DataFrame(rows) def load_or_create_db() -> pd.DataFrame: if os.path.exists(DATA_PATH): return pd.read_csv(DATA_PATH) df = generate_supplier_db(50) df.to_csv(DATA_PATH, index=False) return df df_suppliers = load_or_create_db() def normalize_text(t: str) -> str: return re.sub(r"\s+", " ", (t or "").strip().lower()) def detect_quantity(text: str): patterns = [ r"\bqty[:\s]*([0-9]{1,5})\b", r"\bquantity[:\s]*([0-9]{1,5})\b", r"\b([0-9]{1,5})\s*(pcs|pc|pieces|nos|units)\b", ] for p in patterns: m = re.search(p, text, flags=re.IGNORECASE) if m: return int(m.group(1)) return None def detect_wattage(text: str): m = re.search(r"\b([0-9]{1,4})\s*(w|watt|watts)\b", text, flags=re.IGNORECASE) return int(m.group(1)) if m else None def detect_brand(text: str): t = (text or "").lower() for b in BRANDS: if b.lower() in t: return b return None def detect_category(text: str): t = normalize_text(text) for category, keywords, _rng in PRODUCT_CATALOG: for kw in keywords: if kw in t: return category return None def detect_location(text: str): t = normalize_text(text) loc_map = { "singapore": "SG", "sg": "SG", "jurong": "SG West", "tampines": "SG East", "woodlands": "SG North", "batam": "Batam", "johor": "Johor", "kuala lumpur": "KL", "kl": "KL", } for k, v in loc_map.items(): if k in t: return v return None def parse_inquiry(text: str) -> dict: return { "raw_text": (text or "").strip(), "quantity": detect_quantity(text) or 10, "wattage": detect_wattage(text), "brand": detect_brand(text), "category": detect_category(text), "location": detect_location(text), } def estimate_market_range(category: str | None, wattage: int | None): if not category: return (10.0, 40.0) base = None for c, _kw, rng in PRODUCT_CATALOG: if c == category: base = rng break if not base: return (10.0, 40.0) lo, hi = base if wattage: scale = min(2.0, max(0.7, wattage / 18.0)) lo = lo * (0.85 + 0.15 * scale) hi = hi * (0.85 + 0.20 * scale) return (round(lo, 2), round(hi, 2)) def pick_margin(pricing_mode: str, base_margin: float): if pricing_mode == "Competitive": return max(5, base_margin - 6) if pricing_mode == "High Margin": return min(40, base_margin + 8) return base_margin def compute_offers(req: dict, suppliers: pd.DataFrame, margin_pct: float): category = req.get("category") brand = req.get("brand") qty = int(req.get("quantity") or 10) candidates = suppliers.copy() if category: candidates = candidates[candidates["supported_categories"].astype(str).str.contains(category, na=False)] if brand: bm = candidates["brands_supported"].astype(str).str.contains(brand, na=False) if bm.sum() > 0: candidates = candidates[bm] candidates = candidates[candidates["moq"].fillna(1).astype(int) <= qty] if candidates.empty: return pd.DataFrame() market_lo, market_hi = estimate_market_range(category, req.get("wattage")) market_mid = (market_lo + market_hi) / 2 rows = [] for _, s in candidates.iterrows(): factor = float(s["price_competitiveness_factor"]) supplier_cost = market_mid * factor * random.uniform(0.92, 1.06) region = str(s["region"]) if region in ["Johor", "KL", "Batam"]: supplier_cost *= 1.05 supplier_cost = round(supplier_cost, 2) sell_price = round(supplier_cost / (1 - margin_pct / 100.0), 2) reliability = float(s["reliability_score"]) lead = int(s["lead_time_days"]) score = (1 / max(sell_price, 0.01)) * 100 + reliability * 10 + (1 / max(lead, 1)) * 5 rows.append({ "supplier_id": s["supplier_id"], "supplier_name": s["supplier_name"], "region": s["region"], "reliability_score": reliability, "lead_time_days": lead, "moq": int(s["moq"]), "est_supplier_cost_sgd": supplier_cost, "recommended_sell_price_sgd": sell_price, "score": round(score, 4), "contact_email": s["contact_email"], }) return pd.DataFrame(rows).sort_values("score", ascending=False).head(10).reset_index(drop=True) # Sidebar st.sidebar.header("⚙️ Controls") base_margin = st.sidebar.slider("Base Margin (%)", 5, 40, 20, 1) pricing_mode = st.sidebar.radio("Pricing Mode", ["Balanced", "Competitive", "High Margin"], index=0) top_n = st.sidebar.slider("Top offers to show", 3, 10, 5, 1) with st.sidebar.expander("📦 Supplier DB", expanded=False): st.write(f"Loaded suppliers: **{len(df_suppliers)}**") st.download_button( "Download supplier_db.csv", data=df_suppliers.to_csv(index=False).encode("utf-8"), file_name="supplier_db.csv", mime="text/csv", use_container_width=True, ) if st.button("Regenerate DB (50 suppliers)"): df_suppliers = generate_supplier_db(50) df_suppliers.to_csv(DATA_PATH, index=False) st.success("Regenerated supplier database") st.rerun() # Main left, right = st.columns([1.2, 1.0], gap="large") with left: st.subheader("1) Paste Customer Inquiry") sample = "Hi, please quote best price for 50 pcs Philips 18W LED panel light. Delivery to Singapore in 2 weeks." inquiry = st.text_area("Inquiry", value=sample, height=150) req = parse_inquiry(inquiry) st.subheader("2) Agent Step: Requirement Extraction") st.json(req) with right: st.subheader("3) Agent Step: Market Intelligence (Demo)") market_lo, market_hi = estimate_market_range(req.get("category"), req.get("wattage")) st.metric("Estimated market low (SGD/unit)", f"{market_lo:.2f}") st.metric("Estimated market high (SGD/unit)", f"{market_hi:.2f}") st.divider() margin_to_use = pick_margin(pricing_mode, base_margin) st.subheader("4) Agent Step: Supplier Shortlist + Pricing Recommendation") st.caption(f"Mode: **{pricing_mode}** → Margin applied: **{margin_to_use:.0f}%**") offers_df = compute_offers(req, df_suppliers, margin_to_use) if offers_df.empty: st.error("No matching suppliers found (internal DB).") st.markdown("### 🆕 New Product / No-Match Mode (Prototype)") st.write("**Agent next actions:**") st.write("1) Search online for market range and equivalent SKUs.") st.write("2) Identify relevant supplier categories and shortlist outreach list.") st.write("3) Auto-send RFQs (email/WhatsApp) and wait for quotes.") st.write("4) Add new SKU to internal catalog once confirmed.") else: st.dataframe(offers_df.head(top_n), use_container_width=True) best = offers_df.iloc[0].to_dict() st.success( f"Recommended: **{best['supplier_name']}** | " f"Cost **SGD {best['est_supplier_cost_sgd']:.2f}** → Sell **SGD {best['recommended_sell_price_sgd']:.2f}** | " f"Lead **{best['lead_time_days']}d** | Reliability **{best['reliability_score']:.2f}**" ) st.divider() st.subheader("5) Quote Draft (Copy/Paste Demo)") company_name = st.text_input("Your company name", value="Delight Lighting (Demo)") customer_name = st.text_input("Customer name", value="Customer") quote_valid_days = st.number_input("Quote validity (days)", min_value=1, max_value=30, value=7) if offers_df.empty: st.info("Once suppliers match, the quote draft will be generated here.") else: qty = int(req.get("quantity") or 10) category = req.get("category") or "Lighting Product" brand = req.get("brand") or "Brand-agnostic" wattage = f"{req.get('wattage')}W" if req.get("wattage") else "" unit_price = float(offers_df.iloc[0]["recommended_sell_price_sgd"]) total = round(unit_price * qty, 2) valid_until = (datetime.today() + timedelta(days=int(quote_valid_days))).strftime("%Y-%m-%d") quote_text = f"""Subject: Quotation - {brand} {wattage} {category} (Qty: {qty}) Hi {customer_name}, Thanks for your inquiry. Please find our quotation below: Item: {brand} {wattage} {category} Quantity: {qty} Unit Price: SGD {unit_price:.2f} Total: SGD {total:.2f} Estimated Lead Time: {int(offers_df.iloc[0]["lead_time_days"])} days Validity: Until {valid_until} Terms: 50% advance, balance before delivery (demo terms) Regards, Sales Team {company_name} """ st.text_area("Generated Quote Draft", value=quote_text, height=240) st.download_button( "Download quote draft (.txt)", data=quote_text.encode("utf-8"), file_name="quote_draft.txt", mime="text/plain", use_container_width=True, )