File size: 15,634 Bytes
2da67ff
 
2e002b9
 
2b74f91
 
2e002b9
2da67ff
df111d6
5eccd5d
2da67ff
 
2b74f91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e002b9
 
 
 
 
 
 
 
 
 
 
2b74f91
 
 
 
 
 
2da67ff
 
 
 
 
 
 
2b74f91
 
2da67ff
 
 
 
 
 
 
 
 
2b74f91
2da67ff
 
 
 
 
 
 
 
 
2e002b9
 
 
 
 
 
 
 
 
 
 
2b74f91
 
2e002b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2da67ff
2e002b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2da67ff
 
 
2e002b9
 
 
2da67ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b74f91
2da67ff
 
 
 
 
 
 
 
 
2e002b9
2b74f91
 
2e002b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2da67ff
2b74f91
 
 
 
 
 
 
2da67ff
2b74f91
 
 
 
2da67ff
 
 
 
 
2b74f91
2da67ff
 
2b74f91
2e002b9
2b74f91
 
2da67ff
 
 
 
2e002b9
2da67ff
2b74f91
 
 
 
 
 
 
 
 
 
2da67ff
 
 
 
 
 
 
 
 
2b74f91
2da67ff
 
 
 
 
 
 
 
2b74f91
2da67ff
2b74f91
2da67ff
2b74f91
2da67ff
 
 
 
 
 
 
 
 
 
2b74f91
 
 
 
 
 
3f7b809
2da67ff
3f7b809
 
 
 
 
 
 
5eccd5d
4ec2d5e
5eccd5d
4ec2d5e
5eccd5d
3f7b809
5eccd5d
 
2b74f91
 
 
2da67ff
2b74f91
2da67ff
 
 
 
 
 
 
 
 
 
 
 
 
 
2e002b9
 
 
 
 
f535d35
2c2d126
 
 
 
 
 
 
 
 
 
 
2b74f91
2da67ff
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
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