from __future__ import annotations import re from fractions import Fraction from typing import Any, Dict, List, Optional from context_parser import mentions_choice_letter from formatting import format_reply from models import SolverResult from quant_solver import solve_quant, is_quant_question, extract_choices class ResponseContext: def __init__( self, visible_user_text: str, question_text: str, options_text: str, question_category: str, question_difficulty: str, chat_history: Optional[List[Dict[str, Any]]] = None, ): self.visible_user_text = (visible_user_text or "").strip() self.question_text = (question_text or "").strip() self.options_text = (options_text or "").strip() self.question_category = (question_category or "").strip() self.question_difficulty = (question_difficulty or "").strip() self.chat_history = chat_history or [] @property def combined_question_block(self) -> str: parts: List[str] = [] if self.question_text: parts.append(self.question_text) if self.options_text: parts.append(self.options_text) return "\n".join(parts).strip() def normalize_text(s: str) -> str: return (s or "").strip().lower() def get_last_assistant_message(chat_history: List[Dict[str, Any]]) -> str: for item in reversed(chat_history or []): if str(item.get("role", "")).strip().lower() == "assistant": return str(item.get("text", "")).strip() return "" def build_choice_explanation( chosen_letter: str, result: SolverResult, choices: dict[str, str], help_mode: str, ) -> str: choice_text = choices.get(chosen_letter, "").strip() value_text = result.answer_value.strip() if result.answer_value else "" if help_mode == "hint": if value_text and choice_text: return f"Work out the value first, then compare it with choice {chosen_letter} ({choice_text})." if choice_text: return f"Focus on why choice {chosen_letter} fits better than the others." return f"Focus on whether choice {chosen_letter} matches the result." if help_mode == "walkthrough": if value_text and choice_text: return ( f"The calculation gives {value_text}.\n" f"Choice {chosen_letter} is {choice_text}, so it matches.\n" f"That is why {chosen_letter} is the correct answer." ) if choice_text: return f"Choice {chosen_letter} matches the result, so that is why it is correct." return f"The solved result matches choice {chosen_letter}, so that is why it is correct." if value_text and choice_text: return f"Yes — it’s {chosen_letter}, because the calculation gives {value_text}, and choice {chosen_letter} is {choice_text}." if choice_text: return f"Yes — it’s {chosen_letter}, because that option matches the result." return f"Yes — it’s {chosen_letter}." def parse_percent_value(text: str) -> Optional[int]: m = re.search(r"(\d+(?:\.\d+)?)\s*%", text or "") if not m: return None try: value = float(m.group(1)) if value.is_integer(): return int(value) return None except Exception: return None def percent_to_fraction_text(percent_value: int) -> str: frac = Fraction(percent_value, 100) return f"{frac.numerator}/{frac.denominator}" def find_matching_choice_for_value(options_text: str, target_value: str) -> Optional[str]: for line in (options_text or "").splitlines(): m = re.match(r"\s*([A-E])\)\s*(.+?)\s*$", line.strip(), re.IGNORECASE) if not m: continue letter = m.group(1).upper() value = m.group(2).strip() if value == target_value: return letter return None def handle_percent_fraction_question(ctx: ResponseContext, help_mode: str) -> Optional[SolverResult]: q = ctx.question_text u = normalize_text(ctx.visible_user_text) combined = f"{q}\n{ctx.options_text}".lower() mentions_fraction_task = ( "this equals" in combined or "as a fraction" in combined or "fraction" in combined or "pie chart" in combined or "%" in combined ) if not mentions_fraction_task: return None percent_value = parse_percent_value(q) or parse_percent_value(ctx.visible_user_text) if percent_value is None: return None fraction_text = percent_to_fraction_text(percent_value) answer_letter = find_matching_choice_for_value(ctx.options_text, fraction_text) asking_task = ( "what is the question asking" in u or "what is this asking" in u or "what do i need to do" in u ) asking_hint = ( "hint" in u or "how do i start" in u or "first step" in u or "what do i do first" in u or "can you help" in u or u in {"help", "help me", "i dont get it", "i don't get it"} ) asking_answer = ( "answer" in u or "which one" in u or "what answer" in u or "what is 25% as a fraction" in u or "are you sure" in u ) if asking_task: reply = "It is asking you to convert 25% into an equivalent fraction and then match it to the correct option." return SolverResult( reply=reply, domain="quant", solved=False, help_mode="hint", answer_letter=answer_letter, answer_value=fraction_text, ) if asking_hint or help_mode == "hint": reply = ( "Think of percent as 'out of 100'.\n" f"So {percent_value}% means {percent_value}/100.\n" "Then simplify that fraction and compare it with the options." ) return SolverResult( reply=reply, domain="quant", solved=True, help_mode="hint", answer_letter=answer_letter, answer_value=fraction_text, ) if asking_answer or help_mode == "answer": if answer_letter: reply = f"{percent_value}% = {percent_value}/100 = {fraction_text}, so the correct answer is {answer_letter}." else: reply = f"{percent_value}% = {percent_value}/100 = {fraction_text}." return SolverResult( reply=reply, domain="quant", solved=True, help_mode="answer", answer_letter=answer_letter, answer_value=fraction_text, ) if help_mode == "walkthrough": if answer_letter: reply = ( f"Percent means 'per 100', so {percent_value}% = {percent_value}/100.\n" f"Simplify {percent_value}/100 to {fraction_text}.\n" f"Now compare that with the options: {fraction_text} matches choice {answer_letter}." ) else: reply = ( f"Percent means 'per 100', so {percent_value}% = {percent_value}/100.\n" f"Simplify {percent_value}/100 to {fraction_text}." ) return SolverResult( reply=reply, domain="quant", solved=True, help_mode="walkthrough", answer_letter=answer_letter, answer_value=fraction_text, ) return None def handle_choice_letter_followup(ctx: ResponseContext, help_mode: str) -> Optional[SolverResult]: question_block = ctx.combined_question_block if not question_block: return None user_text = ctx.visible_user_text asked_letter = mentions_choice_letter(user_text) if not asked_letter: return None solved = solve_quant(question_block, "answer") choices = extract_choices(question_block) if asked_letter and solved.answer_letter and asked_letter == solved.answer_letter: reply = build_choice_explanation(asked_letter, solved, choices, help_mode) return SolverResult( reply=reply, domain="quant", solved=solved.solved, help_mode=help_mode, answer_letter=solved.answer_letter, answer_value=solved.answer_value, ) if asked_letter and solved.answer_letter and asked_letter != solved.answer_letter: correct_choice_text = choices.get(solved.answer_letter, "").strip() if help_mode == "hint": reply = f"Check the calculation again and compare your result with choice {solved.answer_letter}, not {asked_letter}." elif help_mode == "walkthrough": reply = ( f"It is not {asked_letter}.\n" f"The calculation leads to choice {solved.answer_letter}" + (f", which is {correct_choice_text}." if correct_choice_text else ".") ) else: reply = f"It is not {asked_letter} — the correct choice is {solved.answer_letter}." return SolverResult( reply=reply, domain="quant", solved=solved.solved, help_mode=help_mode, answer_letter=solved.answer_letter, answer_value=solved.answer_value, ) return None def handle_conversational_followup(ctx: ResponseContext, help_mode: str) -> Optional[SolverResult]: user_text = ctx.visible_user_text lower = normalize_text(user_text) question_block = ctx.combined_question_block if not question_block: return None percent_fraction = handle_percent_fraction_question(ctx, help_mode) if percent_fraction is not None: return percent_fraction choice_followup = handle_choice_letter_followup(ctx, help_mode) if choice_followup is not None: return choice_followup if "what is the question asking" in lower or "what is this asking" in lower: if "variability" in ctx.question_text.lower() or "spread" in ctx.question_text.lower(): reply = "It is asking you to compare how spread out the answer choices are and identify which dataset varies the most." else: reply = "It is asking you to identify the mathematical task and then match the result to one of the options." return SolverResult(reply=reply, domain="quant", solved=False, help_mode="hint") if lower in {"help", "can you help", "help me", "i dont get it", "i don't get it"}: if help_mode == "hint": return solve_quant(question_block, "hint") if help_mode == "walkthrough": return solve_quant(question_block, "walkthrough") return solve_quant(question_block, "answer") if "first step" in lower or "how do i start" in lower or "what do i do first" in lower: return solve_quant(question_block, "hint") if "hint" in lower: return solve_quant(question_block, "hint") if "why" in lower or "explain" in lower: walkthrough_result = solve_quant(question_block, "walkthrough") return walkthrough_result if "answer" in lower or "which one" in lower or "what answer" in lower or "are you sure" in lower: return solve_quant(question_block, "answer") last_ai = get_last_assistant_message(ctx.chat_history) if last_ai and ("that" in lower or "it" in lower): return solve_quant(question_block, "walkthrough") return None def solve_verbal_or_general(user_text: str, help_mode: str) -> SolverResult: lower = normalize_text(user_text) if any( k in lower for k in [ "sentence correction", "grammar", "verbal", "critical reasoning", "reading comprehension", ] ): if help_mode == "hint": reply = ( "First identify the task:\n" "- Sentence Correction: grammar + meaning\n" "- Critical Reasoning: conclusion, evidence, assumption\n" "- Reading Comprehension: passage role, inference, detail" ) elif help_mode == "walkthrough": reply = ( "I can help verbally too, but this backend is strongest on quant-style items. " "For verbal, I’d use elimination based on grammar, logic, scope, or passage support." ) else: reply = "I can help with verbal strategy, but this version is strongest on quant-style questions right now." return SolverResult(reply=reply, domain="verbal", solved=False, help_mode=help_mode) if help_mode == "hint": reply = "I can help. Ask for a hint, an explanation, or the answer." elif help_mode == "walkthrough": reply = "I can talk it through step by step." else: reply = "Ask naturally and I’ll help from the current question context when available." return SolverResult(reply=reply, domain="fallback", solved=False, help_mode=help_mode) def generate_response( raw_user_text: str, tone: float, verbosity: float, transparency: float, help_mode: str, hidden_context: str = "", chat_history: Optional[List[Dict[str, Any]]] = None, question_text: str = "", options_text: str = "", question_category: str = "", question_difficulty: str = "", retrieval_context: str = "", ) -> SolverResult: ctx = ResponseContext( visible_user_text=raw_user_text, question_text=question_text, options_text=options_text, question_category=question_category, question_difficulty=question_difficulty, chat_history=chat_history, ) ctx.retrieval_context = retrieval_context solver_input = question_text.strip() if question_text.strip() else raw_user_text.strip() if is_quant_question(solver_input): return solve_quant(solver_input, help_mode) visible_user_text = ctx.visible_user_text question_block = ctx.combined_question_block if not visible_user_text: result = SolverResult( reply="Ask a question and I’ll help using the current in-game context.", domain="fallback", solved=False, help_mode=help_mode, ) result.reply = format_reply(result.reply, tone, verbosity, transparency, help_mode) return result followup = handle_conversational_followup(ctx, help_mode) if followup is not None: followup.reply = format_reply(followup.reply, tone, verbosity, transparency, followup.help_mode) return followup percent_fraction = handle_percent_fraction_question(ctx, help_mode) if percent_fraction is not None: percent_fraction.reply = format_reply(percent_fraction.reply, tone, verbosity, transparency, percent_fraction.help_mode) return percent_fraction if question_block: result = solve_quant(question_block, help_mode) if result.solved or result.answer_value or result.answer_letter: result.reply = format_reply(result.reply, tone, verbosity, transparency, help_mode) return result if ctx.question_text: result = solve_quant(ctx.question_text, help_mode) if result.solved or result.answer_value or result.answer_letter: result.reply = format_reply(result.reply, tone, verbosity, transparency, help_mode) return result result = solve_verbal_or_general(visible_user_text, help_mode) result.reply = format_reply(result.reply, tone, verbosity, transparency, help_mode) return result