jeffrey1963 commited on
Commit
a895ba6
Β·
verified Β·
1 Parent(s): c6aaeed

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +300 -298
app.py CHANGED
@@ -1,336 +1,338 @@
1
-
2
  import gradio as gr
3
- import json, re, math
4
  import numpy as np
5
  import pandas as pd
6
  import matplotlib.pyplot as plt
7
 
8
- # --------------------
9
- # State/HUD structure
10
- # --------------------
11
- DEFAULTS = {
12
- "model": "logistic",
13
- # Logistic yield: Y(X) = L / (1 + exp(-k*(X - x0)))
14
- "L": None, # max yield
15
- "k": None, # slope
16
- "x0": None, # inflection X
17
- # Prices & costs
18
- "Px": None, # input price ($/lb of fert) -> MIC
19
- "Py": None, # output price ($/unit yield)
20
- "Other": 0.0, # other cost per acre (fixed wrt X)
21
- # Range
22
- "x_min": 0.0,
23
- "x_max": 150.0,
24
- "x_step": 5.0,
25
- # Computed last table/plot
26
- "table_ready": False,
27
- "want_plot": False,
28
- "last_table_cols": [],
29
- "last_focus_x": None # for showing sample calc on a specific X
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
- HELP = '''Say things like:
33
- - "use logistic: L=1200, k=0.06, x0=60"
34
- - "set Py=0.5 and Px=0.25 and other cost=300"
35
- - "range 0 to 150 by 5"
36
- - "make table"
37
- - "graph yield" (or "plot mpp" / "plot profit")
38
- - "compute APP" / "compute MPP" / "compute MVP" / "compute MIC"
39
- - "show production stages"
40
- - "calculate profit"
41
- - "find optimal" (uses rule MVP = MIC in Stage II)
42
- - "change Py to 0.6" or "change Px 0.3"
43
- '''
44
-
45
- # --------------------
46
- # Core economics
47
- # --------------------
 
 
 
 
 
 
 
 
48
  def Y_logistic(X, L, k, x0):
49
- return L / (1.0 + np.exp(-k * (X - x0)))
50
 
51
  def MPP_logistic(X, L, k, x0):
52
- # derivative of logistic: dY/dX = L*k*exp(-k*(X - x0)) / (1 + exp(-k*(X - x0)))^2
53
- e = np.exp(-k * (X - x0))
54
- return L * k * e / (1.0 + e)**2
55
-
56
- def table_from_hud(hud):
57
- L, k, x0 = hud["L"], hud["k"], hud["x0"]
58
- x_min, x_max, x_step = hud["x_min"], hud["x_max"], hud["x_step"]
59
- if None in (L, k, x0):
60
- raise ValueError("Set logistic params first (L, k, x0).")
61
- X = np.arange(float(x_min), float(x_max) + 1e-9, float(x_step))
62
- Y = Y_logistic(X, L, k, x0)
63
- APP = np.divide(Y, X, out=np.zeros_like(Y), where=X>0)
64
- MPP = MPP_logistic(X, L, k, x0)
65
- data = {
66
- "X": X,
67
- "Y": Y,
68
- "APP": APP,
69
- "MPP": MPP
70
- }
71
- # Add MVP, MIC, Profit if prices exist
72
- if hud.get("Py") is not None:
73
- data["MVP"] = MPP * float(hud["Py"])
74
- if hud.get("Px") is not None:
75
- data["MIC"] = np.full_like(X, float(hud["Px"]), dtype=float)
76
- if hud.get("Py") is not None and hud.get("Px") is not None:
77
- Other = float(hud.get("Other", 0.0))
78
- data["Profit"] = Y * float(hud["Py"]) - X * float(hud["Px"]) - Other
79
  df = pd.DataFrame(data)
80
- # Stage classification
81
  stage = []
82
- for i, row in df.iterrows():
83
  if row["MPP"] > 0:
84
- if row["APP"] >= row["MPP"]:
85
- s = "I"
86
- else:
87
- s = "II"
88
  else:
89
- s = "III"
90
- stage.append(s)
91
  df["Stage"] = stage
92
  return df
93
 
94
- def find_optimal(hud):
95
- # Find X that maximizes profit by MVP = MIC (Stage II)
96
- df = table_from_hud(hud)
97
  if "MVP" not in df.columns or "MIC" not in df.columns:
98
  raise ValueError("Need Py (for MVP) and Px (for MIC).")
99
- # Find index minimizing |MVP - MIC| with MPP>0 (Stage II)
100
- mask = (df["Stage"] == "II")
101
- if not mask.any():
102
- # fallback: allow all
103
- mask = df["MPP"] > 0
104
  sub = df[mask].copy()
105
  sub["gap"] = (sub["MVP"] - sub["MIC"]).abs()
106
  j = sub["gap"].idxmin()
107
- row = df.loc[j]
108
- return row # contains X, Y, Profit, etc.
109
-
110
- # --------------------
111
- # NLP parser
112
- # --------------------
113
- def parse(user, hud):
114
- updated = []
115
-
116
- txt = user.strip()
117
-
118
- # logistic params
119
- # "L=1200, k=0.06, x0=60" (order flexible)
120
- mL = re.search(r'\bL\s*=\s*([0-9]*\.?[0-9]+)', txt, flags=re.I)
121
- mk = re.search(r'\bk\s*=\s*([0-9]*\.?[0-9]+)', txt, flags=re.I)
122
- mx0 = re.search(r'\bx0\s*=\s*([0-9]*\.?[0-9]+)', txt, flags=re.I)
123
- if mL:
124
- hud["L"] = float(mL.group(1)); updated.append("L")
125
- if mk:
126
- hud["k"] = float(mk.group(1)); updated.append("k")
127
- if mx0:
128
- hud["x0"] = float(mx0.group(1)); updated.append("x0")
129
-
130
- if re.search(r'logistic', txt, flags=re.I):
131
- hud["model"] = "logistic"; updated.append("model")
132
-
133
- # prices
134
- mPx = re.search(r'(?:Px|MIC|input price)\s*=?\s*([0-9]*\.?[0-9]+)', txt, flags=re.I)
135
- mPy = re.search(r'(?:Py|output price)\s*=?\s*([0-9]*\.?[0-9]+)', txt, flags=re.I)
136
- mOther = re.search(r'(?:Other|other cost)\s*=?\s*([0-9]*\.?[0-9]+)', txt, flags=re.I)
137
- if mPx:
138
- hud["Px"] = float(mPx.group(1)); updated.append("Px")
139
- if mPy:
140
- hud["Py"] = float(mPy.group(1)); updated.append("Py")
141
- if mOther:
142
- hud["Other"] = float(mOther.group(1)); updated.append("Other")
143
-
144
- # quick "change Py to 0.6" etc.
145
- mchg = re.search(r'change\s+(px|py|other)\s*(?:to|=)\s*([0-9]*\.?[0-9]+)', txt, flags=re.I)
146
- if mchg:
147
- key = mchg.group(1).title()
148
- hud[key] = float(mchg.group(2)); updated.append(key)
149
-
150
- # range
151
- mrange = re.search(r'(?:range|fertilize|apply|from)\s*([0-9]*\.?[0-9]+)\s*(?:to|-)\s*([0-9]*\.?[0-9]+)\s*(?:by|in|step)\s*([0-9]*\.?[0-9]+)', txt, flags=re.I)
152
- if mrange:
153
- hud["x_min"] = float(mrange.group(1)); updated.append("x_min")
154
- hud["x_max"] = float(mrange.group(2)); updated.append("x_max")
155
- hud["x_step"] = float(mrange.group(3)); updated.append("x_step")
156
-
157
- # focus X for blackboard example: "at X=80" or "at 80 lbs"
158
- mfocus = re.search(r'at\s*(?:x\s*=?\s*)?([0-9]*\.?[0-9]+)', txt, flags=re.I)
159
- if mfocus:
160
- hud["last_focus_x"] = float(mfocus.group(1)); updated.append("last_focus_x")
161
-
162
- # intents
163
- intent = None
164
- if re.search(r'\bmake table|\btable', txt, flags=re.I):
165
- intent = "table"
166
- if re.search(r'graph|plot', txt, flags=re.I):
167
- # accept "plot yield", "plot mpp", "plot profit", default "yield"
168
- intent = "plot"
169
- if re.search(r'compute\s+app|\bAPP\b', txt, flags=re.I):
170
- intent = "app"
171
- if re.search(r'compute\s+mpp|\bMPP\b', txt, flags=re.I):
172
- intent = "mpp"
173
- if re.search(r'compute\s+mvp|\bMVP\b', txt, flags=re.I):
174
- intent = "mvp"
175
- if re.search(r'compute\s+mic|\bMIC\b', txt, flags=re.I):
176
- intent = "mic"
177
- if re.search(r'profit', txt, flags=re.I):
178
- intent = "profit"
179
- if re.search(r'optimal|find optimal|mvp\s*=\s*mic', txt, flags=re.I):
180
- intent = "optimal"
181
- if re.search(r'stage', txt, flags=re.I):
182
- intent = "stage"
183
- if re.search(r'help', txt, flags=re.I):
184
- intent = "help"
185
-
186
- return hud, updated, intent, txt
187
-
188
- # --------------------
189
- # Blackboard examples
190
- # --------------------
191
- def sample_work(hud, df):
192
- # Return a short textual sample calculation at a representative X.
193
- if hud.get("last_focus_x") is not None:
194
- x0 = hud["last_focus_x"]
195
- j = int(round((x0 - hud["x_min"]) / hud["x_step"]))
196
- j = max(0, min(j, len(df)-1))
197
- else:
198
- j = len(df)//2
199
- row = df.iloc[j]
200
- lines = []
201
- lines.append(f"At X = {row['X']:.3g}:")
202
- lines.append(f" Y = L/(1+exp(-k*(X-x0))) with L={hud['L']}, k={hud['k']}, x0={hud['x0']}")
203
- lines.append(f" => Y β‰ˆ {row['Y']:.4g}")
204
- lines.append(f" APP = Y/X β‰ˆ {row['APP']:.4g}")
205
- lines.append(f" MPP = dY/dX β‰ˆ {row['MPP']:.4g}")
206
- if 'MVP' in df.columns:
207
- lines.append(f" MVP = MPPΒ·Py β‰ˆ {row['MVP']:.4g}")
208
- if 'MIC' in df.columns:
209
- lines.append(f" MIC = Px β‰ˆ {row['MIC']:.4g}")
210
- if 'Profit' in df.columns:
211
- lines.append(f" Profit = PyΒ·Y βˆ’ PxΒ·X βˆ’ Other β‰ˆ {row['Profit']:.4g}")
 
 
 
212
  lines.append(f" Stage = {row['Stage']}")
213
- return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- # --------------------
216
- # Controller
217
- # --------------------
218
- def controller(user_text, hud_json):
219
- # load/ensure hud
220
  try:
221
- hud = json.loads(hud_json) if hud_json else {}
222
- except Exception:
223
- hud = {}
224
- for k, v in DEFAULTS.items():
225
- hud.setdefault(k, v)
226
-
227
- hud, updated, intent, raw = parse(user_text, hud)
228
-
229
- # Decide action
230
- reply = ""
231
- table = pd.DataFrame()
232
- plot = None
233
- blackboard = ""
234
-
235
- # Some actions require table
236
- need_table = intent in ("table","plot","app","mpp","mvp","mic","profit","stage","optimal")
237
- if need_table:
238
- try:
239
- df = table_from_hud(hud)
240
- except Exception as e:
241
- reply = f"Jerry β†’ {e} \n\n{HELP}"
242
- return reply, json.dumps(hud, indent=2), ", ".join(updated) or "(none)", blackboard, table, plot
243
-
244
- if intent == "help" or intent is None:
245
- reply = "Jerry β†’ How can I help?\n\n" + HELP
246
-
247
- elif intent == "table":
248
- hud["table_ready"] = True
249
- hud["last_table_cols"] = list(df.columns)
250
- reply = f"Built table for X from {hud['x_min']} to {hud['x_max']} by {hud['x_step']}."
251
- table = df
252
- blackboard = sample_work(hud, df)
253
-
254
- elif intent == "plot":
255
- # choose what to plot
256
- metric = "Y"
257
- if re.search(r'mpp', raw, flags=re.I): metric = "MPP"
258
- elif re.search(r'app', raw, flags=re.I): metric = "APP"
259
- elif re.search(r'mvp', raw, flags=re.I) and "MVP" in df.columns: metric = "MVP"
260
- elif re.search(r'profit', raw, flags=re.I) and "Profit" in df.columns: metric = "Profit"
261
- fig = plt.figure()
262
- ax = fig.add_subplot(111)
263
- ax.plot(df["X"], df[metric])
264
- ax.set_xlabel("X (fertilizer)")
265
- ax.set_ylabel(metric)
266
- ax.set_title(f"{metric} vs X")
267
- plot = fig
268
- table = df
269
- reply = f"Plotted {metric} vs X."
270
- blackboard = sample_work(hud, df)
271
-
272
- elif intent in ("app","mpp","mvp","mic","profit","stage"):
273
- table = df
274
- parts = []
275
- if intent in ("app","stage"): parts.append("APP")
276
- if intent in ("mpp","stage"): parts.append("MPP")
277
- if intent in ("mvp","stage") and "MVP" in df.columns: parts.append("MVP")
278
- if intent in ("mic","stage") and "MIC" in df.columns: parts.append("MIC")
279
- if intent in ("profit","stage") and "Profit" in df.columns: parts.append("Profit")
280
- if intent == "stage": parts.append("Stage")
281
- show = [c for c in parts if c in df.columns] + ["X"]
282
- show = list(dict.fromkeys(["X"] + show)) # ensure X first, unique
283
- reply = "Computed metrics. Showing relevant columns."
284
- table = df[show]
285
- blackboard = sample_work(hud, df)
286
- if intent == "mvp" and "MVP" not in df.columns:
287
- reply += " (Set Py first)"
288
- if intent == "mic" and "MIC" not in df.columns:
289
- reply += " (Set Px first)"
290
- if intent == "profit" and "Profit" not in df.columns:
291
- reply += " (Set Px and Py first)"
292
-
293
- elif intent == "optimal":
294
- try:
295
- row = find_optimal(hud)
296
- reply = (f"Optimal (MVP β‰ˆ MIC in Stage II):\n"
297
- f"X* β‰ˆ {row['X']:.4g}, Y* β‰ˆ {row['Y']:.4g}, "
298
- f"MVP β‰ˆ {row.get('MVP', float('nan')):.4g}, MIC β‰ˆ {row.get('MIC', float('nan')):.4g}")
299
- if 'Profit' in row:
300
- reply += f", Profit* β‰ˆ {row['Profit']:.4g}"
301
- df = table_from_hud(hud)
302
- cols = ['X','Y','MPP']
303
- if 'MVP' in df.columns: cols.append('MVP')
304
- if 'MIC' in df.columns: cols.append('MIC')
305
- if 'Profit' in df.columns: cols.append('Profit')
306
  table = df[cols]
307
- blackboard = sample_work(hud, df) + "\n\nRule: choose X where MVP = MIC (in Stage II)."
308
- except Exception as e:
309
- reply = f"Jerry β†’ {e} \n(Set Px and Py, and logistic params)."
310
 
311
- else:
312
- reply = "Okay β€” updated HUD. Ask me to 'make table', 'plot yield', 'compute MPP', 'profit', 'find optimal', etc."
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
- return reply, json.dumps(hud, indent=2), ", ".join(updated) or "(none)", blackboard, table, plot
 
 
 
 
 
 
315
 
316
- with gr.Blocks() as demo:
317
- gr.Markdown("### Jerry β€” MIC/Profit Coach (Logistic Yield, NLP HUD)")
318
  with gr.Row():
319
  with gr.Column(scale=1):
320
- user = gr.Textbox(label="Talk to Jerry", value="use logistic L=1200, k=0.06, x0=60; set Py=0.5, Px=0.25, other cost=300; range 0 to 150 by 5; make table", lines=4)
321
- btn = gr.Button("Ask Jerry", variant="primary")
322
  with gr.Column(scale=1):
323
- out = gr.Textbox(label="Jerry says", lines=10)
324
  with gr.Row():
325
  with gr.Column(scale=1):
326
- hud = gr.Textbox(label="Student HUD β€” what Jerry sees (persists)", value=json.dumps(DEFAULTS, indent=2), lines=18)
 
 
 
327
  with gr.Column(scale=1):
328
- changed = gr.Textbox(label="Fields updated by last prompt", value="(none)")
329
- black = gr.Textbox(label="Blackboard β€” show the work", value="(none)", lines=12)
330
- table = gr.Dataframe(label="Main Output Table", wrap=True)
331
- plot = gr.Plot(label="Figure (on demand)")
 
332
 
333
- btn.click(controller, inputs=[user, hud], outputs=[out, hud, changed, black, table, plot])
334
 
335
  if __name__ == "__main__":
336
- demo.launch()
 
1
+ import os, re, json, copy
2
  import gradio as gr
 
3
  import numpy as np
4
  import pandas as pd
5
  import matplotlib.pyplot as plt
6
 
7
+ # ==========================
8
+ # Jerry β€” Logistic Yield Coach (app52) in app53 style
9
+ # - LLM-first parser (OpenAI GPT-5.0), regex fallback
10
+ # - Schema JSON -> HUD merge -> requirements checks
11
+ # - HUD reordered: Py, Px, Other, L, k, x0, range, which_action, last_focus_x
12
+ # - Blackboard "show your work" at any X
13
+ # ==========================
14
+
15
+ # ---------- HUD FIELDS (ORDERED top→bottom) ----------
16
+ HUD_FIELDS = [
17
+ "Py", "Px", "Other",
18
+ "L", "k", "x0",
19
+ "x_min", "x_max", "x_step",
20
+ "which_action",
21
+ "last_focus_x"
22
+ ]
23
+
24
+ DEFAULTS = {k: None for k in HUD_FIELDS}
25
+ DEFAULTS.update({"x_min": 0.0, "x_max": 150.0, "x_step": 5.0, "Other": 0.0})
26
+
27
+ # ---------- Requirements per action ----------
28
+ REQUIREMENTS = {
29
+ "table": ["L","k","x0","x_min","x_max","x_step"],
30
+ "plot": ["L","k","x0","x_min","x_max","x_step"],
31
+ "app": ["L","k","x0","x_min","x_max","x_step"],
32
+ "mpp": ["L","k","x0","x_min","x_max","x_step"],
33
+ "mvp": ["L","k","x0","x_min","x_max","x_step","Py"],
34
+ "mic": ["Px"],
35
+ "profit": ["L","k","x0","x_min","x_max","x_step","Py","Px"],
36
+ "optimal": ["L","k","x0","x_min","x_max","x_step","Py","Px"],
37
+ "stage": ["L","k","x0","x_min","x_max","x_step"],
38
+ "work": ["L","k","x0"]
39
  }
40
 
41
+ # ---------- Utils ----------
42
+ def _pp(x):
43
+ try:
44
+ return json.dumps(x, indent=2, ensure_ascii=False, default=str)
45
+ except Exception:
46
+ return str(x)
47
+
48
+ def merge_into_hud(hud: dict, new_data: dict):
49
+ """Overwrite HUD with any non-null values from new_data. Return (updated_hud, changed_keys)."""
50
+ base = copy.deepcopy(hud) if hud else copy.deepcopy(DEFAULTS)
51
+ changed = []
52
+ for k in HUD_FIELDS:
53
+ if k in new_data and new_data[k] is not None:
54
+ if base.get(k) != new_data[k]:
55
+ base[k] = new_data[k]
56
+ changed.append(k)
57
+ return base, changed
58
+
59
+ def missing_for(action: str, hud: dict):
60
+ need = REQUIREMENTS.get(action or "", [])
61
+ miss = [k for k in need if hud.get(k) in (None, "", float("nan"))]
62
+ return miss
63
+
64
+ # ---------- Model (logistic) ----------
65
  def Y_logistic(X, L, k, x0):
66
+ return float(L) / (1.0 + np.exp(-float(k) * (X - float(x0))))
67
 
68
  def MPP_logistic(X, L, k, x0):
69
+ e = np.exp(-float(k) * (X - float(x0)))
70
+ return float(L) * float(k) * e / (1.0 + e)**2
71
+
72
+ def build_table(h):
73
+ L, k, x0 = float(h["L"]), float(h["k"]), float(h["x0"])
74
+ X = np.arange(float(h["x_min"]), float(h["x_max"]) + 1e-9, float(h["x_step"]))
75
+ Y = [Y_logistic(x,L,k,x0) for x in X]
76
+ APP = [y/x if x!=0 else 0 for x,y in zip(X,Y)]
77
+ MPP = [MPP_logistic(x,L,k,x0) for x in X]
78
+ data = {"X": X, "Y": Y, "APP": APP, "MPP": MPP}
79
+ if h.get("Py") is not None: data["MVP"] = [m*float(h["Py"]) for m in MPP]
80
+ if h.get("Px") is not None: data["MIC"] = [float(h["Px"])]*len(X)
81
+ if h.get("Py") is not None and h.get("Px") is not None:
82
+ data["Profit"] = [y*float(h["Py"]) - x*float(h["Px"]) - float(h.get("Other") or 0.0) for x,y in zip(X,Y)]
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  df = pd.DataFrame(data)
 
84
  stage = []
85
+ for _, row in df.iterrows():
86
  if row["MPP"] > 0:
87
+ stage.append("I" if row["APP"] >= row["MPP"] else "II")
 
 
 
88
  else:
89
+ stage.append("III")
 
90
  df["Stage"] = stage
91
  return df
92
 
93
+ def find_optimal(h):
94
+ df = build_table(h)
 
95
  if "MVP" not in df.columns or "MIC" not in df.columns:
96
  raise ValueError("Need Py (for MVP) and Px (for MIC).")
97
+ mask = (df["Stage"]=="II")
98
+ if not mask.any(): mask = df["MPP"]>0
 
 
 
99
  sub = df[mask].copy()
100
  sub["gap"] = (sub["MVP"] - sub["MIC"]).abs()
101
  j = sub["gap"].idxmin()
102
+ return df.loc[j], df
103
+
104
+ def row_at_x(h, x):
105
+ L, k, x0 = float(h["L"]), float(h["k"]), float(h["x0"])
106
+ x = float(x)
107
+ Y = Y_logistic(x,L,k,x0)
108
+ APP = Y/x if x!=0 else 0.0
109
+ MPP = MPP_logistic(x,L,k,x0)
110
+ out = {"X":x,"Y":Y,"APP":APP,"MPP":MPP}
111
+ if h.get("Py") is not None: out["MVP"] = MPP*float(h["Py"])
112
+ if h.get("Px") is not None: out["MIC"] = float(h["Px"])
113
+ if h.get("Py") is not None and h.get("Px") is not None:
114
+ out["Profit"] = Y*float(h["Py"]) - x*float(h["Px"]) - float(h.get("Other") or 0.0)
115
+ out["Stage"] = "I" if MPP>0 and APP>=MPP else "II" if MPP>0 else "III"
116
+ return pd.DataFrame([out])
117
+
118
+ # ---------- LLM-first parser (OpenAI GPT-5.0), regex fallback ----------
119
+ from openai import OpenAI
120
+
121
+ client = OpenAI()
122
+ LLM_OK = bool(os.getenv("OPENAI_API_KEY"))
123
+
124
+ JERRY_SYSTEM_PROMPT = (
125
+ "You are JERRY, a production economics coach. "
126
+ "Read the student's text and output ONLY a minified JSON object with keys: "
127
+ "Py, Px, Other, L, k, x0, x_min, x_max, x_step, which_action, last_focus_x. "
128
+ "Strip units like $, %, lbs, etc. Use numerals. "
129
+ "Map actions: 'plot/graph'->'plot', 'make table'->'table', "
130
+ "'find optimal' or 'MVP=MIC'->'optimal', 'compute mpp'->'mpp', "
131
+ "'compute app'->'app', 'compute mvp'->'mvp', 'compute mic'->'mic', "
132
+ "'profit'->'profit', 'stage(s)'->'stage', "
133
+ "'show work at X=...' or 'explain ...'->'work' with last_focus_x set."
134
+ )
135
+
136
+ def llm_parse(user: str):
137
+ if not LLM_OK:
138
+ return {}
139
+ try:
140
+ resp = client.chat.completions.create(
141
+ model="gpt-5.0",
142
+ temperature=0,
143
+ messages=[
144
+ {"role": "system", "content": JERRY_SYSTEM_PROMPT},
145
+ {"role": "user", "content": user},
146
+ ]
147
+ )
148
+ txt = (resp.choices[0].message.content or "").strip()
149
+ if txt.startswith("{"):
150
+ # keep it safe: if JSON fails, fall back later
151
+ return json.loads(txt)
152
+ except Exception as e:
153
+ # print for server logs; don't crash the student UX
154
+ print("LLM parse failed:", e)
155
+ return {}
156
+
157
+ NUM = r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?"
158
+ def _floatish(s):
159
+ if s is None: return None
160
+ t = str(s).replace("$","").replace(",","").strip()
161
+ try: return float(t)
162
+ except: return None
163
+
164
+ def which_from_text(t):
165
+ t=t.lower()
166
+ if "optimal" in t or "mvp=mic" in t: return "optimal"
167
+ if "plot" in t or "graph" in t: return "plot"
168
+ if "table" in t: return "table"
169
+ if "work" in t or "explain" in t: return "work"
170
+ if "mvp" in t: return "mvp"
171
+ if "mic" in t: return "mic"
172
+ if "profit" in t: return "profit"
173
+ if "mpp" in t: return "mpp"
174
+ if "app" in t: return "app"
175
+ if "stage" in t: return "stage"
176
+ return None
177
+
178
+ def regex_parse(user: str):
179
+ d={}
180
+ m=re.search(r"(?:Py|output price)\s*=?\s*("+NUM+")",user,re.I); d["Py"]=_floatish(m.group(1)) if m else None
181
+ m=re.search(r"(?:Px|MIC|input price)\s*=?\s*("+NUM+")",user,re.I); d["Px"]=_floatish(m.group(1)) if m else None
182
+ m=re.search(r"(?:Other|other cost)\s*=?\s*("+NUM+")",user,re.I); d["Other"]=_floatish(m.group(1)) if m else None
183
+ m=re.search(r"\bL\s*=\s*("+NUM+")",user,re.I); d["L"]=_floatish(m.group(1)) if m else None
184
+ m=re.search(r"\bk\s*=\s*("+NUM+")",user,re.I); d["k"]=_floatish(m.group(1)) if m else None
185
+ m=re.search(r"\bx0\s*=\s*("+NUM+")",user,re.I); d["x0"]=_floatish(m.group(1)) if m else None
186
+ m=re.search(r"(?:range|from|apply)\s*("+NUM+")\s*(?:to|-)\s*("+NUM+")\s*(?:by|in|step)\s*("+NUM+")",user,re.I)
187
+ if m:
188
+ d["x_min"]=_floatish(m.group(1)); d["x_max"]=_floatish(m.group(2)); d["x_step"]=_floatish(m.group(3))
189
+ m=re.search(r"(?:show\s*work|explain)[^\d]*x\s*=?\s*("+NUM+")",user,re.I)
190
+ if m: d["last_focus_x"]=_floatish(m.group(1))
191
+ d["which_action"]=which_from_text(user)
192
+ return d
193
+
194
+ def parse(user: str):
195
+ # LLM first, then regex fallback
196
+ return llm_parse(user) or regex_parse(user)
197
+
198
+ # ---------- Blackboard ----------
199
+ def explain_row(h,row):
200
+ lines=[
201
+ f"At X = {row['X']:.4g}:",
202
+ f" Y = L/(1+exp(-k*(X-x0))) with L={h.get('L')}, k={h.get('k')}, x0={h.get('x0')}",
203
+ f" β‡’ Y β‰ˆ {row['Y']:.4g}",
204
+ f" APP = Y/X β‰ˆ {row['APP']:.4g}",
205
+ f" MPP = dY/dX β‰ˆ {row['MPP']:.4g}"
206
+ ]
207
+ if 'MVP' in row: lines.append(f" MVP = MPPΒ·Py β‰ˆ {row['MVP']:.4g}")
208
+ if 'MIC' in row: lines.append(f" MIC = Px β‰ˆ {row['MIC']:.4g}")
209
+ if 'Profit' in row: lines.append(f" Profit = PyΒ·Y βˆ’ PxΒ·X βˆ’ Other β‰ˆ {row['Profit']:.4g}")
210
  lines.append(f" Stage = {row['Stage']}")
211
+ return "\\n".join(lines)
212
+
213
+ # ---------- Orchestrator (app53 style) ----------
214
+ def _ask_with_hud(user_text, hud_state):
215
+ """
216
+ Returns 8 outputs:
217
+ 1) student answer
218
+ 2) HUD JSON (pretty)
219
+ 3) changed fields
220
+ 4) teacher debug JSON
221
+ 5) new HUD state (dict)
222
+ 6) Blackboard text
223
+ 7) Table (DataFrame)
224
+ 8) Plot (matplotlib figure or None)
225
+ """
226
+ parsed = parse(user_text)
227
+ new_hud, changed = merge_into_hud(hud_state or DEFAULTS, parsed)
228
+
229
+ action = new_hud.get("which_action")
230
+ if not action:
231
+ msg = "Jerry β†’ Tell me what to do (e.g., 'make table', 'plot mpp', 'find optimal', 'show work at X=…')."
232
+ return msg, _pp(new_hud), ", ".join(changed) or "(none)", _pp({"parsed":parsed}), new_hud, "(none)", pd.DataFrame(), None
233
+
234
+ miss = missing_for(action, new_hud)
235
+ if miss:
236
+ msg = f"Jerry β†’ Not enough data for {action}. Missing: {', '.join(miss)}. Add those and ask again."
237
+ return msg, _pp(new_hud), ", ".join(changed) or "(none)", _pp({"parsed":parsed, "missing":miss}), new_hud, "(none)", pd.DataFrame(), None
238
+
239
+ table = pd.DataFrame(); plot = None; steps = "(none)"; reply = ""
240
 
 
 
 
 
 
241
  try:
242
+ if action in ("table","app","mpp","mvp","mic","profit","stage","plot","optimal"):
243
+ df = build_table(new_hud)
244
+ table = df
245
+
246
+ if action == "table":
247
+ reply = f"Built table for X from {new_hud['x_min']} to {new_hud['x_max']} by {new_hud['x_step']}."
248
+ row = df.iloc[len(df)//2].to_dict()
249
+ steps = explain_row(new_hud, row)
250
+
251
+ elif action == "plot":
252
+ metric = "Y"
253
+ t = user_text.lower()
254
+ if "mpp" in t: metric = "MPP"
255
+ elif "app" in t: metric = "APP"
256
+ elif "mvp" in t and "MVP" in df.columns: metric = "MVP"
257
+ elif "profit" in t and "Profit" in df.columns: metric = "Profit"
258
+ fig = plt.figure()
259
+ ax = fig.add_subplot(111)
260
+ ax.plot(df["X"], df[metric])
261
+ ax.set_xlabel("X (fertilizer)"); ax.set_ylabel(metric); ax.set_title(f"{metric} vs X")
262
+ plot = fig
263
+ reply = f"Plotted {metric} vs X."
264
+ row = df.iloc[len(df)//2].to_dict(); steps = explain_row(new_hud, row)
265
+
266
+ elif action in ("app","mpp","mvp","mic","profit","stage"):
267
+ cols = ["X"]
268
+ if action in ("app","stage"): cols.append("APP")
269
+ if action in ("mpp","stage"): cols.append("MPP")
270
+ if action in ("mvp","stage") and "MVP" in df.columns: cols.append("MVP")
271
+ if action in ("mic","stage") and "MIC" in df.columns: cols.append("MIC")
272
+ if action in ("profit","stage") and "Profit" in df.columns: cols.append("Profit")
273
+ cols.append("Stage")
274
+ table = df[[c for c in cols if c in df.columns]]
275
+ reply = "Computed metrics. Showing relevant columns."
276
+ row = df.iloc[len(df)//2].to_dict(); steps = explain_row(new_hud, row)
277
+ if action=="mvp" and "MVP" not in df.columns: reply += " (Set Py first)"
278
+ if action=="mic" and "MIC" not in df.columns: reply += " (Set Px first)"
279
+ if action=="profit" and "Profit" not in df.columns: reply += " (Set Px and Py first)"
280
+
281
+ elif action == "optimal":
282
+ row_star, df = find_optimal(new_hud)
283
+ reply = (f"Optimal (MVP β‰ˆ MIC in Stage II): X* β‰ˆ {row_star['X']:.4g}, "
284
+ f"Y* β‰ˆ {row_star['Y']:.4g}"
285
+ + (f", Profit* β‰ˆ {row_star['Profit']:.4g}" if "Profit" in df.columns else ""))
286
+ cols = ["X","Y","MPP"]
287
+ if "MVP" in df.columns: cols.append("MVP")
288
+ if "MIC" in df.columns: cols.append("MIC")
289
+ if "Profit" in df.columns: cols.append("Profit")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  table = df[cols]
291
+ steps = explain_row(new_hud, row_star.to_dict()) + "\\n\\nRule: choose X where MVP = MIC (in Stage II)."
 
 
292
 
293
+ elif action == "work":
294
+ x = new_hud.get("last_focus_x")
295
+ if x is None:
296
+ reply = "Tell me 'show work at X=…' with a value."
297
+ else:
298
+ rowdf = row_at_x(new_hud, x)
299
+ table = rowdf
300
+ reply = f"Exact work at X={x}."
301
+ steps = explain_row(new_hud, rowdf.iloc[0].to_dict())
302
+
303
+ else:
304
+ reply = "Okay."
305
+ except Exception as e:
306
+ reply = f"Jerry β†’ {e}"
307
 
308
+ return reply, _pp(new_hud), ", ".join(changed) or "(none)", _pp({"parsed":parsed}), new_hud, steps, table, plot
309
+
310
+ # ---------- UI ----------
311
+ with gr.Blocks(css="footer {visibility: hidden}") as demo:
312
+ gr.Markdown("## Jerry β€” Logistic Yield Coach (app52) β€” OpenAI GPT-5.0 parser (app53-style)")
313
+
314
+ hud_state = gr.State(copy.deepcopy(DEFAULTS))
315
 
 
 
316
  with gr.Row():
317
  with gr.Column(scale=1):
318
+ q = gr.Textbox(label="Talk to Jerry", lines=4, placeholder="e.g., Py=0.5, Px=0.25, L=1200, k=0.06, x0=60; range 0 to 150 by 5; make table")
319
+ ask = gr.Button("Ask Jerry", variant="primary")
320
  with gr.Column(scale=1):
321
+ out = gr.Textbox(label="Jerry says", lines=8)
322
  with gr.Row():
323
  with gr.Column(scale=1):
324
+ gr.Markdown("### Student HUD β€” what Jerry sees (top = most likely inputs)")
325
+ hud_box = gr.Textbox(label="HUD (persists this session)", lines=18, value=_pp(DEFAULTS))
326
+ changed_box = gr.Textbox(label="Fields updated by last prompt", value="(none)", lines=3)
327
+ steps_box = gr.Textbox(label="Blackboard β€” show the work", value="(none)", lines=14)
328
  with gr.Column(scale=1):
329
+ table = gr.Dataframe(label="Main Output Table", wrap=True)
330
+ plot = gr.Plot(label="Figure (on demand)")
331
+
332
+ def runner(user_text, hud):
333
+ return _ask_with_hud(user_text, hud)
334
 
335
+ ask.click(runner, [q, hud_state], [out, hud_box, changed_box, gr.Textbox.update(), hud_state, steps_box, table, plot])
336
 
337
  if __name__ == "__main__":
338
+ demo.queue().launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT","7860")))