PD03 commited on
Commit
0e1a34f
Β·
verified Β·
1 Parent(s): 994118b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -201
app.py CHANGED
@@ -1,45 +1,31 @@
1
- import os, json, logging, tempfile
2
  import gradio as gr
3
  import pandas as pd
4
  import numpy as np
5
 
6
- # quiet logs
7
  logging.getLogger("cmdstanpy").setLevel(logging.WARNING)
8
  logging.getLogger("prophet").setLevel(logging.WARNING)
9
 
10
- # -----------------------------
11
- # Auth: set OPENAI_API_KEY in HF/Colab secrets
12
- # -----------------------------
13
- OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
14
- if not OPENAI_API_KEY:
15
- print("⚠️ OPENAI_API_KEY not set. Set a Space secret or env var. Tools will still run locally; the agent needs it.")
16
-
17
- # -----------------------------
18
- # Tools (your requested @tool style)
19
- # -----------------------------
20
  from smolagents import tool, CodeAgent, OpenAIServerModel
21
 
22
  @tool
23
- def forecast_tool(
24
- horizon_months: int = 1,
25
- use_demo: bool = True,
26
- history_csv_path: str = ""
27
- ) -> str:
28
  """
29
  Forecast monthly demand for finished goods using Prophet (demo-friendly).
30
 
31
  Args:
32
- horizon_months (int): Number of future months to forecast. Defaults to 1.
33
- use_demo (bool): If True, generate synthetic history for two SKUs (FG100/FG200). Defaults to True.
34
- history_csv_path (str): Optional path to CSV with columns [product_id,date,qty] to override demo.
35
 
36
  Returns:
37
- str: JSON string list of objects:
38
- {"product_id": str, "period_start": "YYYY-MM-01", "forecast_qty": float}
39
  """
40
  from prophet import Prophet
41
 
42
- # 1) Build history
43
  if use_demo or not history_csv_path:
44
  rng = pd.date_range("2023-01-01", periods=24, freq="MS")
45
  rows = []
@@ -52,82 +38,62 @@ def forecast_tool(
52
  df = pd.DataFrame(rows)
53
  else:
54
  df = pd.read_csv(history_csv_path)
55
- assert {"product_id", "date", "qty"} <= set(df.columns), "Missing required columns: product_id,date,qty"
56
  df["date"] = pd.to_datetime(df["date"], errors="coerce")
57
  df = df.dropna(subset=["date"])
58
  df["qty"] = pd.to_numeric(df["qty"], errors="coerce").fillna(0.0)
59
 
60
- # 2) Forecast per product with Prophet
61
  out = []
62
- horizon_months = max(1, int(horizon_months))
63
  for pid, g in df.groupby("product_id"):
64
- s = (
65
- g.set_index("date")["qty"]
66
- .resample("MS").sum()
67
- .asfreq("MS").fillna(0.0)
68
- )
69
  m = Prophet(yearly_seasonality=True, weekly_seasonality=False, daily_seasonality=False, n_changepoints=10)
70
  m.fit(pd.DataFrame({"ds": s.index, "y": s.values}))
71
- future = m.make_future_dataframe(periods=horizon_months, freq="MS", include_history=False)
72
  pred = m.predict(future)[["ds", "yhat"]]
73
  for _, r in pred.iterrows():
74
- out.append({
75
- "product_id": str(pid),
76
- "period_start": r["ds"].strftime("%Y-%m-%d"),
77
- "forecast_qty": float(r["yhat"])
78
- })
79
  return json.dumps(out)
80
 
81
 
82
  @tool
83
- def optimize_supply_tool(
84
- forecast_json: str
85
- ) -> str:
86
  """
87
- Optimize a single-month supply plan (demo LP) using forecasted demand.
88
 
89
  Args:
90
  forecast_json (str): JSON string returned by forecast_tool.
91
 
92
  Returns:
93
- str: JSON string with plan summary:
94
- {
95
- "status": "OPTIMAL",
96
- "profit": float,
97
- "products": [{"product_id": ..., "produce_qty": ..., "sell_qty": ...}],
98
- "raw_materials": [{"rm_id": ..., "purchase_qty": ..., "consumption_qty": ...}],
99
- "resources": [{"resource_id": "R1"/"R2", "used_hours": ..., "available_hours": ...}]
100
- }
101
  """
102
- # Demo master data (same as earlier examples)
103
- demand_rows = json.loads(forecast_json)
 
104
  # Use first month per product
105
  demand = {}
106
- for row in demand_rows:
107
- p = row["product_id"]
108
- demand.setdefault(p, row) # first occurrence only
 
109
 
110
- P = sorted(demand.keys()) or ["FG100", "FG200"] # default if empty
111
- # Prices / conversion costs / resource usage
112
  price = {"FG100": 98.0, "FG200": 120.0}
113
  conv = {"FG100": 12.5, "FG200": 15.0}
114
  r1 = {"FG100": 0.03, "FG200": 0.05}
115
  r2 = {"FG100": 0.02, "FG200": 0.01}
116
- # RM data + BOM eff usage
117
  RMs = ["RM_A", "RM_B"]
118
  rm_cost = {"RM_A": 20.0, "RM_B": 30.0}
119
  rm_start = {"RM_A": 1000.0, "RM_B": 100.0}
120
- rm_cap = {"RM_A": 5000.0, "RM_B": 5000.0}
121
  bom = {
122
- "FG100": {"RM_A": 0.8, "RM_B": 0.2 * 1.02}, # scrap on B
123
  "FG200": {"RM_A": 1.0, "RM_B": 0.1},
124
  }
125
  r1_cap, r2_cap = 320.0, 480.0
126
- start_inv = {p: 0.0 for p in P} # keep the LP minimal
127
- safety = {p: 0.0 for p in P}
128
 
129
- # Build LP: variables = produce[p], sell[p], purchase[r], end_inv_rm[r], end_inv[p]
130
- from scipy.optimize import linprog
131
  nP, nR = len(P), len(RMs)
132
  pidx = {p:i for i,p in enumerate(P)}
133
  ridx = {r:i for i,r in enumerate(RMs)}
@@ -142,203 +108,257 @@ def optimize_supply_tool(
142
  c = np.zeros(n_vars)
143
  bounds = [None]*n_vars
144
 
145
- # objective: minimize (costs - revenue)
146
  for p in P:
147
  c[i_prod(p)] += conv[p]
148
  c[i_sell(p)] -= price[p]
149
  c[i_einv(p)] += 0.0
150
  bounds[i_prod(p)] = (0, None)
151
- bounds[i_sell(p)] = (0, float(demand[p]["forecast_qty"]))
152
- bounds[i_einv(p)] = (safety[p], None)
153
  for r in RMs:
154
  c[i_pur(r)] += rm_cost[r]
155
  c[i_einr(r)] += 0.0
156
  bounds[i_pur(r)] = (0, rm_cap[r])
157
  bounds[i_einr(r)] = (0, None)
158
 
159
- # equalities
160
  Aeq, beq = [], []
161
- # FG balance: start + produce - sell - end_inv = 0
162
  for p in P:
163
- row = np.zeros(n_vars)
164
- row[i_prod(p)] = 1; row[i_sell(p)] = -1; row[i_einv(p)] = -1
165
- Aeq.append(row); beq.append(-start_inv[p])
166
- # RM balance: start + purchase - sum(use*produce) - end_inv_rm = 0
167
  for r in RMs:
168
- row = np.zeros(n_vars)
169
- row[i_pur(r)] = 1; row[i_einr(r)] = -1
170
- for p in P:
171
- row[i_prod(p)] -= bom.get(p, {}).get(r, 0.0)
172
  Aeq.append(row); beq.append(-rm_start[r])
173
 
174
- Aeq, beq = np.array(Aeq), np.array(beq)
175
-
176
- # inequalities (resources)
177
  Aub, bub = [], []
178
- row = np.zeros(n_vars)
179
- for p in P: row[i_prod(p)] = r1[p]
180
- Aub.append(row); bub.append(r1_cap)
181
- row = np.zeros(n_vars)
182
- for p in P: row[i_prod(p)] = r2[p]
183
- Aub.append(row); bub.append(r2_cap)
184
- Aub, bub = np.array(Aub), np.array(bub)
185
-
186
- res = linprog(c, A_ub=Aub, b_ub=bub, A_eq=Aeq, b_eq=beq, bounds=bounds, method="highs")
187
  if not res.success:
188
  return json.dumps({"status": "FAILED", "message": res.message})
189
 
190
  x = res.x
191
  def v(idx): return float(x[idx])
192
 
193
- # Build outputs
194
- prod_rows = []
 
195
  for p in P:
196
- prod_rows.append({
197
- "product_id": p,
198
- "produce_qty": v(i_prod(p)),
199
- "sell_qty": v(i_sell(p))
200
- })
201
- # resource usage
202
- r1_used = float(sum(r1[p]*v(i_prod(p)) for p in P))
203
- r2_used = float(sum(r2[p]*v(i_prod(p)) for p in P))
204
- resources = [
205
- {"resource_id": "R1", "used_hours": r1_used, "available_hours": r1_cap, "slack_hours": r1_cap - r1_used},
206
- {"resource_id": "R2", "used_hours": r2_used, "available_hours": r2_cap, "slack_hours": r2_cap - r2_used},
207
- ]
208
- # raw material flows
209
- raw_rows = []
210
  rm_purch_cost = 0.0
211
  for r in RMs:
212
  purchase = v(i_pur(r))
213
- cons = float(sum(bom.get(p, {}).get(r, 0.0)*v(i_prod(p)) for p in P))
214
- rm_purch_cost += purchase*rm_cost[r]
215
- raw_rows.append({
216
- "rm_id": r, "purchase_qty": purchase, "consumption_qty": cons
217
- })
218
- revenue = float(sum(price[p]*v(i_sell(p)) for p in P))
219
- conv_cost = float(sum(conv[p]*v(i_prod(p)) for p in P))
 
 
 
220
  profit = revenue - conv_cost - rm_purch_cost
221
 
222
  out = {
223
  "status": "OPTIMAL",
224
- "profit": profit,
225
- "revenue": revenue,
226
- "conversion_cost": conv_cost,
227
- "rm_purchase_cost": rm_purch_cost,
228
- "products": prod_rows,
229
- "raw_materials": raw_rows,
230
- "resources": resources
231
  }
232
  return json.dumps(out)
233
 
234
 
235
  @tool
236
- def update_sap_md61_tool(
237
- forecast_json: str,
238
- plant: str = "PLANT01",
239
- uom: str = "EA",
240
- mrp_area: str = ""
241
- ) -> str:
242
  """
243
  Prepare an MD61-style demand upload (SIMULATION ONLY).
244
 
245
  Args:
246
  forecast_json (str): JSON string returned by forecast_tool.
247
  plant (str): SAP plant (WERKS). Defaults to 'PLANT01'.
248
- uom (str): Unit of measure to write. Defaults to 'EA'.
249
  mrp_area (str): Optional MRP area.
250
 
251
  Returns:
252
- str: JSON string with {"status":"SIMULATED","csv_path": "...", "preview":[...5 rows...]}
253
  """
254
  rows = json.loads(forecast_json)
255
- md61 = []
256
- for r in rows:
257
- md61.append({
258
- "Material": r["product_id"],
259
- "Plant": plant,
260
- "MRP_Area": mrp_area,
261
- "Req_Date": r["period_start"], # month start; in practice, align to bucket conventions
262
- "Req_Qty": float(r["forecast_qty"]),
263
- "UoM": uom,
264
- "Version": "00" # demo default
265
- })
266
  df = pd.DataFrame(md61)
267
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
268
  df.to_csv(tmp.name, index=False)
269
- return json.dumps({
270
- "status": "SIMULATED",
271
- "csv_path": tmp.name,
272
- "preview": df.head(5).to_dict(orient="records")
273
- })
274
 
275
- # -----------------------------
276
- # Agent: runs forecast -> optimize -> MD61
277
- # -----------------------------
278
  def make_agent():
279
- model = OpenAIServerModel(
280
- model_id="gpt-4o-mini",
281
- api_key=OPENAI_API_KEY,
282
- temperature=0
283
- )
284
- tools = [forecast_tool, optimize_supply_tool, update_sap_md61_tool]
285
- return CodeAgent(tools=tools, model=model, add_base_tools=False, stream_outputs=False)
286
 
287
  SYSTEM_PLAN = (
288
- "Run the following pipeline strictly and return one final JSON object:\n"
289
- "1) Call forecast_tool with the given arguments.\n"
290
- "2) Call optimize_supply_tool using the JSON returned by forecast_tool.\n"
291
- "3) Call update_sap_md61_tool using the JSON returned by forecast_tool (demand), "
292
- " not the optimization plan.\n"
293
- "Return final_answer as JSON with keys: 'forecast', 'plan', and 'md61'."
294
  )
295
 
296
- def run_workflow(horizon, use_demo, plant, file_obj):
297
  agent = make_agent()
298
  if file_obj is not None:
299
- history_path = file_obj.name
300
- user_prompt = (
301
- f"{SYSTEM_PLAN}\n"
302
- f"Args:\n"
303
- f"- forecast_tool: horizon_months={int(horizon)}, use_demo=False, history_csv_path='{history_path}'\n"
304
- f"- optimize_supply_tool: (use forecast JSON)\n"
305
- f"- update_sap_md61_tool: plant='{plant}', uom='EA'\n"
306
- f"Return the final JSON only."
307
- )
308
  else:
309
- user_prompt = (
310
- f"{SYSTEM_PLAN}\n"
311
- f"Args:\n"
312
- f"- forecast_tool: horizon_months={int(horizon)}, use_demo=True\n"
313
- f"- optimize_supply_tool: (use forecast JSON)\n"
314
- f"- update_sap_md61_tool: plant='{plant}', uom='EA'\n"
315
- f"Return the final JSON only."
316
- )
317
- try:
318
- out = agent.run(user_prompt)
319
- except Exception as e:
320
- out = f"Agent error: {e}"
321
- return out
322
-
323
- # -----------------------------
324
- # Gradio UI (simple and clean)
325
- # -----------------------------
326
- with gr.Blocks(title="Forecast β†’ Optimize β†’ SAP MD61 (Demo)") as demo:
327
- gr.Markdown("## Forecast β†’ Optimize β†’ Update SAP MD61 (Demo)\nMinimal agent workflow with Prophet, LP, and an MD61 CSV preview.")
328
- with gr.Row():
329
- horizon = gr.Number(label="Horizon (months)", value=1, precision=0)
330
- plant = gr.Textbox(label="SAP Plant (WERKS)", value="PLANT01")
331
- with gr.Row():
332
- use_demo = gr.Checkbox(label="Use demo synthetic history", value=True)
333
- file = gr.File(label="Or upload history CSV (product_id,date,qty)", file_types=[".csv"])
334
- run_btn = gr.Button("Run end-to-end")
335
- out_box = gr.Textbox(label="Agent Output (JSON)", lines=14)
336
-
337
- def on_run(h, p, demo_flag, f):
338
- # if a file is supplied, ignore demo flag
339
- return run_workflow(h, (f is None) and demo_flag, p, f)
340
-
341
- run_btn.click(on_run, inputs=[horizon, plant, use_demo, file], outputs=[out_box])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
  if __name__ == "__main__":
344
  demo.launch()
 
1
+ import os, json, tempfile, logging
2
  import gradio as gr
3
  import pandas as pd
4
  import numpy as np
5
 
6
+ # Quiet noisy logs
7
  logging.getLogger("cmdstanpy").setLevel(logging.WARNING)
8
  logging.getLogger("prophet").setLevel(logging.WARNING)
9
 
10
+ # ==== Tools (your @tool template) ============================================
 
 
 
 
 
 
 
 
 
11
  from smolagents import tool, CodeAgent, OpenAIServerModel
12
 
13
  @tool
14
+ def forecast_tool(horizon_months: int = 1, use_demo: bool = True, history_csv_path: str = "") -> str:
 
 
 
 
15
  """
16
  Forecast monthly demand for finished goods using Prophet (demo-friendly).
17
 
18
  Args:
19
+ horizon_months (int): Number of future months to forecast (>=1). Defaults to 1.
20
+ use_demo (bool): If True, generate synthetic history for FG100/FG200. Defaults to True.
21
+ history_csv_path (str): Optional CSV path with columns [product_id,date,qty] to override demo.
22
 
23
  Returns:
24
+ str: JSON string list of {"product_id": str, "period_start": "YYYY-MM-01", "forecast_qty": float}.
 
25
  """
26
  from prophet import Prophet
27
 
28
+ # 1) History
29
  if use_demo or not history_csv_path:
30
  rng = pd.date_range("2023-01-01", periods=24, freq="MS")
31
  rows = []
 
38
  df = pd.DataFrame(rows)
39
  else:
40
  df = pd.read_csv(history_csv_path)
41
+ assert {"product_id", "date", "qty"} <= set(df.columns), "CSV must have product_id,date,qty"
42
  df["date"] = pd.to_datetime(df["date"], errors="coerce")
43
  df = df.dropna(subset=["date"])
44
  df["qty"] = pd.to_numeric(df["qty"], errors="coerce").fillna(0.0)
45
 
46
+ # 2) Forecast per product
47
  out = []
48
+ H = max(1, int(horizon_months))
49
  for pid, g in df.groupby("product_id"):
50
+ s = (g.set_index("date")["qty"].resample("MS").sum().asfreq("MS").fillna(0.0))
 
 
 
 
51
  m = Prophet(yearly_seasonality=True, weekly_seasonality=False, daily_seasonality=False, n_changepoints=10)
52
  m.fit(pd.DataFrame({"ds": s.index, "y": s.values}))
53
+ future = m.make_future_dataframe(periods=H, freq="MS", include_history=False)
54
  pred = m.predict(future)[["ds", "yhat"]]
55
  for _, r in pred.iterrows():
56
+ out.append({"product_id": str(pid), "period_start": r["ds"].strftime("%Y-%m-%d"), "forecast_qty": float(r["yhat"])})
 
 
 
 
57
  return json.dumps(out)
58
 
59
 
60
  @tool
61
+ def optimize_supply_tool(forecast_json: str) -> str:
 
 
62
  """
63
+ Optimize a single-month supply plan (LP) using the forecast.
64
 
65
  Args:
66
  forecast_json (str): JSON string returned by forecast_tool.
67
 
68
  Returns:
69
+ str: JSON with summary + readable tables (not raw solver output).
 
 
 
 
 
 
 
70
  """
71
+ from scipy.optimize import linprog
72
+
73
+ rows = json.loads(forecast_json)
74
  # Use first month per product
75
  demand = {}
76
+ for r in rows:
77
+ p = r["product_id"]
78
+ if p not in demand:
79
+ demand[p] = float(r["forecast_qty"])
80
 
81
+ P = sorted(demand.keys()) or ["FG100", "FG200"]
 
82
  price = {"FG100": 98.0, "FG200": 120.0}
83
  conv = {"FG100": 12.5, "FG200": 15.0}
84
  r1 = {"FG100": 0.03, "FG200": 0.05}
85
  r2 = {"FG100": 0.02, "FG200": 0.01}
86
+
87
  RMs = ["RM_A", "RM_B"]
88
  rm_cost = {"RM_A": 20.0, "RM_B": 30.0}
89
  rm_start = {"RM_A": 1000.0, "RM_B": 100.0}
90
+ rm_cap = {"RM_A": 5000.0, "RM_B": 5000.0}
91
  bom = {
92
+ "FG100": {"RM_A": 0.8, "RM_B": 0.2 * 1.02},
93
  "FG200": {"RM_A": 1.0, "RM_B": 0.1},
94
  }
95
  r1_cap, r2_cap = 320.0, 480.0
 
 
96
 
 
 
97
  nP, nR = len(P), len(RMs)
98
  pidx = {p:i for i,p in enumerate(P)}
99
  ridx = {r:i for i,r in enumerate(RMs)}
 
108
  c = np.zeros(n_vars)
109
  bounds = [None]*n_vars
110
 
 
111
  for p in P:
112
  c[i_prod(p)] += conv[p]
113
  c[i_sell(p)] -= price[p]
114
  c[i_einv(p)] += 0.0
115
  bounds[i_prod(p)] = (0, None)
116
+ bounds[i_sell(p)] = (0, demand[p])
117
+ bounds[i_einv(p)] = (0, None)
118
  for r in RMs:
119
  c[i_pur(r)] += rm_cost[r]
120
  c[i_einr(r)] += 0.0
121
  bounds[i_pur(r)] = (0, rm_cap[r])
122
  bounds[i_einr(r)] = (0, None)
123
 
124
+ # Equalities
125
  Aeq, beq = [], []
 
126
  for p in P:
127
+ row = np.zeros(n_vars); row[i_prod(p)]=1; row[i_sell(p)]=-1; row[i_einv(p)]=-1
128
+ Aeq.append(row); beq.append(0.0) # start_inv=0 in this demo
 
 
129
  for r in RMs:
130
+ row = np.zeros(n_vars); row[i_pur(r)]=1; row[i_einr(r)]=-1
131
+ for p in P: row[i_prod(p)] -= bom[p].get(r,0.0)
 
 
132
  Aeq.append(row); beq.append(-rm_start[r])
133
 
134
+ # Inequalities (resources)
 
 
135
  Aub, bub = [], []
136
+ row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r1[p]) for p in P]; Aub.append(row); bub.append(r1_cap)
137
+ row = np.zeros(n_vars); [row.__setitem__(i_prod(p), r2[p]) for p in P]; Aub.append(row); bub.append(r2_cap)
138
+
139
+ res = linprog(c, A_ub=np.array(Aub), b_ub=np.array(bub), A_eq=np.array(Aeq), b_eq=np.array(beq),
140
+ bounds=bounds, method="highs")
 
 
 
 
141
  if not res.success:
142
  return json.dumps({"status": "FAILED", "message": res.message})
143
 
144
  x = res.x
145
  def v(idx): return float(x[idx])
146
 
147
+ # Compose human-friendly tables
148
+ prod_tbl = []
149
+ revenue = 0.0; conv_cost = 0.0
150
  for p in P:
151
+ produce = v(i_prod(p)); sell = v(i_sell(p))
152
+ prod_tbl.append({"Product": p, "Produce": produce, "Sell": sell, "Unit Price": price[p], "Conv. Cost/u": conv[p]})
153
+ revenue += sell*price[p]; conv_cost += produce*conv[p]
154
+
155
+ raw_tbl = []
 
 
 
 
 
 
 
 
 
156
  rm_purch_cost = 0.0
157
  for r in RMs:
158
  purchase = v(i_pur(r))
159
+ cons = float(sum(bom[p].get(r,0.0)*v(i_prod(p)) for p in P))
160
+ cost = purchase*rm_cost[r]; rm_purch_cost += cost
161
+ raw_tbl.append({"Raw": r, "Purchase": purchase, "Consume": cons, "Cost/u": rm_cost[r], "Total Cost": cost})
162
+
163
+ r1_used = float(sum(r1[p]*v(i_prod(p)) for p in P))
164
+ r2_used = float(sum(r2[p]*v(i_prod(p)) for p in P))
165
+ res_tbl = [
166
+ {"Resource": "R1", "Used": r1_used, "Cap": r1_cap, "Slack": r1_cap - r1_used},
167
+ {"Resource": "R2", "Used": r2_used, "Cap": r2_cap, "Slack": r2_cap - r2_used},
168
+ ]
169
  profit = revenue - conv_cost - rm_purch_cost
170
 
171
  out = {
172
  "status": "OPTIMAL",
173
+ "kpis": {"Profit": profit, "Revenue": revenue, "Conv. Cost": conv_cost, "RM Purchase Cost": rm_purch_cost},
174
+ "products": prod_tbl,
175
+ "raw_materials": raw_tbl,
176
+ "resources": res_tbl
 
 
 
177
  }
178
  return json.dumps(out)
179
 
180
 
181
  @tool
182
+ def update_sap_md61_tool(forecast_json: str, plant: str = "PLANT01", uom: str = "EA", mrp_area: str = "") -> str:
 
 
 
 
 
183
  """
184
  Prepare an MD61-style demand upload (SIMULATION ONLY).
185
 
186
  Args:
187
  forecast_json (str): JSON string returned by forecast_tool.
188
  plant (str): SAP plant (WERKS). Defaults to 'PLANT01'.
189
+ uom (str): Unit of measure. Defaults to 'EA'.
190
  mrp_area (str): Optional MRP area.
191
 
192
  Returns:
193
+ str: JSON with {"status":"SIMULATED","csv_path":"...","preview":[...]}.
194
  """
195
  rows = json.loads(forecast_json)
196
+ md61 = [{
197
+ "Material": r["product_id"], "Plant": plant, "MRP_Area": mrp_area,
198
+ "Req_Date": r["period_start"], "Req_Qty": float(r["forecast_qty"]),
199
+ "UoM": uom, "Version": "00"
200
+ } for r in rows]
 
 
 
 
 
 
201
  df = pd.DataFrame(md61)
202
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
203
  df.to_csv(tmp.name, index=False)
204
+ return json.dumps({"status": "SIMULATED", "csv_path": tmp.name, "preview": md61[:5]})
 
 
 
 
205
 
206
+
207
+ # ==== Agent (end-to-end) ======================================================
 
208
  def make_agent():
209
+ api_key = os.environ.get("OPENAI_API_KEY", "")
210
+ if not api_key:
211
+ raise RuntimeError("OPENAI_API_KEY not set. Add it as a Space secret.")
212
+ model = OpenAIServerModel(model_id="gpt-4o-mini", api_key=api_key, temperature=0)
213
+ return CodeAgent(tools=[forecast_tool, optimize_supply_tool, update_sap_md61_tool],
214
+ model=model, add_base_tools=False, stream_outputs=False)
 
215
 
216
  SYSTEM_PLAN = (
217
+ "Run the pipeline and return one JSON:\n"
218
+ "1) forecast_tool(...)\n"
219
+ "2) optimize_supply_tool(forecast_json)\n"
220
+ "3) update_sap_md61_tool(forecast_json, ...)\n"
221
+ "Return: {'forecast': <json>, 'plan': <json>, 'md61': <json>}"
 
222
  )
223
 
224
+ def run_agentic(horizon, plant, use_demo, file_obj):
225
  agent = make_agent()
226
  if file_obj is not None:
227
+ path = file_obj.name
228
+ prompt = (f"{SYSTEM_PLAN}\n"
229
+ f"Use forecast_tool(horizon_months={int(horizon)}, use_demo=False, history_csv_path='{path}'). "
230
+ f"Then run the other two steps as specified. Return only the final JSON.")
 
 
 
 
 
231
  else:
232
+ prompt = (f"{SYSTEM_PLAN}\n"
233
+ f"Use forecast_tool(horizon_months={int(horizon)}, use_demo={bool(use_demo)}). "
234
+ f"Then run the other two steps as specified. Return only the final JSON.")
235
+ return agent.run(prompt)
236
+
237
+
238
+ # ==== UI Helpers (pretty outputs) ============================================
239
+ def parse_forecast(json_str):
240
+ df = pd.DataFrame(json.loads(json_str))
241
+ return df[["product_id","period_start","forecast_qty"]].rename(columns={
242
+ "product_id":"Product","period_start":"Period Start","forecast_qty":"Forecast Qty"
243
+ })
244
+
245
+ def parse_plan(json_str):
246
+ d = json.loads(json_str)
247
+ kpis = pd.DataFrame([d["kpis"]]).rename(columns={
248
+ "Conv. Cost":"Conversion Cost", "RM Purchase Cost":"RM Purchase Cost"
249
+ })
250
+ prod = pd.DataFrame(d["products"])
251
+ raw = pd.DataFrame(d["raw_materials"])
252
+ res = pd.DataFrame(d["resources"])
253
+ return d["status"], kpis, prod, raw, res
254
+
255
+ def parse_md61(json_str):
256
+ d = json.loads(json_str)
257
+ prev = pd.DataFrame(d.get("preview", []))
258
+ path = d.get("csv_path", "")
259
+ return d.get("status",""), prev, path
260
+
261
+
262
+ # ==== Gradio UI ==============================================================
263
+ with gr.Blocks(title="Forecast β†’ Optimize β†’ SAP MD61") as demo:
264
+ gr.Markdown("## 🧭 Workflow\n"
265
+ "### 1) **Forecast** β†’ 2) **Optimize Supply** β†’ 3) **Prepare MD61**\n"
266
+ "Run them **manually** below, or use the **agent** to do end-to-end in one click.")
267
+
268
+ with gr.Tab("Manual (Step-by-step)"):
269
+ with gr.Row():
270
+ horizon = gr.Number(label="Horizon (months)", value=1, precision=0)
271
+ plant = gr.Textbox(label="SAP Plant (WERKS)", value="PLANT01")
272
+ with gr.Row():
273
+ use_demo = gr.Checkbox(label="Use demo synthetic history", value=True)
274
+ file = gr.File(label="Or upload history.csv (product_id,date,qty)", file_types=[".csv"])
275
+
276
+ # States to pass data between steps
277
+ forecast_state = gr.State("")
278
+ plan_state = gr.State("")
279
+ md61_state = gr.State("")
280
+
281
+ gr.Markdown("### ➀ Step 1: Forecast")
282
+ run_f = gr.Button("Run Step 1 β€” Forecast")
283
+ forecast_tbl = gr.Dataframe(label="Forecast (first horizon month per SKU)", interactive=False)
284
+ forecast_note = gr.Markdown("")
285
+
286
+ gr.Markdown("### ➀ Step 2: Optimize Supply")
287
+ run_o = gr.Button("Run Step 2 β€” Optimize")
288
+ plan_status = gr.Markdown("")
289
+ plan_kpis = gr.Dataframe(label="KPIs", interactive=False)
290
+ plan_prod = gr.Dataframe(label="Products Plan", interactive=False)
291
+ plan_raw = gr.Dataframe(label="Raw Materials", interactive=False)
292
+ plan_res = gr.Dataframe(label="Resources", interactive=False)
293
+
294
+ gr.Markdown("### ➀ Step 3: Prepare MD61 (Simulated)")
295
+ run_m = gr.Button("Run Step 3 β€” MD61")
296
+ md61_status = gr.Markdown("")
297
+ md61_prev = gr.Dataframe(label="MD61 Preview", interactive=False)
298
+ md61_file = gr.File(label="Download CSV", interactive=False)
299
+
300
+ # Handlers
301
+ def do_forecast(h, demo_flag, f):
302
+ hist_path = "" if (f is None) else f.name
303
+ fj = forecast_tool(horizon_months=int(h), use_demo=(f is None) and bool(demo_flag),
304
+ history_csv_path=hist_path)
305
+ df = parse_forecast(fj)
306
+ return fj, df, f"Forecast generated for {df['Product'].nunique()} product(s)."
307
+
308
+ run_f.click(do_forecast, inputs=[horizon, use_demo, file], outputs=[forecast_state, forecast_tbl, forecast_note])
309
+
310
+ def do_optimize(fj):
311
+ if not fj:
312
+ return "", pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), "⚠️ Run Step 1 first."
313
+ pj = optimize_supply_tool(fj)
314
+ status, kpis, prod, raw, res = parse_plan(pj)
315
+ return pj, kpis, prod, raw, res, f"Optimization status: **{status}**"
316
+
317
+ run_o.click(do_optimize, inputs=[forecast_state],
318
+ outputs=[plan_state, plan_kpis, plan_prod, plan_raw, plan_res, plan_status])
319
+
320
+ def do_md61(fj, plant):
321
+ if not fj:
322
+ return "", pd.DataFrame(), None, "⚠️ Run Step 1 first."
323
+ mj = update_sap_md61_tool(fj, plant=plant, uom="EA")
324
+ status, prev, path = parse_md61(mj)
325
+ return mj, prev, path, f"MD61 status: **{status}**"
326
+
327
+ run_m.click(do_md61, inputs=[forecast_state, plant], outputs=[md61_state, md61_prev, md61_file, md61_status])
328
+
329
+ with gr.Tab("Agentic (End-to-end)"):
330
+ gr.Markdown("One click: the agent runs all three steps with OpenAI.")
331
+ with gr.Row():
332
+ a_horizon = gr.Number(label="Horizon (months)", value=1, precision=0)
333
+ a_plant = gr.Textbox(label="SAP Plant (WERKS)", value="PLANT01")
334
+ with gr.Row():
335
+ a_demo = gr.Checkbox(label="Use demo synthetic history", value=True)
336
+ a_file = gr.File(label="Or upload history.csv", file_types=[".csv"])
337
+ run_all = gr.Button("Run End-to-end (Agent)")
338
+ out_json = gr.Textbox(label="Agent Raw JSON (for inspection)", lines=6)
339
+ with gr.Accordion("Pretty Outputs", open=True):
340
+ a_forecast_tbl = gr.Dataframe(label="Forecast", interactive=False)
341
+ a_plan_kpis = gr.Dataframe(label="KPIs", interactive=False)
342
+ a_plan_prod = gr.Dataframe(label="Products Plan", interactive=False)
343
+ a_plan_raw = gr.Dataframe(label="Raw Materials", interactive=False)
344
+ a_plan_res = gr.Dataframe(label="Resources", interactive=False)
345
+ a_md61_prev = gr.Dataframe(label="MD61 Preview", interactive=False)
346
+ a_md61_file = gr.File(label="Download MD61 CSV", interactive=False)
347
+
348
+ def do_agent(h, p, demo_flag, f):
349
+ try:
350
+ res = run_agentic(h, p, demo_flag, f)
351
+ out = json.loads(res)
352
+ f_df = parse_forecast(out["forecast"])
353
+ _, kpis, prod, raw, res_tbl = parse_plan(out["plan"])
354
+ _, prev, csv_path = parse_md61(out["md61"])
355
+ return (json.dumps(out, indent=2), f_df, kpis, prod, raw, res_tbl, prev, csv_path)
356
+ except Exception as e:
357
+ return (f"Agent error: {e}", pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
358
+ pd.DataFrame(), pd.DataFrame(), None)
359
+
360
+ run_all.click(do_agent, inputs=[a_horizon, a_plant, a_demo, a_file],
361
+ outputs=[out_json, a_forecast_tbl, a_plan_kpis, a_plan_prod, a_plan_raw, a_plan_res, a_md61_prev, a_md61_file])
362
 
363
  if __name__ == "__main__":
364
  demo.launch()