mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] browser: map-reduce difference/sum computed in code (aux flipped its own arithmetic twice live)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user