@tiens.nguyen/gonext-local-worker 1.0.130 → 1.0.134
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.
- package/gonext_agent_chat.py +103 -4
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -66,6 +66,22 @@ def _http_request_impl(method, url, headers=None, body=None, timeout=25):
|
|
|
66
66
|
return f"Error: {e}"
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _normalize_openai_base(url: str) -> str:
|
|
70
|
+
"""Normalize a model-server URL to its OpenAI-compatible /v1 root.
|
|
71
|
+
|
|
72
|
+
Users paste whatever their server docs show: an MLX root (http://host:8089), an
|
|
73
|
+
Ollama native endpoint (http://host:11434/api/generate), or a full /v1 URL. Ollama
|
|
74
|
+
serves the OpenAI-compatible API at /v1 on the same host, so strip any native
|
|
75
|
+
/api/... path (including a mistakenly appended /v1 after it) and ensure /v1.
|
|
76
|
+
"""
|
|
77
|
+
u = (url or "").strip().rstrip("/")
|
|
78
|
+
if not u:
|
|
79
|
+
return ""
|
|
80
|
+
u = re.sub(r"/api(/(generate|chat|tags|embeddings|embed))?(/v1)?$", "", u,
|
|
81
|
+
flags=re.IGNORECASE)
|
|
82
|
+
return u if u.lower().endswith("/v1") else u + "/v1"
|
|
83
|
+
|
|
84
|
+
|
|
69
85
|
def _html_to_text(html_text, limit=3000):
|
|
70
86
|
"""Strip HTML to readable plain text (zero-dep). Used by fetch_url so the weak
|
|
71
87
|
model receives prose instead of raw tags it cannot parse."""
|
|
@@ -934,7 +950,10 @@ def run_agent_chat(cfg):
|
|
|
934
950
|
# Optional dedicated coding/reasoning model for the CodeAgent's tool-use loop.
|
|
935
951
|
# Routing, plain replies and summarization stay on the chat model (better at
|
|
936
952
|
# natural language); the code model only drives http_request reasoning.
|
|
937
|
-
|
|
953
|
+
# The URL may be a pasted Ollama endpoint (http://host:11434/api/generate) or a
|
|
954
|
+
# bare host — normalize to the OpenAI-compatible /v1 root either way (defensive:
|
|
955
|
+
# also fixes an older API layer that appended /v1 after the native /api path).
|
|
956
|
+
raw_coding_base = _normalize_openai_base(cfg.get("codingBaseURL") or "")
|
|
938
957
|
raw_coding_model = (cfg.get("codingModelId") or "").strip()
|
|
939
958
|
if raw_coding_base:
|
|
940
959
|
same_server = raw_coding_base.rstrip("/") == (agent_base_url or "").rstrip("/")
|
|
@@ -1099,6 +1118,10 @@ def run_agent_chat(cfg):
|
|
|
1099
1118
|
_log("router: agent (HTTP tool use needed)")
|
|
1100
1119
|
_emit({"type": "step", "text": "Choosing a tool…"})
|
|
1101
1120
|
|
|
1121
|
+
# True when the user wants a PDF but its content must first be researched by the
|
|
1122
|
+
# agent loop (set below); read by the max-steps fallback to force-deliver a PDF.
|
|
1123
|
+
agent_pdf_requested = False
|
|
1124
|
+
|
|
1102
1125
|
# ---- Deterministic PDF fast-path -------------------------------------------------
|
|
1103
1126
|
# A small model cannot reliably re-emit a long, emoji/quote-heavy document as a
|
|
1104
1127
|
# Python string literal for create_pdf(text="…"), so the CodeAgent call fails before
|
|
@@ -1137,6 +1160,9 @@ def run_agent_chat(cfg):
|
|
|
1137
1160
|
and _PDF_RESEARCH_INTENT.search(latest_user_text)):
|
|
1138
1161
|
_log("PDF fast-path SKIPPED: content needs research (no provided body) "
|
|
1139
1162
|
"→ agent loop (web_search → create_pdf)")
|
|
1163
|
+
# Remember the user wants a PDF: if the agent loop exhausts its steps without
|
|
1164
|
+
# calling create_pdf, the max-steps fallback renders the gathered facts anyway.
|
|
1165
|
+
agent_pdf_requested = True
|
|
1140
1166
|
else:
|
|
1141
1167
|
doc_title = _derive_pdf_title(doc_text)
|
|
1142
1168
|
_log(
|
|
@@ -1280,6 +1306,9 @@ def run_agent_chat(cfg):
|
|
|
1280
1306
|
"clean one often does not exist. After 1-2 searches/fetches, COMPILE what you have and "
|
|
1281
1307
|
"finish: if a PDF was asked for, call create_pdf(text=<your compiled text>, title=...); "
|
|
1282
1308
|
"otherwise call final_answer. NEVER repeat a web_search/fetch_url you already ran.\n"
|
|
1309
|
+
"- RESEARCH BUDGET — you may make at most 3 web_search/fetch_url calls TOTAL per task "
|
|
1310
|
+
"(rewording a query still counts). Reserve your remaining steps for create_pdf / "
|
|
1311
|
+
"final_answer.\n"
|
|
1283
1312
|
"- Do NOT put final_answer outside the code block.\n\n"
|
|
1284
1313
|
)
|
|
1285
1314
|
# Lead with the TASK so the weak model anchors on what's actually being asked —
|
|
@@ -1306,15 +1335,54 @@ def run_agent_chat(cfg):
|
|
|
1306
1335
|
# / final_answer). Keyed to retrieval tools where the loop actually happens.
|
|
1307
1336
|
_seen_calls: dict = {}
|
|
1308
1337
|
|
|
1338
|
+
# Search queries get near-duplicated with filler tweaks ("schedule" → "FULL schedule"
|
|
1339
|
+
# → "2026 FIFA full schedule"), dodging exact-match dedup — seen live. Normalize:
|
|
1340
|
+
# drop filler words, sort tokens, so all variants share one signature.
|
|
1341
|
+
_QUERY_FILLER = {
|
|
1342
|
+
"full", "complete", "comprehensive", "detailed", "entire", "all", "latest",
|
|
1343
|
+
"current", "the", "a", "an", "of", "with", "and", "for", "to", "in", "on",
|
|
1344
|
+
"more", "about", "info", "information",
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
def _norm_query(q: str) -> str:
|
|
1348
|
+
tokens = [t for t in re.findall(r"[a-z0-9]+", (q or "").lower())
|
|
1349
|
+
if t not in _QUERY_FILLER]
|
|
1350
|
+
return " ".join(sorted(tokens))
|
|
1351
|
+
|
|
1352
|
+
# Hard research budget: at most N retrieval calls (web_search + fetch_url) per run.
|
|
1353
|
+
# Past it, retrieval tools refuse and steer to create_pdf/final_answer. Keeps ≥2 of
|
|
1354
|
+
# the 5 steps free to actually FINISH — the live failure mode was burning ALL steps
|
|
1355
|
+
# on slightly-different searches and never producing the asked-for PDF.
|
|
1356
|
+
_research = {"used": 0, "max": 3}
|
|
1357
|
+
|
|
1358
|
+
def _research_spend(tool_name: str) -> str:
|
|
1359
|
+
"""Return '' if under budget (and spend 1), else a refusal steering to finish."""
|
|
1360
|
+
if _research["used"] >= _research["max"]:
|
|
1361
|
+
_log(f"{tool_name} BLOCKED: research budget ({_research['max']}) exhausted")
|
|
1362
|
+
return (
|
|
1363
|
+
f"Research budget exhausted ({_research['max']} web_search/fetch_url calls "
|
|
1364
|
+
"used). Do NOT search or fetch again. FINISH NOW with what you have: if the "
|
|
1365
|
+
"task asked for a PDF call create_pdf(text=<compile the gathered facts>, "
|
|
1366
|
+
"title=...), otherwise call final_answer(<your best answer from the "
|
|
1367
|
+
"observations above>)."
|
|
1368
|
+
)
|
|
1369
|
+
_research["used"] += 1
|
|
1370
|
+
return ""
|
|
1371
|
+
|
|
1372
|
+
# Everything useful the retrieval tools returned this run, so an exhausted loop can
|
|
1373
|
+
# still deterministically deliver (e.g. render a partial PDF) instead of dead-ending.
|
|
1374
|
+
_gathered: list = []
|
|
1375
|
+
|
|
1309
1376
|
def _dedup(tool_name: str, arg: str, next_hint: str):
|
|
1310
1377
|
"""Return (is_repeat, message). On a repeat, message steers the model forward."""
|
|
1311
|
-
|
|
1378
|
+
key = _norm_query(arg) if tool_name == "web_search" else (arg or "").strip().lower()
|
|
1379
|
+
sig = f"{tool_name}::{key}"
|
|
1312
1380
|
prior = _seen_calls.get(sig)
|
|
1313
1381
|
if prior is not None:
|
|
1314
1382
|
_log(f"{tool_name} REPEAT suppressed ({arg[:60]!r}) → nudging model forward")
|
|
1315
1383
|
return True, (
|
|
1316
|
-
f"You already ran {tool_name}({arg!r})
|
|
1317
|
-
f"above. Do NOT call it again. {next_hint}"
|
|
1384
|
+
f"You already ran {tool_name}({arg!r}) (or an equivalent query) — the "
|
|
1385
|
+
f"result would be the same as shown above. Do NOT call it again. {next_hint}"
|
|
1318
1386
|
)
|
|
1319
1387
|
_seen_calls[sig] = True
|
|
1320
1388
|
return False, ""
|
|
@@ -1418,10 +1486,15 @@ def run_agent_chat(cfg):
|
|
|
1418
1486
|
"'complete' source — one may not exist.")
|
|
1419
1487
|
if dup:
|
|
1420
1488
|
return nudge
|
|
1489
|
+
blocked = _research_spend("web_search")
|
|
1490
|
+
if blocked:
|
|
1491
|
+
return blocked
|
|
1421
1492
|
_emit({"type": "step", "text": f"Searching the web → {query[:80]}"})
|
|
1422
1493
|
result = _web_search_impl(query)
|
|
1423
1494
|
_log(f"web_search {query[:60]!r} → {result[:80]}")
|
|
1424
1495
|
_last_obs["text"] = result
|
|
1496
|
+
if result and not result.startswith("Error"):
|
|
1497
|
+
_gathered.append(result)
|
|
1425
1498
|
return result
|
|
1426
1499
|
|
|
1427
1500
|
@tool
|
|
@@ -1441,6 +1514,9 @@ def run_agent_chat(cfg):
|
|
|
1441
1514
|
"final_answer with what you found.")
|
|
1442
1515
|
if dup:
|
|
1443
1516
|
return nudge
|
|
1517
|
+
blocked = _research_spend("fetch_url")
|
|
1518
|
+
if blocked:
|
|
1519
|
+
return blocked
|
|
1444
1520
|
_emit({"type": "step", "text": f"Reading page → {url[:80]}"})
|
|
1445
1521
|
status, ctype, raw = _fetch_page_impl(url)
|
|
1446
1522
|
if status is None:
|
|
@@ -1464,6 +1540,7 @@ def run_agent_chat(cfg):
|
|
|
1464
1540
|
out = f"{url}\n{text}"
|
|
1465
1541
|
_log(f"fetch_url {url} → {len(text)} chars")
|
|
1466
1542
|
_last_obs["text"] = out
|
|
1543
|
+
_gathered.append(out)
|
|
1467
1544
|
return out
|
|
1468
1545
|
|
|
1469
1546
|
@tool
|
|
@@ -1773,6 +1850,28 @@ def run_agent_chat(cfg):
|
|
|
1773
1850
|
class _ToolAgent(_AgentBase):
|
|
1774
1851
|
def provide_final_answer(self, task, *args, **kwargs):
|
|
1775
1852
|
from smolagents.models import ChatMessage, MessageRole
|
|
1853
|
+
# PDF-on-exhaustion guarantee: the user asked for a PDF and the loop burned
|
|
1854
|
+
# all steps researching without ever calling create_pdf (seen live: 5 steps
|
|
1855
|
+
# of search/fetch, zero deliverable). Render whatever WAS gathered into a
|
|
1856
|
+
# (possibly partial) PDF instead of returning raw page text.
|
|
1857
|
+
if (agent_pdf_requested and _gathered
|
|
1858
|
+
and not (_last_obs.get("text") or "").startswith("✅")):
|
|
1859
|
+
_log(f"max-steps fallback → forcing create_pdf from {len(_gathered)} "
|
|
1860
|
+
"gathered observation(s)")
|
|
1861
|
+
try:
|
|
1862
|
+
compiled = "\n\n---\n\n".join(_gathered)[:12000]
|
|
1863
|
+
out = create_pdf(
|
|
1864
|
+
text=compiled, title=_derive_pdf_title(latest_user_text)
|
|
1865
|
+
)
|
|
1866
|
+
if isinstance(out, str) and out.startswith("✅"):
|
|
1867
|
+
out += (
|
|
1868
|
+
"\n\n(Note: I hit my research step limit, so this PDF contains "
|
|
1869
|
+
"what I could gather — it may be partial.)"
|
|
1870
|
+
)
|
|
1871
|
+
return ChatMessage(role=MessageRole.ASSISTANT, content=out)
|
|
1872
|
+
_log(f"forced create_pdf did not succeed: {str(out)[:120]}")
|
|
1873
|
+
except Exception as e: # noqa: BLE001
|
|
1874
|
+
_log(f"forced create_pdf error: {type(e).__name__}: {e}")
|
|
1776
1875
|
text = (_last_obs.get("text") or "").strip()
|
|
1777
1876
|
if text:
|
|
1778
1877
|
_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.
|
|
3
|
+
"version": "1.0.134",
|
|
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",
|