Spaces:
Build error
Build error
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +507 -1
src/streamlit_app.py
CHANGED
|
@@ -101,4 +101,510 @@ st.markdown("""
|
|
| 101 |
border: none;
|
| 102 |
border-radius: 8px;
|
| 103 |
padding: 0.5rem 1rem;
|
| 104 |
-
font
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
border: none;
|
| 102 |
border-radius: 8px;
|
| 103 |
padding: 0.5rem 1rem;
|
| 104 |
+
font-weight: 600;
|
| 105 |
+
transition: all 0.3s ease;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.stButton > button:hover {
|
| 109 |
+
transform: translateY(-2px);
|
| 110 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
| 111 |
+
}
|
| 112 |
+
</style>
|
| 113 |
+
""", unsafe_allow_html=True)
|
| 114 |
+
|
| 115 |
+
# Data generation function (embedded in main file)
|
| 116 |
+
@st.cache_data
|
| 117 |
+
def generate_synthetic_procurement_data():
|
| 118 |
+
"""Generate synthetic SAP S/4HANA procurement data"""
|
| 119 |
+
fake = Faker()
|
| 120 |
+
|
| 121 |
+
# Vendors data
|
| 122 |
+
vendors = [
|
| 123 |
+
"Siemens AG", "BASF SE", "BMW Group", "Mercedes-Benz", "Bosch GmbH",
|
| 124 |
+
"ThyssenKrupp", "Bayer AG", "Continental AG", "Henkel AG", "SAP SE"
|
| 125 |
+
]
|
| 126 |
+
|
| 127 |
+
# Material categories
|
| 128 |
+
material_categories = [
|
| 129 |
+
"Raw Materials", "Components", "Packaging", "Services",
|
| 130 |
+
"IT Equipment", "Office Supplies", "Machinery", "Chemicals"
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
# Generate purchase orders
|
| 134 |
+
purchase_orders = []
|
| 135 |
+
for i in range(500):
|
| 136 |
+
order_date = fake.date_between(start_date='-2y', end_date='today')
|
| 137 |
+
delivery_date = order_date + timedelta(days=random.randint(1, 30))
|
| 138 |
+
|
| 139 |
+
po = {
|
| 140 |
+
'po_number': f"PO{str(i+1).zfill(6)}",
|
| 141 |
+
'vendor': random.choice(vendors),
|
| 142 |
+
'material_category': random.choice(material_categories),
|
| 143 |
+
'order_date': order_date,
|
| 144 |
+
'delivery_date': delivery_date,
|
| 145 |
+
'order_value': round(random.uniform(1000, 100000), 2),
|
| 146 |
+
'quantity': random.randint(1, 1000),
|
| 147 |
+
'unit_price': round(random.uniform(10, 500), 2),
|
| 148 |
+
'status': random.choice(['Open', 'Delivered', 'Invoiced', 'Paid']),
|
| 149 |
+
'plant': random.choice(['Plant_001', 'Plant_002', 'Plant_003']),
|
| 150 |
+
'buyer': fake.name(),
|
| 151 |
+
'currency': 'EUR',
|
| 152 |
+
'payment_terms': random.choice(['30 Days', '60 Days', '90 Days']),
|
| 153 |
+
'on_time_delivery': random.choice([True, False]),
|
| 154 |
+
'quality_score': round(random.uniform(7, 10), 1)
|
| 155 |
+
}
|
| 156 |
+
purchase_orders.append(po)
|
| 157 |
+
|
| 158 |
+
# Generate spend analytics data
|
| 159 |
+
spend_data = []
|
| 160 |
+
for vendor in vendors:
|
| 161 |
+
for category in material_categories:
|
| 162 |
+
spend = {
|
| 163 |
+
'vendor': vendor,
|
| 164 |
+
'category': category,
|
| 165 |
+
'total_spend': round(random.uniform(10000, 500000), 2),
|
| 166 |
+
'contract_compliance': round(random.uniform(80, 100), 1),
|
| 167 |
+
'risk_score': round(random.uniform(1, 10), 1),
|
| 168 |
+
'savings_potential': round(random.uniform(5, 25), 1)
|
| 169 |
+
}
|
| 170 |
+
spend_data.append(spend)
|
| 171 |
+
|
| 172 |
+
return pd.DataFrame(purchase_orders), pd.DataFrame(spend_data)
|
| 173 |
+
|
| 174 |
+
# AI Agent Classes (embedded in main file)
|
| 175 |
+
class LLMPoweredProcurementAgent:
|
| 176 |
+
"""AI Agent powered by OpenAI GPT for intelligent procurement analysis"""
|
| 177 |
+
|
| 178 |
+
def __init__(self, po_data: pd.DataFrame, spend_data: pd.DataFrame):
|
| 179 |
+
self.po_data = po_data
|
| 180 |
+
self.spend_data = spend_data
|
| 181 |
+
# Check if OpenAI API key is available
|
| 182 |
+
if 'OPENAI_API_KEY' in st.secrets:
|
| 183 |
+
self.client = openai.OpenAI(api_key=st.secrets["OPENAI_API_KEY"])
|
| 184 |
+
self.llm_available = True
|
| 185 |
+
else:
|
| 186 |
+
self.client = None
|
| 187 |
+
self.llm_available = False
|
| 188 |
+
|
| 189 |
+
def generate_executive_summary(self) -> str:
|
| 190 |
+
"""Generate an executive summary using GPT"""
|
| 191 |
+
|
| 192 |
+
if not self.llm_available:
|
| 193 |
+
# Fallback to rule-based summary if no API key
|
| 194 |
+
total_spend = self.po_data['order_value'].sum()
|
| 195 |
+
total_orders = len(self.po_data)
|
| 196 |
+
on_time_rate = self.po_data['on_time_delivery'].mean() * 100
|
| 197 |
+
|
| 198 |
+
return f"""
|
| 199 |
+
**Executive Summary - Procurement Performance**
|
| 200 |
+
|
| 201 |
+
π **Key Metrics**: Processed {total_orders:,} orders worth β¬{total_spend:,.0f} across {len(self.po_data['vendor'].unique())} vendors.
|
| 202 |
+
|
| 203 |
+
π― **Performance Highlights**:
|
| 204 |
+
β’ On-time delivery rate: {on_time_rate:.1f}%
|
| 205 |
+
β’ Average quality score: {self.po_data['quality_score'].mean():.1f}/10
|
| 206 |
+
β’ Top spending category: {self.po_data.groupby('material_category')['order_value'].sum().idxmax()}
|
| 207 |
+
|
| 208 |
+
π‘ **Strategic Recommendations**:
|
| 209 |
+
β’ Focus on vendor consolidation opportunities
|
| 210 |
+
β’ Improve contract compliance monitoring
|
| 211 |
+
β’ Implement performance-based partnerships
|
| 212 |
+
|
| 213 |
+
*Note: Connect OpenAI API for enhanced AI insights*
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
# Prepare data summary for LLM
|
| 217 |
+
data_summary = {
|
| 218 |
+
"total_spend": float(self.po_data['order_value'].sum()),
|
| 219 |
+
"total_orders": len(self.po_data),
|
| 220 |
+
"unique_vendors": len(self.po_data['vendor'].unique()),
|
| 221 |
+
"avg_order_value": float(self.po_data['order_value'].mean()),
|
| 222 |
+
"on_time_delivery_rate": float(self.po_data['on_time_delivery'].mean()),
|
| 223 |
+
"top_vendors": self.po_data.groupby('vendor')['order_value'].sum().nlargest(3).to_dict(),
|
| 224 |
+
"top_categories": self.po_data.groupby('material_category')['order_value'].sum().nlargest(3).to_dict(),
|
| 225 |
+
"quality_score_avg": float(self.po_data['quality_score'].mean())
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
prompt = f"""
|
| 229 |
+
As a senior procurement analyst, provide an executive summary of the procurement performance based on this data:
|
| 230 |
+
|
| 231 |
+
{json.dumps(data_summary, indent=2)}
|
| 232 |
+
|
| 233 |
+
Please provide:
|
| 234 |
+
1. A concise executive overview (2-3 sentences)
|
| 235 |
+
2. Key performance highlights
|
| 236 |
+
3. Areas of concern (if any)
|
| 237 |
+
4. Strategic recommendations (2-3 bullet points)
|
| 238 |
+
|
| 239 |
+
Keep the tone professional and actionable. Focus on business impact.
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
try:
|
| 243 |
+
response = self.client.chat.completions.create(
|
| 244 |
+
model="gpt-4",
|
| 245 |
+
messages=[
|
| 246 |
+
{"role": "system", "content": "You are an expert procurement analyst with 15+ years of experience in SAP S/4HANA systems."},
|
| 247 |
+
{"role": "user", "content": prompt}
|
| 248 |
+
],
|
| 249 |
+
max_tokens=500,
|
| 250 |
+
temperature=0.7
|
| 251 |
+
)
|
| 252 |
+
return response.choices[0].message.content
|
| 253 |
+
except Exception as e:
|
| 254 |
+
return f"AI Analysis temporarily unavailable: {str(e)}"
|
| 255 |
+
|
| 256 |
+
def chat_with_data(self, user_question: str) -> str:
|
| 257 |
+
"""Natural language interface to query procurement data"""
|
| 258 |
+
|
| 259 |
+
if not self.llm_available:
|
| 260 |
+
# Simple rule-based responses
|
| 261 |
+
question_lower = user_question.lower()
|
| 262 |
+
|
| 263 |
+
if "spend" in question_lower or "cost" in question_lower:
|
| 264 |
+
total_spend = self.po_data['order_value'].sum()
|
| 265 |
+
return f"Your total procurement spend is β¬{total_spend:,.0f}. The top spending category is {self.po_data.groupby('material_category')['order_value'].sum().idxmax()}. Connect OpenAI API for detailed analysis."
|
| 266 |
+
|
| 267 |
+
elif "vendor" in question_lower or "supplier" in question_lower:
|
| 268 |
+
top_vendor = self.po_data.groupby('vendor')['order_value'].sum().idxmax()
|
| 269 |
+
return f"Your top vendor by spend is {top_vendor}. You work with {len(self.po_data['vendor'].unique())} vendors total. Connect OpenAI API for detailed vendor analysis."
|
| 270 |
+
|
| 271 |
+
elif "risk" in question_lower:
|
| 272 |
+
return "Risk analysis shows mixed performance across vendors. Connect OpenAI API for comprehensive risk assessment."
|
| 273 |
+
|
| 274 |
+
else:
|
| 275 |
+
return "I can help with procurement analysis! Try asking about spending, vendors, or risks. Connect OpenAI API for advanced AI conversation."
|
| 276 |
+
|
| 277 |
+
# Create a data context for the LLM
|
| 278 |
+
data_context = {
|
| 279 |
+
"procurement_summary": {
|
| 280 |
+
"total_spend": float(self.po_data['order_value'].sum()),
|
| 281 |
+
"order_count": len(self.po_data),
|
| 282 |
+
"vendor_count": len(self.po_data['vendor'].unique()),
|
| 283 |
+
"date_range": f"{self.po_data['order_date'].min()} to {self.po_data['order_date'].max()}",
|
| 284 |
+
"categories": self.po_data['material_category'].unique().tolist(),
|
| 285 |
+
"vendors": self.po_data['vendor'].unique().tolist()
|
| 286 |
+
},
|
| 287 |
+
"performance_metrics": {
|
| 288 |
+
"avg_quality_score": float(self.po_data['quality_score'].mean()),
|
| 289 |
+
"on_time_delivery_rate": float(self.po_data['on_time_delivery'].mean()),
|
| 290 |
+
"avg_order_value": float(self.po_data['order_value'].mean())
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
prompt = f"""
|
| 295 |
+
User Question: {user_question}
|
| 296 |
+
|
| 297 |
+
Procurement Data Context:
|
| 298 |
+
{json.dumps(data_context, indent=2)}
|
| 299 |
+
|
| 300 |
+
Please answer the user's question based on the procurement data provided. If you need specific calculations or data analysis that isn't available in the context, explain what additional analysis would be needed.
|
| 301 |
+
|
| 302 |
+
Keep your response conversational but professional, and always relate back to business impact where possible.
|
| 303 |
+
"""
|
| 304 |
+
|
| 305 |
+
try:
|
| 306 |
+
response = self.client.chat.completions.create(
|
| 307 |
+
model="gpt-4",
|
| 308 |
+
messages=[
|
| 309 |
+
{"role": "system", "content": "You are an AI procurement analyst assistant. Answer questions about procurement data in a helpful, conversational way while maintaining professional expertise."},
|
| 310 |
+
{"role": "user", "content": prompt}
|
| 311 |
+
],
|
| 312 |
+
max_tokens=400,
|
| 313 |
+
temperature=0.7
|
| 314 |
+
)
|
| 315 |
+
return response.choices[0].message.content
|
| 316 |
+
except Exception as e:
|
| 317 |
+
return f"I'm having trouble accessing the AI right now. Please try again later. Error: {str(e)}"
|
| 318 |
+
|
| 319 |
+
def analyze_spend_patterns(self) -> Dict[str, Any]:
|
| 320 |
+
"""Analyze spending patterns and generate insights"""
|
| 321 |
+
total_spend = self.po_data['order_value'].sum()
|
| 322 |
+
avg_order_value = self.po_data['order_value'].mean()
|
| 323 |
+
|
| 324 |
+
# Top spending categories
|
| 325 |
+
category_spend = self.po_data.groupby('material_category')['order_value'].sum().sort_values(ascending=False)
|
| 326 |
+
|
| 327 |
+
# Vendor performance analysis
|
| 328 |
+
vendor_performance = self.po_data.groupby('vendor').agg({
|
| 329 |
+
'order_value': 'sum',
|
| 330 |
+
'on_time_delivery': 'mean',
|
| 331 |
+
'quality_score': 'mean'
|
| 332 |
+
}).round(2)
|
| 333 |
+
|
| 334 |
+
return {
|
| 335 |
+
'total_spend': total_spend,
|
| 336 |
+
'avg_order_value': avg_order_value,
|
| 337 |
+
'top_categories': category_spend.to_dict(),
|
| 338 |
+
'vendor_performance': vendor_performance.to_dict('index')
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
# Initialize session state and data
|
| 342 |
+
if 'data_loaded' not in st.session_state:
|
| 343 |
+
with st.spinner('π Generating synthetic SAP S/4HANA data...'):
|
| 344 |
+
st.session_state.po_df, st.session_state.spend_df = generate_synthetic_procurement_data()
|
| 345 |
+
st.session_state.data_loaded = True
|
| 346 |
+
|
| 347 |
+
# Initialize AI agents
|
| 348 |
+
@st.cache_resource
|
| 349 |
+
def initialize_agents():
|
| 350 |
+
analytics_agent = LLMPoweredProcurementAgent(st.session_state.po_df, st.session_state.spend_df)
|
| 351 |
+
return analytics_agent
|
| 352 |
+
|
| 353 |
+
analytics_agent = initialize_agents()
|
| 354 |
+
|
| 355 |
+
# API Key status check
|
| 356 |
+
api_key_status = "π’ Connected" if 'OPENAI_API_KEY' in st.secrets else "π΄ Not Connected"
|
| 357 |
+
|
| 358 |
+
# Main header
|
| 359 |
+
st.markdown(f"""
|
| 360 |
+
<div class="main-header">
|
| 361 |
+
<h1>π€ SAP S/4HANA Agentic AI Procurement Analytics</h1>
|
| 362 |
+
<p>Autonomous Intelligence for Procurement Excellence</p>
|
| 363 |
+
<small>OpenAI Status: {api_key_status}</small>
|
| 364 |
+
</div>
|
| 365 |
+
""", unsafe_allow_html=True)
|
| 366 |
+
|
| 367 |
+
# Sidebar navigation
|
| 368 |
+
with st.sidebar:
|
| 369 |
+
st.markdown("### π€ AI-Powered Analytics")
|
| 370 |
+
st.markdown(f"**OpenAI Status:** {api_key_status}")
|
| 371 |
+
|
| 372 |
+
if 'OPENAI_API_KEY' not in st.secrets:
|
| 373 |
+
st.warning("β οΈ Add OpenAI API key in Spaces settings for full AI features")
|
| 374 |
+
|
| 375 |
+
selected = option_menu(
|
| 376 |
+
"Navigation",
|
| 377 |
+
["π Dashboard", "π¬ AI Chat", "π Analytics", "π― Recommendations"],
|
| 378 |
+
icons=['house', 'chat', 'graph-up', 'target'],
|
| 379 |
+
menu_icon="cast",
|
| 380 |
+
default_index=0,
|
| 381 |
+
styles={
|
| 382 |
+
"container": {"padding": "0!important", "background-color": "#fafafa"},
|
| 383 |
+
"icon": {"color": "#0066cc", "font-size": "18px"},
|
| 384 |
+
"nav-link": {"font-size": "16px", "text-align": "left", "margin": "0px", "--hover-color": "#eee"},
|
| 385 |
+
"nav-link-selected": {"background-color": "#0066cc"},
|
| 386 |
+
}
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
if selected == "π Dashboard":
|
| 390 |
+
# AI-generated insights at the top
|
| 391 |
+
st.markdown("### π§ AI Executive Summary")
|
| 392 |
+
|
| 393 |
+
with st.spinner('π€ AI is analyzing your procurement data...'):
|
| 394 |
+
executive_summary = analytics_agent.generate_executive_summary()
|
| 395 |
+
|
| 396 |
+
st.markdown(f"""
|
| 397 |
+
<div class="ai-insight">
|
| 398 |
+
<h4>π Intelligent Analysis</h4>
|
| 399 |
+
<div style="white-space: pre-line;">{executive_summary}</div>
|
| 400 |
+
</div>
|
| 401 |
+
""", unsafe_allow_html=True)
|
| 402 |
+
|
| 403 |
+
# Key metrics
|
| 404 |
+
insights = analytics_agent.analyze_spend_patterns()
|
| 405 |
+
|
| 406 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 407 |
+
|
| 408 |
+
with col1:
|
| 409 |
+
st.markdown("""
|
| 410 |
+
<div class="metric-card">
|
| 411 |
+
<h3 style="color: var(--primary-color); margin: 0;">Total Spend</h3>
|
| 412 |
+
<h2 style="margin: 0.5rem 0;">β¬{:,.0f}</h2>
|
| 413 |
+
<p style="color: #28a745; margin: 0;">π +12% vs last period</p>
|
| 414 |
+
</div>
|
| 415 |
+
""".format(insights['total_spend']), unsafe_allow_html=True)
|
| 416 |
+
|
| 417 |
+
with col2:
|
| 418 |
+
st.markdown("""
|
| 419 |
+
<div class="metric-card">
|
| 420 |
+
<h3 style="color: var(--primary-color); margin: 0;">Avg Order Value</h3>
|
| 421 |
+
<h2 style="margin: 0.5rem 0;">β¬{:,.0f}</h2>
|
| 422 |
+
<p style="color: #ffc107; margin: 0;">π Stable trend</p>
|
| 423 |
+
</div>
|
| 424 |
+
""".format(insights['avg_order_value']), unsafe_allow_html=True)
|
| 425 |
+
|
| 426 |
+
with col3:
|
| 427 |
+
active_vendors = len(st.session_state.po_df['vendor'].unique())
|
| 428 |
+
st.markdown("""
|
| 429 |
+
<div class="metric-card">
|
| 430 |
+
<h3 style="color: var(--primary-color); margin: 0;">Active Vendors</h3>
|
| 431 |
+
<h2 style="margin: 0.5rem 0;">{}</h2>
|
| 432 |
+
<p style="color: #17a2b8; margin: 0;">π€ Partnership focus</p>
|
| 433 |
+
</div>
|
| 434 |
+
""".format(active_vendors), unsafe_allow_html=True)
|
| 435 |
+
|
| 436 |
+
with col4:
|
| 437 |
+
on_time_delivery = st.session_state.po_df['on_time_delivery'].mean() * 100
|
| 438 |
+
st.markdown("""
|
| 439 |
+
<div class="metric-card">
|
| 440 |
+
<h3 style="color: var(--primary-color); margin: 0;">On-Time Delivery</h3>
|
| 441 |
+
<h2 style="margin: 0.5rem 0;">{:.1f}%</h2>
|
| 442 |
+
<p style="color: #28a745; margin: 0;">β° Excellent performance</p>
|
| 443 |
+
</div>
|
| 444 |
+
""".format(on_time_delivery), unsafe_allow_html=True)
|
| 445 |
+
|
| 446 |
+
# Charts
|
| 447 |
+
st.markdown("### π Executive Dashboard")
|
| 448 |
+
|
| 449 |
+
col1, col2 = st.columns(2)
|
| 450 |
+
|
| 451 |
+
with col1:
|
| 452 |
+
# Spend by category
|
| 453 |
+
category_spend = st.session_state.po_df.groupby('material_category')['order_value'].sum().reset_index()
|
| 454 |
+
fig_pie = px.pie(
|
| 455 |
+
category_spend,
|
| 456 |
+
values='order_value',
|
| 457 |
+
names='material_category',
|
| 458 |
+
title='Spend Distribution by Category',
|
| 459 |
+
color_discrete_sequence=px.colors.qualitative.Set3
|
| 460 |
+
)
|
| 461 |
+
fig_pie.update_layout(
|
| 462 |
+
title_font_size=16,
|
| 463 |
+
title_x=0.5,
|
| 464 |
+
showlegend=True,
|
| 465 |
+
height=400
|
| 466 |
+
)
|
| 467 |
+
st.plotly_chart(fig_pie, use_container_width=True)
|
| 468 |
+
|
| 469 |
+
with col2:
|
| 470 |
+
# Top vendors
|
| 471 |
+
vendor_spend = st.session_state.po_df.groupby('vendor')['order_value'].sum().reset_index()
|
| 472 |
+
vendor_spend = vendor_spend.nlargest(8, 'order_value')
|
| 473 |
+
|
| 474 |
+
fig_bar = px.bar(
|
| 475 |
+
vendor_spend,
|
| 476 |
+
x='vendor',
|
| 477 |
+
y='order_value',
|
| 478 |
+
title='Top Vendors by Spend',
|
| 479 |
+
color='order_value',
|
| 480 |
+
color_continuous_scale='Blues'
|
| 481 |
+
)
|
| 482 |
+
fig_bar.update_layout(
|
| 483 |
+
title_font_size=16,
|
| 484 |
+
title_x=0.5,
|
| 485 |
+
xaxis_tickangle=45,
|
| 486 |
+
height=400
|
| 487 |
+
)
|
| 488 |
+
st.plotly_chart(fig_bar, use_container_width=True)
|
| 489 |
+
|
| 490 |
+
elif selected == "π¬ AI Chat":
|
| 491 |
+
st.markdown("### π¬ Chat with Your Procurement Data")
|
| 492 |
+
|
| 493 |
+
st.markdown(f"""
|
| 494 |
+
<div class="ai-insight">
|
| 495 |
+
<h4>π€ Intelligent Procurement Assistant</h4>
|
| 496 |
+
<p>Ask me anything about your procurement data! I can help you understand trends, analyze vendor performance, identify opportunities, and more.</p>
|
| 497 |
+
<p><small>Status: {api_key_status}</small></p>
|
| 498 |
+
</div>
|
| 499 |
+
""", unsafe_allow_html=True)
|
| 500 |
+
|
| 501 |
+
# Chat interface
|
| 502 |
+
if "messages" not in st.session_state:
|
| 503 |
+
st.session_state.messages = [
|
| 504 |
+
{"role": "assistant", "content": "Hello! I'm your AI procurement analyst. What would you like to know about your procurement data?"}
|
| 505 |
+
]
|
| 506 |
+
|
| 507 |
+
# Display chat messages
|
| 508 |
+
for message in st.session_state.messages:
|
| 509 |
+
with st.chat_message(message["role"]):
|
| 510 |
+
st.markdown(message["content"])
|
| 511 |
+
|
| 512 |
+
# Chat input
|
| 513 |
+
if prompt := st.chat_input("Ask about your procurement data..."):
|
| 514 |
+
# Add user message to chat history
|
| 515 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 516 |
+
with st.chat_message("user"):
|
| 517 |
+
st.markdown(prompt)
|
| 518 |
+
|
| 519 |
+
# Generate AI response
|
| 520 |
+
with st.chat_message("assistant"):
|
| 521 |
+
with st.spinner("π€ Analyzing your question..."):
|
| 522 |
+
response = analytics_agent.chat_with_data(prompt)
|
| 523 |
+
st.markdown(response)
|
| 524 |
+
|
| 525 |
+
# Add assistant response to chat history
|
| 526 |
+
st.session_state.messages.append({"role": "assistant", "content": response})
|
| 527 |
+
|
| 528 |
+
# Suggested questions
|
| 529 |
+
st.markdown("#### π‘ Try asking:")
|
| 530 |
+
|
| 531 |
+
col1, col2, col3 = st.columns(3)
|
| 532 |
+
|
| 533 |
+
sample_questions = [
|
| 534 |
+
"π What are my spending trends?",
|
| 535 |
+
"π Who are my best vendors?",
|
| 536 |
+
"β οΈ What risks should I worry about?"
|
| 537 |
+
]
|
| 538 |
+
|
| 539 |
+
for i, (col, question) in enumerate(zip([col1, col2, col3], sample_questions)):
|
| 540 |
+
with col:
|
| 541 |
+
if st.button(question, key=f"q_{i}"):
|
| 542 |
+
# Add the question to chat
|
| 543 |
+
st.session_state.messages.append({"role": "user", "content": question})
|
| 544 |
+
with st.spinner("π€ Analyzing..."):
|
| 545 |
+
response = analytics_agent.chat_with_data(question)
|
| 546 |
+
st.session_state.messages.append({"role": "assistant", "content": response})
|
| 547 |
+
st.rerun()
|
| 548 |
+
|
| 549 |
+
elif selected == "π Analytics":
|
| 550 |
+
st.markdown("### π Advanced Analytics Dashboard")
|
| 551 |
+
|
| 552 |
+
# Vendor performance analysis
|
| 553 |
+
vendor_performance = st.session_state.po_df.groupby('vendor').agg({
|
| 554 |
+
'order_value': 'sum',
|
| 555 |
+
'on_time_delivery': 'mean',
|
| 556 |
+
'quality_score': 'mean',
|
| 557 |
+
'po_number': 'count'
|
| 558 |
+
}).round(2)
|
| 559 |
+
vendor_performance.columns = ['Total Spend', 'On-Time Delivery', 'Quality Score', 'Order Count']
|
| 560 |
+
|
| 561 |
+
st.markdown("#### π Vendor Performance Analysis")
|
| 562 |
+
st.dataframe(vendor_performance.sort_values('Total Spend', ascending=False), use_container_width=True)
|
| 563 |
+
|
| 564 |
+
# Performance scatter plot
|
| 565 |
+
fig_scatter = px.scatter(
|
| 566 |
+
st.session_state.po_df,
|
| 567 |
+
x='on_time_delivery',
|
| 568 |
+
y='quality_score',
|
| 569 |
+
size='order_value',
|
| 570 |
+
color='vendor',
|
| 571 |
+
title='Vendor Performance: Quality vs Delivery',
|
| 572 |
+
labels={'on_time_delivery': 'On-Time Delivery Rate', 'quality_score': 'Quality Score'}
|
| 573 |
+
)
|
| 574 |
+
fig_scatter.update_layout(height=500)
|
| 575 |
+
st.plotly_chart(fig_scatter, use_container_width=True)
|
| 576 |
+
|
| 577 |
+
elif selected == "π― Recommendations":
|
| 578 |
+
st.markdown("### π AI-Generated Strategic Recommendations")
|
| 579 |
+
|
| 580 |
+
st.markdown("""
|
| 581 |
+
<div class="ai-insight">
|
| 582 |
+
<h3>π― Strategic Procurement Optimization</h3>
|
| 583 |
+
<p>Based on data analysis, here are actionable recommendations to optimize your procurement strategy.</p>
|
| 584 |
+
</div>
|
| 585 |
+
""", unsafe_allow_html=True)
|
| 586 |
+
|
| 587 |
+
recommendations = [
|
| 588 |
+
"π― **Vendor Consolidation**: Consider consolidating to top 5-7 vendors to reduce costs by 10-15%",
|
| 589 |
+
"π **Contract Optimization**: Renegotiate contracts with top 3 spending vendors for volume discounts",
|
| 590 |
+
"β‘ **Process Automation**: Implement automated PO approval for orders under β¬5,000",
|
| 591 |
+
"π **Performance Monitoring**: Set up real-time dashboards for vendor performance tracking",
|
| 592 |
+
"π€ **Strategic Partnerships**: Develop long-term partnerships with high-performing vendors"
|
| 593 |
+
]
|
| 594 |
+
|
| 595 |
+
for i, rec in enumerate(recommendations, 1):
|
| 596 |
+
st.markdown(f"""
|
| 597 |
+
<div class="alert alert-success">
|
| 598 |
+
<h4>Recommendation #{i}</h4>
|
| 599 |
+
<p>{rec}</p>
|
| 600 |
+
</div>
|
| 601 |
+
""", unsafe_allow_html=True)
|
| 602 |
+
|
| 603 |
+
# Footer
|
| 604 |
+
st.markdown("---")
|
| 605 |
+
st.markdown("""
|
| 606 |
+
<div style="text-align: center; padding: 1rem; color: #666;">
|
| 607 |
+
<p>π€ Powered by Agentic AI | Built with Streamlit | SAP S/4HANA Integration Demo</p>
|
| 608 |
+
<p><em>This is a demonstration application with synthetic data for learning purposes</em></p>
|
| 609 |
+
</div>
|
| 610 |
+
""", unsafe_allow_html=True)
|