Skip to content

Commit a6a0cd0

Browse files
authored
Eight steps that are worth drawing, and arithmetic that is settled rather than hoped (#11)
* Check the model's sums with mpeqs: correct, and nearly inert mpeqs evaluates a sum exactly and refuses rather than guessing, so a step claiming 14 * 24 * 60 = 10080 can be contradicted without asking the model to check its own work. Wired into the loop, tested, and it does work when it fires: the wrong figure is handed back and the step is redone around it. IT HARDLY EVER FIRES. One correction across 50 runs. A scan of 153 saved reasoning steps found 11 checkable sums and none of them wrong. The reason points at what would work. This model writes arithmetic as prose -- "a fortnight is 14 days, and each day has 1440 minutes, so 20160" -- not as 14 * 1440 = 20160. An extractor keyed on an explicit expression sees almost nothing, and the errors that did occur were stated in the ANSWER as a bare number with no working to check. Getting real value from an exact solver means asking the model for a SPEC it can evaluate, not scraping prose for something shaped like a sum. That is a different experiment. AND A MORE IMPORTANT FINDING, BY ACCIDENT. Two arms here are the same configuration measured twice: decompose8 predates the gate, and arith_off runs those settings with the gate explicitly off. They scored 84 % and 74 %. That is the noise floor -- about ten points at two runs per question, on 25 gradeable questions against a 3B model. It was not designed as a replication and is more useful for having been one. Every comparison in docs/development.md is now written against it. The large results survive: 55 % to 91 % is five times the noise. But the four-point gap between the gate on and off is not a result, and neither was "decomposition beats a floor by four points" -- that claim is now stated with the uncertainty it actually has. The gate is conservative by construction, because a wrong correction is worse than none: the left side must be arithmetic and nothing else, so "x = 5" is left alone, and the tolerance comes from how many decimals the model chose to write, so 28.27 for 28.2743 is correct rounding rather than an error. Seven tests pin those, including the two real failures this was built for. * Ask the model for the expression, and make sure the answer gets the result The prose-scraping gate was nearly inert: 11 checkable sums in 153 steps, none wrong, because a model writes "14 days at 1440 minutes each, so 20160" rather than an expression an evaluator can settle. So ask for it. The step schema gains a `calc` field, and mpeqs evaluates it in exact fractions. Three things had to be true for that to work, and each was measured rather than assumed: * The field must be REQUIRED. Optional, the model filled it in zero times across three questions -- structured output omits what it is not obliged to produce. Required, with "" as the legal way to say "no sum here", it is filled in every time: 13 expressions on a single question. * Exactness is not enough on its own. An expression is settled exactly and can still be the wrong expression. A provenance check -- reject literals that appear neither in the question nor in an earlier result -- was built and then dropped: it flagged 1440*14 for a fortnight, since "14 days" is world knowledge and not an invented number. One true positive against three false ones is not a gate worth having. * The result has to reach the answer. This was the real failure. The loop computed 20 - 13.5 = 6.5 correctly and still answered 2.50, because the step holding the value was off the strongest path and the synthesis never saw it. Settled sums are now handed to the synthesis directly, separately from the thread, since they are not the model's opinion and are not up for revision. Measured on the questions that were failing: change from a 20 note 10.50 -> 6.50, minutes in a fortnight 336 -> 1680 -> 20160. Bat-and-ball still misses, which is a reasoning trap rather than an arithmetic one. * Show the sums that were settled exactly A run's steps are the model's opinion. The sums are not: they were evaluated in exact fractions and a reader can check "20-13.5 = 6.5" at a glance in a way they cannot check a paragraph of reasoning. That makes them the most useful thing on the page per pixel spent, so they get a compact strip under the progress bar. Shown once per distinct expression -- models re-derive the same sum across angles, and four copies say nothing the first did not -- and cleared between runs, since a stale figure left over from the previous question would be read as belonging to this one. * Only show the synthesis sums from the path the answer is written from Handing over every settled sum was worse than handing over none. A model exploring eight angles also writes 17*250 and (17/1)*100, and listing those under a heading that says they were calculated exactly is an invitation to pick one: multi-step accuracy fell to 50% from 100%, and the wrong answers name the mechanism -- "604800 minutes in a fortnight" is exactly right, for seconds in a week, and "840" is 14*60. So each sum now keeps the index and title of the step that produced it, and only those on the strongest path reach the synthesis. That recovered multi-step from 50% to 72%. What this does NOT establish is that the gate improves accuracy. Measured against a matched control on the same questions, twice: arithmetic on 13/18 (72%) then 12/18 (67%) arithmetic off 7/18 (39%) then 12/18 (67%) Pooled that is 69.4% against 52.8%, +16.7 points, 95% CI [-5.5, +38.9]. The interval contains zero. More importantly the control swung 28 points between two runs of an identical configuration, so the noise floor here is far wider than the 10 points previously assumed, and no comparison at this sample size decides anything. What is directly measured, and needs no inference: the model supplies the expression when the field is required, and mpeqs settles it exactly. That is a visibility feature. It is not yet an accuracy feature. Also adds --group to the eval, so a change that can only affect one group can be checked without paying for the 22 questions it cannot affect. * Depend on mpeqs, and work through the angles that were planned Two failures found by CI and by measurement, both of the same kind: something was asked for and then quietly not used. CI was green on tests that could not run. mpeqs was not a dependency at all, so the gate degraded to a no-op and the tests asserting it fires failed the moment they met a machine without it. It is pure Python with no dependencies of its own, so it is now a real dependency rather than an optional extra -- a feature that silently does not exist for most users is worse than a small install. And decompose asked the model for eight angles worth checking, then let the run stop after one step. Measured: "how many minutes in a fortnight" planned eight angles and produced a single step. The loop no longer accepts final_answer while angles remain unworked. A plain step floor was tried before and produced padding -- the model reached the answer at step 4, was told it had given 4 of 5, and wrote three that added nothing. This is not that. Each remaining step has a named job rather than a quota, and the steps it produces are measurably distinct: title-to-title similarity median 0.47, max 0.81, against the 0.96 that counts as restatement. Fortnight went from 1 step to 7, still answering 20160; "capital of France" from 1 to 8, still answering Paris. * Pin the step count, its cost, and the noise floor that governs both Eight steps was the requirement, at no cost to accuracy. Measured over 84 runs: steps 6.67 -> 8.56, and no group below 8.22 correct 73.3% -> 74.7%, +1.3 points, 95% CI [-12.7, +15.4] tokens 5324 -> 7129 prompt tokens, 1.34x So the steps arrived and the accuracy did not move. "Equal" is what was asked for and "equal" is what this is -- the interval is far too wide to claim better. The last claim pinned here is the one that matters most, and it is about the measurements rather than the code: two runs of an IDENTICAL configuration -- same questions, same model, same code, differing in nothing at all -- scored 39% and 67%. A 28-point noise floor at n=18. Every comparison in this repository should be read against it, including the ones already recorded here, and any claim resting on a smaller gap is folklore rather than measurement. Pinning the minimum across groups rather than only the mean is deliberate: a mean of 8.5 is also what you get from simple questions stopping at 3 and hard ones running to 14.
1 parent 23f6e39 commit a6a0cd0

21 files changed

Lines changed: 6856 additions & 15 deletions

dev/scripts/check_numbers.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,53 @@
288288
0.0,
289289
"no run in the eval errored",
290290
),
291+
# --- Working through the planned angles, rather than abandoning them ---
292+
(
293+
EVAL,
294+
lambda d: float(d["angle_floor"]["overall"]["steps"]["mean"]),
295+
8.56,
296+
1.0,
297+
"a run works through its angles: about 8.5 steps, up from 6.7",
298+
),
299+
(
300+
EVAL,
301+
# Every group, not just the mean. A mean of 8.5 is also what you get from
302+
# simple questions stopping at 3 and hard ones running to 14.
303+
lambda d: min(g["steps"]["mean"] for g in d["angle_floor"]["per_group"].values()),
304+
8.22,
305+
0.9,
306+
"no group falls below eight steps",
307+
),
308+
(
309+
EVAL,
310+
# The requirement was eight steps at no cost to accuracy. This pins the
311+
# "no cost" half: the arms are within a point and a half of each other.
312+
lambda d: abs(d["angle_floor"]["overall"]["correct_rate"]
313+
- d["calc_on"]["overall"]["correct_rate"]) * 100,
314+
1.3,
315+
6.0,
316+
"the extra steps cost no accuracy",
317+
),
318+
(
319+
EVAL,
320+
lambda d: (d["angle_floor"]["overall"]["prompt_tokens"]["mean"]
321+
/ d["calc_on"]["overall"]["prompt_tokens"]["mean"]),
322+
1.34,
323+
0.25,
324+
"and cost about a third more prompt tokens",
325+
),
326+
(
327+
EVAL,
328+
# THE NOISE FLOOR, pinned because it governs how every other comparison
329+
# here should be read. Two runs of an identical configuration -- same
330+
# questions, same model, same code, differing in nothing -- scored 39% and
331+
# 67%. Any claim resting on a gap smaller than that is folklore.
332+
lambda d: abs(d["calc_control"]["overall"]["correct_rate"]
333+
- d["calc_control_b"]["overall"]["correct_rate"]) * 100,
334+
27.8,
335+
6.0,
336+
"identical configurations differ by ~28 points at n=18: read every gap against this",
337+
),
291338
]
292339

293340

dev/scripts/eval.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,19 +153,22 @@ def grade(answer: str, spec: dict) -> bool | None:
153153

154154

155155
def run_once(spec: dict, chat, embedder, *, synthesise=True, budget=120.0, detect=True,
156-
system_prompt="", min_steps=0, decompose=0) -> dict:
156+
system_prompt="", min_steps=0, decompose=0, check_arith=True) -> dict:
157157
chat.reset_usage()
158158
started = time.time()
159159
steps, answer, error = [], "", None
160160

161-
repeats = 0
161+
repeats, sums_fixed = 0, 0
162162
for event in reason(spec["q"], chat=chat, embedder=embedder, synthesise=synthesise,
163163
time_budget=budget, detect_repeats=detect, system_prompt=system_prompt,
164-
min_steps=min_steps, decompose=decompose):
164+
min_steps=min_steps, decompose=decompose,
165+
check_arithmetic=check_arith):
165166
if event["type"] == "step":
166167
steps.append(event["content"])
167168
elif event["type"] == "repeat":
168169
repeats += 1
170+
elif event["type"] == "arithmetic":
171+
sums_fixed += 1
169172
elif event["type"] == "final":
170173
answer = event["content"]
171174
elif event["type"] == "error":
@@ -184,6 +187,7 @@ def run_once(spec: dict, chat, embedder, *, synthesise=True, budget=120.0, detec
184187
"calls": usage["calls"],
185188
"seconds": round(time.time() - started, 2),
186189
"repeats_caught": repeats,
190+
"sums_fixed": sums_fixed,
187191
"error": error,
188192
}
189193

@@ -211,6 +215,7 @@ def stat(key):
211215
"calls": stat("calls"),
212216
"seconds": stat("seconds"),
213217
"repeats_caught": sum(r.get("repeats_caught", 0) for r in results),
218+
"sums_fixed": sum(r.get("sums_fixed", 0) for r in results),
214219
"errors": sum(1 for r in results if r["error"]),
215220
}
216221

@@ -220,11 +225,16 @@ def main() -> int:
220225
parser.add_argument("--repeats", type=int, default=3,
221226
help="runs per question; one has no error bar (default 3)")
222227
parser.add_argument("--questions", type=int, default=0, help="cap the question count (0 = all)")
228+
# Measuring one group is how a targeted change gets checked without paying for
229+
# the 28 questions it cannot affect.
230+
parser.add_argument("--group", default="", help="only this group (simple, multi_step, hard, ...)")
223231
parser.add_argument("--label", default="baseline", help="what this arm is called in the output")
224232
parser.add_argument("--out", default="docs/claims/eval.json")
225233
parser.add_argument("--no-synthesis", action="store_true",
226234
help="take the last step as the answer, as the baseline did")
227235
parser.add_argument("--budget", type=float, default=120.0, help="seconds per run")
236+
parser.add_argument("--no-arithmetic-check", action="store_true",
237+
help="do not verify the model's sums with mpeqs")
228238
parser.add_argument("--decompose", type=int, default=0,
229239
help="plan N angles first, then take one step per angle")
230240
parser.add_argument("--min-steps", type=int, default=0,
@@ -241,7 +251,11 @@ def main() -> int:
241251

242252
chat = backends.OllamaChat(backends.pick_chat_model())
243253
embedder = backends.OllamaEmbedding("")
244-
specs = QUESTIONS[: args.questions] if args.questions else QUESTIONS
254+
specs = [q for q in QUESTIONS if q["group"] == args.group] if args.group else QUESTIONS
255+
if args.group and not specs:
256+
parser.error(f"no group {args.group!r}; have "
257+
+ ", ".join(sorted({q['group'] for q in QUESTIONS})))
258+
specs = specs[: args.questions] if args.questions else specs
245259

246260
print(f"arm={args.label} chat={chat.model} embeddings={embedder.model}")
247261
print(f"{len(specs)} questions x {args.repeats} runs = {len(specs) * args.repeats} runs\n")
@@ -253,7 +267,8 @@ def main() -> int:
253267
result = run_once(spec, chat, embedder, synthesise=not args.no_synthesis,
254268
budget=args.budget, detect=not args.no_repeat_detection,
255269
system_prompt=SHORT_SYSTEM_PROMPT if args.short_prompt else "",
256-
min_steps=args.min_steps, decompose=args.decompose)
270+
min_steps=args.min_steps, decompose=args.decompose,
271+
check_arith=not args.no_arithmetic_check)
257272
results.append(result)
258273
marks.append("-" if result["correct"] is None else ("." if result["correct"] else "x"))
259274
print(f" [{n:>2}/{len(specs)}] {''.join(marks)} {spec['group']:<11} {spec['q'][:52]}", flush=True)

dev/tests/conftest.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
from mpe_lkg.backends import DeterministicEmbedding, ScriptedChat # noqa: E402
1616

1717

18-
def step(title: str, content: str, next_action: str = "continue") -> str:
19-
return json.dumps({"title": title, "content": content, "next_action": next_action})
18+
def step(title: str, content: str, next_action: str = "continue", calc: str = "") -> str:
19+
# calc is omitted unless asked for, so the scripts that predate it stay honest
20+
# about what a model without the field sends.
21+
body = {"title": title, "content": content, "next_action": next_action}
22+
if calc:
23+
body["calc"] = calc
24+
return json.dumps(body)
2025

2126

2227
def normal_script(n_steps: int = 6) -> list[str]:

dev/tests/test_arithmetic.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""The arithmetic gate, and the part of it that turned out to matter.
2+
3+
The gate has two halves, and measurement separated them sharply:
4+
5+
* Scraping sums out of prose found 11 checkable claims in 153 steps and zero
6+
errors, because a model writes "14 days at 1440 minutes each, so 20160" rather
7+
than an expression. Nearly inert.
8+
* Asking for the expression outright works -- but only once the schema field is
9+
*required*. Optional, the model never filled it in across three questions;
10+
structured output simply omits what it is not obliged to produce.
11+
12+
And then the failure that outlived both: the loop computed 20 - 13.5 = 6.5
13+
exactly and still answered 2.50, because the step holding the value was not on
14+
the strongest path and the synthesis never saw it. These tests pin that the
15+
settled sums reach the synthesis, which is the only place they can do any good.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from fractions import Fraction
21+
22+
import pytest
23+
24+
from mpe_lkg.arithmetic import as_text, evaluate
25+
from mpe_lkg.backends import STEP_SCHEMA
26+
from mpe_lkg.reasoning import _synthesise
27+
28+
29+
class Recorder:
30+
"""A chat backend that answers nothing and remembers what it was asked."""
31+
32+
def __init__(self) -> None:
33+
self.prompts: list[str] = []
34+
35+
def stream(self, messages, max_tokens, schema=None):
36+
self.prompts.append(messages[-1]["content"])
37+
yield "an answer"
38+
39+
40+
def test_calc_is_required_so_the_model_cannot_skip_it():
41+
# Not a style preference. As an optional property this field was supplied
42+
# zero times out of three real runs; required, it was supplied every time.
43+
assert "calc" in STEP_SCHEMA["required"]
44+
assert STEP_SCHEMA["properties"]["calc"]["type"] == "string"
45+
46+
47+
@pytest.mark.parametrize(
48+
("expression", "expected"),
49+
[
50+
("(17/100)*250", "42.5"),
51+
("14*24*60", "20160"),
52+
("20-13.5", "6.5"),
53+
("3*4.5", "13.5"),
54+
# Written the way models write it, rather than the way Python parses it.
55+
("60 × 24", "1440"),
56+
],
57+
)
58+
def test_expressions_evaluate_exactly(expression, expected):
59+
assert as_text(evaluate(expression)) == expected
60+
61+
62+
@pytest.mark.parametrize("text", ["", " ", "the answer is 42", "20160", None])
63+
def test_non_expressions_yield_nothing_rather_than_guessing(text):
64+
# Silence is the safe failure here: a wrong correction is worse than none.
65+
assert evaluate(text) is None
66+
67+
68+
def test_settled_sums_reach_the_synthesis():
69+
"""The measured bug: a correct value computed and then dropped.
70+
71+
The value lived in a step that was not on the strongest path, so the thread
72+
handed to the synthesis did not contain it and the answer was wrong. The sums
73+
are passed separately for exactly that reason.
74+
"""
75+
chat = Recorder()
76+
_synthesise(chat, "What is my change?", ["I worked out the total."], ["20-13.5 = 6.5"])
77+
78+
asked = chat.prompts[0]
79+
assert "20-13.5 = 6.5" in asked
80+
assert "Do not recompute" in asked
81+
82+
83+
def test_repeated_sums_are_stated_once():
84+
# Models re-derive the same expression across angles; seeing it four times
85+
# tells the synthesis nothing extra and crowds out the thread.
86+
chat = Recorder()
87+
_synthesise(chat, "q", ["step"], ["3*4.5 = 13.5", "3*4.5 = 13.5", "20-13.5 = 6.5"])
88+
assert chat.prompts[0].count("3*4.5 = 13.5") == 1
89+
90+
91+
def test_synthesis_without_sums_is_unchanged():
92+
"""No sums must mean no extra instruction -- most questions have no arithmetic."""
93+
chat = Recorder()
94+
_synthesise(chat, "Why is the sky blue?", ["Rayleigh scattering."], [])
95+
assert "calculated exactly" not in chat.prompts[0]
96+
97+
98+
def test_fractions_survive_without_becoming_floats():
99+
# The reason for mpeqs rather than eval(): a third stays a third.
100+
assert evaluate("1/3") == Fraction(1, 3)
101+
assert evaluate("(1/3)*3") == Fraction(1)

dev/tests/test_render.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,36 @@ def test_steps_carry_the_id_the_graph_looks_them_up_by(self, page, tmp_path):
289289
run_query(page, server)
290290
ids = page.evaluate("() => [...document.querySelectorAll('#steps .step')].map(e => e.id)")
291291
assert ids == [f"step-{i}" for i in range(1, len(ids) + 1)]
292+
293+
def test_an_exactly_evaluated_sum_is_shown(self, page, tmp_path):
294+
"""The one part of a run the model did not decide, so it is worth showing.
295+
296+
A reader can check "20-13.5 = 6.5" at a glance in a way they cannot check a
297+
paragraph of reasoning.
298+
"""
299+
script = [
300+
step("Total", "Three items at 4.50 each.", calc="3*4.5"),
301+
step("Change", "Subtract from the note.", "final_answer", calc="20-13.5"),
302+
"Your change is 6.50.",
303+
]
304+
with LiveServer(script, tmp_path) as server:
305+
run_query(page, server, "What is my change?")
306+
chips = page.locator("#sums span")
307+
assert chips.count() == 2
308+
shown = [chips.nth(i).inner_text() for i in range(2)]
309+
assert "3*4.5 = 13.5" in shown
310+
assert "20-13.5 = 6.5" in shown
311+
312+
def test_sums_from_a_previous_run_are_cleared(self, page, tmp_path):
313+
# A stale "= 6.5" left over from the last question is worse than none.
314+
script = [
315+
step("A", "x", "final_answer", calc="2+2"), "Four.",
316+
step("B", "y", "final_answer", calc="3+3"), "Six.",
317+
]
318+
with LiveServer(script, tmp_path) as server:
319+
run_query(page, server, "What is two plus two?")
320+
assert [c.inner_text() for c in page.locator("#sums span").all()] == ["2+2 = 4"]
321+
run_query(page, server, "What is three plus three?")
322+
# Not ["2+2 = 4", "3+3 = 6"]: the first run's sum belongs to the first
323+
# question, and leaving it up would attribute it to this one.
324+
assert [c.inner_text() for c in page.locator("#sums span").all()] == ["3+3 = 6"]

dev/tests/test_stream.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,70 @@ def test_detection_can_be_turned_off(self, flask_client):
354354
embedder=DeterministicEmbedding(32),
355355
detect_repeats=False, synthesise=False))
356356
assert not [e for e in events if e["type"] == "repeat"]
357+
358+
359+
class TestArithmeticGate:
360+
"""Sums the record can evaluate exactly are not a matter of opinion.
361+
362+
Measured motivation: pushing runs to eight steps lifted hard questions from
363+
50 % to 62 % and dropped multi-step arithmetic from 94 % to 67 %. The failures
364+
were "10080 minutes in a fortnight" -- that is a week -- and "6.00 change" from
365+
a 20 note on 13.50 of goods. Neither is a reasoning failure.
366+
"""
367+
368+
def test_a_wrong_sum_is_caught_and_redone(self, flask_client):
369+
wrong = step("Compute", "A fortnight is 14 * 24 * 60 = 10080 minutes.")
370+
right = step("Compute", "A fortnight is 14 * 24 * 60 = 20160 minutes.", "final_answer")
371+
client, _ = flask_client([wrong, right, "20160 minutes."])
372+
373+
events = read_events(client.get("/query?query=q"))
374+
caught = [e for e in events if e["type"] == "arithmetic"]
375+
376+
assert caught, "the sum is wrong and the record can prove it"
377+
assert "20160" in caught[0]["errors"][0]
378+
assert not [e for e in events if e["type"] == "step" and "10080" in e["content"]]
379+
380+
def test_a_correct_sum_passes_untouched(self, flask_client):
381+
client, _ = flask_client([step("Compute", "14 * 24 * 60 = 20160 minutes.", "final_answer"),
382+
"20160 minutes."])
383+
events = read_events(client.get("/query?query=q"))
384+
assert not [e for e in events if e["type"] == "arithmetic"]
385+
386+
def test_rounding_is_not_treated_as_an_error(self, flask_client):
387+
"""28.27 for 28.2743 is correct rounding, and flagging it would be wrong."""
388+
client, _ = flask_client([step("Area", "The area is 3.14159 * 9 = 28.27.", "final_answer"),
389+
"About 28.27."])
390+
events = read_events(client.get("/query?query=q"))
391+
assert not [e for e in events if e["type"] == "arithmetic"]
392+
393+
def test_a_definition_is_not_a_sum(self, flask_client):
394+
"""x = 5 is a definition. Checking it would invent an error."""
395+
client, _ = flask_client([step("Set up", "Let x = 5 and y = 12.", "final_answer"),
396+
"x is 5."])
397+
events = read_events(client.get("/query?query=q"))
398+
assert not [e for e in events if e["type"] == "arithmetic"]
399+
400+
def test_a_model_that_will_not_correct_itself_still_terminates(self, flask_client):
401+
wrong = step("Compute", "14 * 24 * 60 = 10080 minutes.")
402+
client, _ = flask_client([wrong], repeat_last=True)
403+
404+
events = read_events(client.get("/query?query=q"))
405+
assert events[-1]["type"] == "done_stream"
406+
assert len([e for e in events if e["type"] == "arithmetic"]) <= 3
407+
408+
def test_how_many_sums_were_checked_is_reported(self, flask_client):
409+
client, _ = flask_client([step("Compute", "2 + 2 = 5.", "final_answer"),
410+
step("Compute", "2 + 2 = 4.", "final_answer"), "Four."])
411+
done = [e for e in read_events(client.get("/query?query=q")) if e["type"] == "done"][0]
412+
assert done["sums_checked"] >= 1
413+
assert done["sums_corrected"] >= 1
414+
415+
def test_the_gate_can_be_turned_off(self, flask_client):
416+
from mpe_lkg.backends import DeterministicEmbedding, ScriptedChat
417+
from mpe_lkg.reasoning import reason
418+
419+
wrong = step("Compute", "14 * 24 * 60 = 10080 minutes.", "final_answer")
420+
events = list(reason("q", chat=ScriptedChat([wrong], repeat_last=True),
421+
embedder=DeterministicEmbedding(32),
422+
check_arithmetic=False, synthesise=False))
423+
assert not [e for e in events if e["type"] == "arithmetic"]

0 commit comments

Comments
 (0)