PD03 commited on
Commit
a288d8d
·
verified ·
1 Parent(s): 6d1da45

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +551 -566
src/streamlit_app.py CHANGED
@@ -1,27 +1,33 @@
1
- import streamlit as st
2
- import pandas as pd
 
 
 
 
 
 
3
  import numpy as np
 
 
4
  import plotly.express as px
5
  import plotly.graph_objects as go
6
  from streamlit_option_menu import option_menu
7
- import time
8
  from faker import Faker
9
  from datetime import datetime, timedelta
10
- import random
11
- import json
12
- from typing import Dict, List, Any
13
- import os
14
 
15
- # Page configuration
 
 
16
  st.set_page_config(
17
  page_title="SAP S/4HANA Agentic AI Procurement Analytics",
18
  page_icon="🤖",
19
  layout="wide",
20
- initial_sidebar_state="expanded"
21
  )
22
 
23
- # Custom CSS (same as before)
24
- st.markdown("""
 
25
  <style>
26
  :root {
27
  --primary-color: #0066cc;
@@ -31,11 +37,11 @@ st.markdown("""
31
  --warning-color: #ffc107;
32
  --danger-color: #dc3545;
33
  }
34
-
35
  #MainMenu {visibility: hidden;}
36
  footer {visibility: hidden;}
37
  header {visibility: hidden;}
38
-
39
  .main-header {
40
  background: linear-gradient(90deg, #0066cc, #004c99);
41
  padding: 1rem;
@@ -44,481 +50,477 @@ st.markdown("""
44
  color: white;
45
  text-align: center;
46
  }
47
-
48
  .metric-card {
49
  background: white;
50
- padding: 1.5rem;
51
- border-radius: 10px;
52
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
53
  border-left: 4px solid var(--primary-color);
54
  margin-bottom: 1rem;
55
  }
56
-
57
  .ai-insight {
58
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
59
  color: white;
60
  padding: 1rem;
61
- border-radius: 10px;
62
- margin: 1rem 0;
63
- }
64
-
65
- .alert {
66
- padding: 1rem;
67
- border-radius: 8px;
68
  margin: 1rem 0;
69
- border-left: 4px solid;
70
- }
71
-
72
- .alert-success {
73
- background-color: #d4edda;
74
- border-color: var(--success-color);
75
- color: #155724;
76
- }
77
-
78
- .alert-warning {
79
- background-color: #fff3cd;
80
- border-color: var(--warning-color);
81
- color: #856404;
82
- }
83
-
84
- .alert-info {
85
- background-color: #d1ecf1;
86
- border-color: #17a2b8;
87
- color: #0c5460;
88
- }
89
-
90
- .stButton > button {
91
- background: linear-gradient(90deg, #0066cc, #004c99);
92
- color: white;
93
- border: none;
94
- border-radius: 8px;
95
- padding: 0.5rem 1rem;
96
- font-weight: 600;
97
- transition: all 0.3s ease;
98
- }
99
-
100
- .stButton > button:hover {
101
- transform: translateY(-2px);
102
- box-shadow: 0 4px 8px rgba(0,0,0,0.2);
103
  }
 
 
 
 
 
 
 
 
104
  </style>
105
- """, unsafe_allow_html=True)
106
-
107
- # Safe OpenAI integration that works with ANY version
108
- def get_openai_api_key():
109
- """Get API key from environment variables (Hugging Face Spaces compatible)"""
110
- return (
111
- os.getenv('OPENAI_API_KEY') or
112
- os.getenv('OPENAI_API_TOKEN') or
113
- os.getenv('OPENAI_KEY')
114
- )
115
 
116
- def safe_openai_chat(api_key, messages, model="gpt-3.5-turbo", max_tokens=400):
117
- """Universal OpenAI chat function that works with any OpenAI version"""
118
-
119
- if not api_key:
120
- return None, "No API key available"
121
-
122
- try:
123
- # Try using requests directly (most reliable method)
124
- import requests
125
-
126
- headers = {
127
- "Authorization": f"Bearer {api_key}",
128
- "Content-Type": "application/json"
129
- }
130
-
131
- data = {
132
- "model": model,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  "messages": messages,
134
  "max_tokens": max_tokens,
135
- "temperature": 0.7
136
  }
137
-
138
- response = requests.post(
139
- "https://api.openai.com/v1/chat/completions",
140
- headers=headers,
141
- json=data,
142
- timeout=30
143
- )
144
-
145
- if response.status_code == 200:
146
- result = response.json()
147
- return result['choices'][0]['message']['content'], "Success"
148
- elif response.status_code == 401:
149
- return None, "Invalid API key"
150
- elif response.status_code == 429:
151
- return None, "Rate limit exceeded"
152
- else:
153
- return None, f"API error: {response.status_code}"
154
-
155
- except requests.exceptions.RequestException as e:
156
- return None, f"Network error: {str(e)}"
157
- except Exception as e:
158
- return None, f"Request failed: {str(e)}"
159
-
160
- # Data generation function
161
- @st.cache_data
162
- def generate_synthetic_procurement_data():
163
- """Generate synthetic SAP S/4HANA procurement data"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  fake = Faker()
165
- np.random.seed(42)
166
- random.seed(42)
167
-
168
  vendors = [
169
  "Siemens AG", "BASF SE", "BMW Group", "Mercedes-Benz", "Bosch GmbH",
170
- "ThyssenKrupp", "Bayer AG", "Continental AG", "Henkel AG", "SAP SE"
171
  ]
172
-
173
- material_categories = [
174
- "Raw Materials", "Components", "Packaging", "Services",
175
- "IT Equipment", "Office Supplies", "Machinery", "Chemicals"
176
  ]
177
-
178
- purchase_orders = []
179
- for i in range(500):
180
- order_date = fake.date_between(start_date='-2y', end_date='today')
181
- delivery_date = order_date + timedelta(days=random.randint(1, 30))
182
-
 
 
 
 
 
 
 
 
 
 
183
  po = {
184
  'po_number': f"PO{str(i+1).zfill(6)}",
185
  'vendor': random.choice(vendors),
186
- 'material_category': random.choice(material_categories),
187
  'order_date': order_date,
 
188
  'delivery_date': delivery_date,
189
- 'order_value': round(random.uniform(1000, 100000), 2),
190
- 'quantity': random.randint(1, 1000),
191
- 'unit_price': round(random.uniform(10, 500), 2),
 
 
 
192
  'status': random.choice(['Open', 'Delivered', 'Invoiced', 'Paid']),
193
  'plant': random.choice(['Plant_001', 'Plant_002', 'Plant_003']),
194
  'buyer': fake.name(),
195
  'currency': 'EUR',
196
- 'payment_terms': random.choice(['30 Days', '60 Days', '90 Days']),
197
- 'on_time_delivery': random.choice([True, False]),
198
- 'quality_score': round(random.uniform(7, 10), 1)
199
  }
200
  purchase_orders.append(po)
201
-
202
- spend_data = []
203
- for vendor in vendors:
204
- for category in material_categories:
205
- spend = {
206
- 'vendor': vendor,
207
- 'category': category,
208
- 'total_spend': round(random.uniform(10000, 500000), 2),
209
- 'contract_compliance': round(random.uniform(80, 100), 1),
210
  'risk_score': round(random.uniform(1, 10), 1),
211
- 'savings_potential': round(random.uniform(5, 25), 1)
212
- }
213
- spend_data.append(spend)
214
-
215
- return pd.DataFrame(purchase_orders), pd.DataFrame(spend_data)
216
 
217
- # Updated AI Agent with bulletproof OpenAI integration
218
- class UniversalProcurementAgent:
219
- """AI Agent with universal OpenAI compatibility"""
220
-
221
- def __init__(self, po_data: pd.DataFrame, spend_data: pd.DataFrame):
222
- self.po_data = po_data
223
- self.spend_data = spend_data
224
- self.api_key = get_openai_api_key()
225
- self.llm_available = bool(self.api_key)
226
-
227
- # Test connection if API key exists
228
- if self.llm_available:
229
- self._test_connection()
230
-
231
- def _test_connection(self):
232
- """Test OpenAI connection using direct API call"""
233
- test_response, status = safe_openai_chat(
234
- self.api_key,
235
- [{"role": "user", "content": "Hello"}],
236
- max_tokens=5
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  )
238
-
239
- if test_response is None:
240
- self.llm_available = False
241
- self.connection_error = status
242
- else:
243
- self.connection_error = "Connected successfully"
244
-
245
- def get_status_info(self) -> Dict[str, Any]:
246
- """Get connection status information"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  return {
248
- "api_key_available": bool(self.api_key),
249
- "api_key_length": len(self.api_key) if self.api_key else 0,
250
- "llm_available": self.llm_available,
251
- "connection_status": getattr(self, 'connection_error', 'Not tested'),
252
- "method": "Direct API calls (version-agnostic)"
 
253
  }
254
-
255
- def test_llm_connection(self) -> Dict[str, Any]:
256
- """Test LLM connection with detailed status"""
257
- if not self.api_key:
258
- return {
259
- "status": "❌ No API Key",
260
- "details": "Add OPENAI_API_KEY to your Hugging Face Space secrets",
261
- "recommendation": "Go to Settings → Variables & Secrets → Add OPENAI_API_KEY"
262
- }
263
-
264
- response, status = safe_openai_chat(
265
- self.api_key,
266
- [{"role": "user", "content": "Test connection"}],
267
- max_tokens=10
 
 
 
268
  )
269
-
270
- if response:
271
- return {
272
- "status": "✅ Connected",
273
- "details": "OpenAI API responding normally",
274
- "method": "Direct API calls (universal compatibility)"
275
- }
276
- else:
277
- return {
278
- "status": "⚠️ Connection Failed",
279
- "details": status,
280
- "recommendation": "Check API key validity or try regenerating it"
281
- }
282
-
283
- def generate_executive_summary(self) -> str:
284
- """Generate executive summary with AI or fallback"""
285
-
286
- if not self.llm_available:
287
- return self._generate_rule_based_summary()
288
-
289
- # Prepare data for AI analysis
290
  data_summary = {
291
  "total_spend": float(self.po_data['order_value'].sum()),
292
- "total_orders": len(self.po_data),
293
- "vendor_count": len(self.po_data['vendor'].unique()),
294
  "avg_order_value": float(self.po_data['order_value'].mean()),
295
- "on_time_delivery": float(self.po_data['on_time_delivery'].mean()),
296
- "avg_quality": float(self.po_data['quality_score'].mean())
297
  }
298
-
299
  messages = [
300
- {
301
- "role": "system",
302
- "content": "You are a senior procurement analyst with expertise in SAP S/4HANA systems. Provide professional, actionable insights."
303
- },
304
- {
305
- "role": "user",
306
- "content": f"""Analyze this procurement data and provide an executive summary:
307
-
308
- {json.dumps(data_summary, indent=2)}
309
-
310
- Include:
311
- 1. Executive overview (2-3 sentences)
312
- 2. Key performance highlights with metrics
313
- 3. Areas needing attention
314
- 4. Strategic recommendations (3-4 actionable items)
315
-
316
- Keep it professional and actionable for C-level executives."""
317
- }
318
  ]
319
-
320
- ai_response, status = safe_openai_chat(self.api_key, messages, max_tokens=600)
321
-
322
- if ai_response:
323
- return f"🧠 **[AI-Powered Analysis - OpenAI GPT]**\n\n{ai_response}"
324
- else:
325
- fallback = self._generate_rule_based_summary()
326
- return f"⚠️ **[AI Temporarily Unavailable]**\n\n{fallback}\n\n*Error: {status}*"
327
-
328
- def _generate_rule_based_summary(self) -> str:
329
- """Enhanced rule-based summary"""
330
- total_spend = self.po_data['order_value'].sum()
331
- total_orders = len(self.po_data)
332
- on_time_rate = self.po_data['on_time_delivery'].mean() * 100
333
- quality_avg = self.po_data['quality_score'].mean()
334
- top_category = self.po_data.groupby('material_category')['order_value'].sum().idxmax()
335
- top_vendor = self.po_data.groupby('vendor')['order_value'].sum().idxmax()
336
-
337
- return f"""🤖 **[Smart Analysis - Rule-Based Engine]**
338
-
339
- **🎯 Executive Summary - Procurement Performance Dashboard**
340
-
341
- 📊 **Portfolio Overview**
342
- • Total spend: €{total_spend:,.0f} across {total_orders:,} purchase orders
343
- • Active suppliers: {len(self.po_data['vendor'].unique())} strategic partners
344
- • Average order value: €{self.po_data['order_value'].mean():,.0f}
345
-
346
- 🏆 **Performance Metrics**
347
- • On-time delivery: {on_time_rate:.1f}% (Industry benchmark: 85%)
348
- • Quality score: {quality_avg:.1f}/10 (Excellent: >8.5)
349
- • Top category: {top_category}
350
- • Leading partner: {top_vendor}
351
-
352
- ⚡ **Strategic Opportunities**
353
- • Vendor consolidation from {len(self.po_data['vendor'].unique())} to 6-7 strategic partners
354
- • Contract optimization with high-performing suppliers
355
- • Process automation for routine purchases
356
-
357
- 💡 **Recommended Actions**
358
- • Implement strategic sourcing for {top_category}
359
- • Develop KPI-driven vendor agreements
360
- • Deploy automated approval workflows
361
-
362
- *🔧 Advanced AI analysis available with OpenAI connection*"""
363
-
364
- def chat_with_data(self, user_question: str) -> str:
365
- """Chat interface with universal compatibility"""
366
-
367
- if not self.llm_available:
368
- return self._get_rule_based_response(user_question)
369
-
370
- # Prepare context for AI
371
  context = {
372
  "total_spend": float(self.po_data['order_value'].sum()),
373
- "order_count": len(self.po_data),
374
- "vendor_count": len(self.po_data['vendor'].unique()),
375
- "avg_quality": float(self.po_data['quality_score'].mean()),
376
- "on_time_rate": float(self.po_data['on_time_delivery'].mean())
377
  }
378
-
379
  messages = [
380
- {
381
- "role": "system",
382
- "content": "You are an expert procurement analyst. Answer questions about procurement data professionally with specific metrics when relevant."
383
- },
384
- {
385
- "role": "user",
386
- "content": f"User Question: {user_question}\n\nProcurement Data Context: {json.dumps(context, indent=2)}\n\nProvide a helpful, professional response."
387
- }
388
  ]
389
-
390
- ai_response, status = safe_openai_chat(self.api_key, messages, max_tokens=400)
391
-
392
- if ai_response:
393
- return f"🧠 **[AI Response - OpenAI GPT]**\n\n{ai_response}"
394
- else:
395
- fallback = self._get_rule_based_response(user_question)
396
- return f"⚠️ **[Smart Fallback]** AI temporarily unavailable\n\n{fallback}\n\n*Error: {status}*"
397
-
398
- def _get_rule_based_response(self, question: str) -> str:
399
- """Enhanced rule-based responses"""
400
- question_lower = question.lower()
401
-
402
- if any(word in question_lower for word in ["spend", "cost", "money", "budget"]):
403
- total_spend = self.po_data['order_value'].sum()
404
- top_category = self.po_data.groupby('material_category')['order_value'].sum().idxmax()
405
- monthly_avg = total_spend / 24
406
-
407
- return f"""🤖 **[Smart Analysis Engine]**
408
-
409
- 💰 **Spend Analysis**
410
- **Total spend**: €{total_spend:,.0f}
411
- **Monthly average**: €{monthly_avg:,.0f}
412
- **Top category**: {top_category}
413
- **Order average**: €{self.po_data['order_value'].mean():,.0f}
414
-
415
- **Distribution**: Spend spans {len(self.po_data['material_category'].unique())} categories with {top_category} leading investment.
416
-
417
- *🚀 Connect OpenAI for advanced spend optimization strategies*"""
418
-
419
- elif any(word in question_lower for word in ["vendor", "supplier", "partner"]):
420
- top_vendor = self.po_data.groupby('vendor')['order_value'].sum().idxmax()
421
- vendor_count = len(self.po_data['vendor'].unique())
422
- performance = self.po_data[self.po_data['vendor'] == top_vendor]['on_time_delivery'].mean() * 100
423
-
424
- return f"""🤖 **[Smart Analysis Engine]**
425
-
426
- 🤝 **Vendor Portfolio**
427
- **Active suppliers**: {vendor_count}
428
- • **Top partner**: {top_vendor}
429
- • **Performance**: {performance:.1f}% on-time delivery
430
- • **Portfolio health**: Well-diversified supply base
431
-
432
- **Strategic insight**: {top_vendor} represents your strongest partnership with excellent delivery performance.
433
-
434
- *🚀 Connect OpenAI for detailed vendor relationship strategies*"""
435
-
436
- else:
437
- return f"""🤖 **[Smart Analysis Engine]**
438
-
439
- **Available Analysis:**
440
- • 💰 **Spending insights**: "What are my biggest costs?"
441
- • 🤝 **Vendor performance**: "How are my suppliers doing?"
442
- • ⚠️ **Risk assessment**: "What risks should I monitor?"
443
- • 📈 **Trend analysis**: "Show me spending patterns"
444
-
445
- **Current scope**: {len(self.po_data):,} orders • {len(self.po_data['vendor'].unique())} vendors • €{self.po_data['order_value'].sum():,.0f} total spend
446
-
447
- *🚀 Connect OpenAI for natural language conversations and advanced insights*"""
448
-
449
- def analyze_spend_patterns(self) -> Dict[str, Any]:
450
- """Analyze spending patterns"""
451
- total_spend = self.po_data['order_value'].sum()
452
- avg_order_value = self.po_data['order_value'].mean()
453
-
454
- category_spend = self.po_data.groupby('material_category')['order_value'].sum().sort_values(ascending=False)
455
- vendor_performance = self.po_data.groupby('vendor').agg({
456
- 'order_value': 'sum',
457
- 'on_time_delivery': 'mean',
458
- 'quality_score': 'mean'
459
- }).round(2)
460
-
461
- return {
462
- 'total_spend': total_spend,
463
- 'avg_order_value': avg_order_value,
464
- 'top_categories': category_spend.to_dict(),
465
- 'vendor_performance': vendor_performance.to_dict('index')
466
- }
467
 
468
- # Initialize session state and data
 
 
469
  if 'data_loaded' not in st.session_state:
470
  with st.spinner('🔄 Generating synthetic SAP S/4HANA procurement data...'):
471
  st.session_state.po_df, st.session_state.spend_df = generate_synthetic_procurement_data()
472
  st.session_state.data_loaded = True
473
 
474
- # Initialize AI agent
475
- analytics_agent = UniversalProcurementAgent(st.session_state.po_df, st.session_state.spend_df)
 
476
 
477
- # Get status
478
- status_info = analytics_agent.get_status_info()
479
- api_key_status = "🟢 Connected" if status_info['llm_available'] else "🔴 Not Connected"
480
 
481
- # Main header
482
- st.markdown(f"""
 
 
 
 
 
 
483
  <div class="main-header">
484
- <h1>🤖 SAP S/4HANA Agentic AI Procurement Analytics</h1>
485
- <p>Autonomous Intelligence for Procurement Excellence</p>
486
- <small>OpenAI Status: {api_key_status} | Data: {len(st.session_state.po_df):,} Purchase Orders</small>
487
  </div>
488
- """, unsafe_allow_html=True)
 
 
489
 
490
- # Sidebar with status and navigation
 
 
491
  with st.sidebar:
492
  st.markdown("### 🤖 AI System Status")
493
- st.markdown(f"**Connection:** {api_key_status}")
494
- st.markdown(f"**Method:** {status_info.get('method', 'N/A')}")
495
-
496
- # Enhanced debug info
497
  with st.expander("🔍 System Information"):
498
- st.json(status_info)
499
-
500
- # Connection test
 
501
  if st.button("🔄 Test AI Connection"):
502
- test_result = analytics_agent.test_llm_connection()
503
- st.markdown(f"**Status:** {test_result['status']}")
504
- st.markdown(f"**Details:** {test_result['details']}")
505
- if 'recommendation' in test_result:
506
- st.info(test_result['recommendation'])
507
-
508
- if not status_info['llm_available']:
509
- st.markdown("""
510
- <div class="alert alert-info">
511
- <small><strong>💡 Enable AI Features</strong><br>
512
- Add your OpenAI API key as OPENAI_API_KEY in Hugging Face Space secrets for advanced AI capabilities!</small>
513
- </div>
514
- """, unsafe_allow_html=True)
515
-
516
  st.markdown("---")
517
-
518
  selected = option_menu(
519
  "Navigation",
520
- ["🏠 Dashboard", "💬 AI Chat", "📊 Analytics", "🎯 Recommendations"],
521
- icons=['house', 'chat', 'graph-up', 'target'],
522
  menu_icon="cast",
523
  default_index=0,
524
  styles={
@@ -526,186 +528,169 @@ with st.sidebar:
526
  "icon": {"color": "#0066cc", "font-size": "18px"},
527
  "nav-link": {"font-size": "16px", "text-align": "left", "margin": "0px", "--hover-color": "#eee"},
528
  "nav-link-selected": {"background-color": "#0066cc"},
529
- }
530
  )
531
 
532
- # Main content sections
 
 
533
  if selected == "🏠 Dashboard":
534
  st.markdown("### 🧠 AI Executive Summary")
535
-
536
  with st.spinner('🤖 Analyzing procurement data...'):
537
- executive_summary = analytics_agent.generate_executive_summary()
538
-
539
  st.markdown(f"""
540
  <div class="ai-insight">
541
  <h4>📊 Intelligent Analysis</h4>
542
- <div style="white-space: pre-line; line-height: 1.6;">{executive_summary}</div>
543
  </div>
544
  """, unsafe_allow_html=True)
545
-
546
- # Key metrics
547
- insights = analytics_agent.analyze_spend_patterns()
548
-
549
- col1, col2, col3, col4 = st.columns(4)
550
-
551
- with col1:
552
- st.markdown(f"""
553
- <div class="metric-card">
554
- <h3 style="color: var(--primary-color); margin: 0;">Total Spend</h3>
555
- <h2 style="margin: 0.5rem 0;">€{insights['total_spend']:,.0f}</h2>
556
- <p style="color: #28a745; margin: 0;">📈 Active Portfolio</p>
557
- </div>
558
- """, unsafe_allow_html=True)
559
-
560
- with col2:
561
- st.markdown(f"""
562
- <div class="metric-card">
563
- <h3 style="color: var(--primary-color); margin: 0;">Avg Order Value</h3>
564
- <h2 style="margin: 0.5rem 0;">€{insights['avg_order_value']:,.0f}</h2>
565
- <p style="color: #17a2b8; margin: 0;">📊 Order Efficiency</p>
566
- </div>
567
- """, unsafe_allow_html=True)
568
-
569
- with col3:
570
- active_vendors = len(st.session_state.po_df['vendor'].unique())
571
- st.markdown(f"""
572
- <div class="metric-card">
573
- <h3 style="color: var(--primary-color); margin: 0;">Active Vendors</h3>
574
- <h2 style="margin: 0.5rem 0;">{active_vendors}</h2>
575
- <p style="color: #6f42c1; margin: 0;">🤝 Strategic Partners</p>
576
- </div>
577
- """, unsafe_allow_html=True)
578
-
579
- with col4:
580
- on_time_delivery = st.session_state.po_df['on_time_delivery'].mean() * 100
581
- st.markdown(f"""
582
- <div class="metric-card">
583
- <h3 style="color: var(--primary-color); margin: 0;">On-Time Delivery</h3>
584
- <h2 style="margin: 0.5rem 0;">{on_time_delivery:.1f}%</h2>
585
- <p style="color: #28a745; margin: 0;">⏰ Performance</p>
586
- </div>
587
- """, unsafe_allow_html=True)
588
-
589
- # Charts
590
  st.markdown("### 📊 Executive Dashboard")
591
-
592
- col1, col2 = st.columns(2)
593
-
594
- with col1:
595
- category_spend = st.session_state.po_df.groupby('material_category')['order_value'].sum().reset_index()
596
- fig_pie = px.pie(
597
- category_spend,
598
- values='order_value',
599
- names='material_category',
600
- title='Spend Distribution by Category'
601
- )
602
- fig_pie.update_layout(title_font_size=16, title_x=0.5, height=400)
603
- st.plotly_chart(fig_pie, use_container_width=True)
604
-
605
- with col2:
606
- vendor_spend = st.session_state.po_df.groupby('vendor')['order_value'].sum().reset_index()
607
- vendor_spend = vendor_spend.nlargest(8, 'order_value')
608
-
609
- fig_bar = px.bar(
610
- vendor_spend,
611
- x='vendor',
612
- y='order_value',
613
- title='Top Vendors by Spend'
614
- )
615
- fig_bar.update_layout(title_font_size=16, title_x=0.5, xaxis_tickangle=45, height=400)
616
- st.plotly_chart(fig_bar, use_container_width=True)
617
 
618
  elif selected == "💬 AI Chat":
619
  st.markdown("### 💬 Chat with Your Procurement Data")
620
-
621
  st.markdown(f"""
622
  <div class="ai-insight">
623
  <h4>🤖 Universal AI Assistant</h4>
624
- <p>Ask me anything about your procurement data! I use direct API calls for maximum compatibility.</p>
625
- <p><small>Status: {api_key_status} | Method: Universal Compatibility</small></p>
626
  </div>
627
  """, unsafe_allow_html=True)
628
-
629
- # Chat interface
630
  if "messages" not in st.session_state:
631
  st.session_state.messages = [
632
- {"role": "assistant", "content": "Hello! I'm your universal procurement analyst. I've loaded your data and I'm ready to help with any questions!"}
633
  ]
634
-
635
- # Display chat messages
636
- for message in st.session_state.messages:
637
- with st.chat_message(message["role"]):
638
- st.markdown(message["content"])
639
-
640
- # Chat input
641
- if prompt := st.chat_input("Ask about your procurement data..."):
642
  st.session_state.messages.append({"role": "user", "content": prompt})
643
  with st.chat_message("user"):
644
  st.markdown(prompt)
645
-
646
  with st.chat_message("assistant"):
647
- with st.spinner("🤖 Analyzing..."):
648
- response = analytics_agent.chat_with_data(prompt)
649
- st.markdown(response)
650
-
651
- st.session_state.messages.append({"role": "assistant", "content": response})
652
-
653
- # Sample questions
654
- st.markdown("#### 💡 Try these questions:")
655
- col1, col2, col3 = st.columns(3)
656
-
657
- questions = [
658
- "What are my biggest spending areas?",
659
- "How are my vendors performing?",
660
- "What optimization opportunities exist?"
661
- ]
662
-
663
- for i, (col, question) in enumerate(zip([col1, col2, col3], questions)):
664
- with col:
665
- if st.button(f"💭 {question}", key=f"q_{i}"):
666
- st.session_state.messages.append({"role": "user", "content": question})
667
- response = analytics_agent.chat_with_data(question)
668
- st.session_state.messages.append({"role": "assistant", "content": response})
669
  st.rerun()
670
 
671
  elif selected == "📊 Analytics":
672
  st.markdown("### 📈 Advanced Analytics Dashboard")
673
-
674
- vendor_performance = st.session_state.po_df.groupby('vendor').agg({
675
- 'order_value': 'sum',
676
- 'on_time_delivery': 'mean',
677
- 'quality_score': 'mean',
678
- 'po_number': 'count'
679
- }).round(2)
680
- vendor_performance.columns = ['Total Spend (€)', 'On-Time Delivery', 'Quality Score', 'Order Count']
681
- vendor_performance['On-Time Delivery'] = (vendor_performance['On-Time Delivery'] * 100).round(1)
682
-
683
- st.dataframe(vendor_performance.sort_values('Total Spend ()', ascending=False), use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
 
685
  elif selected == "🎯 Recommendations":
686
  st.markdown("### 🚀 Strategic Recommendations")
687
-
688
- recommendations = [
689
- "🎯 **Vendor Consolidation**: Reduce supplier base for 12-18% cost reduction",
690
- " **Process Automation**: Implement automated approval workflows",
691
- "📊 **Performance Contracts**: Establish KPI-driven vendor agreements",
692
- "🛡️ **Risk Monitoring**: Deploy real-time supplier assessment tools",
693
- "🚀 **Digital Platform**: Upgrade to AI-powered procurement system"
694
  ]
695
-
696
- for i, rec in enumerate(recommendations, 1):
697
- st.markdown(f"""
698
- <div class="alert alert-success">
699
- <h4>Recommendation #{i}</h4>
700
- <p>{rec}</p>
701
- </div>
702
- """, unsafe_allow_html=True)
 
 
703
 
 
704
  # Footer
 
705
  st.markdown("---")
706
- st.markdown(f"""
707
- <div style="text-align: center; padding: 1rem; color: #666;">
708
- <p>🤖 <strong>Universal AI Procurement Analytics</strong> | Built with Direct API Integration</p>
709
- <p><em>Demo with synthetic data {len(st.session_state.po_df):,} orders OpenAI {api_key_status}</em></p>
 
710
  </div>
711
- """, unsafe_allow_html=True)
 
 
 
1
+ import os
2
+ import time
3
+ import json
4
+ import math
5
+ import random
6
+ from dataclasses import dataclass
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
  import numpy as np
10
+ import pandas as pd
11
+ import streamlit as st
12
  import plotly.express as px
13
  import plotly.graph_objects as go
14
  from streamlit_option_menu import option_menu
 
15
  from faker import Faker
16
  from datetime import datetime, timedelta
 
 
 
 
17
 
18
+ # =============================
19
+ # Page / Theme Configuration
20
+ # =============================
21
  st.set_page_config(
22
  page_title="SAP S/4HANA Agentic AI Procurement Analytics",
23
  page_icon="🤖",
24
  layout="wide",
25
+ initial_sidebar_state="expanded",
26
  )
27
 
28
+ # --- CSS ---
29
+ st.markdown(
30
+ """
31
  <style>
32
  :root {
33
  --primary-color: #0066cc;
 
37
  --warning-color: #ffc107;
38
  --danger-color: #dc3545;
39
  }
40
+
41
  #MainMenu {visibility: hidden;}
42
  footer {visibility: hidden;}
43
  header {visibility: hidden;}
44
+
45
  .main-header {
46
  background: linear-gradient(90deg, #0066cc, #004c99);
47
  padding: 1rem;
 
50
  color: white;
51
  text-align: center;
52
  }
53
+
54
  .metric-card {
55
  background: white;
56
+ padding: 1.25rem;
57
+ border-radius: 12px;
58
+ box-shadow: 0 2px 10px rgba(0,0,0,0.08);
59
  border-left: 4px solid var(--primary-color);
60
  margin-bottom: 1rem;
61
  }
62
+
63
  .ai-insight {
64
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
65
  color: white;
66
  padding: 1rem;
67
+ border-radius: 12px;
 
 
 
 
 
 
68
  margin: 1rem 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  }
70
+
71
+ .alert { padding: 1rem; border-radius: 10px; margin: 0.6rem 0; border-left: 4px solid; }
72
+ .alert-success { background-color: #d4edda; border-color: var(--success-color); color: #155724; }
73
+ .alert-warning { background-color: #fff3cd; border-color: var(--warning-color); color: #856404; }
74
+ .alert-info { background-color: #d1ecf1; border-color: #17a2b8; color: #0c5460; }
75
+
76
+ .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; }
77
+ .stButton > button:hover { transform: translateY(-1px); box-shadow: 0 6px 14px rgba(0,0,0,0.15); }
78
  </style>
79
+ """,
80
+ unsafe_allow_html=True,
81
+ )
 
 
 
 
 
 
 
82
 
83
+ # =============================
84
+ # Config & LLM Client (robust, version-agnostic)
85
+ # =============================
86
+ @dataclass
87
+ class LLMConfig:
88
+ provider: str = os.getenv("LLM_PROVIDER", "openai").lower() # openai | azure | compatible
89
+ base_url: Optional[str] = os.getenv("OPENAI_BASE_URL") # for compatible endpoints
90
+ api_key: Optional[str] = (
91
+ os.getenv("OPENAI_API_KEY")
92
+ or os.getenv("OPENAI_API_TOKEN")
93
+ or os.getenv("OPENAI_KEY")
94
+ )
95
+ model: str = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
96
+ timeout: int = int(os.getenv("OPENAI_TIMEOUT", "45"))
97
+ max_retries: int = int(os.getenv("OPENAI_MAX_RETRIES", "5"))
98
+ temperature: float = float(os.getenv("OPENAI_TEMPERATURE", "0.6"))
99
+
100
+
101
+ def _post_json(url: str, headers: Dict[str, str], payload: Dict[str, Any], timeout: int):
102
+ import requests
103
+ return requests.post(url, headers=headers, json=payload, timeout=timeout)
104
+
105
+
106
+ class UniversalLLMClient:
107
+ """A resilient client that works with OpenAI, Azure OpenAI, and compatible APIs.
108
+ - Prefers /chat/completions
109
+ - Falls back to /responses if available
110
+ - Retries with exponential backoff and respects Retry-After
111
+ """
112
+
113
+ def __init__(self, cfg: LLMConfig):
114
+ self.cfg = cfg
115
+ self.available = bool(cfg.api_key)
116
+ self.last_error: Optional[str] = None
117
+ if self.available:
118
+ self._smoke_test()
119
+
120
+ def _headers(self) -> Dict[str, str]:
121
+ return {"Authorization": f"Bearer {self.cfg.api_key}", "Content-Type": "application/json"}
122
+
123
+ def _base_url(self) -> str:
124
+ if self.cfg.provider == "azure":
125
+ # Use Azure env format if provided
126
+ endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
127
+ api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-15-preview")
128
+ deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT", self.cfg.model)
129
+ # Azure uses deployment name in path
130
+ return f"{endpoint}/openai/deployments/{deployment}?api-version={api_version}"
131
+ return (self.cfg.base_url or "https://api.openai.com/v1").rstrip("/")
132
+
133
+ def _smoke_test(self):
134
+ try:
135
+ _ = self.chat([
136
+ {"role": "user", "content": "ping"}
137
+ ], max_tokens=4)
138
+ except Exception as e:
139
+ self.available = False
140
+ self.last_error = str(e)
141
+
142
+ def chat(self, messages: List[Dict[str, str]], max_tokens: int = 400) -> str:
143
+ if not self.available:
144
+ raise RuntimeError("No API key configured")
145
+
146
+ headers = self._headers()
147
+ base = self._base_url()
148
+
149
+ # Endpoint selection
150
+ chat_url = f"{base}/chat/completions" if self.cfg.provider != "azure" else f"{base}&api-version-override=false" # azure path already includes params
151
+ responses_url = f"{base}/responses"
152
+
153
+ payload = {
154
+ "model": self.cfg.model,
155
  "messages": messages,
156
  "max_tokens": max_tokens,
157
+ "temperature": self.cfg.temperature,
158
  }
159
+
160
+ # Retry with backoff
161
+ delay = 1.0
162
+ for attempt in range(self.cfg.max_retries):
163
+ try:
164
+ resp = _post_json(chat_url, headers, payload, self.cfg.timeout)
165
+ if resp.status_code == 200:
166
+ data = resp.json()
167
+ return data["choices"][0]["message"]["content"].strip()
168
+ # Try /responses fallback for some providers
169
+ if resp.status_code in (404, 400):
170
+ alt = _post_json(
171
+ responses_url,
172
+ headers,
173
+ {"model": self.cfg.model, "input": messages, "max_output_tokens": max_tokens, "temperature": self.cfg.temperature},
174
+ self.cfg.timeout,
175
+ )
176
+ if alt.status_code == 200:
177
+ return alt.json()["output"][0]["content"][0]["text"].strip()
178
+
179
+ if resp.status_code in (429, 500, 502, 503, 504):
180
+ retry_after = float(resp.headers.get("Retry-After", delay))
181
+ time.sleep(retry_after)
182
+ delay = min(delay * 2, 8.0)
183
+ continue
184
+ # Other errors → raise
185
+ try:
186
+ j = resp.json()
187
+ msg = j.get("error", {}).get("message", str(j))
188
+ except Exception:
189
+ msg = resp.text
190
+ raise RuntimeError(f"API error {resp.status_code}: {msg}")
191
+ except Exception as e:
192
+ if attempt == self.cfg.max_retries - 1:
193
+ self.last_error = str(e)
194
+ raise
195
+ time.sleep(delay)
196
+ delay = min(delay * 2, 8.0)
197
+ raise RuntimeError("Exhausted retries")
198
+
199
+
200
+ # =============================
201
+ # Data Generation & Utils
202
+ # =============================
203
+ @st.cache_data(show_spinner=False)
204
+ def generate_synthetic_procurement_data(seed: int = 42) -> Tuple[pd.DataFrame, pd.DataFrame]:
205
+ """Generate richer synthetic SAP S/4HANA procurement data, including lead times and late flags."""
206
  fake = Faker()
207
+ np.random.seed(seed)
208
+ random.seed(seed)
209
+
210
  vendors = [
211
  "Siemens AG", "BASF SE", "BMW Group", "Mercedes-Benz", "Bosch GmbH",
212
+ "ThyssenKrupp", "Bayer AG", "Continental AG", "Henkel AG", "SAP SE",
213
  ]
214
+
215
+ categories = [
216
+ "Raw Materials", "Components", "Packaging", "Services",
217
+ "IT Equipment", "Office Supplies", "Machinery", "Chemicals",
218
  ]
219
+
220
+ purchase_orders: List[Dict[str, Any]] = []
221
+ today = datetime.utcnow().date()
222
+
223
+ for i in range(900):
224
+ order_date = fake.date_between(start_date='-24m', end_date='today')
225
+ promised_days = random.randint(3, 30)
226
+ promised_date = order_date + timedelta(days=promised_days)
227
+ actual_lag = max(1, int(np.random.normal(promised_days, 5)))
228
+ delivery_date = order_date + timedelta(days=actual_lag)
229
+ late = delivery_date > promised_date
230
+
231
+ unit_price = round(random.uniform(10, 500), 2)
232
+ qty = random.randint(1, 1200)
233
+ order_value = round(unit_price * qty, 2)
234
+
235
  po = {
236
  'po_number': f"PO{str(i+1).zfill(6)}",
237
  'vendor': random.choice(vendors),
238
+ 'material_category': random.choice(categories),
239
  'order_date': order_date,
240
+ 'promised_date': promised_date,
241
  'delivery_date': delivery_date,
242
+ 'lead_time_days': (delivery_date - order_date).days,
243
+ 'promised_days': promised_days,
244
+ 'late_delivery': late,
245
+ 'order_value': order_value,
246
+ 'quantity': qty,
247
+ 'unit_price': unit_price,
248
  'status': random.choice(['Open', 'Delivered', 'Invoiced', 'Paid']),
249
  'plant': random.choice(['Plant_001', 'Plant_002', 'Plant_003']),
250
  'buyer': fake.name(),
251
  'currency': 'EUR',
252
+ 'payment_terms': random.choice(['30 Days', '45 Days', '60 Days', '90 Days']),
253
+ 'quality_score': round(np.clip(np.random.normal(8.5, 0.8), 5.0, 10.0), 1),
 
254
  }
255
  purchase_orders.append(po)
256
+
257
+ spend_rows = []
258
+ for v in vendors:
259
+ for c in categories:
260
+ spend_rows.append({
261
+ 'vendor': v,
262
+ 'category': c,
263
+ 'total_spend': round(random.uniform(10000, 700000), 2),
264
+ 'contract_compliance': round(random.uniform(78, 100), 1),
265
  'risk_score': round(random.uniform(1, 10), 1),
266
+ 'savings_potential': round(random.uniform(5, 25), 1),
267
+ })
 
 
 
268
 
269
+ po_df = pd.DataFrame(purchase_orders)
270
+ spend_df = pd.DataFrame(spend_rows)
271
+ return po_df, spend_df
272
+
273
+
274
+ def eur(x: float) -> str:
275
+ return f"€{x:,.0f}"
276
+
277
+
278
+ # =============================
279
+ # Analytics Engine
280
+ # =============================
281
+ class ProcurementAnalytics:
282
+ def __init__(self, po_df: pd.DataFrame):
283
+ self.df = po_df.copy()
284
+ self.df['order_date'] = pd.to_datetime(self.df['order_date'])
285
+ self.df['month'] = self.df['order_date'].dt.to_period('M').dt.to_timestamp()
286
+
287
+ @st.cache_data(show_spinner=False)
288
+ def kpis(_self, df_hash: int) -> Dict[str, Any]:
289
+ df = _self.df
290
+ return {
291
+ 'total_spend': float(df['order_value'].sum()),
292
+ 'avg_order_value': float(df['order_value'].mean()),
293
+ 'active_vendors': int(df['vendor'].nunique()),
294
+ 'on_time_rate': float((~df['late_delivery']).mean()),
295
+ 'quality_avg': float(df['quality_score'].mean()),
296
+ }
297
+
298
+ def category_spend(self) -> pd.DataFrame:
299
+ return (
300
+ self.df.groupby('material_category', as_index=False)['order_value'].sum()
301
+ .sort_values('order_value', ascending=False)
302
  )
303
+
304
+ def vendor_spend(self, top_n: int = 8) -> pd.DataFrame:
305
+ g = self.df.groupby('vendor', as_index=False)['order_value'].sum()
306
+ return g.sort_values('order_value', ascending=False).head(top_n)
307
+
308
+ def monthly_spend(self) -> pd.DataFrame:
309
+ return self.df.groupby('month', as_index=False)['order_value'].sum().sort_values('month')
310
+
311
+ def vendor_performance(self) -> pd.DataFrame:
312
+ g = self.df.groupby('vendor').agg(
313
+ total_spend=('order_value', 'sum'),
314
+ on_time=('late_delivery', lambda s: 1 - s.mean()),
315
+ quality=('quality_score', 'mean'),
316
+ orders=('po_number', 'count'),
317
+ lead_time=('lead_time_days', 'mean'),
318
+ )
319
+ g['on_time'] = (g['on_time'] * 100).round(1)
320
+ g['quality'] = g['quality'].round(2)
321
+ g['lead_time'] = g['lead_time'].round(1)
322
+ g['total_spend'] = g['total_spend'].round(2)
323
+ return g.sort_values('total_spend', ascending=False)
324
+
325
+ def anomalies(self) -> pd.DataFrame:
326
+ # Simple IQR for order_value anomalies
327
+ q1, q3 = self.df['order_value'].quantile([0.25, 0.75])
328
+ iqr = q3 - q1
329
+ hi = q3 + 1.5 * iqr
330
+ lo = max(0, q1 - 1.5 * iqr)
331
+ a = self.df[(self.df['order_value'] > hi) | (self.df['order_value'] < lo)].copy()
332
+ a['anomaly_reason'] = np.where(a['order_value'] > hi, 'High value', 'Low value')
333
+ return a.sort_values('order_value', ascending=False).head(50)
334
+
335
+ def simulate_vendor_consolidation(self, keep_top: int) -> Dict[str, Any]:
336
+ g = self.df.groupby('vendor')['order_value'].sum().sort_values(ascending=False)
337
+ kept_vendors = list(g.head(keep_top).index)
338
+ kept_spend = self.df[self.df['vendor'].isin(kept_vendors)]['order_value'].sum()
339
+ total_spend = self.df['order_value'].sum()
340
+ share = kept_spend / total_spend if total_spend else 0
341
+ est_savings = 0.05 + (0.12 * (1 - share)) # heuristic: better leverage when consolidating
342
+ return {
343
+ 'kept_vendors': kept_vendors,
344
+ 'kept_share': share,
345
+ 'estimated_savings_pct': max(0.03, min(0.18, est_savings)),
346
+ }
347
+
348
+
349
+ # =============================
350
+ # Agent (uses UniversalLLMClient with safe fallback)
351
+ # =============================
352
+ class UniversalProcurementAgent:
353
+ def __init__(self, po_df: pd.DataFrame, spend_df: pd.DataFrame, client: UniversalLLMClient):
354
+ self.po_data = po_df
355
+ self.spend_data = spend_df
356
+ self.llm = client
357
+
358
+ def llm_status(self) -> Dict[str, Any]:
359
  return {
360
+ "api_key_available": bool(self.llm.cfg.api_key),
361
+ "llm_available": self.llm.available,
362
+ "last_error": self.llm.last_error or "Connected successfully" if self.llm.available else "Unavailable",
363
+ "provider": self.llm.cfg.provider,
364
+ "model": self.llm.cfg.model,
365
+ "base_url": self.llm.cfg.base_url or "https://api.openai.com/v1",
366
  }
367
+
368
+ def _rule_summary(self) -> str:
369
+ total_spend = float(self.po_data['order_value'].sum())
370
+ on_time = float((~self.po_data['late_delivery']).mean()) * 100
371
+ quality = float(self.po_data['quality_score'].mean())
372
+ top_cat = self.po_data.groupby('material_category')['order_value'].sum().idxmax()
373
+ top_vendor = self.po_data.groupby('vendor')['order_value'].sum().idxmax()
374
+ return (
375
+ "🤖 **[Smart Analysis - Rule-Based Engine]**\n"
376
+ "**Executive Snapshot**\n"
377
+ f"• Total spend: {eur(total_spend)} across {len(self.po_data):,} POs\n"
378
+ f"• On-time delivery: {on_time:.1f}% • Avg quality: {quality:.1f}/10\n"
379
+ f"• Top category: {top_cat} • Lead vendor: {top_vendor}\n\n"
380
+ "**Opportunities**\n"
381
+ "• Consolidate long tail vendors to improve pricing power (5–12% potential).\n"
382
+ "• Tighten SLAs for late deliveries and extend performance-based contracts.\n"
383
+ "• Automate low-value buys to reduce cycle time."
384
  )
385
+
386
+ def executive_summary(self) -> str:
387
+ if not self.llm.available:
388
+ return self._rule_summary()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  data_summary = {
390
  "total_spend": float(self.po_data['order_value'].sum()),
391
+ "total_orders": int(len(self.po_data)),
392
+ "vendor_count": int(self.po_data['vendor'].nunique()),
393
  "avg_order_value": float(self.po_data['order_value'].mean()),
394
+ "on_time_delivery": float((~self.po_data['late_delivery']).mean()),
395
+ "avg_quality": float(self.po_data['quality_score'].mean()),
396
  }
 
397
  messages = [
398
+ {"role": "system", "content": "You are a senior procurement analyst with expertise in SAP S/4HANA. Be concise, metric-driven, and actionable."},
399
+ {"role": "user", "content": (
400
+ "Create an executive summary covering: 1) overview (2-3 sentences), 2) KPI highlights, 3) risks/alerts, 4) 3-4 strategic recommendations with quantified impact.\n"
401
+ f"Data: {json.dumps(data_summary)}"
402
+ )},
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  ]
404
+ try:
405
+ return "🧠 **[AI-Powered Analysis]**\n\n" + self.llm.chat(messages, max_tokens=650)
406
+ except Exception as e:
407
+ return self._rule_summary() + f"\n\n*AI fallback due to: {e}*"
408
+
409
+ def chat_with_data(self, question: str) -> str:
410
+ if not self.llm.available:
411
+ return self._rule_answer(question)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  context = {
413
  "total_spend": float(self.po_data['order_value'].sum()),
414
+ "orders": int(len(self.po_data)),
415
+ "vendors": int(self.po_data['vendor'].nunique()),
416
+ "on_time": float((~self.po_data['late_delivery']).mean()),
417
+ "quality": float(self.po_data['quality_score'].mean()),
418
  }
 
419
  messages = [
420
+ {"role": "system", "content": "You are an expert procurement co-pilot. Use the provided context and respond with precise metrics and concrete actions."},
421
+ {"role": "user", "content": f"Question: {question}\nContext: {json.dumps(context)}"},
 
 
 
 
 
 
422
  ]
423
+ try:
424
+ return "🧠 **[AI Response]**\n\n" + self.llm.chat(messages, max_tokens=450)
425
+ except Exception as e:
426
+ return self._rule_answer(question) + f"\n\n*AI fallback due to: {e}*"
427
+
428
+ def _rule_answer(self, question: str) -> str:
429
+ q = question.lower()
430
+ if any(w in q for w in ["spend", "cost", "budget"]):
431
+ total = float(self.po_data['order_value'].sum())
432
+ monthly = total / max(1, self.po_data['order_date'].nunique()/30)
433
+ top_cat = self.po_data.groupby('material_category')['order_value'].sum().idxmax()
434
+ return (
435
+ "🤖 **[Smart Analysis] Spend**\n"
436
+ f"• Total spend: {eur(total)}\n"
437
+ f"• Monthly average (approx): {eur(monthly)}\n"
438
+ f"• Top category: {top_cat}\n"
439
+ "Tip: prioritize competitive events for the top 2 categories to unlock 4–8% savings."
440
+ )
441
+ if any(w in q for w in ["vendor", "supplier", "partner"]):
442
+ vp = self.po_data.groupby('vendor').agg(
443
+ spend=('order_value','sum'),
444
+ on_time=('late_delivery', lambda s: 1 - s.mean()),
445
+ ).sort_values('spend', ascending=False).head(1)
446
+ top = vp.index[0]
447
+ on_time = float(vp.iloc[0]['on_time'])*100
448
+ return (
449
+ "🤖 **[Smart Analysis] Vendor**\n"
450
+ f"• Top vendor: {top} • On-time: {on_time:.1f}%\n"
451
+ "Action: lock in volume tiers and add delivery penalties to the contract."
452
+ )
453
+ if any(w in q for w in ["risk", "late", "delay"]):
454
+ late_rate = float(self.po_data['late_delivery'].mean())*100
455
+ return (
456
+ "🤖 **[Smart Analysis] Risk**\n"
457
+ f"• Late delivery rate: {late_rate:.1f}%\n"
458
+ "Action: add buffer to planning lead times and escalate chronic late suppliers."
459
+ )
460
+ return (
461
+ "🤖 **[Smart Analysis]** I can help with spend, vendor performance, risk, savings, and trends. Try: \"Where can I save 10%?\""
462
+ )
463
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
 
465
+ # =============================
466
+ # App State & Initialization
467
+ # =============================
468
  if 'data_loaded' not in st.session_state:
469
  with st.spinner('🔄 Generating synthetic SAP S/4HANA procurement data...'):
470
  st.session_state.po_df, st.session_state.spend_df = generate_synthetic_procurement_data()
471
  st.session_state.data_loaded = True
472
 
473
+ @st.cache_resource(show_spinner=False)
474
+ def get_llm_client() -> UniversalLLMClient:
475
+ return UniversalLLMClient(LLMConfig())
476
 
477
+ client = get_llm_client()
478
+ agent = UniversalProcurementAgent(st.session_state.po_df, st.session_state.spend_df, client)
479
+ analytics = ProcurementAnalytics(st.session_state.po_df)
480
 
481
+ status = agent.llm_status()
482
+ api_status = "🟢 Connected" if status['llm_available'] else "🔴 Not Connected"
483
+
484
+ # =============================
485
+ # Header
486
+ # =============================
487
+ st.markdown(
488
+ f"""
489
  <div class="main-header">
490
+ <h1>🤖 SAP S/4HANA Agentic AI Procurement Analytics</h1>
491
+ <p>Autonomous Intelligence for Procurement Excellence</p>
492
+ <small>OpenAI: {api_status} · Data: {len(st.session_state.po_df):,} POs</small>
493
  </div>
494
+ """,
495
+ unsafe_allow_html=True,
496
+ )
497
 
498
+ # =============================
499
+ # Sidebar
500
+ # =============================
501
  with st.sidebar:
502
  st.markdown("### 🤖 AI System Status")
503
+ st.markdown(f"**Connection:** {api_status}")
504
+ st.markdown(f"**Provider:** {status['provider']} ")
505
+ st.markdown(f"**Model:** {status['model']}")
506
+
507
  with st.expander("🔍 System Information"):
508
+ safe = status.copy()
509
+ # Do not expose API key
510
+ st.json({k: v for k, v in safe.items() if k != 'api_key'})
511
+
512
  if st.button("🔄 Test AI Connection"):
513
+ if status['llm_available']:
514
+ st.success("LLM is reachable and ready.")
515
+ else:
516
+ st.error(f"LLM unavailable: {status['last_error']}")
517
+
 
 
 
 
 
 
 
 
 
518
  st.markdown("---")
519
+
520
  selected = option_menu(
521
  "Navigation",
522
+ ["🏠 Dashboard", "💬 AI Chat", "📊 Analytics", "🧪 What‑If", "🎯 Recommendations"],
523
+ icons=['house', 'chat', 'bar-chart', 'beaker', 'target'],
524
  menu_icon="cast",
525
  default_index=0,
526
  styles={
 
528
  "icon": {"color": "#0066cc", "font-size": "18px"},
529
  "nav-link": {"font-size": "16px", "text-align": "left", "margin": "0px", "--hover-color": "#eee"},
530
  "nav-link-selected": {"background-color": "#0066cc"},
531
+ },
532
  )
533
 
534
+ # =============================
535
+ # Main Views
536
+ # =============================
537
  if selected == "🏠 Dashboard":
538
  st.markdown("### 🧠 AI Executive Summary")
 
539
  with st.spinner('🤖 Analyzing procurement data...'):
540
+ summary = agent.executive_summary()
 
541
  st.markdown(f"""
542
  <div class="ai-insight">
543
  <h4>📊 Intelligent Analysis</h4>
544
+ <div style="white-space: pre-line; line-height: 1.55;">{summary}</div>
545
  </div>
546
  """, unsafe_allow_html=True)
547
+
548
+ k = analytics.kpis(hash(tuple(st.session_state.po_df['po_number'])))
549
+
550
+ c1, c2, c3, c4 = st.columns(4)
551
+ with c1:
552
+ st.markdown(f"<div class='metric-card'><h3 style='color: var(--primary-color); margin:0;'>Total Spend</h3><h2 style='margin: .5rem 0;'>{eur(k['total_spend'])}</h2><p style='color:#28a745;margin:0;'>📈 Active Portfolio</p></div>", unsafe_allow_html=True)
553
+ with c2:
554
+ st.markdown(f"<div class='metric-card'><h3 style='color: var(--primary-color); margin:0;'>Avg Order Value</h3><h2 style='margin: .5rem 0;'>{eur(k['avg_order_value'])}</h2><p style='color:#17a2b8;margin:0;'>📊 Order Efficiency</p></div>", unsafe_allow_html=True)
555
+ with c3:
556
+ st.markdown(f"<div class='metric-card'><h3 style='color: var(--primary-color); margin:0;'>Active Vendors</h3><h2 style='margin: .5rem 0;'>{k['active_vendors']}</h2><p style='color:#6f42c1;margin:0;'>🤝 Strategic Partners</p></div>", unsafe_allow_html=True)
557
+ with c4:
558
+ st.markdown(f"<div class='metric-card'><h3 style='color: var(--primary-color); margin:0;'>On‑Time Delivery</h3><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)
559
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  st.markdown("### 📊 Executive Dashboard")
561
+ colA, colB = st.columns(2)
562
+
563
+ with colA:
564
+ cat = analytics.category_spend()
565
+ fig = px.pie(cat, values='order_value', names='material_category', title='Spend Distribution by Category')
566
+ fig.update_layout(title_font_size=16, title_x=0.5, height=420)
567
+ st.plotly_chart(fig, use_container_width=True)
568
+
569
+ with colB:
570
+ vend = analytics.vendor_spend(top_n=8)
571
+ fig2 = px.bar(vend, x='vendor', y='order_value', title='Top Vendors by Spend')
572
+ fig2.update_layout(title_font_size=16, title_x=0.5, xaxis_tickangle=45, height=420)
573
+ st.plotly_chart(fig2, use_container_width=True)
574
+
575
+ colC, colD = st.columns(2)
576
+ with colC:
577
+ ms = analytics.monthly_spend()
578
+ fig3 = px.line(ms, x='month', y='order_value', markers=True, title='Monthly Spend Trend')
579
+ fig3.update_layout(title_font_size=16, title_x=0.5, height=420)
580
+ st.plotly_chart(fig3, use_container_width=True)
581
+
582
+ with colD:
583
+ ano = analytics.anomalies()
584
+ st.markdown("#### 🔎 High/Low Value Anomalies (Top 50)")
585
+ st.dataframe(ano[['po_number','vendor','material_category','order_value','anomaly_reason']].reset_index(drop=True), use_container_width=True, height=380)
 
586
 
587
  elif selected == "💬 AI Chat":
588
  st.markdown("### 💬 Chat with Your Procurement Data")
 
589
  st.markdown(f"""
590
  <div class="ai-insight">
591
  <h4>🤖 Universal AI Assistant</h4>
592
+ <p>Ask me anything about your procurement data! I'm provider-agnostic and resilient to API versions.</p>
593
+ <p><small>Status: {api_status} | Provider: {status['provider']} | Model: {status['model']}</small></p>
594
  </div>
595
  """, unsafe_allow_html=True)
596
+
 
597
  if "messages" not in st.session_state:
598
  st.session_state.messages = [
599
+ {"role": "assistant", "content": "Hello! I loaded your data and I'm ready to help—try asking about spend, vendors, or risk."}
600
  ]
601
+
602
+ for m in st.session_state.messages:
603
+ with st.chat_message(m["role"]):
604
+ st.markdown(m["content"])
605
+
606
+ if prompt := st.chat_input("Ask about your procurement data…"):
 
 
607
  st.session_state.messages.append({"role": "user", "content": prompt})
608
  with st.chat_message("user"):
609
  st.markdown(prompt)
 
610
  with st.chat_message("assistant"):
611
+ with st.spinner("🤖 Analyzing"):
612
+ reply = agent.chat_with_data(prompt)
613
+ st.markdown(reply)
614
+ st.session_state.messages.append({"role": "assistant", "content": reply})
615
+
616
+ st.markdown("#### 💡 Try quick questions:")
617
+ c1, c2, c3 = st.columns(3)
618
+ qs = ["What are my biggest spending areas?", "How are my vendors performing?", "Where can I save 10%?"]
619
+ for i, (c, q) in enumerate(zip([c1, c2, c3], qs)):
620
+ with c:
621
+ if st.button(f"💭 {q}", key=f"q_{i}"):
622
+ st.session_state.messages.append({"role": "user", "content": q})
623
+ st.session_state.messages.append({"role": "assistant", "content": agent.chat_with_data(q)})
 
 
 
 
 
 
 
 
 
624
  st.rerun()
625
 
626
  elif selected == "📊 Analytics":
627
  st.markdown("### 📈 Advanced Analytics Dashboard")
628
+ vp = analytics.vendor_performance()
629
+ st.dataframe(vp.rename(columns={
630
+ 'total_spend': 'Total Spend (€)',
631
+ 'on_time': 'On-Time Delivery %',
632
+ 'quality': 'Quality Score',
633
+ 'orders': 'Order Count',
634
+ 'lead_time': 'Avg Lead Time (days)'
635
+ }), use_container_width=True)
636
+
637
+ st.download_button(
638
+ label="⬇️ Download Vendor Performance (CSV)",
639
+ data=vp.to_csv().encode('utf-8'),
640
+ file_name="vendor_performance.csv",
641
+ mime="text/csv",
642
+ )
643
+
644
+ elif selected == "🧪 What‑If":
645
+ st.markdown("### 🧪 What‑If: Vendor Consolidation Simulator")
646
+ top_n = st.slider("Keep top N vendors by spend", min_value=2, max_value=10, value=6, step=1)
647
+ sim = analytics.simulate_vendor_consolidation(keep_top=top_n)
648
+
649
+ kept_names = ", ".join(sim['kept_vendors'])
650
+ st.markdown(
651
+ f"""
652
+ <div class='alert alert-info'>
653
+ <strong>Scenario:</strong> Keep top <b>{top_n}</b> vendors. Estimated addressable spend share: <b>{sim['kept_share']*100:.1f}%</b>.<br/>
654
+ <strong>Potential savings:</strong> <b>{sim['estimated_savings_pct']*100:.1f}%</b> (heuristic).<br/>
655
+ <small>Kept Vendors:</small> {kept_names}
656
+ </div>
657
+ """,
658
+ unsafe_allow_html=True,
659
+ )
660
+
661
+ if st.checkbox("Show detailed vendor spend"):
662
+ st.dataframe(analytics.vendor_spend(top_n=999), use_container_width=True)
663
 
664
  elif selected == "🎯 Recommendations":
665
  st.markdown("### 🚀 Strategic Recommendations")
666
+ recs = [
667
+ "🎯 **Vendor Consolidation**: Reduce long-tail suppliers; target 8–15% price improvement via volume tiers.",
668
+ " **Process Automation**: Auto-approve low-value POs to cut cycle time by 35–50%.",
669
+ "📊 **Performance Contracts**: KPI-linked clauses for on-time delivery; add service credits for misses.",
670
+ "🛡️ **Risk Monitoring**: Score suppliers on late rate, quality, and concentration; escalate chronic offenders.",
671
+ "🧠 **AI Copilot**: Use LLM to draft RFQs, summarize bids, and propose award scenarios.",
 
672
  ]
673
+ for i, rec in enumerate(recs, start=1):
674
+ st.markdown(
675
+ f"""
676
+ <div class="alert alert-success">
677
+ <h4>Recommendation #{i}</h4>
678
+ <p>{rec}</p>
679
+ </div>
680
+ """,
681
+ unsafe_allow_html=True,
682
+ )
683
 
684
+ # =============================
685
  # Footer
686
+ # =============================
687
  st.markdown("---")
688
+ st.markdown(
689
+ f"""
690
+ <div style="text-align:center; padding: 1rem; color:#666;">
691
+ <p>🤖 <strong>Universal AI Procurement Analytics</strong> | Provider‑agnostic LLM integration with resilient fallbacks</p>
692
+ <p><em>Demo with synthetic data • {len(st.session_state.po_df):,} orders • OpenAI {api_status}</em></p>
693
  </div>
694
+ """,
695
+ unsafe_allow_html=True,
696
+ )