@tiens.nguyen/gonext-local-worker 1.0.130 → 1.0.132

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1099,6 +1099,10 @@ def run_agent_chat(cfg):
1099
1099
  _log("router: agent (HTTP tool use needed)")
1100
1100
  _emit({"type": "step", "text": "Choosing a tool…"})
1101
1101
 
1102
+ # True when the user wants a PDF but its content must first be researched by the
1103
+ # agent loop (set below); read by the max-steps fallback to force-deliver a PDF.
1104
+ agent_pdf_requested = False
1105
+
1102
1106
  # ---- Deterministic PDF fast-path -------------------------------------------------
1103
1107
  # A small model cannot reliably re-emit a long, emoji/quote-heavy document as a
1104
1108
  # Python string literal for create_pdf(text="…"), so the CodeAgent call fails before
@@ -1137,6 +1141,9 @@ def run_agent_chat(cfg):
1137
1141
  and _PDF_RESEARCH_INTENT.search(latest_user_text)):
1138
1142
  _log("PDF fast-path SKIPPED: content needs research (no provided body) "
1139
1143
  "→ agent loop (web_search → create_pdf)")
1144
+ # Remember the user wants a PDF: if the agent loop exhausts its steps without
1145
+ # calling create_pdf, the max-steps fallback renders the gathered facts anyway.
1146
+ agent_pdf_requested = True
1140
1147
  else:
1141
1148
  doc_title = _derive_pdf_title(doc_text)
1142
1149
  _log(
@@ -1280,6 +1287,9 @@ def run_agent_chat(cfg):
1280
1287
  "clean one often does not exist. After 1-2 searches/fetches, COMPILE what you have and "
1281
1288
  "finish: if a PDF was asked for, call create_pdf(text=<your compiled text>, title=...); "
1282
1289
  "otherwise call final_answer. NEVER repeat a web_search/fetch_url you already ran.\n"
1290
+ "- RESEARCH BUDGET — you may make at most 3 web_search/fetch_url calls TOTAL per task "
1291
+ "(rewording a query still counts). Reserve your remaining steps for create_pdf / "
1292
+ "final_answer.\n"
1283
1293
  "- Do NOT put final_answer outside the code block.\n\n"
1284
1294
  )
1285
1295
  # Lead with the TASK so the weak model anchors on what's actually being asked —
@@ -1306,15 +1316,54 @@ def run_agent_chat(cfg):
1306
1316
  # / final_answer). Keyed to retrieval tools where the loop actually happens.
1307
1317
  _seen_calls: dict = {}
1308
1318
 
1319
+ # Search queries get near-duplicated with filler tweaks ("schedule" → "FULL schedule"
1320
+ # → "2026 FIFA full schedule"), dodging exact-match dedup — seen live. Normalize:
1321
+ # drop filler words, sort tokens, so all variants share one signature.
1322
+ _QUERY_FILLER = {
1323
+ "full", "complete", "comprehensive", "detailed", "entire", "all", "latest",
1324
+ "current", "the", "a", "an", "of", "with", "and", "for", "to", "in", "on",
1325
+ "more", "about", "info", "information",
1326
+ }
1327
+
1328
+ def _norm_query(q: str) -> str:
1329
+ tokens = [t for t in re.findall(r"[a-z0-9]+", (q or "").lower())
1330
+ if t not in _QUERY_FILLER]
1331
+ return " ".join(sorted(tokens))
1332
+
1333
+ # Hard research budget: at most N retrieval calls (web_search + fetch_url) per run.
1334
+ # Past it, retrieval tools refuse and steer to create_pdf/final_answer. Keeps ≥2 of
1335
+ # the 5 steps free to actually FINISH — the live failure mode was burning ALL steps
1336
+ # on slightly-different searches and never producing the asked-for PDF.
1337
+ _research = {"used": 0, "max": 3}
1338
+
1339
+ def _research_spend(tool_name: str) -> str:
1340
+ """Return '' if under budget (and spend 1), else a refusal steering to finish."""
1341
+ if _research["used"] >= _research["max"]:
1342
+ _log(f"{tool_name} BLOCKED: research budget ({_research['max']}) exhausted")
1343
+ return (
1344
+ f"Research budget exhausted ({_research['max']} web_search/fetch_url calls "
1345
+ "used). Do NOT search or fetch again. FINISH NOW with what you have: if the "
1346
+ "task asked for a PDF call create_pdf(text=<compile the gathered facts>, "
1347
+ "title=...), otherwise call final_answer(<your best answer from the "
1348
+ "observations above>)."
1349
+ )
1350
+ _research["used"] += 1
1351
+ return ""
1352
+
1353
+ # Everything useful the retrieval tools returned this run, so an exhausted loop can
1354
+ # still deterministically deliver (e.g. render a partial PDF) instead of dead-ending.
1355
+ _gathered: list = []
1356
+
1309
1357
  def _dedup(tool_name: str, arg: str, next_hint: str):
1310
1358
  """Return (is_repeat, message). On a repeat, message steers the model forward."""
1311
- sig = f"{tool_name}::{(arg or '').strip().lower()}"
1359
+ key = _norm_query(arg) if tool_name == "web_search" else (arg or "").strip().lower()
1360
+ sig = f"{tool_name}::{key}"
1312
1361
  prior = _seen_calls.get(sig)
1313
1362
  if prior is not None:
1314
1363
  _log(f"{tool_name} REPEAT suppressed ({arg[:60]!r}) → nudging model forward")
1315
1364
  return True, (
1316
- f"You already ran {tool_name}({arg!r}) its result is unchanged and shown "
1317
- f"above. Do NOT call it again. {next_hint}"
1365
+ f"You already ran {tool_name}({arg!r}) (or an equivalent query) the "
1366
+ f"result would be the same as shown above. Do NOT call it again. {next_hint}"
1318
1367
  )
1319
1368
  _seen_calls[sig] = True
1320
1369
  return False, ""
@@ -1418,10 +1467,15 @@ def run_agent_chat(cfg):
1418
1467
  "'complete' source — one may not exist.")
1419
1468
  if dup:
1420
1469
  return nudge
1470
+ blocked = _research_spend("web_search")
1471
+ if blocked:
1472
+ return blocked
1421
1473
  _emit({"type": "step", "text": f"Searching the web → {query[:80]}"})
1422
1474
  result = _web_search_impl(query)
1423
1475
  _log(f"web_search {query[:60]!r} → {result[:80]}")
1424
1476
  _last_obs["text"] = result
1477
+ if result and not result.startswith("Error"):
1478
+ _gathered.append(result)
1425
1479
  return result
1426
1480
 
1427
1481
  @tool
@@ -1441,6 +1495,9 @@ def run_agent_chat(cfg):
1441
1495
  "final_answer with what you found.")
1442
1496
  if dup:
1443
1497
  return nudge
1498
+ blocked = _research_spend("fetch_url")
1499
+ if blocked:
1500
+ return blocked
1444
1501
  _emit({"type": "step", "text": f"Reading page → {url[:80]}"})
1445
1502
  status, ctype, raw = _fetch_page_impl(url)
1446
1503
  if status is None:
@@ -1464,6 +1521,7 @@ def run_agent_chat(cfg):
1464
1521
  out = f"{url}\n{text}"
1465
1522
  _log(f"fetch_url {url} → {len(text)} chars")
1466
1523
  _last_obs["text"] = out
1524
+ _gathered.append(out)
1467
1525
  return out
1468
1526
 
1469
1527
  @tool
@@ -1773,6 +1831,28 @@ def run_agent_chat(cfg):
1773
1831
  class _ToolAgent(_AgentBase):
1774
1832
  def provide_final_answer(self, task, *args, **kwargs):
1775
1833
  from smolagents.models import ChatMessage, MessageRole
1834
+ # PDF-on-exhaustion guarantee: the user asked for a PDF and the loop burned
1835
+ # all steps researching without ever calling create_pdf (seen live: 5 steps
1836
+ # of search/fetch, zero deliverable). Render whatever WAS gathered into a
1837
+ # (possibly partial) PDF instead of returning raw page text.
1838
+ if (agent_pdf_requested and _gathered
1839
+ and not (_last_obs.get("text") or "").startswith("✅")):
1840
+ _log(f"max-steps fallback → forcing create_pdf from {len(_gathered)} "
1841
+ "gathered observation(s)")
1842
+ try:
1843
+ compiled = "\n\n---\n\n".join(_gathered)[:12000]
1844
+ out = create_pdf(
1845
+ text=compiled, title=_derive_pdf_title(latest_user_text)
1846
+ )
1847
+ if isinstance(out, str) and out.startswith("✅"):
1848
+ out += (
1849
+ "\n\n(Note: I hit my research step limit, so this PDF contains "
1850
+ "what I could gather — it may be partial.)"
1851
+ )
1852
+ return ChatMessage(role=MessageRole.ASSISTANT, content=out)
1853
+ _log(f"forced create_pdf did not succeed: {str(out)[:120]}")
1854
+ except Exception as e: # noqa: BLE001
1855
+ _log(f"forced create_pdf error: {type(e).__name__}: {e}")
1776
1856
  text = (_last_obs.get("text") or "").strip()
1777
1857
  if text:
1778
1858
  _log(f"max-steps fallback (last tool obs) → {text[:80]}")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.130",
3
+ "version": "1.0.132",
4
4
  "description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
5
5
  "type": "module",
6
6
  "license": "MIT",