PD03 commited on
Commit
bd80cb5
·
verified ·
1 Parent(s): 0d05102

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +185 -7
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os, json, tempfile, logging
2
  import gradio as gr
3
  import pandas as pd
@@ -146,6 +147,7 @@ def optimize_supply_tool(forecast_json: str) -> str:
146
  row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r1[p]) for p in P]; Aub.append(row); bub.append(r1_cap)
147
  row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r2[p]) for p in P]; Aub.append(row); bub.append(r2_cap)
148
 
 
149
  res = linprog(c, A_ub=np.array(Aub), b_ub=np.array(bub), A_eq=np.array(Aeq), b_eq=np.array(beq),
150
  bounds=bounds, method="highs")
151
  if not res.success:
@@ -268,7 +270,6 @@ def data_quality_guard_tool(use_demo: bool = True, history_csv_path: str = "", z
268
  if y == 0 and prev_nonzero and next_nonzero:
269
  flag = flag or "ZERO_BETWEEN_NONZERO"
270
  action = action or "impute_neighbor_avg"
271
- # simple neighbor average
272
  idx = s.index.get_loc(ts)
273
  neighbors = []
274
  if idx>0: neighbors.append(s.iloc[idx-1])
@@ -322,7 +323,6 @@ def scenario_explorer_tool(forecast_json: str) -> str:
322
  }
323
  r1_cap0, r2_cap0 = 320.0, 480.0
324
 
325
- # Define scenarios
326
  scenarios = [
327
  {"name":"Base", "mult":1.0, "r1_cap":r1_cap0, "r2_cap":r2_cap0, "rm_cost_B_add":0.0, "conv_fg100_mult":1.0, "rmA_start_mult":1.0},
328
  {"name":"+10% Demand", "demand_mult":1.10, "r1_cap":r1_cap0, "r2_cap":r2_cap0, "rm_cost_B_add":0.0, "conv_fg100_mult":1.0, "rmA_start_mult":1.0},
@@ -372,6 +372,7 @@ def scenario_explorer_tool(forecast_json: str) -> str:
372
  row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r1[p]) for p in P]; Aub.append(row); bub.append(r1_cap)
373
  row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r2[p]) for p in P]; Aub.append(row); bub.append(r2_cap)
374
 
 
375
  res = linprog(c, A_ub=np.array(Aub), b_ub=np.array(bub), A_eq=np.array(Aeq), b_eq=np.array(beq),
376
  bounds=bounds, method="highs")
377
  if not res.success:
@@ -415,8 +416,7 @@ def plan_explainer_tool(plan_json: str) -> str:
415
  products = d.get("products", [])
416
  resources = d.get("resources", [])
417
 
418
- # Simple insights
419
- # 1) Top product by revenue contribution (price*Sell - conv*Produce) — using demo fields available
420
  contribs = []
421
  for row in products:
422
  rev = row["Sell"] * row["Unit Price"]
@@ -425,7 +425,7 @@ def plan_explainer_tool(plan_json: str) -> str:
425
  contribs.sort(key=lambda x: x[1], reverse=True)
426
  top_prod = contribs[0][0] if contribs else "N/A"
427
 
428
- # 2) Binding resource (min slack)
429
  bind_res = None
430
  if resources:
431
  r_sorted = sorted(resources, key=lambda r: r.get("Slack", 0.0))
@@ -440,6 +440,158 @@ def plan_explainer_tool(plan_json: str) -> str:
440
 
441
  return json.dumps({"status":"OK","summary": summary})
442
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  # ==== Agent (end-to-end) ======================================================
444
  def make_agent():
445
  api_key = os.environ.get("OPENAI_API_KEY", "")
@@ -448,7 +600,7 @@ def make_agent():
448
  model = OpenAIServerModel(model_id="gpt-4o-mini", api_key=api_key, temperature=0)
449
  return CodeAgent(tools=[
450
  forecast_tool, optimize_supply_tool, update_sap_md61_tool,
451
- data_quality_guard_tool, scenario_explorer_tool, plan_explainer_tool
452
  ], model=model, add_base_tools=False, stream_outputs=False)
453
 
454
  SYSTEM_PLAN = (
@@ -511,6 +663,16 @@ def parse_scenarios(json_str):
511
  "r1_used":"R1 Used","r1_slack":"R1 Slack","r2_used":"R2 Used","r2_slack":"R2 Slack"})
512
  return df
513
 
 
 
 
 
 
 
 
 
 
 
514
  # ==== Gradio UI ==============================================================
515
  with gr.Blocks(title="Forecast → Optimize → SAP MD61") as demo:
516
  gr.Markdown("## 🧭 Workflow\n"
@@ -628,7 +790,7 @@ with gr.Blocks(title="Forecast → Optimize → SAP MD61") as demo:
628
  run_all.click(do_agent, inputs=[a_horizon, a_plant, a_demo, a_file],
629
  outputs=[out_json, a_forecast_tbl, a_plan_kpis, a_plan_prod, a_plan_raw, a_plan_res, a_md61_prev, a_md61_file])
630
 
631
- # --------- New Tab: Upgrades ---------
632
  with gr.Tab("Upgrades"):
633
  gr.Markdown("### 🧹 Data Quality Guard")
634
  with gr.Row():
@@ -670,6 +832,22 @@ with gr.Blocks(title="Forecast → Optimize → SAP MD61") as demo:
670
 
671
  run_ex.click(do_explain, inputs=[plan_state], outputs=[expl_md])
672
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
  if __name__ == "__main__":
674
  # Needs OPENAI_API_KEY in env for agent tab; manual tabs work without it.
675
  if not os.environ.get("OPENAI_API_KEY"):
 
1
+ # app.py
2
  import os, json, tempfile, logging
3
  import gradio as gr
4
  import pandas as pd
 
147
  row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r1[p]) for p in P]; Aub.append(row); bub.append(r1_cap)
148
  row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r2[p]) for p in P]; Aub.append(row); bub.append(r2_cap)
149
 
150
+ from scipy.optimize import linprog
151
  res = linprog(c, A_ub=np.array(Aub), b_ub=np.array(bub), A_eq=np.array(Aeq), b_eq=np.array(beq),
152
  bounds=bounds, method="highs")
153
  if not res.success:
 
270
  if y == 0 and prev_nonzero and next_nonzero:
271
  flag = flag or "ZERO_BETWEEN_NONZERO"
272
  action = action or "impute_neighbor_avg"
 
273
  idx = s.index.get_loc(ts)
274
  neighbors = []
275
  if idx>0: neighbors.append(s.iloc[idx-1])
 
323
  }
324
  r1_cap0, r2_cap0 = 320.0, 480.0
325
 
 
326
  scenarios = [
327
  {"name":"Base", "mult":1.0, "r1_cap":r1_cap0, "r2_cap":r2_cap0, "rm_cost_B_add":0.0, "conv_fg100_mult":1.0, "rmA_start_mult":1.0},
328
  {"name":"+10% Demand", "demand_mult":1.10, "r1_cap":r1_cap0, "r2_cap":r2_cap0, "rm_cost_B_add":0.0, "conv_fg100_mult":1.0, "rmA_start_mult":1.0},
 
372
  row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r1[p]) for p in P]; Aub.append(row); bub.append(r1_cap)
373
  row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r2[p]) for p in P]; Aub.append(row); bub.append(r2_cap)
374
 
375
+ from scipy.optimize import linprog
376
  res = linprog(c, A_ub=np.array(Aub), b_ub=np.array(bub), A_eq=np.array(Aeq), b_eq=np.array(beq),
377
  bounds=bounds, method="highs")
378
  if not res.success:
 
416
  products = d.get("products", [])
417
  resources = d.get("resources", [])
418
 
419
+ # Contribution by product
 
420
  contribs = []
421
  for row in products:
422
  rev = row["Sell"] * row["Unit Price"]
 
425
  contribs.sort(key=lambda x: x[1], reverse=True)
426
  top_prod = contribs[0][0] if contribs else "N/A"
427
 
428
+ # Binding resource (min slack)
429
  bind_res = None
430
  if resources:
431
  r_sorted = sorted(resources, key=lambda r: r.get("Slack", 0.0))
 
440
 
441
  return json.dumps({"status":"OK","summary": summary})
442
 
443
+ # ---------- NEW: Bottleneck Search (finite-difference + policy) ----------
444
+ @tool
445
+ def bottleneck_search_tool(forecast_json: str, policy_json: str = "") -> str:
446
+ """
447
+ Find the best practical lever to relax (within ~1 month) via small scenario probes.
448
+
449
+ Args:
450
+ forecast_json (str): JSON from forecast_tool (first month per SKU used).
451
+ policy_json (str): Optional JSON with relaxable levers and costs.
452
+ Defaults include overtime and RM expedite; blocks factory expansion.
453
+
454
+ Returns:
455
+ str: JSON {"status":"OK","recommendations":[{lever, recommended, est_profit_lift, est_net_gain, rationale}]}
456
+ """
457
+ # --- Defaults (edit to your reality) ---
458
+ policy = {
459
+ "R1_overtime": {"type":"resource","target":"R1","step":10.0,"max_delta":80.0,"unit_cost":600.0},
460
+ "R2_overtime": {"type":"resource","target":"R2","step":10.0,"max_delta":40.0,"unit_cost":800.0},
461
+ "RM_A_expedite": {"type":"rm_expedite","target":"RM_A","step":200.0,"max_delta":1500.0,"unit_premium":8.0},
462
+ "RM_B_expedite": {"type":"rm_expedite","target":"RM_B","step":100.0,"max_delta":800.0,"unit_premium":12.0},
463
+ "Factory_expansion": {"type":"blocked","reason":"Not relaxable within 1 month"}
464
+ }
465
+ if policy_json:
466
+ try:
467
+ policy.update(json.loads(policy_json))
468
+ except Exception:
469
+ pass
470
+
471
+ # --- 1) Baseline solve ---
472
+ base_plan = json.loads(optimize_supply_tool(forecast_json))
473
+ if base_plan.get("status") != "OPTIMAL":
474
+ return json.dumps({"status":"FAILED","message":"Baseline plan not optimal."})
475
+ base_profit = float(base_plan["kpis"]["Profit"])
476
+
477
+ # --- 2) Helper to run a perturbed solve ---
478
+ def run_with(delta_map):
479
+ from scipy.optimize import linprog
480
+
481
+ rows = json.loads(forecast_json)
482
+ demand = {}
483
+ for r in rows:
484
+ p = r["product_id"]
485
+ if p not in demand:
486
+ demand[p] = float(r["forecast_qty"])
487
+ P = sorted(demand.keys()) or ["FG100","FG200"]
488
+ price = {"FG100":98.0,"FG200":120.0}
489
+ conv = {"FG100":12.5,"FG200":15.0}
490
+ r1 = {"FG100":0.03,"FG200":0.05}
491
+ r2 = {"FG100":0.02,"FG200":0.01}
492
+ RMs = ["RM_A","RM_B"]
493
+ rm_cost = {"RM_A":20.0,"RM_B":30.0}
494
+ rm_start = {"RM_A":1000.0,"RM_B":100.0}
495
+ rm_cap = {"RM_A":5000.0,"RM_B":5000.0}
496
+ bom = {"FG100":{"RM_A":0.8,"RM_B":0.204},"FG200":{"RM_A":1.0,"RM_B":0.1}}
497
+
498
+ # Apply deltas
499
+ r1_cap = 320.0 + delta_map.get("R1_cap",0.0)
500
+ r2_cap = 480.0 + delta_map.get("R2_cap",0.0)
501
+ rm_cap_adj = {
502
+ "RM_A": rm_cap["RM_A"] + delta_map.get("RM_A_cap",0.0),
503
+ "RM_B": rm_cap["RM_B"] + delta_map.get("RM_B_cap",0.0),
504
+ }
505
+
506
+ nP, nR = len(P), len(RMs)
507
+ pidx = {p:i for i,p in enumerate(P)}; ridx = {r:i for i,r in enumerate(RMs)}
508
+ def i_prod(p): return pidx[p]
509
+ def i_sell(p): return nP + pidx[p]
510
+ def i_einv(p): return 2*nP + pidx[p]
511
+ def i_pur(r): return 3*nP + ridx[r]
512
+ def i_einr(r): return 3*nP + nR + ridx[r]
513
+ n_vars = 3*nP + 2*nR
514
+ c = np.zeros(n_vars); bounds=[None]*n_vars
515
+
516
+ for p in P:
517
+ c[i_prod(p)] += conv[p]
518
+ c[i_sell(p)] -= price[p]
519
+ bounds[i_prod(p)] = (0,None)
520
+ bounds[i_sell(p)] = (0,demand[p])
521
+ bounds[i_einv(p)] = (0,None)
522
+ for r in RMs:
523
+ c[i_pur(r)] += rm_cost[r]
524
+ bounds[i_pur(r)] = (0, rm_cap_adj[r])
525
+ bounds[i_einr(r)] = (0,None)
526
+
527
+ Aeq, beq = [], []
528
+ for p in P:
529
+ row = np.zeros(n_vars); row[i_prod(p)]=1; row[i_sell(p)]=-1; row[i_einv(p)]=-1
530
+ Aeq.append(row); beq.append(0.0)
531
+ for r in RMs:
532
+ row = np.zeros(n_vars); row[i_pur(r)]=1; row[i_einr(r)]=-1
533
+ for p in P: row[i_prod(p)] -= bom[p].get(r,0.0)
534
+ Aeq.append(row); beq.append(-rm_start[r])
535
+
536
+ Aub, bub = [], []
537
+ row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r1[p]) for p in P]; Aub.append(row); bub.append(r1_cap)
538
+ row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r2[p]) for p in P]; Aub.append(row); bub.append(r2_cap)
539
+
540
+ res = linprog(c, A_ub=np.array(Aub), b_ub=np.array(bub), A_eq=np.array(Aeq), b_eq=np.array(beq),
541
+ bounds=bounds, method="highs")
542
+ if not res.success:
543
+ return None
544
+ x = res.x
545
+ def v(idx): return float(x[idx])
546
+ revenue = sum(price[p]*v(i_sell(p)) for p in P)
547
+ conv_cost = sum(conv[p]*v(i_prod(p)) for p in P)
548
+ rm_purch_cost = sum(v(i_pur(r))*rm_cost[r] for r in RMs)
549
+ profit = revenue - conv_cost - rm_purch_cost
550
+ return profit
551
+
552
+ # --- 3) Probe levers; compute net gain ---
553
+ recs = []
554
+ for name, cfg in policy.items():
555
+ if cfg.get("type") == "blocked":
556
+ recs.append({"lever":name,"recommended":0,"est_profit_lift":0,"est_net_gain":0,
557
+ "rationale":cfg.get("reason","Not feasible in horizon")})
558
+ continue
559
+ step = float(cfg["step"]); maxd = float(cfg["max_delta"])
560
+ best_gain = 0.0; best_delta = 0.0; best_profit_lift = 0.0
561
+ d = 0.0
562
+ while d <= maxd + 1e-6:
563
+ delta_map = {}
564
+ relax_cost = 0.0
565
+ if cfg["type"] == "resource" and cfg["target"]=="R1":
566
+ delta_map["R1_cap"] = d; relax_cost = d * float(cfg["unit_cost"])
567
+ elif cfg["type"] == "resource" and cfg["target"]=="R2":
568
+ delta_map["R2_cap"] = d; relax_cost = d * float(cfg["unit_cost"])
569
+ elif cfg["type"] == "rm_expedite":
570
+ key = "RM_A_cap" if cfg["target"]=="RM_A" else "RM_B_cap"
571
+ delta_map[key] = d; relax_cost = d * float(cfg["unit_premium"])
572
+ else:
573
+ d += step; continue
574
+
575
+ prof = run_with(delta_map)
576
+ if prof is None:
577
+ d += step; continue
578
+ lift = prof - base_profit
579
+ net = lift - relax_cost
580
+ if net > best_gain:
581
+ best_gain, best_delta, best_profit_lift = net, d, lift
582
+ d += step
583
+
584
+ rationale = ("Beneficial at small Δ" if best_gain>0 else "No positive net gain within feasible range")
585
+ recs.append({"lever":name,"recommended":best_delta,"est_profit_lift":best_profit_lift,
586
+ "est_net_gain":best_gain,"rationale":rationale})
587
+
588
+ recs.sort(key=lambda r: r["est_net_gain"], reverse=True)
589
+ for r in recs:
590
+ for k in ["recommended","est_profit_lift","est_net_gain"]:
591
+ if r[k] is not None:
592
+ r[k] = round(float(r[k]), 2)
593
+ return json.dumps({"status":"OK","recommendations":recs})
594
+
595
  # ==== Agent (end-to-end) ======================================================
596
  def make_agent():
597
  api_key = os.environ.get("OPENAI_API_KEY", "")
 
600
  model = OpenAIServerModel(model_id="gpt-4o-mini", api_key=api_key, temperature=0)
601
  return CodeAgent(tools=[
602
  forecast_tool, optimize_supply_tool, update_sap_md61_tool,
603
+ data_quality_guard_tool, scenario_explorer_tool, plan_explainer_tool, bottleneck_search_tool
604
  ], model=model, add_base_tools=False, stream_outputs=False)
605
 
606
  SYSTEM_PLAN = (
 
663
  "r1_used":"R1 Used","r1_slack":"R1 Slack","r2_used":"R2 Used","r2_slack":"R2 Slack"})
664
  return df
665
 
666
+ def parse_recs(json_str):
667
+ d = json.loads(json_str)
668
+ df = _round_df(pd.DataFrame(d.get("recommendations", [])))
669
+ if not df.empty:
670
+ df = df.rename(columns={
671
+ "lever":"Lever","recommended":"Recommended Δ","est_profit_lift":"Est. Profit Lift",
672
+ "est_net_gain":"Est. Net Gain","rationale":"Rationale"
673
+ })
674
+ return df
675
+
676
  # ==== Gradio UI ==============================================================
677
  with gr.Blocks(title="Forecast → Optimize → SAP MD61") as demo:
678
  gr.Markdown("## 🧭 Workflow\n"
 
790
  run_all.click(do_agent, inputs=[a_horizon, a_plant, a_demo, a_file],
791
  outputs=[out_json, a_forecast_tbl, a_plan_kpis, a_plan_prod, a_plan_raw, a_plan_res, a_md61_prev, a_md61_file])
792
 
793
+ # --------- Upgrades ---------
794
  with gr.Tab("Upgrades"):
795
  gr.Markdown("### 🧹 Data Quality Guard")
796
  with gr.Row():
 
832
 
833
  run_ex.click(do_explain, inputs=[plan_state], outputs=[expl_md])
834
 
835
+ gr.Markdown("### 🎯 Bottleneck Finder (Practical Levers)")
836
+ policy_box = gr.Textbox(
837
+ label="Optional policy JSON (overtime/expedite limits & costs). Leave blank for sensible defaults.",
838
+ lines=4, value=""
839
+ )
840
+ run_bn = gr.Button("Find Best Bottleneck (uses current Forecast)")
841
+ bn_tbl = gr.Dataframe(label="Ranked Recommendations", interactive=False)
842
+
843
+ def do_bn(fj, policy):
844
+ if not fj:
845
+ return pd.DataFrame()
846
+ out = bottleneck_search_tool(fj, policy_json=policy or "")
847
+ return parse_recs(out)
848
+
849
+ run_bn.click(do_bn, inputs=[forecast_state, policy_box], outputs=[bn_tbl])
850
+
851
  if __name__ == "__main__":
852
  # Needs OPENAI_API_KEY in env for agent tab; manual tabs work without it.
853
  if not os.environ.get("OPENAI_API_KEY"):