TradingGameAI / conversation_logic.py
j-js's picture
Update conversation_logic.py
df111d6 verified
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