j-js commited on
Commit
020d9c6
·
verified ·
1 Parent(s): 8ff3d0f

Update formatting.py

Browse files
Files changed (1) hide show
  1. formatting.py +264 -269
formatting.py CHANGED
@@ -1,302 +1,297 @@
1
  from __future__ import annotations
2
 
3
  import re
4
- from typing import Any, Dict, List, Optional
5
 
6
- from question_support_loader import question_support_bank
7
 
 
 
 
 
 
 
 
 
8
 
9
- class QuestionFallbackRouter:
10
- def _clean(self, text: Optional[str]) -> str:
11
- return (text or "").strip()
12
 
13
- def _listify(self, value: Any) -> List[str]:
14
- if value is None:
15
- return []
16
- if isinstance(value, list):
17
- return [str(v).strip() for v in value if str(v).strip()]
18
- if isinstance(value, tuple):
19
- return [str(v).strip() for v in value if str(v).strip()]
20
- if isinstance(value, str):
21
- text = value.strip()
22
- return [text] if text else []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  return []
 
 
 
 
 
 
 
 
 
 
24
 
25
- def _normalize_topic(self, topic: Optional[str], question_text: str) -> str:
26
- q = (question_text or "").lower()
27
- t = (topic or "").strip().lower()
28
- if t and t not in {"general", "unknown", "general_quant"}:
29
- return t
30
- if "%" in q or "percent" in q:
31
- return "percent"
32
- if "ratio" in q or re.search(r"\b\d+\s*:\s*\d+\b", q):
33
- return "ratio"
34
- if any(k in q for k in ["probability", "odds", "chance", "random"]):
35
- return "probability"
36
- if any(k in q for k in ["remainder", "factor", "multiple", "prime", "divisible"]):
37
- return "number_theory"
38
- if any(k in q for k in ["triangle", "circle", "angle", "area", "perimeter"]):
39
- return "geometry"
40
- if any(k in q for k in ["mean", "median", "average", "standard deviation"]):
41
- return "statistics"
42
- if "=" in q or re.search(r"\b[xyz]\b", q):
43
- return "algebra"
44
- return "general"
45
-
46
- def _preview_question(self, question_text: str) -> str:
47
- cleaned = " ".join(question_text.split())
48
- if len(cleaned) <= 120:
49
- return cleaned
50
- return cleaned[:117].rstrip() + "..."
51
-
52
- def _topic_defaults(self, topic: str, question_text: str, options_text: Optional[List[str]]) -> Dict[str, Any]:
53
- preview = self._preview_question(question_text)
54
- has_options = bool(options_text)
55
-
56
- generic = {
57
- "first_step": f"Focus on what the question is asking in: {preview}",
58
- "hint_ladder": [
59
- "Identify the exact quantity the question wants.",
60
- "Translate the relationships in the stem into a setup you can work with.",
61
- "Check each step against the wording before choosing an option.",
62
- ],
63
- "walkthrough_steps": [
64
- "Underline what is given and what must be found.",
65
- "Set up the relationship before you calculate.",
66
- "Work step by step and keep the units or labels consistent.",
67
- ],
68
- "method_steps": [
69
- "Start from the structure of the problem rather than jumping into arithmetic.",
70
- "Use the wording to decide which relationship matters most.",
71
- ],
72
- "answer_path": [
73
- "Set up the correct structure first.",
74
- "Only then simplify or evaluate the result.",
75
- ],
76
- "common_trap": "Rushing into calculation before setting up the relationship.",
77
- }
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  if topic == "algebra":
80
- generic.update(
81
- {
82
- "first_step": "Write the equation exactly as given and identify the operation attached to the variable.",
83
- "hint_ladder": [
84
- "Look at which operation is attached to the variable first.",
85
- "Undo that operation on both sides, not just one side.",
86
- "Keep simplifying until the variable is isolated.",
87
- ],
88
- "walkthrough_steps": [
89
- "Copy the equation exactly as written.",
90
- "Use inverse operations to undo what is attached to the variable.",
91
- "Do the same thing to both sides each time.",
92
- "Once the variable is isolated, compare with the answer choices if there are any.",
93
- ],
94
- "method_steps": [
95
- "Algebra questions are about preserving balance while isolating the unknown.",
96
- "Treat each operation in reverse order to peel the equation back.",
97
- ],
98
- "common_trap": "Changing only one side of the equation or combining terms too early.",
99
- }
100
- )
101
- elif topic == "percent":
102
- generic.update(
103
- {
104
- "first_step": "Identify the base quantity before doing any percent calculation.",
105
- "hint_ladder": [
106
- "Decide what quantity the percent is 'of'.",
107
- "Rewrite the percent as a decimal or fraction if that helps.",
108
- "Set up part = percent × base, or reverse it if the base is unknown.",
109
- ],
110
- "walkthrough_steps": [
111
- "Find the base quantity the percent refers to.",
112
- "Translate the wording into a percent relationship.",
113
- "Solve for the missing quantity.",
114
- "Check whether the question asks for the part, the whole, or a percent change.",
115
- ],
116
- "method_steps": [
117
- "Percent questions become easier once you identify the correct base.",
118
- "Do not apply the percent to the wrong quantity.",
119
- ],
120
- "common_trap": "Using the wrong base quantity.",
121
- }
122
- )
123
- elif topic == "ratio":
124
- generic.update(
125
- {
126
- "first_step": "Keep the ratio order consistent and assign one shared multiplier.",
127
- "hint_ladder": [
128
- "Write each part of the ratio with the same multiplier.",
129
- "Use the total or known part to solve for that multiplier.",
130
- "Substitute back into the specific part you need.",
131
- ],
132
- "walkthrough_steps": [
133
- "Translate the ratio into algebraic parts with one common scale factor.",
134
- "Use the total or given piece to solve for the factor.",
135
- "Find the required part and check the order of the ratio.",
136
- ],
137
- "method_steps": [
138
- "Ratio questions are controlled by one shared multiplier.",
139
- "Preserve the ratio order all the way through.",
140
- ],
141
- "common_trap": "Reversing the ratio order or using different multipliers for different parts.",
142
- }
143
- )
144
- elif topic == "probability":
145
- generic.update(
146
- {
147
- "first_step": "Count the successful outcomes and the total possible outcomes separately.",
148
- "hint_ladder": [
149
- "Decide exactly what counts as a success.",
150
- "Count all possible outcomes under the same rules.",
151
- "Write probability as successful over total, then simplify if needed.",
152
- ],
153
- "walkthrough_steps": [
154
- "Identify the event the question cares about.",
155
- "Count the successful outcomes.",
156
- "Count the full sample space.",
157
- "Form the probability and simplify carefully.",
158
- ],
159
- "method_steps": [
160
- "Probability is a comparison of favorable outcomes to all valid outcomes.",
161
- "Make sure the numerator and denominator come from the same setup.",
162
- ],
163
- "common_trap": "Counting the right numerator with the wrong denominator.",
164
- }
165
- )
166
- elif topic == "number_theory":
167
- generic.update(
168
- {
169
- "first_step": "Identify which number property matters most: factors, multiples, divisibility, or remainders.",
170
- "common_trap": "Using arithmetic intuition instead of the actual number-property rule being tested.",
171
- }
172
- )
173
-
174
- if has_options:
175
- generic["answer_path"].append("Use the choices to check which one matches your setup instead of guessing.")
176
-
177
- return generic
178
-
179
- def get_support_pack(
180
- self,
181
- *,
182
- question_id: Optional[str],
183
- question_text: str,
184
- options_text: Optional[List[str]],
185
- topic: Optional[str],
186
- category: Optional[str],
187
- ) -> Dict[str, Any]:
188
- stored = question_support_bank.get(question_id=question_id, question_text=question_text)
189
- resolved_topic = self._normalize_topic(topic, question_text)
190
- if stored:
191
- pack = dict(stored)
192
- pack.setdefault("topic", resolved_topic)
193
- pack.setdefault("category", category or "General")
194
- pack.setdefault("support_source", "question_bank")
195
- return pack
196
-
197
- generated = self._topic_defaults(resolved_topic, question_text, options_text)
198
- generated.update(
199
- {
200
- "question_id": question_id,
201
- "question_text": question_text,
202
- "options_text": list(options_text or []),
203
- "topic": resolved_topic,
204
- "category": category or "General",
205
- "support_source": "generated_question_specific",
206
- }
207
- )
208
- return generated
209
-
210
- def build_response(
211
- self,
212
- *,
213
- question_id: Optional[str],
214
- question_text: str,
215
- options_text: Optional[List[str]],
216
- topic: Optional[str],
217
- category: Optional[str],
218
- help_mode: str,
219
- hint_stage: int,
220
- verbosity: float,
221
- ) -> Dict[str, Any]:
222
- pack = self.get_support_pack(
223
- question_id=question_id,
224
- question_text=question_text,
225
- options_text=options_text,
226
- topic=topic,
227
- category=category,
228
- )
229
-
230
- mode = (help_mode or "answer").lower()
231
- stage = max(1, min(int(hint_stage or 1), 3))
232
-
233
- hint_ladder = self._listify(pack.get("hint_ladder") or pack.get("hints"))
234
- walkthrough_steps = self._listify(pack.get("walkthrough_steps"))
235
- method_steps = self._listify(pack.get("method_steps") or pack.get("method_explanation"))
236
- answer_path = self._listify(pack.get("answer_path"))
237
- common_trap = self._clean(pack.get("common_trap"))
238
- first_step = self._clean(pack.get("first_step"))
239
-
240
- if mode == "hint":
241
- selected = []
242
- if first_step and stage == 1:
243
- selected.append(first_step)
244
- if hint_ladder:
245
- idx = min(stage - 1, len(hint_ladder) - 1)
246
- selected.append(hint_ladder[idx])
247
- if common_trap and verbosity >= 0.55:
248
- selected.append(f"Watch out for this trap: {common_trap}")
249
- lines = selected or [first_step or "Start by identifying the structure of the question."]
250
- elif mode in {"walkthrough", "step_by_step", "instruction"}:
251
- lines = walkthrough_steps or answer_path or hint_ladder or [first_step or "Start by setting up the problem."]
252
- if first_step and (not lines or lines[0] != first_step):
253
- lines = [first_step] + lines
254
- elif mode in {"method", "explain", "concept", "definition"}:
255
- lines = method_steps or walkthrough_steps or answer_path or [first_step or "Start from the problem structure."]
256
- if common_trap and verbosity >= 0.65:
257
- lines = list(lines) + [f"Common trap: {common_trap}"]
258
  else:
259
- lines = answer_path or walkthrough_steps or hint_ladder or [first_step or "Start by identifying the relationship in the question."]
260
- if first_step and (not lines or lines[0] != first_step):
261
- lines = [first_step] + lines
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
- return {
264
- "lines": lines,
265
- "pack": pack,
266
- }
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
- question_fallback_router = QuestionFallbackRouter()
270
 
271
  def format_explainer_response(
272
- result,
273
  tone: float,
274
  verbosity: float,
275
  transparency: float,
276
  help_mode: str = "explain",
277
  hint_stage: int = 0,
278
  ) -> str:
279
- """
280
- Fallback wrapper so old conversation_logic still works.
281
- Routes everything through format_reply.
282
- """
283
-
284
  if not result:
285
- return "Start by identifying what the question is asking."
286
 
287
- core_lines = []
 
288
 
289
- # Try to extract useful text from result
290
- if hasattr(result, "summary") and result.summary:
291
- core_lines.append(result.summary)
292
 
293
- if hasattr(result, "teaching_points") and result.teaching_points:
294
- core_lines.extend(result.teaching_points)
 
 
 
295
 
296
- core = "\n".join(core_lines).strip()
 
297
 
298
  return format_reply(
299
- core=core,
300
  tone=tone,
301
  verbosity=verbosity,
302
  transparency=transparency,
 
1
  from __future__ import annotations
2
 
3
  import re
4
+ from typing import Any, List, Optional
5
 
 
6
 
7
+ def style_prefix(tone: float) -> str:
8
+ if tone < 0.2:
9
+ return ""
10
+ if tone < 0.45:
11
+ return "Ok —"
12
+ if tone < 0.75:
13
+ return "Let’s work through it."
14
+ return "You’ve got this — let’s work through it step by step."
15
 
 
 
 
16
 
17
+ def _clean_lines(core: str) -> List[str]:
18
+ lines: List[str] = []
19
+ for line in (core or "").splitlines():
20
+ cleaned = line.strip()
21
+ if cleaned:
22
+ lines.append(cleaned)
23
+ return lines
24
+
25
+
26
+ def _normalize_key(text: str) -> str:
27
+ text = (text or "").strip().lower()
28
+ text = text.replace("’", "'")
29
+ text = re.sub(r"\s+", " ", text)
30
+ return text
31
+
32
+
33
+ def _dedupe_lines(lines: List[str]) -> List[str]:
34
+ seen = set()
35
+ output: List[str] = []
36
+ for line in lines:
37
+ key = _normalize_key(line)
38
+ if key and key not in seen:
39
+ seen.add(key)
40
+ output.append(line.strip())
41
+ return output
42
+
43
+
44
+ def _strip_bullet_prefix(text: str) -> str:
45
+ return re.sub(r"^\s*[-•]\s*", "", (text or "").strip())
46
+
47
+
48
+ def _normalize_display_lines(lines: List[str]) -> List[str]:
49
+ cleaned: List[str] = []
50
+ for line in lines:
51
+ item = _strip_bullet_prefix(line)
52
+ if item:
53
+ cleaned.append(item)
54
+ return cleaned
55
+
56
+
57
+ def _limit_steps(steps: List[str], verbosity: float, minimum: int = 1) -> List[str]:
58
+ if not steps:
59
  return []
60
+ if verbosity < 0.2:
61
+ limit = minimum
62
+ elif verbosity < 0.45:
63
+ limit = max(minimum, 2)
64
+ elif verbosity < 0.7:
65
+ limit = max(minimum, 3)
66
+ else:
67
+ limit = max(minimum, 5)
68
+ return steps[:limit]
69
+
70
 
71
+ def _extract_topic_from_text(text: str, fallback: Optional[str] = None) -> str:
72
+ low = (text or "").lower()
73
+ if fallback:
74
+ return fallback
75
+ if any(word in low for word in ["equation", "variable", "isolate", "algebra"]):
76
+ return "algebra"
77
+ if any(word in low for word in ["percent", "percentage", "%"]):
78
+ return "percent"
79
+ if any(word in low for word in ["ratio", "proportion"]):
80
+ return "ratio"
81
+ if any(word in low for word in ["probability", "outcome", "chance", "odds"]):
82
+ return "probability"
83
+ if any(word in low for word in ["mean", "median", "average", "data", "variance", "standard deviation"]):
84
+ return "statistics"
85
+ if any(word in low for word in ["triangle", "circle", "angle", "area", "perimeter", "circumference", "rectangle"]):
86
+ return "geometry"
87
+ if any(word in low for word in ["integer", "factor", "multiple", "prime", "remainder", "divisible"]):
88
+ return "number_theory"
89
+ return "general"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+
92
+ def _why_line(topic: Optional[str]) -> str:
93
+ topic = (topic or "general").strip().lower()
94
+
95
+ if topic == "algebra":
96
+ return "Why this helps: algebra becomes easier when you reverse operations in order and keep the variable isolated."
97
+ if topic == "percent":
98
+ return "Why this helps: percent problems work best when you identify the base amount and apply each change to the correct value."
99
+ if topic == "ratio":
100
+ return "Why this helps: ratios represent parts of a whole, so turning them into total parts keeps the set-up consistent."
101
+ if topic == "probability":
102
+ return "Why this helps: probability is favorable outcomes over total outcomes, so clear counting matters first."
103
+ if topic == "statistics":
104
+ return "Why this helps: statistics questions depend on choosing the right measure before you calculate anything."
105
+ if topic == "geometry":
106
+ return "Why this helps: geometry usually becomes manageable once you identify the correct formula and substitute carefully."
107
+ if topic == "number_theory":
108
+ return "Why this helps: number theory questions usually depend on patterns, divisibility, or factor structure rather than brute force."
109
+ return "Why this helps: identifying the structure first makes the next step clearer and reduces avoidable mistakes."
110
+
111
+
112
+ def _tone_rewrite(line: str, tone: float, position: int = 0) -> str:
113
+ text = (line or "").strip()
114
+ if not text:
115
+ return text
116
+
117
+ if tone < 0.2:
118
+ return text
119
+ if tone < 0.45:
120
+ return text[:1].upper() + text[1:] if text else text
121
+ if tone < 0.75:
122
+ return f"Start here: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text
123
+ return f"A good place to start is this: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text
124
+
125
+
126
+ def _transparency_expansion(line: str, topic: str, transparency: float, position: int = 0) -> str:
127
+ text = (line or "").strip()
128
+ if not text:
129
+ return text
130
+
131
+ if transparency < 0.35:
132
+ return text
133
+ if transparency < 0.7:
134
+ if position == 0:
135
+ if topic == "algebra":
136
+ return f"{text} This helps isolate the variable."
137
+ if topic == "percent":
138
+ return f"{text} This keeps track of the percent relationship correctly."
139
+ if topic == "ratio":
140
+ return f"{text} This helps turn the ratio into usable parts."
141
+ if topic == "probability":
142
+ return f"{text} This sets up favorable versus total outcomes."
143
+ return text
144
+
145
+ if position == 0:
146
  if topic == "algebra":
147
+ return f"{text} In algebra, the goal is to isolate the variable by reversing operations in the opposite order."
148
+ if topic == "percent":
149
+ return f"{text} Percent questions are easiest when you identify the base amount and apply the change to the right quantity."
150
+ if topic == "ratio":
151
+ return f"{text} Ratio questions become easier once you treat the ratio numbers as parts of one total."
152
+ if topic == "probability":
153
+ return f"{text} Probability depends on counting the favorable cases and the total possible cases accurately."
154
+ if topic == "statistics":
155
+ return f"{text} Statistics questions usually depend on choosing the correct measure before calculating."
156
+ if topic == "geometry":
157
+ return f"{text} Geometry problems are usually solved by matching the shape to the correct formula first."
158
+ return f"{text} This works because it clarifies the structure before you calculate."
159
+ return text
160
+
161
+
162
+ def _styled_lines(lines: List[str], tone: float, transparency: float, topic: str) -> List[str]:
163
+ output: List[str] = []
164
+ for i, line in enumerate(lines):
165
+ rewritten = _tone_rewrite(line, tone, i)
166
+ rewritten = _transparency_expansion(rewritten, topic, transparency, i)
167
+ output.append(rewritten)
168
+ return output
169
+
170
+
171
+ def format_reply(
172
+ core: str,
173
+ tone: float,
174
+ verbosity: float,
175
+ transparency: float,
176
+ help_mode: str,
177
+ hint_stage: int = 0,
178
+ topic: Optional[str] = None,
179
+ ) -> str:
180
+ prefix = style_prefix(tone)
181
+ core = (core or "").strip()
182
+
183
+ if not core:
184
+ return prefix or "Start with the structure of the problem."
185
+
186
+ lines = _dedupe_lines(_clean_lines(core))
187
+ if not lines:
188
+ return prefix or "Start with the structure of the problem."
189
+
190
+ resolved_topic = _extract_topic_from_text(core, topic)
191
+ normalized_lines = _normalize_display_lines(lines)
192
+
193
+ output: List[str] = []
194
+ if prefix:
195
+ output.append(prefix)
196
+ output.append("")
197
+
198
+ if help_mode == "hint":
199
+ if verbosity < 0.25:
200
+ idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1))
201
+ shown = [normalized_lines[idx]]
202
+ elif verbosity < 0.6:
203
+ idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1))
204
+ shown = normalized_lines[idx:idx + 2] or [normalized_lines[idx]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  else:
206
+ shown = normalized_lines[: min(3, len(normalized_lines))]
207
+
208
+ shown = _styled_lines(shown, tone, transparency, resolved_topic)
209
+ output.append("Hint:")
210
+ for line in shown:
211
+ output.append(f"- {line}")
212
+
213
+ if transparency >= 0.75:
214
+ output.append("")
215
+ output.append(_why_line(resolved_topic))
216
+ return "\n".join(output).strip()
217
+
218
+ if help_mode in {"walkthrough", "instruction", "step_by_step"}:
219
+ shown = _limit_steps(normalized_lines, verbosity, minimum=2 if help_mode == "walkthrough" else 1)
220
+ shown = _styled_lines(shown, tone, transparency, resolved_topic)
221
+ output.append("Walkthrough:" if help_mode == "walkthrough" else "Step-by-step path:")
222
+ for line in shown:
223
+ output.append(f"- {line}")
224
+
225
+ if transparency >= 0.7:
226
+ output.append("")
227
+ output.append(_why_line(resolved_topic))
228
+ return "\n".join(output).strip()
229
+
230
+ if help_mode in {"method", "explain", "concept", "definition"}:
231
+ shown = _limit_steps(normalized_lines, verbosity, minimum=1)
232
+ shown = _styled_lines(shown, tone, transparency, resolved_topic)
233
+ output.append("Explanation:")
234
+ for line in shown:
235
+ output.append(f"- {line}")
236
 
237
+ if transparency >= 0.6:
238
+ output.append("")
239
+ output.append(_why_line(resolved_topic))
240
+ return "\n".join(output).strip()
241
 
242
+ if help_mode == "answer":
243
+ shown = _limit_steps(normalized_lines, verbosity, minimum=2)
244
+ shown = _styled_lines(shown, tone, transparency if verbosity >= 0.45 else min(transparency, 0.4), resolved_topic)
245
+ output.append("Answer path:")
246
+ for line in shown:
247
+ output.append(f"- {line}")
248
+
249
+ if transparency >= 0.75:
250
+ output.append("")
251
+ output.append(_why_line(resolved_topic))
252
+ return "\n".join(output).strip()
253
+
254
+ shown = _limit_steps(normalized_lines, verbosity, minimum=1)
255
+ shown = _styled_lines(shown, tone, transparency, resolved_topic)
256
+ for line in shown:
257
+ output.append(f"- {line}")
258
+
259
+ if transparency >= 0.8:
260
+ output.append("")
261
+ output.append(_why_line(resolved_topic))
262
+
263
+ return "\n".join(output).strip()
264
 
 
265
 
266
  def format_explainer_response(
267
+ result: Any,
268
  tone: float,
269
  verbosity: float,
270
  transparency: float,
271
  help_mode: str = "explain",
272
  hint_stage: int = 0,
273
  ) -> str:
 
 
 
 
 
274
  if not result:
275
+ return "I can help explain what the question is asking, but I need the full wording of the question."
276
 
277
+ summary = getattr(result, "summary", "") or ""
278
+ teaching_points = getattr(result, "teaching_points", []) or []
279
 
280
+ core_lines: List[str] = []
281
+ if isinstance(summary, str) and summary.strip():
282
+ core_lines.append(summary.strip())
283
 
284
+ if isinstance(teaching_points, list):
285
+ for item in teaching_points:
286
+ text = str(item).strip()
287
+ if text:
288
+ core_lines.append(text)
289
 
290
+ if not core_lines:
291
+ core_lines = ["Start by identifying what the question is asking."]
292
 
293
  return format_reply(
294
+ core="\n".join(core_lines),
295
  tone=tone,
296
  verbosity=verbosity,
297
  transparency=transparency,