from __future__ import annotations import re from typing import Any, List, Optional def style_prefix(tone: float) -> str: if tone < 0.2: return "" if tone < 0.45: return "Ok —" if tone < 0.75: return "Let’s work through it." return "You’ve got this — let’s work through it step by step." def _clean_lines(core: str) -> List[str]: lines: List[str] = [] for line in (core or "").splitlines(): cleaned = line.strip() if cleaned: lines.append(cleaned.lstrip("- ").strip()) return lines def _normalize_key(text: str) -> str: return re.sub(r"\s+", " ", (text or "").strip().lower()) def _dedupe_lines(lines: List[str]) -> List[str]: seen = set() out: List[str] = [] for line in lines: key = _normalize_key(line) if key and key not in seen: seen.add(key) out.append(line.strip()) return out def _extract_topic_from_text(core: str, topic: Optional[str]) -> str: if topic: return str(topic).strip().lower() text = (core or "").lower() if "probability" in text or "favorable" in text or "sample space" in text: return "probability" if "percent" in text or "%" in text: return "percent" if "ratio" in text or "multiplier" in text: return "ratio" if "variable" in text or "equation" in text: return "algebra" if "variability" in text or "standard deviation" in text or "spread" in text: return "statistics" if "rectangle" in text or "perimeter" in text or "area" in text: return "geometry" return "general" def _normalize_display_lines(lines: List[str]) -> List[str]: return [re.sub(r"\s+", " ", (line or "").strip()) for line in lines if str(line).strip()] def _limit_steps(lines: List[str], verbosity: float, minimum: int = 1) -> List[str]: if not lines: return [] if verbosity < 0.22: limit = minimum elif verbosity < 0.55: limit = max(minimum, min(2, len(lines))) elif verbosity < 0.82: limit = max(minimum, min(4, len(lines))) else: limit = len(lines) return lines[:limit] def _why_line(topic: str) -> str: if topic == "algebra": return "Why this helps: reversing operations in the right order keeps the equation equivalent while you isolate the variable." if topic == "percent": return "Why this helps: percent questions usually break when the base quantity is chosen incorrectly." if topic == "ratio": return "Why this helps: ratio numbers are usually parts, not the final quantities themselves." if topic == "probability": return "Why this helps: the numerator and denominator must be counted under the same rules." if topic == "statistics": return "Why this helps: statistics questions depend on choosing the right measure before calculating." if topic == "geometry": return "Why this helps: matching the right formula to the shape simplifies the rest of the work." return "Why this helps: getting the structure right first makes the next step clearer." def _tone_rewrite(line: str, tone: float, position: int = 0) -> str: text = (line or "").strip() if not text: return text if tone < 0.25: return text if tone < 0.55: return f"Start here: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text if tone < 0.8: 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 return f"You can start with this: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text def _transparency_expansion(line: str, topic: str, transparency: float, position: int = 0) -> str: text = (line or "").strip() if not text or transparency < 0.35: return text if transparency < 0.7: if position == 0: if topic == "algebra": return f"{text} This keeps the equation balanced while you isolate the variable." if topic == "percent": return f"{text} This keeps the percent relationship tied to the correct base quantity." if topic == "ratio": return f"{text} This turns the ratio into usable quantities instead of labels." if topic == "probability": return f"{text} This separates successful outcomes from total outcomes." return text if position == 0: if topic == "algebra": return f"{text} In algebra, each step should preserve an equivalent equation so the solution does not change while the variable is isolated." if topic == "percent": return f"{text} Percent problems become clearer once the base quantity is fixed, because every percentage must refer back to some amount." if topic == "ratio": return f"{text} Ratio numbers usually describe relative parts, so turning them into multiples of one common quantity is what makes the setup usable." if topic == "probability": return f"{text} Probability depends on a consistent sample space, so the numerator and denominator must be counted under the same rules." if topic == "statistics": return f"{text} Statistics questions often hinge on choosing the right measure first, because different measures capture different features of the data." if topic == "geometry": return f"{text} Geometry problems often become routine once the correct formula is chosen, because the rest is usually substitution and algebra." return f"{text} This makes the underlying structure explicit before you calculate." return text def _styled_lines(lines: List[str], tone: float, transparency: float, topic: str) -> List[str]: output: List[str] = [] for i, line in enumerate(lines): rewritten = _tone_rewrite(line, tone, i) rewritten = _transparency_expansion(rewritten, topic, transparency, i) output.append(rewritten) return output def format_reply( core: str, tone: float, verbosity: float, transparency: float, help_mode: str, hint_stage: int = 0, topic: Optional[str] = None, ) -> str: prefix = style_prefix(tone) core = (core or "").strip() if not core: return prefix or "Start with the structure of the problem." lines = _dedupe_lines(_clean_lines(core)) if not lines: return prefix or "Start with the structure of the problem." resolved_topic = _extract_topic_from_text(core, topic) normalized_lines = _normalize_display_lines(lines) output: List[str] = [] if prefix: output.extend([prefix, ""]) if help_mode == "hint": idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1)) if verbosity < 0.25: shown = [normalized_lines[idx]] elif verbosity < 0.62: shown = normalized_lines[idx: idx + 2] or [normalized_lines[idx]] else: shown = normalized_lines[: min(4, len(normalized_lines))] shown = _styled_lines(shown, tone, transparency, resolved_topic) output.append("Hint:") output.extend(f"- {line}" for line in shown) if transparency >= 0.75: output.extend(["", _why_line(resolved_topic)]) return "\n".join(output).strip() if help_mode in {"walkthrough", "instruction", "step_by_step"}: shown = _limit_steps(normalized_lines, verbosity, minimum=2 if help_mode == "walkthrough" else 1) shown = _styled_lines(shown, tone, transparency, resolved_topic) output.append("Walkthrough:" if help_mode == "walkthrough" else "Step-by-step path:") output.extend(f"- {line}" for line in shown) if transparency >= 0.7: output.extend(["", _why_line(resolved_topic)]) return "\n".join(output).strip() if help_mode in {"method", "explain", "concept", "definition"}: shown = _limit_steps(normalized_lines, verbosity, minimum=1) shown = _styled_lines(shown, tone, transparency, resolved_topic) output.append("Explanation:") output.extend(f"- {line}" for line in shown) if transparency >= 0.6: output.extend(["", _why_line(resolved_topic)]) return "\n".join(output).strip() if help_mode == "answer": shown = _limit_steps(normalized_lines, verbosity, minimum=2) answer_transparency = transparency if verbosity >= 0.45 else min(transparency, 0.45) shown = _styled_lines(shown, tone, answer_transparency, resolved_topic) output.append("Answer path:") output.extend(f"- {line}" for line in shown) if transparency >= 0.75: output.extend(["", _why_line(resolved_topic)]) return "\n".join(output).strip() shown = _limit_steps(normalized_lines, verbosity, minimum=1) shown = _styled_lines(shown, tone, transparency, resolved_topic) output.extend(f"- {line}" for line in shown) if transparency >= 0.8: output.extend(["", _why_line(resolved_topic)]) return "\n".join(output).strip() def format_explainer_response( result: Any, tone: float, verbosity: float, transparency: float, help_mode: str = "explain", hint_stage: int = 0, ) -> str: if not result: return "I can help explain what the question is asking, but I need the full wording of the question." summary = getattr(result, "summary", "") or "" teaching_points = getattr(result, "teaching_points", []) or [] core_lines: List[str] = [] if isinstance(summary, str) and summary.strip(): core_lines.append(summary.strip()) if isinstance(teaching_points, list): for item in teaching_points: text = str(item).strip() if text: core_lines.append(text) if not core_lines: core_lines = ["Start by identifying what the question is asking."] return format_reply( core="\n".join(core_lines), tone=tone, verbosity=verbosity, transparency=transparency, help_mode=help_mode, hint_stage=hint_stage, topic=getattr(result, "topic", None), )