[eric] browser: map-reduce difference/sum computed in code (aux flipped its own arithmetic twice live)

This commit is contained in:
ciregenz
2026-07-17 10:37:08 -07:00
parent 2a5fa50f13
commit 9e478e9c2e
@@ -16,6 +16,7 @@ import asyncio
import json
import logging
import os
import re
import time
from backend.apps.agents.browser import browser_fast_read as fr
@@ -53,6 +54,60 @@ def enabled() -> bool:
return os.environ.get("OSW_MAP_REDUCE_READ", "1") != "0"
# The aux reduce got the VALUES right but flipped the arithmetic twice in ~10 live runs ("taller
# by 360.2m" beside its own 113.2 math; "1096-1636=-540, not older" beside "540 years older"), so
# for the two shapes that are pure arithmetic the number is computed HERE and the model never
# does subtraction. Anything unparseable falls open to the aux reduce.
P_DIFF_RE = re.compile(r"\b(difference|older|younger|taller|shorter|higher|lower|farther|further|longer|heavier|lighter|bigger|smaller|faster|slower)\b", re.I)
P_SUM_RE = re.compile(r"\b(combined|total|sum|together|altogether)\b", re.I)
P_VALUE_RE = re.compile(r"VALUE:\s*([0-9]+(?:\.[0-9]+)?)\s*([a-zA-Z%]*)", re.I)
P_VALUE_LINE = (
"\nEnd with one extra line: VALUE: <the single number the request needs from this page, "
"digits only with no thousands separators, followed by its unit if any (m, ft, km, %, ...)>."
)
def op_for(prompt: str) -> str:
"""'difference' | 'sum' | '' from the request's own wording; '' = aux reduce as before."""
low = prompt or ""
if P_SUM_RE.search(low):
return "sum"
if P_DIFF_RE.search(low):
return "difference"
return ""
def fmt_num(n: float) -> str:
"""Human numbers: 35,842,039 not 3.5842e+07; two decimals max on non-integers."""
return f"{n:,.0f}" if float(n).is_integer() else f"{n:,.2f}"
def computed_answer(op: str, plan: list[tuple[str, str]], subs: list) -> str:
"""The deterministic answer when every sub-answer carries a parseable VALUE in agreeing
units; '' means fall open to the aux reduce. States both values and the computed number,
and deliberately asserts NO direction prose (that is exactly what the aux got wrong)."""
vals: list[tuple[float, str]] = []
for s in subs:
m = P_VALUE_RE.search(s or "")
if not m:
return ""
vals.append((float(m.group(1)), m.group(2).lower()))
units = {u for _, u in vals}
if len(units) > 1:
return ""
unit = f" {vals[0][1]}" if vals[0][1] else ""
shown = "\n".join(f"- {q}: {fmt_num(v)}{unit}" for (q, _), (v, _) in zip(plan, vals))
if op == "difference" and len(vals) == 2:
n = abs(vals[0][0] - vals[1][0])
return (f"**Answer: {fmt_num(n)}{unit}**\n\n{shown}\n"
f"(computed: |{fmt_num(vals[0][0])} - {fmt_num(vals[1][0])}| = {fmt_num(n)})")
if op == "sum":
n = sum(v for v, _ in vals)
return (f"**Answer: {fmt_num(n)}{unit}**\n\n{shown}\n"
f"(computed: {' + '.join(fmt_num(v) for v, _ in vals)} = {fmt_num(n)})")
return ""
def parse_plan(text: str) -> list[tuple[str, str]]:
"""(question, url) pairs from the decompose JSON; [] on anything unparseable
or single-source. Bounded to P_MAX_SOURCES so a runaway plan can't fan out."""
@@ -73,10 +128,11 @@ def parse_plan(text: str) -> list[tuple[str, str]]:
return out[:P_MAX_SOURCES]
async def p_fetch_and_extract(client, aux_model: str, q: str, url: str) -> str | None:
async def p_fetch_and_extract(client, aux_model: str, q: str, url: str, ask_value: bool) -> str | None:
"""One source: fetch the page, aux-extract the answer to q, or None if the
page is thin or insufficient (so the whole map-reduce fails open, never
fabricates a missing piece)."""
fabricates a missing piece). ask_value appends the machine-parseable VALUE
line the code-side arithmetic needs."""
try:
raw = await fr.fetch_raw(url)
text = fr.strip_tags(raw)
@@ -85,7 +141,7 @@ async def p_fetch_and_extract(client, aux_model: str, q: str, url: str) -> str |
if fr.page_is_thin(text):
return None
ans = await fr.ask_aux(
client, aux_model, fr.P_ANSWER_SYSTEM,
client, aux_model, fr.P_ANSWER_SYSTEM + (P_VALUE_LINE if ask_value else ""),
f"Request: {q}\n\nPage text from {url}:\n{text[:fr.P_MAX_PAGE_CHARS]}")
if not ans or ans.upper().startswith("INSUFFICIENT"):
return None
@@ -114,12 +170,20 @@ async def try_map_reduce_read(prompt: str, brief: str, settings, primary_api: st
return None
logger.info(f"[browser-mapreduce] {len(plan)} sources: {[u for _, u in plan]}")
subs = await asyncio.gather(*[p_fetch_and_extract(client, aux_model, q, u) for q, u in plan])
p_op = op_for(prompt)
subs = await asyncio.gather(*[p_fetch_and_extract(client, aux_model, q, u, bool(p_op)) for q, u in plan])
if any(s is None for s in subs):
logger.info(f"[browser-mapreduce] a source came back thin/insufficient in "
f"{int((time.monotonic() - t0) * 1000)}ms; browser fallback")
return None
if p_op:
p_coded = computed_answer(p_op, plan, subs)
if p_coded:
logger.info(f"[browser-mapreduce] {p_op} computed in code from {len(plan)} sources "
f"in {int((time.monotonic() - t0) * 1000)}ms")
return f"{p_coded}\n\n(Sources: {', '.join(u for _, u in plan)})"
joined = "\n\n".join(f"Sub-question: {q}\nAnswer (from {u}): {s}"
for (q, u), s in zip(plan, subs))
final = await fr.ask_aux(client, aux_model, P_REDUCE_SYSTEM,