j-js commited on
Commit
1503061
·
verified ·
1 Parent(s): 60b42a6

Update conversation_logic.py

Browse files
Files changed (1) hide show
  1. conversation_logic.py +417 -127
conversation_logic.py CHANGED
@@ -1,42 +1,119 @@
1
  from __future__ import annotations
2
 
3
- from typing import Any, Dict, List, Optional
 
4
 
 
5
  from formatting import format_reply
6
  from generator_engine import GeneratorEngine
7
  from models import RetrievedChunk, SolverResult
8
  from quant_solver import is_quant_question, solve_quant
9
  from retrieval_engine import RetrievalEngine
10
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  def _teaching_lines(chunks: List[RetrievedChunk]) -> List[str]:
13
- lines: List[str] = []
14
  for chunk in chunks:
15
- text = (chunk.text or "").strip().replace("\n", " ")
16
  if len(text) > 220:
17
  text = text[:217].rstrip() + "…"
18
- topic = chunk.topic or "general"
19
  lines.append(f"- {topic}: {text}")
20
  return lines
21
 
22
 
23
- def _should_retrieve(intent: str, result: SolverResult) -> bool:
24
- if not result.solved:
25
- return True
26
- return intent in {"hint", "method", "step_by_step", "full_working", "walkthrough", "explain"}
27
-
28
-
29
- def _retrieval_query(user_text: str, question_text: str, options_text: str) -> str:
30
- parts = []
31
- if question_text.strip():
32
- parts.append(question_text.strip())
33
- if options_text.strip():
34
- parts.append(options_text.strip())
35
- if user_text.strip():
36
- parts.append(user_text.strip())
37
- return "\n".join(parts).strip()
38
-
39
-
40
  def _compose_quant_reply(
41
  result: SolverResult,
42
  intent: str,
@@ -44,128 +121,341 @@ def _compose_quant_reply(
44
  verbosity: float,
45
  ) -> str:
46
  steps = result.steps or []
47
- internal = (
48
- getattr(result, "internal_answer", None)
49
- or getattr(result, "internal_answer_value", None)
50
- or getattr(result, "answer_value", None)
51
- or ""
52
- )
53
 
54
  if intent == "hint":
55
- if steps:
56
- return f"Hint:\n- {steps[0]}"
57
- return "Hint:\n- Start by translating the wording into an equation."
58
 
59
- if intent in {"method", "walkthrough"}:
60
- body = "Use this method:"
61
  if steps:
62
- limit = 3 if verbosity < 0.75 else 4
63
- body += "\n" + "\n".join(f"- {s}" for s in steps[:limit])
64
- else:
65
- body += "\n- Treat the statement as an equation.\n- Isolate the unknown step by step."
66
- if reveal_answer and internal:
67
- body += f"\n\nThat gives {internal}."
68
- return body
69
 
70
- if intent in {"step_by_step", "full_working"}:
71
  if steps:
72
- limit = 4 if verbosity < 0.75 else min(6, len(steps))
73
- body = "\n".join(f"{i+1}. {s}" for i, s in enumerate(steps[:limit]))
74
- else:
75
- body = "1. Translate the wording into an equation.\n2. Isolate the variable carefully.\n3. Check the result."
76
- if reveal_answer and internal:
77
- body += f"\n\nSo the result is {internal}."
78
- return body
79
 
80
- if intent == "explain":
81
- if steps:
82
- body = "Here is the idea:\n" + "\n".join(f"- {s}" for s in steps[:3])
 
 
 
 
 
83
  else:
84
- body = "Here is the idea:\n- Turn the wording into a clear mathematical relationship.\n- Solve it step by step."
 
85
  if reveal_answer and internal:
86
- body += f"\n\nThat leads to {internal}."
87
- return body
88
 
 
89
  if reveal_answer and internal:
 
 
 
 
90
  return f"The result is {internal}."
91
 
92
  if steps:
93
- return "\n".join(f"- {s}" for s in steps[:2])
94
-
95
- return result.reply or "I can help solve this, but I need a little more structure from the question."
96
-
97
-
98
- class ConversationEngine:
99
- def __init__(self, retriever: RetrievalEngine, generator: Optional[GeneratorEngine] = None):
100
- self.retriever = retriever
101
- self.generator = generator or GeneratorEngine()
102
-
103
- def generate_response(
104
- self,
105
- raw_user_text: str,
106
- tone: float,
107
- verbosity: float,
108
- transparency: float,
109
- intent: str,
110
- help_mode: str,
111
- chat_history: Optional[List[Dict[str, Any]]] = None,
112
- question_text: str = "",
113
- options_text: str = "",
114
- retrieval_context: str = "",
115
- ) -> SolverResult:
116
- user_text = (raw_user_text or "").strip()
117
- question_text = (question_text or "").strip()
118
- options_text = (options_text or "").strip()
119
-
120
- question_block = "\n".join([x for x in [question_text, options_text] if x]).strip()
121
- solver_input = user_text or question_block or question_text
122
-
123
- quant_from_user = bool(user_text and is_quant_question(user_text))
124
- quant_from_question = bool(question_text and is_quant_question(question_block or question_text))
125
-
126
- if quant_from_user or quant_from_question:
127
- solve_text = user_text if quant_from_user else (question_block or question_text)
128
- result = solve_quant(solve_text)
129
- result.help_mode = help_mode
130
-
131
- reveal_answer = intent in {"answer", "full_working"} or transparency >= 0.85
132
-
133
- chunks: List[RetrievedChunk] = []
134
- if _should_retrieve(intent, result):
135
- topic = result.topic or getattr(result, "detected_topic", None) or "general"
136
- query = _retrieval_query(user_text, question_text, options_text)
137
- chunks = self.retriever.search(query, topic=topic, intent=intent, k=3)
138
-
139
- result.teaching_chunks = chunks
140
- result.used_retrieval = bool(chunks)
141
-
142
- core = _compose_quant_reply(
143
- result,
144
- intent=intent,
145
- reveal_answer=reveal_answer,
146
- verbosity=verbosity,
147
- )
148
 
149
- if chunks and intent in {"hint", "method", "step_by_step", "full_working", "walkthrough", "explain"}:
150
- core += "\n\nRelevant study notes:\n" + "\n".join(_teaching_lines(chunks))
151
 
152
- result.reply = format_reply(core, tone, verbosity, transparency, help_mode)
153
- return result
154
 
155
- result = SolverResult(domain="general", solved=False, help_mode=help_mode)
 
 
156
 
157
- prompt = (
158
- "You are a helpful study assistant. Reply naturally and briefly. "
159
- "Do not invent facts. If the user is asking for emotional support or general help, be supportive and practical.\n\n"
160
- f"User message: {user_text}"
161
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
- generated = self.generator.generate(prompt) if self.generator and self.generator.available() else None
164
- if generated:
165
- result.reply = format_reply(generated, tone, verbosity, transparency, help_mode)
166
- result.used_generator = True
167
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
- fallback = "I can help with the current question, explain a method, or talk through the next step."
170
- result.reply = format_reply(fallback, tone, verbosity, transparency, help_mode)
171
- return result
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ import re
4
+ from typing import Any, Dict, List, Optional, Set
5
 
6
+ from context_parser import detect_intent, intent_to_help_mode
7
  from formatting import format_reply
8
  from generator_engine import GeneratorEngine
9
  from models import RetrievedChunk, SolverResult
10
  from quant_solver import is_quant_question, solve_quant
11
  from retrieval_engine import RetrievalEngine
12
+ from utils import short_lines
13
+
14
+
15
+ # -----------------------------
16
+ # Retrieval intent configuration
17
+ # -----------------------------
18
+
19
+ RETRIEVAL_ALLOWED_INTENTS = {
20
+ "walkthrough",
21
+ "step_by_step",
22
+ "explain",
23
+ "method",
24
+ "hint",
25
+ "definition",
26
+ "concept",
27
+ "instruction",
28
+ }
29
+
30
+ DIRECT_SOLVE_PATTERNS = [
31
+ r"\bsolve\b",
32
+ r"\bwhat is\b",
33
+ r"\bfind\b",
34
+ r"\bgive (?:me )?the answer\b",
35
+ r"\bjust the answer\b",
36
+ r"\banswer only\b",
37
+ r"\bcalculate\b",
38
+ ]
39
+
40
+ STRUCTURE_KEYWORDS = {
41
+ "algebra": [
42
+ "equation", "solve", "isolate", "variable", "linear", "expression",
43
+ "unknown", "algebra", "substitute", "rearrange"
44
+ ],
45
+ "percent": [
46
+ "percent", "%", "percentage", "increase", "decrease", "of"
47
+ ],
48
+ "ratio": [
49
+ "ratio", "proportion", "proportional", "part", "share"
50
+ ],
51
+ "statistics": [
52
+ "mean", "median", "mode", "range", "average", "standard deviation"
53
+ ],
54
+ "probability": [
55
+ "probability", "chance", "likely", "odds", "event"
56
+ ],
57
+ "geometry": [
58
+ "triangle", "circle", "angle", "area", "perimeter", "radius", "diameter"
59
+ ],
60
+ "number_properties": [
61
+ "integer", "odd", "even", "prime", "divisible", "factor", "multiple"
62
+ ],
63
+ }
64
+
65
+ INTENT_KEYWORDS = {
66
+ "walkthrough": ["walkthrough", "work through", "step by step", "full working"],
67
+ "step_by_step": ["step", "first step", "next step", "step by step"],
68
+ "explain": ["explain", "why", "understand"],
69
+ "method": ["method", "approach", "how do i solve", "how to solve"],
70
+ "hint": ["hint", "nudge", "clue"],
71
+ "definition": ["define", "definition", "what does", "what is meant by"],
72
+ "concept": ["concept", "idea", "principle", "rule"],
73
+ "instruction": ["how do i", "how to", "what should i do first", "what step"],
74
+ }
75
+
76
+ MISMATCH_TERMS = {
77
+ "algebra": [
78
+ "absolute value", "modulus", "square root", "quadratic", "inequality",
79
+ "roots", "parabola", "simultaneous equations"
80
+ ],
81
+ "percent": [
82
+ "triangle", "circle", "prime", "absolute value"
83
+ ],
84
+ "ratio": [
85
+ "absolute value", "quadratic", "circle"
86
+ ],
87
+ "statistics": [
88
+ "absolute value", "prime", "triangle"
89
+ ],
90
+ "probability": [
91
+ "absolute value", "circle area", "quadratic"
92
+ ],
93
+ "geometry": [
94
+ "absolute value", "prime", "median salary"
95
+ ],
96
+ "number_properties": [
97
+ "circle", "triangle", "absolute value"
98
+ ],
99
+ }
100
+
101
+
102
+ # -----------------------------
103
+ # Reply building
104
+ # -----------------------------
105
 
106
  def _teaching_lines(chunks: List[RetrievedChunk]) -> List[str]:
107
+ lines = []
108
  for chunk in chunks:
109
+ text = chunk.text.strip().replace("\n", " ")
110
  if len(text) > 220:
111
  text = text[:217].rstrip() + "…"
112
+ topic = getattr(chunk, "topic", "general") or "general"
113
  lines.append(f"- {topic}: {text}")
114
  return lines
115
 
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  def _compose_quant_reply(
118
  result: SolverResult,
119
  intent: str,
 
121
  verbosity: float,
122
  ) -> str:
123
  steps = result.steps or []
124
+ internal = result.internal_answer or result.answer_value or ""
 
 
 
 
 
125
 
126
  if intent == "hint":
127
+ return steps[0] if steps else "Start by translating the wording into an equation."
 
 
128
 
129
+ if intent == "instruction":
 
130
  if steps:
131
+ return f"First step: {steps[0]}"
132
+ return "First, turn the wording into a mathematical relationship."
 
 
 
 
 
133
 
134
+ if intent == "definition":
135
  if steps:
136
+ return f"Here is the idea in context:\n- {steps[0]}"
137
+ return "This means identifying the mathematical idea being used and expressing it clearly."
 
 
 
 
 
138
 
139
+ if intent in {"walkthrough", "step_by_step", "explain", "method", "concept"}:
140
+ if not steps:
141
+ if reveal_answer and internal:
142
+ return f"The result is {internal}."
143
+ return "I can explain the method, but I do not have enough structured steps yet."
144
+
145
+ if verbosity >= 0.66:
146
+ body = "\n".join(f"- {s}" for s in steps)
147
  else:
148
+ body = "\n".join(f"- {s}" for s in steps[: min(3, len(steps))])
149
+
150
  if reveal_answer and internal:
151
+ return f"Walkthrough:\n{body}\n\nThat gives {internal}."
152
+ return f"Walkthrough:\n{body}"
153
 
154
+ # answer/default
155
  if reveal_answer and internal:
156
+ if result.answer_value and str(result.answer_value).startswith("x ="):
157
+ return f"The result is {result.answer_value}."
158
+ if result.answer_value:
159
+ return f"The answer is {result.answer_value}."
160
  return f"The result is {internal}."
161
 
162
  if steps:
163
+ return steps[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ return "I can help with this, but I cannot confidently solve it from the current parse alone yet."
 
166
 
 
 
167
 
168
+ # -----------------------------
169
+ # Intent / retrieval helpers
170
+ # -----------------------------
171
 
172
+ def _normalize_text(text: str) -> str:
173
+ return re.sub(r"\s+", " ", (text or "").strip().lower())
174
+
175
+
176
+ def _extract_keywords(text: str) -> Set[str]:
177
+ raw = re.findall(r"[a-zA-Z][a-zA-Z0-9_+-]*", text.lower())
178
+ stop = {
179
+ "the", "a", "an", "is", "are", "to", "of", "for", "and", "or", "in", "on",
180
+ "at", "by", "this", "that", "it", "be", "do", "i", "me", "my", "you",
181
+ "how", "what", "why", "give", "show", "please", "can"
182
+ }
183
+ return {w for w in raw if len(w) > 2 and w not in stop}
184
+
185
+
186
+ def _infer_structure_terms(question_text: str, topic: Optional[str]) -> List[str]:
187
+ terms: List[str] = []
188
+ if topic and topic in STRUCTURE_KEYWORDS:
189
+ terms.extend(STRUCTURE_KEYWORDS[topic])
190
+
191
+ q = question_text.lower()
192
+
193
+ if "=" in q:
194
+ terms.extend(["equation", "solve"])
195
+ if "x" in q or "y" in q:
196
+ terms.extend(["variable", "isolate"])
197
+ if "/" in q or "divide" in q:
198
+ terms.extend(["divide", "undo operations"])
199
+ if "*" in q or "times" in q or "multiply" in q:
200
+ terms.extend(["multiply", "undo operations"])
201
+ if "%" in q or "percent" in q:
202
+ terms.extend(["percent", "percentage"])
203
+
204
+ return list(dict.fromkeys(terms))
205
+
206
+
207
+ def _infer_mismatch_terms(topic: Optional[str], question_text: str) -> List[str]:
208
+ if not topic or topic not in MISMATCH_TERMS:
209
+ return []
210
+ q = question_text.lower()
211
+ terms = []
212
+ for term in MISMATCH_TERMS[topic]:
213
+ if term not in q:
214
+ terms.append(term)
215
+ return terms
216
+
217
+
218
+ def _intent_keywords(intent: str) -> List[str]:
219
+ return INTENT_KEYWORDS.get(intent, [])
220
+
221
+
222
+ def _is_direct_solve_request(text: str, intent: str) -> bool:
223
+ if intent == "answer":
224
+ return True
225
+
226
+ t = _normalize_text(text)
227
 
228
+ if any(re.search(p, t) for p in DIRECT_SOLVE_PATTERNS):
229
+ if not any(word in t for word in ["how", "explain", "why", "method", "hint", "define", "definition", "step"]):
230
+ return True
231
+
232
+ return False
233
+
234
+
235
+ def should_retrieve(intent: str, solved: bool, raw_user_text: str) -> bool:
236
+ if intent in RETRIEVAL_ALLOWED_INTENTS:
237
+ return True
238
+
239
+ if not solved:
240
+ return True
241
+
242
+ if _is_direct_solve_request(raw_user_text, intent):
243
+ return False
244
+
245
+ return False
246
+
247
+
248
+ def _score_chunk(
249
+ chunk: RetrievedChunk,
250
+ intent: str,
251
+ topic: Optional[str],
252
+ question_text: str,
253
+ ) -> float:
254
+ text = f"{getattr(chunk, 'topic', '')} {chunk.text}".lower()
255
+ score = 0.0
256
+
257
+ # topic match
258
+ if topic:
259
+ chunk_topic = (getattr(chunk, "topic", "") or "").lower()
260
+ if chunk_topic == topic.lower():
261
+ score += 4.0
262
+ elif topic.lower() in text:
263
+ score += 2.0
264
+
265
+ # structure match
266
+ structure_terms = _infer_structure_terms(question_text, topic)
267
+ for term in structure_terms:
268
+ if term.lower() in text:
269
+ score += 1.5
270
+
271
+ # intent match
272
+ for term in _intent_keywords(intent):
273
+ if term.lower() in text:
274
+ score += 1.2
275
+
276
+ # question keyword overlap
277
+ q_keywords = _extract_keywords(question_text)
278
+ overlap = sum(1 for kw in q_keywords if kw in text)
279
+ score += min(overlap * 0.4, 3.0)
280
+
281
+ # penalties for obvious mismatch
282
+ mismatch_terms = _infer_mismatch_terms(topic, question_text)
283
+ for bad in mismatch_terms:
284
+ if bad.lower() in text:
285
+ score -= 2.5
286
+
287
+ return score
288
+
289
+
290
+ def _filter_retrieved_chunks(
291
+ chunks: List[RetrievedChunk],
292
+ intent: str,
293
+ topic: Optional[str],
294
+ question_text: str,
295
+ min_score: float = 2.5,
296
+ max_chunks: int = 3,
297
+ ) -> List[RetrievedChunk]:
298
+ scored = []
299
+ for chunk in chunks:
300
+ s = _score_chunk(chunk, intent, topic, question_text)
301
+ if s >= min_score:
302
+ scored.append((s, chunk))
303
+
304
+ scored.sort(key=lambda x: x[0], reverse=True)
305
+ return [chunk for _, chunk in scored[:max_chunks]]
306
+
307
+
308
+ def _build_retrieval_query(
309
+ raw_user_text: str,
310
+ question_text: str,
311
+ intent: str,
312
+ topic: Optional[str],
313
+ solved: bool,
314
+ ) -> str:
315
+ parts: List[str] = []
316
+
317
+ base = question_text.strip() if question_text.strip() else raw_user_text.strip()
318
+ if base:
319
+ parts.append(base)
320
+
321
+ if topic:
322
+ parts.append(topic)
323
+
324
+ if intent in {"definition", "concept"}:
325
+ parts.append("definition concept explanation")
326
+ elif intent in {"walkthrough", "step_by_step", "method", "instruction"}:
327
+ parts.append("method steps worked example")
328
+ elif intent == "hint":
329
+ parts.append("hint strategy first step")
330
+ elif intent == "explain":
331
+ parts.append("explanation reasoning")
332
+ elif not solved:
333
+ parts.append("teaching explanation method")
334
+
335
+ return " ".join(parts).strip()
336
+
337
+
338
+ # -----------------------------
339
+ # Public entry point
340
+ # -----------------------------
341
+
342
+ def generate_response(
343
+ raw_user_text: str,
344
+ tone: float = 0.5,
345
+ verbosity: float = 0.5,
346
+ transparency: float = 0.5,
347
+ retrieval_engine: Optional[RetrievalEngine] = None,
348
+ generator_engine: Optional[GeneratorEngine] = None,
349
+ retrieval_context: Optional[List[RetrievedChunk]] = None,
350
+ chat_history: Optional[List[Dict[str, Any]]] = None,
351
+ question_text: Optional[str] = None,
352
+ ) -> Dict[str, Any]:
353
+ solver_input = (question_text or raw_user_text or "").strip()
354
+ user_text = (raw_user_text or "").strip()
355
+
356
+ intent = detect_intent(user_text)
357
+ help_mode = intent_to_help_mode(intent)
358
+
359
+ reveal_answer = help_mode == "answer" or transparency >= 0.8
360
+
361
+ result = SolverResult(
362
+ domain="general",
363
+ solved=False,
364
+ answer_letter=None,
365
+ answer_value=None,
366
+ internal_answer=None,
367
+ steps=[],
368
+ topic=None,
369
+ )
370
+
371
+ used_retrieval = False
372
+ used_generator = False
373
+ selected_chunks: List[RetrievedChunk] = []
374
+
375
+ if is_quant_question(solver_input):
376
+ result = solve_quant(solver_input)
377
+
378
+ reply = _compose_quant_reply(
379
+ result=result,
380
+ intent=intent,
381
+ reveal_answer=reveal_answer,
382
+ verbosity=verbosity,
383
+ )
384
+
385
+ allow_retrieval = should_retrieve(
386
+ intent=intent,
387
+ solved=bool(result.solved),
388
+ raw_user_text=user_text or solver_input,
389
+ )
390
+
391
+ # Use passed-in retrieval context only if retrieval is allowed
392
+ if allow_retrieval and retrieval_context:
393
+ filtered = _filter_retrieved_chunks(
394
+ chunks=retrieval_context,
395
+ intent=intent,
396
+ topic=result.topic,
397
+ question_text=solver_input,
398
+ )
399
+ if filtered:
400
+ selected_chunks = filtered
401
+ used_retrieval = True
402
+
403
+ # Otherwise retrieve fresh if allowed
404
+ elif allow_retrieval and retrieval_engine is not None:
405
+ query = _build_retrieval_query(
406
+ raw_user_text=user_text,
407
+ question_text=solver_input,
408
+ intent=intent,
409
+ topic=result.topic,
410
+ solved=bool(result.solved),
411
+ )
412
+ retrieved = retrieval_engine.search(query, top_k=6)
413
+ filtered = _filter_retrieved_chunks(
414
+ chunks=retrieved,
415
+ intent=intent,
416
+ topic=result.topic,
417
+ question_text=solver_input,
418
+ )
419
+ if filtered:
420
+ selected_chunks = filtered
421
+ used_retrieval = True
422
+
423
+ # Add teaching notes only if they survived filtering
424
+ if selected_chunks:
425
+ reply = f"{reply}\n\nRelevant study notes:\n" + "\n".join(_teaching_lines(selected_chunks))
426
+
427
+ # Optional generator fallback for non-quant / weak cases
428
+ if not result.solved and generator_engine is not None:
429
+ try:
430
+ generated = generator_engine.generate(
431
+ user_text=user_text or solver_input,
432
+ intent=intent,
433
+ topic=result.topic,
434
+ chat_history=chat_history or [],
435
+ )
436
+ if generated and generated.strip():
437
+ reply = generated.strip()
438
+ used_generator = True
439
+ except Exception:
440
+ pass
441
+
442
+ reply = format_reply(
443
+ text=reply,
444
+ tone=tone,
445
+ verbosity=verbosity,
446
+ transparency=transparency,
447
+ )
448
 
449
+ return {
450
+ "reply": short_lines(reply),
451
+ "meta": {
452
+ "domain": result.domain,
453
+ "solved": result.solved,
454
+ "help_mode": help_mode,
455
+ "answer_letter": result.answer_letter,
456
+ "answer_value": result.answer_value,
457
+ "topic": result.topic,
458
+ "used_retrieval": used_retrieval,
459
+ "used_generator": used_generator,
460
+ },
461
+ }