Spaces:
Sleeping
Sleeping
import os | |
import time | |
import json | |
import random | |
from dataclasses import dataclass | |
from typing import Any, Dict, List, Optional, Tuple | |
import numpy as np | |
import pandas as pd | |
import streamlit as st | |
import plotly.express as px | |
from streamlit_option_menu import option_menu | |
from faker import Faker | |
from datetime import datetime, timedelta | |
# ============================= | |
# Page / Theme Configuration | |
# ============================= | |
st.set_page_config( | |
page_title="SAP S/4HANA Agentic AI Procurement Analytics", | |
page_icon="🤖", | |
layout="wide", | |
initial_sidebar_state="expanded", | |
) | |
# --- CSS --- | |
st.markdown( | |
""" | |
<style> | |
:root { | |
--primary-color: #0066cc; | |
--secondary-color: #f0f8ff; | |
--accent-color: #ff6b35; | |
--success-color: #28a745; | |
--warning-color: #ffc107; | |
--danger-color: #dc3545; | |
} | |
#MainMenu {visibility: hidden;} | |
footer {visibility: hidden;} | |
header {visibility: hidden;} | |
.main-header { | |
background: linear-gradient(90deg, #0066cc, #004c99); | |
padding: 1rem; | |
border-radius: 10px; | |
margin-bottom: 2rem; | |
color: white; | |
text-align: center; | |
} | |
.metric-card { | |
background: white; | |
padding: 1.25rem; | |
border-radius: 12px; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.08); | |
border-left: 4px solid var(--primary-color); | |
margin-bottom: 1rem; | |
} | |
.ai-insight { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
padding: 1rem; | |
border-radius: 12px; | |
margin: 1rem 0; | |
} | |
.alert { padding: 1rem; border-radius: 10px; margin: 0.6rem 0; border-left: 4px solid; } | |
.alert-success { background-color: #d4edda; border-color: var(--success-color); color: #155724; } | |
.alert-warning { background-color: #fff3cd; border-color: var(--warning-color); color: #856404; } | |
.alert-info { background-color: #d1ecf1; border-color: #17a2b8; color: #0c5460; } | |
.stButton > button { background: linear-gradient(90deg, #0066cc, #004c99); color: white; border: none; border-radius: 8px; padding: 0.5rem 1rem; font-weight: 600; transition: all 0.2s ease; } | |
.stButton > button:hover { transform: translateY(-1px); box-shadow: 0 6px 14px rgba(0,0,0,0.15); } | |
</style> | |
""", | |
unsafe_allow_html=True, | |
) | |
# ============================= | |
# Currency helpers (₹ with Indian grouping) | |
# ============================= | |
CURRENCY = "₹" | |
def format_inr(x: float) -> str: | |
"""Format number with Indian digit grouping, no decimals.""" | |
try: | |
n = int(round(float(x))) | |
except Exception: | |
return f"{CURRENCY}{x}" | |
s = str(n) | |
if len(s) <= 3: | |
return f"{CURRENCY}{s}" | |
last3 = s[-3:] | |
rest = s[:-3] | |
parts = [] | |
while len(rest) > 2: | |
parts.insert(0, rest[-2:]) | |
rest = rest[:-2] | |
if rest: | |
parts.insert(0, rest) | |
return f"{CURRENCY}{','.join(parts + [last3])}" | |
def fmt_currency(x: float) -> str: | |
return format_inr(x) | |
# ============================= | |
# LLM client (resilient) | |
# ============================= | |
class LLMConfig: | |
base_url: Optional[str] = os.getenv("OPENAI_BASE_URL") | |
api_key: Optional[str] = ( | |
os.getenv("OPENAI_API_KEY") | |
or os.getenv("OPENAI_API_TOKEN") | |
or os.getenv("OPENAI_KEY") | |
) | |
model: str = os.getenv("OPENAI_MODEL", "gpt-4o-mini") | |
timeout: int = int(os.getenv("OPENAI_TIMEOUT", "45")) | |
max_retries: int = int(os.getenv("OPENAI_MAX_RETRIES", "5")) | |
temperature: float = float(os.getenv("OPENAI_TEMPERATURE", "0.5")) | |
def _post_json(url: str, headers: Dict[str, str], payload: Dict[str, Any], timeout: int): | |
import requests | |
return requests.post(url, headers=headers, json=payload, timeout=timeout) | |
class UniversalLLMClient: | |
def __init__(self, cfg: LLMConfig): | |
self.cfg = cfg | |
self.available = bool(cfg.api_key) | |
self.last_error: Optional[str] = None | |
if self.available: | |
self._smoke_test() | |
def _headers(self) -> Dict[str, str]: | |
return {"Authorization": f"Bearer {self.cfg.api_key}", "Content-Type": "application/json"} | |
def _base_url(self) -> str: | |
return (self.cfg.base_url or "https://api.openai.com/v1").rstrip("/") | |
def _smoke_test(self) -> None: | |
try: | |
_ = self.chat([{"role": "user", "content": "ping"}], max_tokens=4) | |
except Exception as e: | |
self.available = False | |
self.last_error = str(e) | |
def chat(self, messages: List[Dict[str, str]], max_tokens: int = 400) -> str: | |
if not self.available: | |
raise RuntimeError("No API key configured") | |
headers = self._headers() | |
base = self._base_url() | |
chat_url = f"{base}/chat/completions" | |
payload = { | |
"model": self.cfg.model, | |
"messages": messages, | |
"max_tokens": max_tokens, | |
"temperature": self.cfg.temperature, | |
} | |
delay = 1.0 | |
for attempt in range(self.cfg.max_retries): | |
try: | |
resp = _post_json(chat_url, headers, payload, self.cfg.timeout) | |
if resp.status_code == 200: | |
data = resp.json() | |
return data["choices"][0]["message"]["content"].strip() | |
if resp.status_code in (429, 500, 502, 503, 504): | |
time.sleep(delay) | |
delay = min(delay * 2, 8.0) | |
continue | |
try: | |
j = resp.json() | |
msg = j.get("error", {}).get("message", str(j)) | |
except Exception: | |
msg = resp.text | |
raise RuntimeError(f"API error {resp.status_code}: {msg}") | |
except Exception as e: | |
if attempt == self.cfg.max_retries - 1: | |
self.last_error = str(e) | |
raise | |
time.sleep(delay) | |
delay = min(delay * 2, 8.0) | |
raise RuntimeError("Exhausted retries") | |
# ============================= | |
# Data Generation | |
# ============================= | |
def generate_synthetic_procurement_data(seed: int = 42) -> Tuple[pd.DataFrame, pd.DataFrame]: | |
fake = Faker() | |
np.random.seed(seed) | |
random.seed(seed) | |
vendors = [ | |
"Siemens AG", "BASF SE", "BMW Group", "Mercedes-Benz", "Bosch GmbH", | |
"ThyssenKrupp", "Bayer AG", "Continental AG", "Henkel AG", "SAP SE", | |
] | |
categories = [ | |
"Raw Materials", "Components", "Packaging", "Services", | |
"IT Equipment", "Office Supplies", "Machinery", "Chemicals", | |
] | |
purchase_orders: List[Dict[str, Any]] = [] | |
for i in range(900): | |
order_date = fake.date_between(start_date='-24m', end_date='today') | |
promised_days = random.randint(3, 30) | |
promised_date = order_date + timedelta(days=promised_days) | |
actual_lag = max(1, int(np.random.normal(promised_days, 5))) | |
delivery_date = order_date + timedelta(days=actual_lag) | |
late = delivery_date > promised_date | |
unit_price = round(random.uniform(10, 500), 2) | |
qty = random.randint(1, 1200) | |
order_value = round(unit_price * qty, 2) | |
po = { | |
"po_number": f"PO{str(i+1).zfill(6)}", | |
"vendor": random.choice(vendors), | |
"material_category": random.choice(categories), | |
"order_date": order_date, | |
"promised_date": promised_date, | |
"delivery_date": delivery_date, | |
"late_delivery": late, | |
"order_value": order_value, | |
"quantity": qty, | |
"unit_price": unit_price, | |
"status": random.choice(["Open", "Delivered", "Invoiced", "Paid"]), | |
"plant": random.choice(["Plant_001", "Plant_002", "Plant_003"]), | |
"buyer": fake.name(), | |
"currency": "INR", | |
"payment_terms": random.choice(["30 Days", "45 Days", "60 Days", "90 Days"]), | |
"quality_score": round(np.clip(np.random.normal(8.5, 0.8), 5.0, 10.0), 1), | |
} | |
purchase_orders.append(po) | |
spend_rows = [] | |
for v in vendors: | |
for c in categories: | |
spend_rows.append({ | |
"vendor": v, | |
"category": c, | |
"total_spend": round(random.uniform(10000, 700000), 2), | |
"contract_compliance": round(random.uniform(78, 100), 1), | |
"risk_score": round(random.uniform(1, 10), 1), | |
"savings_potential": round(random.uniform(5, 25), 1), | |
}) | |
po_df = pd.DataFrame(purchase_orders) | |
spend_df = pd.DataFrame(spend_rows) | |
return po_df, spend_df | |
# ============================= | |
# Analytics Engine | |
# ============================= | |
class ProcurementAnalytics: | |
def __init__(self, po_df: pd.DataFrame): | |
self.df = po_df.copy() | |
self.df["order_date"] = pd.to_datetime(self.df["order_date"]) | |
self.df["month"] = self.df["order_date"].dt.to_period("M").dt.to_timestamp() | |
def kpis(self) -> Dict[str, Any]: | |
df = self.df | |
return { | |
"total_spend": float(df["order_value"].sum()), | |
"avg_order_value": float(df["order_value"].mean()), | |
"active_vendors": int(df["vendor"].nunique()), | |
"on_time_rate": float((~df["late_delivery"]).mean()), | |
"quality_avg": float(df["quality_score"].mean()), | |
} | |
def category_spend(self) -> pd.DataFrame: | |
return ( | |
self.df.groupby("material_category", as_index=False)["order_value"] | |
.sum() | |
.sort_values("order_value", ascending=False) | |
) | |
def vendor_spend(self, top_n: int = 8) -> pd.DataFrame: | |
return ( | |
self.df.groupby("vendor", as_index=False)["order_value"] | |
.sum() | |
.sort_values("order_value", ascending=False) | |
.head(top_n) | |
) | |
def monthly_spend(self) -> pd.DataFrame: | |
return ( | |
self.df.groupby("month", as_index=False)["order_value"] | |
.sum() | |
.sort_values("month") | |
) | |
def vendor_performance(self) -> pd.DataFrame: | |
g = self.df.groupby("vendor").agg( | |
total_spend=("order_value", "sum"), | |
on_time=("late_delivery", lambda s: 1 - s.mean()), | |
quality=("quality_score", "mean"), | |
orders=("po_number", "count"), | |
) | |
g["on_time"] = (g["on_time"] * 100).round(1) | |
g["quality"] = g["quality"].round(2) | |
g["total_spend"] = g["total_spend"].round(2) | |
return g.sort_values("total_spend", ascending=False) | |
def top_n_categories(self, n: int = 3) -> List[Tuple[str, float]]: | |
cat = self.category_spend() | |
total = float(cat["order_value"].sum()) or 1.0 | |
return [(r["material_category"], (r["order_value"] / total) * 100) for _, r in cat.head(n).iterrows()] | |
def top_n_vendors(self, n: int = 3) -> List[Tuple[str, float]]: | |
ven = ( | |
self.df.groupby("vendor", as_index=False)["order_value"] | |
.sum() | |
.sort_values("order_value", ascending=False) | |
) | |
total = float(ven["order_value"].sum()) or 1.0 | |
return [(r["vendor"], (r["order_value"] / total) * 100) for _, r in ven.head(n).iterrows()] | |
# ============================= | |
# Agent with tighter prompts & INR formatting | |
# ============================= | |
class UniversalProcurementAgent: | |
def __init__(self, po_df: pd.DataFrame, spend_df: pd.DataFrame, client: UniversalLLMClient): | |
self.po_data = po_df | |
self.spend_data = spend_df | |
self.llm = client | |
self.analytics = ProcurementAnalytics(po_df) | |
def executive_summary(self) -> str: | |
if not self.llm.available: | |
return self._rule_summary() | |
k = self.analytics.kpis() | |
top_cats = self.analytics.top_n_categories(3) | |
top_vens = self.analytics.top_n_vendors(3) | |
data_summary = { | |
"total_spend": k["total_spend"], | |
"total_orders": int(len(self.po_data)), | |
"vendor_count": int(self.po_data["vendor"].nunique()), | |
"avg_order_value": k["avg_order_value"], | |
"on_time_delivery": k["on_time_rate"], | |
"avg_quality": k["quality_avg"], | |
"top_categories": top_cats, | |
"top_vendors": top_vens, | |
} | |
messages = [ | |
{ | |
"role": "system", | |
"content": ( | |
"You are a senior procurement analyst. Use bullet points, be concise, " | |
"and always use the ₹ symbol. When summarizing, include top categories " | |
"and vendors with percentages, then 2-3 quantified actions." | |
), | |
}, | |
{ | |
"role": "user", | |
"content": ( | |
"Executive summary. Format amounts with Indian commas (e.g., ₹12,34,567).\n\n" | |
f"Data: {json.dumps(data_summary)}" | |
), | |
}, | |
] | |
try: | |
return ( | |
"🧠 **[AI-Powered Analysis]**\n\n" | |
+ self.llm.chat(messages, max_tokens=550) | |
) | |
except Exception as e: | |
return self._rule_summary() + f"\n\n*AI fallback due to: {e}*" | |
def _rule_summary(self) -> str: | |
k = self.analytics.kpis() | |
top_c = self.analytics.top_n_categories(3) | |
top_v = self.analytics.top_n_vendors(3) | |
topc_str = ", ".join([f"{n} – {s:.0f}%" for n, s in top_c]) | |
topv_str = ", ".join([f"{n} – {s:.0f}%" for n, s in top_v]) | |
return ( | |
"🤖 **[Rule-Based Summary]**\n" | |
+ f"• Total spend: {fmt_currency(k['total_spend'])} across {len(self.po_data):,} POs\n" | |
+ f"• On-time delivery: {k['on_time_rate']*100:.1f}% | Avg quality: {k['quality_avg']:.1f}/10\n" | |
+ f"• Top categories: {topc_str}\n" | |
+ f"• Top vendors: {topv_str}\n" | |
+ "Actions: Consolidate long tail; multi-year terms with top vendors; auto-approve low-value POs." | |
) | |
def chat_with_data(self, question: str) -> str: | |
if not self.llm.available: | |
return self._rule_answer(question) | |
k = self.analytics.kpis() | |
top_c = self.analytics.top_n_categories(3) | |
top_v = self.analytics.top_n_vendors(3) | |
context = { | |
"total_spend": k["total_spend"], | |
"orders": int(len(self.po_data)), | |
"vendors": int(self.po_data["vendor"].nunique()), | |
"on_time": k["on_time_rate"], | |
"quality": k["quality_avg"], | |
"top_categories": top_c, | |
"top_vendors": top_v, | |
} | |
style_rules = ( | |
"Rules: Answer in ≤6 bullet points, use ₹, no generic how-to steps. " | |
"If question mentions spend, list top 3 categories and top 3 vendors with shares. " | |
"If vendors, show best & worst by on-time and spend. If risk, show late % and actions." | |
) | |
messages = [ | |
{"role": "system", "content": "You are a precise procurement co-pilot. Be direct, metric-first, and action-oriented."}, | |
{"role": "user", "content": f"Q: {question}\n\nContext: {json.dumps(context)}\n\n{style_rules}"}, | |
] | |
try: | |
return ( | |
"🧠 **[AI Response]**\n\n" | |
+ self.llm.chat(messages, max_tokens=450) | |
) | |
except Exception as e: | |
return self._rule_answer(question) + f"\n\n*AI fallback due to: {e}*" | |
def _rule_answer(self, question: str) -> str: | |
q = question.lower() | |
k = self.analytics.kpis() | |
top_c = self.analytics.top_n_categories(3) | |
top_v = self.analytics.top_n_vendors(3) | |
if ("spend" in q) or ("spending" in q) or ("cost" in q): | |
lines = [ | |
f"• Total spend: {fmt_currency(k['total_spend'])}", | |
"• Top categories: " + ", ".join([f"{n} – {s:.0f}%" for n, s in top_c]), | |
"• Top vendors: " + ", ".join([f"{n} – {s:.0f}%" for n, s in top_v]), | |
"• Action: Run sourcing events for top 2 categories; target 8–12% savings via volume tiers.", | |
] | |
return "🤖 **[Rule-Based Spend]**\n" + "\n".join(lines) | |
if ("vendor" in q) or ("supplier" in q) or ("partner" in q): | |
vp = self.po_data.groupby("vendor").agg( | |
spend=("order_value", "sum"), | |
late_rate=("late_delivery", "mean"), | |
quality=("quality_score", "mean"), | |
).sort_values("spend", ascending=False) | |
best = vp.head(1) | |
worst = vp.sort_values("late_rate", ascending=False).head(1) | |
bname, wname = best.index[0], worst.index[0] | |
blate = float(best.iloc[0]["late_rate"]) * 100 | |
wlate = float(worst.iloc[0]["late_rate"]) * 100 | |
lines = [ | |
f"• Best by spend: {bname} (late {blate:.1f}%)", | |
f"• Worst by late deliveries: {wname} (late {wlate:.1f}%)", | |
"• Action: Extend terms with best performer; corrective plan and SLA penalties for the worst.", | |
] | |
return "🤖 **[Rule-Based Vendor]**\n" + "\n".join(lines) | |
if ("risk" in q) or ("late" in q) or ("delay" in q): | |
late = float(self.po_data["late_delivery"].mean()) * 100 | |
lines = [ | |
f"• Late delivery rate: {late:.1f}%", | |
"• Action: Add 5–10 day buffers; fast-track chronic offenders; add service credits for misses.", | |
] | |
return "🤖 **[Rule-Based Risk]**\n" + "\n".join(lines) | |
return ( | |
"🤖 **[Rule-Based]**\n" | |
+ "• I can analyze spend (top categories/vendors), vendor performance (best/worst), risk (late %), and trends.\n" | |
+ f"• Snapshot: {fmt_currency(k['total_spend'])}, {len(self.po_data):,} POs, {self.po_data['vendor'].nunique()} vendors, on-time {k['on_time_rate']*100:.1f}%" | |
) | |
# ============================= | |
# App State & Initialization | |
# ============================= | |
if "data_loaded" not in st.session_state: | |
with st.spinner("🔄 Generating synthetic SAP S/4HANA procurement data..."): | |
st.session_state.po_df, st.session_state.spend_df = generate_synthetic_procurement_data() | |
st.session_state.data_loaded = True | |
def get_llm_client() -> UniversalLLMClient: | |
return UniversalLLMClient(LLMConfig()) | |
client = get_llm_client() | |
agent = UniversalProcurementAgent(st.session_state.po_df, st.session_state.spend_df, client) | |
analytics = ProcurementAnalytics(st.session_state.po_df) | |
status = { | |
"available": client.available, | |
"last_error": client.last_error or "OK", | |
"model": client.cfg.model, | |
} | |
api_status = "🟢 Connected" if status["available"] else "🔴 Not Connected" | |
# ============================= | |
# Header | |
# ============================= | |
st.markdown( | |
( | |
"<div class=\"main-header\">" | |
"<h1>🤖 SAP S/4HANA Agentic AI Procurement Analytics</h1>" | |
"<p>Autonomous Intelligence for Procurement Excellence</p>" | |
+ f"<small>LLM: {api_status} · Data: {len(st.session_state.po_df):,} POs</small>" | |
"</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
# ============================= | |
# Sidebar | |
# ============================= | |
with st.sidebar: | |
st.markdown("### 🤖 AI System Status") | |
st.markdown(f"**Connection:** {api_status}") | |
st.markdown(f"**Model:** {status['model']}") | |
with st.expander("🔍 System Information"): | |
st.json(status) | |
selected = option_menu( | |
"Navigation", | |
["🏠 Dashboard", "💬 AI Chat", "📊 Analytics", "🧪 What-If", "🎯 Recommendations"], | |
icons=["house", "chat", "bar-chart", "beaker", "target"], | |
menu_icon="cast", | |
default_index=0, | |
styles={ | |
"container": {"padding": "0!important", "background-color": "#fafafa"}, | |
"icon": {"color": "#0066cc", "font-size": "18px"}, | |
"nav-link": {"font-size": "16px", "text-align": "left", "margin": "0px", "--hover-color": "#eee"}, | |
"nav-link-selected": {"background-color": "#0066cc"}, | |
}, | |
) | |
# ============================= | |
# Main Views | |
# ============================= | |
if selected == "🏠 Dashboard": | |
st.markdown("### 🧠 AI Executive Summary") | |
with st.spinner("🤖 Analyzing procurement data..."): | |
summary = agent.executive_summary() | |
st.markdown( | |
( | |
"<div class=\"ai-insight\">" | |
"<h4>📊 Intelligent Analysis</h4>" | |
+ f"<div style=\"white-space: pre-line; line-height: 1.55;\">{summary}</div>" | |
"</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
k = analytics.kpis() | |
c1, c2, c3, c4 = st.columns(4) | |
with c1: | |
st.markdown( | |
( | |
"<div class='metric-card'>" | |
"<h3 style='color: var(--primary-color); margin:0;'>Total Spend</h3>" | |
+ f"<h2 style='margin: .5rem 0;'>{fmt_currency(k['total_spend'])}</h2>" | |
"<p style='color:#28a745;margin:0;'>📈 Active Portfolio</p>" | |
"</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
with c2: | |
st.markdown( | |
( | |
"<div class='metric-card'>" | |
"<h3 style='color: var(--primary-color); margin:0;'>Avg Order Value</h3>" | |
+ f"<h2 style='margin: .5rem 0;'>{fmt_currency(k['avg_order_value'])}</h2>" | |
"<p style='color:#17a2b8;margin:0;'>📊 Order Efficiency</p>" | |
"</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
with c3: | |
st.markdown( | |
( | |
"<div class='metric-card'>" | |
"<h3 style='color: var(--primary-color); margin:0;'>Active Vendors</h3>" | |
+ f"<h2 style='margin: .5rem 0;'>{k['active_vendors']}</h2>" | |
"<p style='color:#6f42c1;margin:0;'>🤝 Strategic Partners</p>" | |
"</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
with c4: | |
st.markdown( | |
( | |
"<div class='metric-card'>" | |
"<h3 style='color: var(--primary-color); margin:0;'>On-Time Delivery</h3>" | |
+ f"<h2 style='margin: .5rem 0;'>{k['on_time_rate']*100:.1f}%</h2>" | |
"<p style='color:#28a745;margin:0;'>⏱ Performance</p>" | |
"</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
st.markdown("### 📊 Executive Dashboard") | |
colA, colB = st.columns(2) | |
with colA: | |
cat = analytics.category_spend() | |
fig = px.pie(cat, values="order_value", names="material_category", title="Spend Distribution by Category") | |
fig.update_layout(title_font_size=16, title_x=0.5, height=420) | |
st.plotly_chart(fig, use_container_width=True) | |
with colB: | |
vend = analytics.vendor_spend(top_n=8) | |
fig2 = px.bar(vend, x="vendor", y="order_value", title="Top Vendors by Spend") | |
fig2.update_layout(title_font_size=16, title_x=0.5, xaxis_tickangle=45, height=420) | |
st.plotly_chart(fig2, use_container_width=True) | |
colC, colD = st.columns(2) | |
with colC: | |
ms = analytics.monthly_spend() | |
fig3 = px.line(ms, x="month", y="order_value", markers=True, title="Monthly Spend Trend") | |
fig3.update_layout(title_font_size=16, title_x=0.5, height=420) | |
st.plotly_chart(fig3, use_container_width=True) | |
with colD: | |
st.markdown("#### 🔎 Quick Top Areas") | |
tcat = ", ".join([f"{n} – {s:.0f}%" for n, s in analytics.top_n_categories(3)]) | |
tven = ", ".join([f"{n} – {s:.0f}%" for n, s in analytics.top_n_vendors(3)]) | |
st.markdown(f"**Top Categories:** {tcat}") | |
st.markdown(f"**Top Vendors:** {tven}") | |
elif selected == "💬 AI Chat": | |
st.markdown("### 💬 Chat with Your Procurement Data") | |
st.markdown( | |
( | |
"<div class=\"ai-insight\">" | |
"<h4>🤖 Universal AI Assistant</h4>" | |
"<p>Ask me anything about your procurement data. I will answer with crisp bullets and actual metrics.</p>" | |
+ f"<p><small>Status: {api_status} | Model: {status['model']}</small></p>" | |
"</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
if "messages" not in st.session_state: | |
st.session_state.messages = [ | |
{"role": "assistant", "content": "Hello! Try: 'What are my biggest spending areas?' or 'Which vendor is the weakest on delivery?'"}, | |
] | |
for m in st.session_state.messages: | |
with st.chat_message(m["role"]): | |
st.markdown(m["content"]) | |
if prompt := st.chat_input("Ask about your procurement data…"): | |
st.session_state.messages.append({"role": "user", "content": prompt}) | |
with st.chat_message("user"): | |
st.markdown(prompt) | |
with st.chat_message("assistant"): | |
with st.spinner("🤖 Analyzing…"): | |
reply = agent.chat_with_data(prompt) | |
st.markdown(reply) | |
st.session_state.messages.append({"role": "assistant", "content": reply}) | |
st.markdown("#### 💡 Quick asks:") | |
c1, c2, c3 = st.columns(3) | |
qs = [ | |
"What are my biggest spending areas?", | |
"Which vendors perform the best and worst?", | |
"What risks should I monitor right now?", | |
] | |
for i, (c, q) in enumerate(zip([c1, c2, c3], qs)): | |
with c: | |
if st.button(f"💭 {q}", key=f"q_{i}"): | |
st.session_state.messages.append({"role": "user", "content": q}) | |
st.session_state.messages.append({"role": "assistant", "content": agent.chat_with_data(q)}) | |
st.rerun() | |
elif selected == "📊 Analytics": | |
st.markdown("### 📈 Advanced Analytics Dashboard") | |
vp = analytics.vendor_performance() | |
st.dataframe( | |
vp.rename( | |
columns={ | |
"total_spend": "Total Spend (₹)", | |
"on_time": "On-Time Delivery %", | |
"quality": "Quality Score", | |
"orders": "Order Count", | |
} | |
), | |
use_container_width=True, | |
) | |
st.download_button( | |
label="⬇️ Download Vendor Performance (CSV)", | |
data=vp.to_csv().encode("utf-8"), | |
file_name="vendor_performance.csv", | |
mime="text/csv", | |
) | |
elif selected == "🧪 What-If": | |
st.markdown("### 🧪 What-If: Vendor Consolidation Simulator") | |
top_n = st.slider("Keep top N vendors by spend", min_value=2, max_value=10, value=6, step=1) | |
g = st.session_state.po_df.groupby("vendor")["order_value"].sum().sort_values(ascending=False) | |
kept_vendors = list(g.head(top_n).index) | |
kept_spend = st.session_state.po_df[st.session_state.po_df["vendor"].isin(kept_vendors)]["order_value"].sum() | |
total_spend = st.session_state.po_df["order_value"].sum() | |
share = (kept_spend / total_spend) if total_spend else 0 | |
est_savings = max(0.03, min(0.18, 0.05 + (0.12 * (1 - share)))) | |
kept_names = ", ".join(kept_vendors) | |
st.markdown( | |
( | |
"<div class='alert alert-info'>" | |
+ f"<strong>Scenario:</strong> Keep top <b>{top_n}</b> vendors. Addressable share: <b>{share*100:.1f}%</b>.<br/>" | |
+ f"<strong>Potential savings:</strong> <b>{est_savings*100:.1f}%</b> (heuristic).<br/>" | |
+ f"<small>Kept Vendors:</small> {kept_names}" | |
+ "</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
if st.checkbox("Show detailed vendor spend"): | |
st.dataframe(g.reset_index().rename(columns={"index": "vendor", "order_value": "spend (₹)"}), use_container_width=True) | |
elif selected == "🎯 Recommendations": | |
st.markdown("### 🚀 Strategic Recommendations") | |
recs = [ | |
"🎯 **Vendor Consolidation**: Reduce long-tail suppliers; target 8–15% price improvement via volume tiers.", | |
"⚡ **Process Automation**: Auto-approve low-value POs to cut cycle time by 35–50%.", | |
"📊 **Performance Contracts**: KPI-linked clauses for on-time delivery; add service credits for misses.", | |
"🛡️ **Risk Monitoring**: Score suppliers on late rate, quality, and concentration; escalate chronic offenders.", | |
"🧠 **AI Copilot**: Use LLM to draft RFQs, summarize bids, and propose award scenarios.", | |
] | |
for i, rec in enumerate(recs, start=1): | |
st.markdown( | |
( | |
"<div class=\"alert alert-success\">" | |
+ f"<h4>Recommendation #{i}</h4>" | |
+ f"<p>{rec}</p>" | |
+ "</div>" | |
), | |
unsafe_allow_html=True, | |
) | |
# ============================= | |
# Footer | |
# ============================= | |
st.markdown("---") | |
st.markdown( | |
( | |
"<div style=\"text-align:center; padding: 1rem; color:#666;\">" | |
"<p>🤖 <strong>Universal AI Procurement Analytics</strong> | Crisp, metric-first answers in ₹</p>" | |
+ f"<p><em>Demo with synthetic data • {len(st.session_state.po_df):,} orders • LLM {api_status}</em></p>" | |
"</div>" | |
), | |
unsafe_allow_html=True, | |
) | |