PD03 commited on
Commit
3fa5696
Β·
verified Β·
1 Parent(s): 6c62860

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -84
app.py CHANGED
@@ -94,7 +94,6 @@ def fmt_currency(x: float) -> str:
94
  # =============================
95
  @dataclass
96
  class LLMConfig:
97
- provider: str = os.getenv("LLM_PROVIDER", "openai").lower()
98
  base_url: Optional[str] = os.getenv("OPENAI_BASE_URL")
99
  api_key: Optional[str] = (
100
  os.getenv("OPENAI_API_KEY")
@@ -126,7 +125,7 @@ class UniversalLLMClient:
126
  def _base_url(self) -> str:
127
  return (self.cfg.base_url or "https://api.openai.com/v1").rstrip("/")
128
 
129
- def _smoke_test(self):
130
  try:
131
  _ = self.chat([{"role": "user", "content": "ping"}], max_tokens=4)
132
  except Exception as e:
@@ -279,7 +278,6 @@ class ProcurementAnalytics:
279
  g['total_spend'] = g['total_spend'].round(2)
280
  return g.sort_values('total_spend', ascending=False)
281
 
282
- # helper: top N with shares
283
  def top_n_categories(self, n: int = 3) -> List[Tuple[str, float]]:
284
  cat = self.category_spend()
285
  total = float(cat['order_value'].sum()) or 1.0
@@ -316,33 +314,34 @@ class UniversalProcurementAgent:
316
  "top_categories": top_cats,
317
  "top_vendors": top_vens,
318
  }
319
- messages = [
320
- {
321
- "role": "system",
322
- "content": (
323
- "You are a senior procurement analyst. Use bullet points, be concise, "
324
- "and always use the β‚Ή symbol. "
325
- "When summarizing, include top categories and vendors with percentages, "
326
- "then 2-3 quantified actions."
327
- )
328
- },
329
- {
330
- "role": "user",
331
- "content": (
332
- f"Executive summary. Format amounts with commas (e.g., β‚Ή12,34,567).\n\n"
333
- f"Data: {json.dumps(data_summary)}"
334
- )
335
- },
336
- ]
337
 
338
- try:
339
- return (
340
- "🧠 **[AI-Powered Analysis]**\n\n"
341
- + self.llm.chat(messages, max_tokens=550)
342
- )
343
- except Exception as e:
344
- return self._rule_summary() + f"\n\n*AI fallback due to: {e}*"
 
345
 
 
 
 
 
 
346
 
347
  *AI fallback due to: {e}*"
348
 
@@ -355,15 +354,15 @@ except Exception as e:
355
  return (
356
  "πŸ€– **[Rule-Based Summary]**
357
  "
358
- f"β€’ Total spend: {fmt_currency(k['total_spend'])} across {len(self.po_data):,} POs
359
  "
360
- f"β€’ On-time delivery: {k['on_time_rate']*100:.1f}% | Avg quality: {k['quality_avg']:.1f}/10
361
  "
362
- f"β€’ Top categories: {topc_str}
363
  "
364
- f"β€’ Top vendors: {topv_str}
365
  "
366
- "Actions: Consolidate long tail; multi-year terms with top vendors; auto-approve low-value POs."
367
  )
368
 
369
  def chat_with_data(self, question: str) -> str:
@@ -395,9 +394,12 @@ Context: {json.dumps(context)}
395
  {style_rules}"},
396
  ]
397
  try:
398
- return "🧠 **[AI Response]**
 
399
 
400
- " + self.llm.chat(messages, max_tokens=450)
 
 
401
  except Exception as e:
402
  return self._rule_answer(question) + f"
403
 
@@ -408,7 +410,7 @@ Context: {json.dumps(context)}
408
  k = self.analytics.kpis()
409
  top_c = self.analytics.top_n_categories(3)
410
  top_v = self.analytics.top_n_vendors(3)
411
- if "spend" in q or "spending" in q or "cost" in q:
412
  lines = [
413
  f"β€’ Total spend: {fmt_currency(k['total_spend'])}",
414
  "β€’ Top categories: " + ", ".join([f"{n} – {s:.0f}%" for n, s in top_c]),
@@ -418,7 +420,7 @@ Context: {json.dumps(context)}
418
  return "πŸ€– **[Rule-Based Spend]**
419
  " + "
420
  ".join(lines)
421
- if "vendor" in q or "supplier" in q or "partner" in q:
422
  vp = self.po_data.groupby('vendor').agg(
423
  spend=('order_value','sum'),
424
  late_rate=('late_delivery','mean'),
@@ -437,7 +439,7 @@ Context: {json.dumps(context)}
437
  return "πŸ€– **[Rule-Based Vendor]**
438
  " + "
439
  ".join(lines)
440
- if "risk" in q or "late" in q or "delay" in q:
441
  late = float(self.po_data['late_delivery'].mean())*100
442
  lines = [
443
  f"β€’ Late delivery rate: {late:.1f}%",
@@ -446,13 +448,12 @@ Context: {json.dumps(context)}
446
  return "πŸ€– **[Rule-Based Risk]**
447
  " + "
448
  ".join(lines)
449
- # default
450
  return (
451
  "πŸ€– **[Rule-Based]**
452
  "
453
- "β€’ I can analyze spend (top categories/vendors), vendor performance (best/worst), risk (late %), and trends.
454
  "
455
- 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}%"
456
  )
457
 
458
  # =============================
@@ -482,13 +483,13 @@ api_status = "🟒 Connected" if status['available'] else "πŸ”΄ Not Connected"
482
  # Header
483
  # =============================
484
  st.markdown(
485
- f"""
486
- <div class="main-header">
487
- <h1>πŸ€– SAP S/4HANA Agentic AI Procurement Analytics</h1>
488
- <p>Autonomous Intelligence for Procurement Excellence</p>
489
- <small>LLM: {api_status} Β· Data: {len(st.session_state.po_df):,} POs</small>
490
- </div>
491
- """,
492
  unsafe_allow_html=True,
493
  )
494
 
@@ -524,24 +525,63 @@ if selected == "🏠 Dashboard":
524
  st.markdown("### 🧠 AI Executive Summary")
525
  with st.spinner('πŸ€– Analyzing procurement data...'):
526
  summary = agent.executive_summary()
527
- st.markdown(f"""
528
- <div class="ai-insight">
529
- <h4>πŸ“Š Intelligent Analysis</h4>
530
- <div style="white-space: pre-line; line-height: 1.55;">{summary}</div>
531
- </div>
532
- """, unsafe_allow_html=True)
 
 
 
533
 
534
  k = analytics.kpis()
535
 
536
  c1, c2, c3, c4 = st.columns(4)
537
  with c1:
538
- st.markdown(f"<div class='metric-card'><h3 style='color: var(--primary-color); margin:0;'>Total Spend</h3><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)
 
 
 
 
 
 
 
 
 
539
  with c2:
540
- st.markdown(f"<div class='metric-card'><h3 style='color: var(--primary-color); margin:0;'>Avg Order Value</h3><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)
 
 
 
 
 
 
 
 
 
541
  with c3:
542
- 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)
 
 
 
 
 
 
 
 
 
543
  with c4:
544
- 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)
 
 
 
 
 
 
 
 
 
545
 
546
  st.markdown("### πŸ“Š Executive Dashboard")
547
  colA, colB = st.columns(2)
@@ -574,13 +614,16 @@ if selected == "🏠 Dashboard":
574
 
575
  elif selected == "πŸ’¬ AI Chat":
576
  st.markdown("### πŸ’¬ Chat with Your Procurement Data")
577
- st.markdown(f"""
578
- <div class=\"ai-insight\">
579
- <h4>πŸ€– Universal AI Assistant</h4>
580
- <p>Ask me anything about your procurement data. I will answer with crisp bullets and actual metrics.</p>
581
- <p><small>Status: {api_status} | Model: {status['model']}</small></p>
582
- </div>
583
- """, unsafe_allow_html=True)
 
 
 
584
 
585
  if "messages" not in st.session_state:
586
  st.session_state.messages = [
@@ -645,13 +688,13 @@ elif selected == "πŸ§ͺ What‑If":
645
 
646
  kept_names = ", ".join(kept_vendors)
647
  st.markdown(
648
- f"""
649
- <div class='alert alert-info'>
650
- <strong>Scenario:</strong> Keep top <b>{top_n}</b> vendors. Addressable share: <b>{share*100:.1f}%</b>.<br/>
651
- <strong>Potential savings:</strong> <b>{est_savings*100:.1f}%</b> (heuristic).<br/>
652
- <small>Kept Vendors:</small> {kept_names}
653
- </div>
654
- """,
655
  unsafe_allow_html=True,
656
  )
657
 
@@ -669,12 +712,12 @@ elif selected == "🎯 Recommendations":
669
  ]
670
  for i, rec in enumerate(recs, start=1):
671
  st.markdown(
672
- f"""
673
- <div class="alert alert-success">
674
- <h4>Recommendation #{i}</h4>
675
- <p>{rec}</p>
676
- </div>
677
- """,
678
  unsafe_allow_html=True,
679
  )
680
 
@@ -683,11 +726,11 @@ elif selected == "🎯 Recommendations":
683
  # =============================
684
  st.markdown("---")
685
  st.markdown(
686
- f"""
687
- <div style="text-align:center; padding: 1rem; color:#666;">
688
- <p>πŸ€– <strong>Universal AI Procurement Analytics</strong> | Crisp, metric-first answers in β‚Ή</p>
689
- <p><em>Demo with synthetic data β€’ {len(st.session_state.po_df):,} orders β€’ LLM {api_status}</em></p>
690
- </div>
691
- """,
692
  unsafe_allow_html=True,
693
  )
 
94
  # =============================
95
  @dataclass
96
  class LLMConfig:
 
97
  base_url: Optional[str] = os.getenv("OPENAI_BASE_URL")
98
  api_key: Optional[str] = (
99
  os.getenv("OPENAI_API_KEY")
 
125
  def _base_url(self) -> str:
126
  return (self.cfg.base_url or "https://api.openai.com/v1").rstrip("/")
127
 
128
+ def _smoke_test(self) -> None:
129
  try:
130
  _ = self.chat([{"role": "user", "content": "ping"}], max_tokens=4)
131
  except Exception as e:
 
278
  g['total_spend'] = g['total_spend'].round(2)
279
  return g.sort_values('total_spend', ascending=False)
280
 
 
281
  def top_n_categories(self, n: int = 3) -> List[Tuple[str, float]]:
282
  cat = self.category_spend()
283
  total = float(cat['order_value'].sum()) or 1.0
 
314
  "top_categories": top_cats,
315
  "top_vendors": top_vens,
316
  }
317
+ messages = [
318
+ {
319
+ "role": "system",
320
+ "content": (
321
+ "You are a senior procurement analyst. Use bullet points, be concise, "
322
+ "and always use the β‚Ή symbol. When summarizing, include top categories "
323
+ "and vendors with percentages, then 2-3 quantified actions."
324
+ ),
325
+ },
326
+ {
327
+ "role": "user",
328
+ "content": (
329
+ "Executive summary. Format amounts with commas (e.g., β‚Ή12,34,567).
 
 
 
 
 
330
 
331
+ "
332
+ + f"Data: {json.dumps(data_summary)}"
333
+ ),
334
+ },
335
+ ]
336
+ try:
337
+ return (
338
+ "🧠 **[AI-Powered Analysis]**
339
 
340
+ "
341
+ + self.llm.chat(messages, max_tokens=550)
342
+ )
343
+ except Exception as e:
344
+ return self._rule_summary() + f"
345
 
346
  *AI fallback due to: {e}*"
347
 
 
354
  return (
355
  "πŸ€– **[Rule-Based Summary]**
356
  "
357
+ + f"β€’ Total spend: {fmt_currency(k['total_spend'])} across {len(self.po_data):,} POs
358
  "
359
+ + f"β€’ On-time delivery: {k['on_time_rate']*100:.1f}% | Avg quality: {k['quality_avg']:.1f}/10
360
  "
361
+ + f"β€’ Top categories: {topc_str}
362
  "
363
+ + f"β€’ Top vendors: {topv_str}
364
  "
365
+ + "Actions: Consolidate long tail; multi-year terms with top vendors; auto-approve low-value POs."
366
  )
367
 
368
  def chat_with_data(self, question: str) -> str:
 
394
  {style_rules}"},
395
  ]
396
  try:
397
+ return (
398
+ "🧠 **[AI Response]**
399
 
400
+ "
401
+ + self.llm.chat(messages, max_tokens=450)
402
+ )
403
  except Exception as e:
404
  return self._rule_answer(question) + f"
405
 
 
410
  k = self.analytics.kpis()
411
  top_c = self.analytics.top_n_categories(3)
412
  top_v = self.analytics.top_n_vendors(3)
413
+ if ("spend" in q) or ("spending" in q) or ("cost" in q):
414
  lines = [
415
  f"β€’ Total spend: {fmt_currency(k['total_spend'])}",
416
  "β€’ Top categories: " + ", ".join([f"{n} – {s:.0f}%" for n, s in top_c]),
 
420
  return "πŸ€– **[Rule-Based Spend]**
421
  " + "
422
  ".join(lines)
423
+ if ("vendor" in q) or ("supplier" in q) or ("partner" in q):
424
  vp = self.po_data.groupby('vendor').agg(
425
  spend=('order_value','sum'),
426
  late_rate=('late_delivery','mean'),
 
439
  return "πŸ€– **[Rule-Based Vendor]**
440
  " + "
441
  ".join(lines)
442
+ if ("risk" in q) or ("late" in q) or ("delay" in q):
443
  late = float(self.po_data['late_delivery'].mean())*100
444
  lines = [
445
  f"β€’ Late delivery rate: {late:.1f}%",
 
448
  return "πŸ€– **[Rule-Based Risk]**
449
  " + "
450
  ".join(lines)
 
451
  return (
452
  "πŸ€– **[Rule-Based]**
453
  "
454
+ + "β€’ I can analyze spend (top categories/vendors), vendor performance (best/worst), risk (late %), and trends.
455
  "
456
+ + 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}%"
457
  )
458
 
459
  # =============================
 
483
  # Header
484
  # =============================
485
  st.markdown(
486
+ (
487
+ "<div class=\"main-header\">"
488
+ "<h1>πŸ€– SAP S/4HANA Agentic AI Procurement Analytics</h1>"
489
+ "<p>Autonomous Intelligence for Procurement Excellence</p>"
490
+ + f"<small>LLM: {api_status} Β· Data: {len(st.session_state.po_df):,} POs</small>"
491
+ "</div>"
492
+ ),
493
  unsafe_allow_html=True,
494
  )
495
 
 
525
  st.markdown("### 🧠 AI Executive Summary")
526
  with st.spinner('πŸ€– Analyzing procurement data...'):
527
  summary = agent.executive_summary()
528
+ st.markdown(
529
+ (
530
+ "<div class=\"ai-insight\">"
531
+ "<h4>πŸ“Š Intelligent Analysis</h4>"
532
+ + f"<div style=\"white-space: pre-line; line-height: 1.55;\">{summary}</div>"
533
+ "</div>"
534
+ ),
535
+ unsafe_allow_html=True,
536
+ )
537
 
538
  k = analytics.kpis()
539
 
540
  c1, c2, c3, c4 = st.columns(4)
541
  with c1:
542
+ st.markdown(
543
+ (
544
+ "<div class='metric-card'>"
545
+ "<h3 style='color: var(--primary-color); margin:0;'>Total Spend</h3>"
546
+ + f"<h2 style='margin: .5rem 0;'>{fmt_currency(k['total_spend'])}</h2>"
547
+ "<p style='color:#28a745;margin:0;'>πŸ“ˆ Active Portfolio</p>"
548
+ "</div>"
549
+ ),
550
+ unsafe_allow_html=True,
551
+ )
552
  with c2:
553
+ st.markdown(
554
+ (
555
+ "<div class='metric-card'>"
556
+ "<h3 style='color: var(--primary-color); margin:0;'>Avg Order Value</h3>"
557
+ + f"<h2 style='margin: .5rem 0;'>{fmt_currency(k['avg_order_value'])}</h2>"
558
+ "<p style='color:#17a2b8;margin:0;'>πŸ“Š Order Efficiency</p>"
559
+ "</div>"
560
+ ),
561
+ unsafe_allow_html=True,
562
+ )
563
  with c3:
564
+ st.markdown(
565
+ (
566
+ "<div class='metric-card'>"
567
+ "<h3 style='color: var(--primary-color); margin:0;'>Active Vendors</h3>"
568
+ + f"<h2 style='margin: .5rem 0;'>{k['active_vendors']}</h2>"
569
+ "<p style='color:#6f42c1;margin:0;'>🀝 Strategic Partners</p>"
570
+ "</div>"
571
+ ),
572
+ unsafe_allow_html=True,
573
+ )
574
  with c4:
575
+ st.markdown(
576
+ (
577
+ "<div class='metric-card'>"
578
+ "<h3 style='color: var(--primary-color); margin:0;'>On‑Time Delivery</h3>"
579
+ + f"<h2 style='margin: .5rem 0;'>{k['on_time_rate']*100:.1f}%</h2>"
580
+ "<p style='color:#28a745;margin:0;'>⏱ Performance</p>"
581
+ "</div>"
582
+ ),
583
+ unsafe_allow_html=True,
584
+ )
585
 
586
  st.markdown("### πŸ“Š Executive Dashboard")
587
  colA, colB = st.columns(2)
 
614
 
615
  elif selected == "πŸ’¬ AI Chat":
616
  st.markdown("### πŸ’¬ Chat with Your Procurement Data")
617
+ st.markdown(
618
+ (
619
+ "<div class=\"ai-insight\">"
620
+ "<h4>πŸ€– Universal AI Assistant</h4>"
621
+ "<p>Ask me anything about your procurement data. I will answer with crisp bullets and actual metrics.</p>"
622
+ + f"<p><small>Status: {api_status} | Model: {status['model']}</small></p>"
623
+ "</div>"
624
+ ),
625
+ unsafe_allow_html=True,
626
+ )
627
 
628
  if "messages" not in st.session_state:
629
  st.session_state.messages = [
 
688
 
689
  kept_names = ", ".join(kept_vendors)
690
  st.markdown(
691
+ (
692
+ "<div class='alert alert-info'>"
693
+ + f"<strong>Scenario:</strong> Keep top <b>{top_n}</b> vendors. Addressable share: <b>{share*100:.1f}%</b>.<br/>"
694
+ + f"<strong>Potential savings:</strong> <b>{est_savings*100:.1f}%</b> (heuristic).<br/>"
695
+ + f"<small>Kept Vendors:</small> {kept_names}"
696
+ + "</div>"
697
+ ),
698
  unsafe_allow_html=True,
699
  )
700
 
 
712
  ]
713
  for i, rec in enumerate(recs, start=1):
714
  st.markdown(
715
+ (
716
+ "<div class=\"alert alert-success\">"
717
+ + f"<h4>Recommendation #{i}</h4>"
718
+ + f"<p>{rec}</p>"
719
+ + "</div>"
720
+ ),
721
  unsafe_allow_html=True,
722
  )
723
 
 
726
  # =============================
727
  st.markdown("---")
728
  st.markdown(
729
+ (
730
+ "<div style=\"text-align:center; padding: 1rem; color:#666;\">"
731
+ "<p>πŸ€– <strong>Universal AI Procurement Analytics</strong> | Crisp, metric-first answers in β‚Ή</p>"
732
+ + f"<p><em>Demo with synthetic data β€’ {len(st.session_state.po_df):,} orders β€’ LLM {api_status}</em></p>"
733
+ "</div>"
734
+ ),
735
  unsafe_allow_html=True,
736
  )