MBG0903's picture
Update app.py
e0c8a9e verified
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,
)