j-js commited on
Commit
e87053e
·
verified ·
1 Parent(s): 3139428

Update conversation_logic.py

Browse files
Files changed (1) hide show
  1. conversation_logic.py +281 -100
conversation_logic.py CHANGED
@@ -1,4 +1,3 @@
1
- # conversation_logic.py
2
  from __future__ import annotations
3
 
4
  import re
@@ -152,6 +151,203 @@ def _safe_steps(steps: List[str]) -> List[str]:
152
  return cleaned
153
 
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  def _compose_reply(
156
  result: SolverResult,
157
  intent: str,
@@ -160,6 +356,17 @@ def _compose_reply(
160
  ) -> str:
161
  steps = _safe_steps(result.steps or [])
162
  topic = (result.topic or "").lower().strip()
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  def topic_hint_fallback() -> str:
165
  if topic == "algebra":
@@ -179,6 +386,9 @@ def _compose_reply(
179
  return "Focus on the main relationship first."
180
 
181
  def topic_method_fallback() -> str:
 
 
 
182
  if topic == "algebra":
183
  return "\n".join([
184
  "- Treat it as an equation.",
@@ -217,6 +427,9 @@ def _compose_reply(
217
  return "I can explain the method, but I do not have enough structured steps yet."
218
 
219
  if intent == "hint":
 
 
 
220
  if steps:
221
  first = steps[0].lower()
222
 
@@ -234,11 +447,15 @@ def _compose_reply(
234
  return topic_hint_fallback()
235
 
236
  if intent == "instruction":
 
 
237
  if steps:
238
  return f"First step: {steps[0]}"
239
  return "First, identify the key relationship or comparison in the question."
240
 
241
  if intent == "definition":
 
 
242
  if steps:
243
  return f"Here is the idea in context:\n- {steps[0]}"
244
  return "This is asking for the meaning of the term or idea in the question."
@@ -288,6 +505,9 @@ def _compose_reply(
288
 
289
  return "\n".join(f"- {s}" for s in shown_steps)
290
 
 
 
 
291
  if normalize_category(category) == "Verbal":
292
  return "I can help analyse the wording or logic, but I need the full question text to guide you properly."
293
 
@@ -328,19 +548,6 @@ def is_explainer_request(text: str) -> bool:
328
  return any(p in t for p in explainer_signals)
329
 
330
 
331
- def _normalize_text(text: str) -> str:
332
- return re.sub(r"\s+", " ", (text or "").strip().lower())
333
-
334
-
335
- def _extract_keywords(text: str) -> Set[str]:
336
- raw = re.findall(r"[a-zA-Z][a-zA-Z0-9_+-]*", (text or "").lower())
337
- stop = {
338
- "the", "a", "an", "is", "are", "to", "of", "for", "and", "or", "in", "on", "at", "by", "this", "that",
339
- "it", "be", "do", "i", "me", "my", "you", "how", "what", "why", "give", "show", "please", "can",
340
- }
341
- return {w for w in raw if len(w) > 2 and w not in stop}
342
-
343
-
344
  def _infer_structure_terms(question_text: str, topic: Optional[str], question_type: Optional[str]) -> List[str]:
345
  terms: List[str] = []
346
 
@@ -822,48 +1029,6 @@ def _pick_teaching_line(
822
  return best_line
823
 
824
 
825
- def _safe_meta_list(items: Any) -> List[str]:
826
- if not items:
827
- return []
828
- if isinstance(items, list):
829
- return [str(x).strip() for x in items if str(x).strip()]
830
- if isinstance(items, tuple):
831
- return [str(x).strip() for x in items if str(x).strip()]
832
- if isinstance(items, str):
833
- text = items.strip()
834
- return [text] if text else []
835
- return []
836
-
837
-
838
- def _safe_meta_text(value: Any) -> Optional[str]:
839
- if value is None:
840
- return None
841
- text = str(value).strip()
842
- return text or None
843
-
844
-
845
- def _extract_explainer_scaffold(explainer_result: Any) -> Dict[str, Any]:
846
- scaffold = getattr(explainer_result, "scaffold", None)
847
-
848
- if scaffold is None:
849
- return {}
850
-
851
- return {
852
- "concept": _safe_meta_text(getattr(scaffold, "concept", None)),
853
- "ask": _safe_meta_text(getattr(scaffold, "ask", None)),
854
- "givens": _safe_meta_list(getattr(scaffold, "givens", [])),
855
- "target": _safe_meta_text(getattr(scaffold, "target", None)),
856
- "setup_actions": _safe_meta_list(getattr(scaffold, "setup_actions", [])),
857
- "intermediate_steps": _safe_meta_list(getattr(scaffold, "intermediate_steps", [])),
858
- "first_move": _safe_meta_text(getattr(scaffold, "first_move", None)),
859
- "next_hint": _safe_meta_text(getattr(scaffold, "next_hint", None)),
860
- "common_traps": _safe_meta_list(getattr(scaffold, "common_traps", [])),
861
- "variables_to_define": _safe_meta_list(getattr(scaffold, "variables_to_define", [])),
862
- "equations_to_form": _safe_meta_list(getattr(scaffold, "equations_to_form", [])),
863
- "answer_hidden": bool(getattr(scaffold, "answer_hidden", True)),
864
- }
865
-
866
-
867
  class ConversationEngine:
868
  def __init__(
869
  self,
@@ -925,47 +1090,49 @@ class ConversationEngine:
925
  meta={},
926
  )
927
 
928
- # 1. explainer path first
929
- if is_explainer_request(user_text or solver_input):
930
- explainer_result = route_explainer(solver_input)
931
-
932
- if explainer_result is not None and getattr(explainer_result, "understood", False):
933
- reply = format_explainer_response(
934
- result=explainer_result,
935
- tone=tone,
936
- verbosity=verbosity,
937
- transparency=transparency,
938
- )
 
 
 
 
 
 
939
 
940
- scaffold_meta = _extract_explainer_scaffold(explainer_result)
941
-
942
- result.domain = "quant" if is_quant else "general"
943
- result.solved = False
944
- result.help_mode = "explain"
945
- result.topic = getattr(explainer_result, "topic", None) or question_topic
946
- result.answer_letter = None
947
- result.answer_value = None
948
- result.internal_answer = None
949
- result.used_retrieval = False
950
- result.used_generator = False
951
- result.reply = reply
952
- result.meta = {
953
- "intent": "explain_question",
954
- "question_text": question_text or "",
955
- "options_count": len(options_text or []),
956
- "category": inferred_category,
957
- "question_type": question_type,
958
- "classified_topic": question_topic,
959
- "explainer_used": True,
960
- "bridge_ready": bool(getattr(explainer_result, "meta", {}).get("bridge_ready", False)),
961
- "hint_style": getattr(explainer_result, "meta", {}).get("hint_style"),
962
- "explainer_summary": getattr(explainer_result, "summary", None),
963
- "explainer_teaching_points": _safe_meta_list(
964
- getattr(explainer_result, "teaching_points", [])
965
- ),
966
- "scaffold": scaffold_meta,
967
- }
968
- return result
969
 
970
  # 2. normal solver path
971
  if is_quant:
@@ -977,10 +1144,22 @@ class ConversationEngine:
977
  result.help_mode = resolved_help_mode
978
 
979
  if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
980
- result.topic = question_topic
981
 
982
  result.domain = "quant"
983
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  # 3. compose base reply
985
  reply = _compose_reply(
986
  result=result,
@@ -1110,14 +1289,16 @@ class ConversationEngine:
1110
  result.internal_answer = None
1111
  result.reply = reply
1112
  result.help_mode = resolved_help_mode
1113
- result.meta = {
 
 
1114
  "intent": resolved_intent,
1115
  "question_text": question_text or "",
1116
  "options_count": len(options_text or []),
1117
  "category": inferred_category,
1118
  "question_type": question_type,
1119
  "classified_topic": question_topic,
1120
- "explainer_used": False,
1121
- }
1122
 
1123
  return result
 
 
1
  from __future__ import annotations
2
 
3
  import re
 
151
  return cleaned
152
 
153
 
154
+ def _normalize_text(text: str) -> str:
155
+ return re.sub(r"\s+", " ", (text or "").strip().lower())
156
+
157
+
158
+ def _extract_keywords(text: str) -> Set[str]:
159
+ raw = re.findall(r"[a-zA-Z][a-zA-Z0-9_+-]*", (text or "").lower())
160
+ stop = {
161
+ "the", "a", "an", "is", "are", "to", "of", "for", "and", "or", "in", "on", "at", "by", "this", "that",
162
+ "it", "be", "do", "i", "me", "my", "you", "how", "what", "why", "give", "show", "please", "can",
163
+ }
164
+ return {w for w in raw if len(w) > 2 and w not in stop}
165
+
166
+
167
+ def _safe_meta_list(items: Any) -> List[str]:
168
+ if not items:
169
+ return []
170
+ if isinstance(items, list):
171
+ return [str(x).strip() for x in items if str(x).strip()]
172
+ if isinstance(items, tuple):
173
+ return [str(x).strip() for x in items if str(x).strip()]
174
+ if isinstance(items, str):
175
+ text = items.strip()
176
+ return [text] if text else []
177
+ return []
178
+
179
+
180
+ def _safe_meta_text(value: Any) -> Optional[str]:
181
+ if value is None:
182
+ return None
183
+ text = str(value).strip()
184
+ return text or None
185
+
186
+
187
+ def _extract_explainer_scaffold(explainer_result: Any) -> Dict[str, Any]:
188
+ scaffold = getattr(explainer_result, "scaffold", None)
189
+
190
+ if scaffold is None:
191
+ return {}
192
+
193
+ return {
194
+ "concept": _safe_meta_text(getattr(scaffold, "concept", None)),
195
+ "ask": _safe_meta_text(getattr(scaffold, "ask", None)),
196
+ "givens": _safe_meta_list(getattr(scaffold, "givens", [])),
197
+ "target": _safe_meta_text(getattr(scaffold, "target", None)),
198
+ "setup_actions": _safe_meta_list(getattr(scaffold, "setup_actions", [])),
199
+ "intermediate_steps": _safe_meta_list(getattr(scaffold, "intermediate_steps", [])),
200
+ "first_move": _safe_meta_text(getattr(scaffold, "first_move", None)),
201
+ "next_hint": _safe_meta_text(getattr(scaffold, "next_hint", None)),
202
+ "common_traps": _safe_meta_list(getattr(scaffold, "common_traps", [])),
203
+ "variables_to_define": _safe_meta_list(getattr(scaffold, "variables_to_define", [])),
204
+ "equations_to_form": _safe_meta_list(getattr(scaffold, "equations_to_form", [])),
205
+ "answer_hidden": bool(getattr(scaffold, "answer_hidden", True)),
206
+ }
207
+
208
+
209
+ def _build_scaffold_reply(
210
+ intent: str,
211
+ help_mode: str,
212
+ scaffold: Dict[str, Any],
213
+ summary: Optional[str],
214
+ teaching_points: List[str],
215
+ verbosity: float,
216
+ transparency: float,
217
+ ) -> Optional[str]:
218
+ if not scaffold and not summary and not teaching_points:
219
+ return None
220
+
221
+ ask = _safe_meta_text(scaffold.get("ask")) if scaffold else None
222
+ first_move = _safe_meta_text(scaffold.get("first_move")) if scaffold else None
223
+ next_hint = _safe_meta_text(scaffold.get("next_hint")) if scaffold else None
224
+ setup_actions = _safe_meta_list(scaffold.get("setup_actions", [])) if scaffold else []
225
+ intermediate_steps = _safe_meta_list(scaffold.get("intermediate_steps", [])) if scaffold else []
226
+ variables_to_define = _safe_meta_list(scaffold.get("variables_to_define", [])) if scaffold else []
227
+ equations_to_form = _safe_meta_list(scaffold.get("equations_to_form", [])) if scaffold else []
228
+ common_traps = _safe_meta_list(scaffold.get("common_traps", [])) if scaffold else []
229
+
230
+ target_mode = help_mode or intent
231
+
232
+ if target_mode == "hint" or intent == "hint":
233
+ if first_move:
234
+ return first_move
235
+ if next_hint:
236
+ return next_hint
237
+ if setup_actions:
238
+ return setup_actions[0]
239
+ if ask:
240
+ return ask
241
+ if teaching_points:
242
+ return teaching_points[0]
243
+ return None
244
+
245
+ if target_mode == "instruction" or intent == "instruction":
246
+ if first_move:
247
+ return f"First step: {first_move}"
248
+ if setup_actions:
249
+ return f"First step: {setup_actions[0]}"
250
+ if ask:
251
+ return f"First, identify this: {ask}"
252
+ return None
253
+
254
+ if target_mode == "definition" or intent == "definition":
255
+ if summary:
256
+ return summary
257
+ if teaching_points:
258
+ return f"Here is the idea in context:\n- {teaching_points[0]}"
259
+ if ask:
260
+ return ask
261
+ return None
262
+
263
+ if target_mode in {"walkthrough", "step_by_step"} or intent in {"walkthrough", "step_by_step"}:
264
+ lines: List[str] = []
265
+
266
+ sequence: List[str] = []
267
+ if ask:
268
+ sequence.append(f"Identify this first: {ask}")
269
+ sequence.extend(setup_actions)
270
+ sequence.extend(intermediate_steps)
271
+
272
+ if first_move and first_move not in sequence:
273
+ sequence.insert(0, first_move)
274
+
275
+ if next_hint and next_hint not in sequence:
276
+ sequence.append(next_hint)
277
+
278
+ if not sequence and summary:
279
+ sequence.append(summary)
280
+
281
+ if not sequence and teaching_points:
282
+ sequence.extend(teaching_points[:3])
283
+
284
+ if not sequence:
285
+ return None
286
+
287
+ if verbosity < 0.25:
288
+ shown = sequence[:1]
289
+ elif verbosity < 0.6:
290
+ shown = sequence[:2]
291
+ elif verbosity < 0.85:
292
+ shown = sequence[:4]
293
+ else:
294
+ shown = sequence[:6]
295
+
296
+ return "\n".join(f"- {s}" for s in shown)
297
+
298
+ if target_mode in {"method", "concept", "explain"} or intent in {"method", "concept", "explain"}:
299
+ lines: List[str] = []
300
+
301
+ if summary:
302
+ lines.append(summary)
303
+
304
+ if ask:
305
+ lines.append(f"Start by identifying: {ask}")
306
+
307
+ core_steps: List[str] = []
308
+ if first_move:
309
+ core_steps.append(first_move)
310
+ core_steps.extend(setup_actions[:3])
311
+
312
+ if transparency >= 0.45:
313
+ core_steps.extend(intermediate_steps[:2])
314
+
315
+ if core_steps:
316
+ lines.extend(core_steps[:1] if verbosity < 0.35 else core_steps[:3] if verbosity < 0.75 else core_steps[:5])
317
+
318
+ if transparency >= 0.55 and variables_to_define:
319
+ lines.append(f"Useful variable setup: {variables_to_define[0]}")
320
+
321
+ if transparency >= 0.6 and equations_to_form:
322
+ lines.append(f"Key equation: {equations_to_form[0]}")
323
+
324
+ if transparency >= 0.65 and next_hint:
325
+ lines.append(f"Next idea: {next_hint}")
326
+
327
+ if (transparency >= 0.75 or verbosity >= 0.75) and common_traps:
328
+ lines.append(f"Watch out for: {common_traps[0]}")
329
+
330
+ if not lines and teaching_points:
331
+ lines.extend(teaching_points[:2])
332
+
333
+ if not lines:
334
+ return None
335
+
336
+ return "\n".join(f"- {s}" if not s.startswith("- ") and len(lines) > 1 else s for s in lines)
337
+
338
+ # generic fallback
339
+ if first_move:
340
+ return first_move
341
+ if setup_actions:
342
+ return setup_actions[0]
343
+ if summary:
344
+ return summary
345
+ if teaching_points:
346
+ return teaching_points[0]
347
+
348
+ return None
349
+
350
+
351
  def _compose_reply(
352
  result: SolverResult,
353
  intent: str,
 
356
  ) -> str:
357
  steps = _safe_steps(result.steps or [])
358
  topic = (result.topic or "").lower().strip()
359
+ meta = result.meta or {}
360
+
361
+ scaffold_reply = _build_scaffold_reply(
362
+ intent=intent,
363
+ help_mode=result.help_mode,
364
+ scaffold=meta.get("scaffold", {}) if isinstance(meta, dict) else {},
365
+ summary=_safe_meta_text(meta.get("explainer_summary")) if isinstance(meta, dict) else None,
366
+ teaching_points=_safe_meta_list(meta.get("explainer_teaching_points", [])) if isinstance(meta, dict) else [],
367
+ verbosity=verbosity,
368
+ transparency=0.5,
369
+ )
370
 
371
  def topic_hint_fallback() -> str:
372
  if topic == "algebra":
 
386
  return "Focus on the main relationship first."
387
 
388
  def topic_method_fallback() -> str:
389
+ if scaffold_reply:
390
+ return scaffold_reply
391
+
392
  if topic == "algebra":
393
  return "\n".join([
394
  "- Treat it as an equation.",
 
427
  return "I can explain the method, but I do not have enough structured steps yet."
428
 
429
  if intent == "hint":
430
+ if scaffold_reply:
431
+ return scaffold_reply
432
+
433
  if steps:
434
  first = steps[0].lower()
435
 
 
447
  return topic_hint_fallback()
448
 
449
  if intent == "instruction":
450
+ if scaffold_reply:
451
+ return scaffold_reply
452
  if steps:
453
  return f"First step: {steps[0]}"
454
  return "First, identify the key relationship or comparison in the question."
455
 
456
  if intent == "definition":
457
+ if scaffold_reply:
458
+ return scaffold_reply
459
  if steps:
460
  return f"Here is the idea in context:\n- {steps[0]}"
461
  return "This is asking for the meaning of the term or idea in the question."
 
505
 
506
  return "\n".join(f"- {s}" for s in shown_steps)
507
 
508
+ if scaffold_reply:
509
+ return scaffold_reply
510
+
511
  if normalize_category(category) == "Verbal":
512
  return "I can help analyse the wording or logic, but I need the full question text to guide you properly."
513
 
 
548
  return any(p in t for p in explainer_signals)
549
 
550
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  def _infer_structure_terms(question_text: str, topic: Optional[str], question_type: Optional[str]) -> List[str]:
552
  terms: List[str] = []
553
 
 
1029
  return best_line
1030
 
1031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1032
  class ConversationEngine:
1033
  def __init__(
1034
  self,
 
1090
  meta={},
1091
  )
1092
 
1093
+ # 1. Try explainer early so scaffold is available even when solver is weak
1094
+ explainer_result = route_explainer(solver_input)
1095
+ explainer_understood = bool(explainer_result is not None and getattr(explainer_result, "understood", False))
1096
+ explainer_scaffold = _extract_explainer_scaffold(explainer_result) if explainer_understood else {}
1097
+ explainer_summary = getattr(explainer_result, "summary", None) if explainer_understood else None
1098
+ explainer_teaching_points = _safe_meta_list(
1099
+ getattr(explainer_result, "teaching_points", [])
1100
+ ) if explainer_understood else []
1101
+
1102
+ # 1a. Explicit explainer request returns scaffold-rich explainer response
1103
+ if is_explainer_request(user_text or solver_input) and explainer_understood:
1104
+ reply = format_explainer_response(
1105
+ result=explainer_result,
1106
+ tone=tone,
1107
+ verbosity=verbosity,
1108
+ transparency=transparency,
1109
+ )
1110
 
1111
+ result.domain = "quant" if is_quant else "general"
1112
+ result.solved = False
1113
+ result.help_mode = "explain"
1114
+ result.topic = getattr(explainer_result, "topic", None) or question_topic
1115
+ result.answer_letter = None
1116
+ result.answer_value = None
1117
+ result.internal_answer = None
1118
+ result.used_retrieval = False
1119
+ result.used_generator = False
1120
+ result.reply = reply
1121
+ result.meta = {
1122
+ "intent": "explain_question",
1123
+ "question_text": question_text or "",
1124
+ "options_count": len(options_text or []),
1125
+ "category": inferred_category,
1126
+ "question_type": question_type,
1127
+ "classified_topic": question_topic,
1128
+ "explainer_used": True,
1129
+ "bridge_ready": bool(getattr(explainer_result, "meta", {}).get("bridge_ready", False)),
1130
+ "hint_style": getattr(explainer_result, "meta", {}).get("hint_style"),
1131
+ "explainer_summary": explainer_summary,
1132
+ "explainer_teaching_points": explainer_teaching_points,
1133
+ "scaffold": explainer_scaffold,
1134
+ }
1135
+ return result
 
 
 
 
1136
 
1137
  # 2. normal solver path
1138
  if is_quant:
 
1144
  result.help_mode = resolved_help_mode
1145
 
1146
  if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
1147
+ result.topic = getattr(explainer_result, "topic", None) if explainer_understood else question_topic
1148
 
1149
  result.domain = "quant"
1150
 
1151
+ # 2a. Attach explainer scaffold into result meta so generic paths can use it
1152
+ if result.meta is None:
1153
+ result.meta = {}
1154
+
1155
+ if explainer_understood:
1156
+ result.meta["explainer_used"] = True
1157
+ result.meta["bridge_ready"] = bool(getattr(explainer_result, "meta", {}).get("bridge_ready", False))
1158
+ result.meta["hint_style"] = getattr(explainer_result, "meta", {}).get("hint_style")
1159
+ result.meta["explainer_summary"] = explainer_summary
1160
+ result.meta["explainer_teaching_points"] = explainer_teaching_points
1161
+ result.meta["scaffold"] = explainer_scaffold
1162
+
1163
  # 3. compose base reply
1164
  reply = _compose_reply(
1165
  result=result,
 
1289
  result.internal_answer = None
1290
  result.reply = reply
1291
  result.help_mode = resolved_help_mode
1292
+
1293
+ final_meta = dict(result.meta or {})
1294
+ final_meta.update({
1295
  "intent": resolved_intent,
1296
  "question_text": question_text or "",
1297
  "options_count": len(options_text or []),
1298
  "category": inferred_category,
1299
  "question_type": question_type,
1300
  "classified_topic": question_topic,
1301
+ })
1302
+ result.meta = final_meta
1303
 
1304
  return result