@tiens.nguyen/gonext-local-worker 1.0.121 → 1.0.124
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-local-worker.mjs +1 -1
- package/gonext_agent_chat.py +111 -49
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1506,7 +1506,7 @@ async function runAgentChatJob(job) {
|
|
|
1506
1506
|
codingBaseURL: payload?.codingBaseURL ?? "",
|
|
1507
1507
|
codingModelId: payload?.codingModelId ?? "",
|
|
1508
1508
|
tools: payload?.tools ?? ["http_request"],
|
|
1509
|
-
maxSteps: payload?.maxSteps ??
|
|
1509
|
+
maxSteps: payload?.maxSteps ?? 8,
|
|
1510
1510
|
// Tool-invocation mode for the agent: "code" (default, python CodeAgent) or
|
|
1511
1511
|
// "toolcall" (structured JSON tool calls). Passed through only; the API/web can
|
|
1512
1512
|
// set payload.agentToolMode to A/B — default keeps current behavior.
|
package/gonext_agent_chat.py
CHANGED
|
@@ -11,7 +11,7 @@ Reads on stdin:
|
|
|
11
11
|
"codingBaseURL": str, # optional: dedicated coding/reasoning model for the
|
|
12
12
|
"codingModelId": str, # CodeAgent's tool-use loop; empty = reuse agentModelId
|
|
13
13
|
"tools": ["http_request"], # v1: only http_request
|
|
14
|
-
"maxSteps": int # default
|
|
14
|
+
"maxSteps": int # multi-step ReAct budget; default 8
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
Emits NDJSON lines on stdout:
|
|
@@ -570,6 +570,27 @@ def _strip_tool_tags(text: str) -> str:
|
|
|
570
570
|
return "\n".join(out).strip()
|
|
571
571
|
|
|
572
572
|
|
|
573
|
+
_THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _strip_think(text: str) -> str:
|
|
577
|
+
"""Remove Qwen3-style <think>…</think> reasoning traces from a model reply.
|
|
578
|
+
|
|
579
|
+
Reasoning models (e.g. Qwen3) emit their chain-of-thought inside <think> tags
|
|
580
|
+
before the actual answer/code. We keep thinking ON (it improves the output) but
|
|
581
|
+
strip the trace before smolagents parses the step, so the code/final_answer parser
|
|
582
|
+
only sees the real content — and the trace never leaks to the user. Handles a
|
|
583
|
+
dangling </think> too (some chat templates open <think> server-side)."""
|
|
584
|
+
if not text:
|
|
585
|
+
return text
|
|
586
|
+
text = _THINK_BLOCK.sub("", text)
|
|
587
|
+
low = text.lower()
|
|
588
|
+
if "</think>" in low and "<think>" not in low:
|
|
589
|
+
# Template opened the think tag for the model; drop everything up to the close.
|
|
590
|
+
text = text[low.find("</think>") + len("</think>"):]
|
|
591
|
+
return text.strip()
|
|
592
|
+
|
|
593
|
+
|
|
573
594
|
_PDF_INSTALL_HINT = (
|
|
574
595
|
"PDF engine not installed on the worker. On macOS:\n"
|
|
575
596
|
" brew install pango libffi\n"
|
|
@@ -883,28 +904,48 @@ def run_agent_chat(cfg):
|
|
|
883
904
|
raw_coding_base = (cfg.get("codingBaseURL") or "").strip()
|
|
884
905
|
raw_coding_model = (cfg.get("codingModelId") or "").strip()
|
|
885
906
|
if raw_coding_base:
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
coding_model_id = detected
|
|
893
|
-
else:
|
|
907
|
+
same_server = raw_coding_base.rstrip("/") == (agent_base_url or "").rstrip("/")
|
|
908
|
+
if same_server and not raw_coding_model:
|
|
909
|
+
# The coding URL points at the SAME server as the chat model. Do NOT
|
|
910
|
+
# auto-detect: /v1/models can list several cached models and return a
|
|
911
|
+
# SMALLER one first (e.g. a 3B), silently downgrading the agent's reasoning.
|
|
912
|
+
# Reuse the chat model id directly — it's the model actually loaded here.
|
|
894
913
|
_log(
|
|
895
|
-
f"coding
|
|
896
|
-
"
|
|
914
|
+
f"coding base == chat base ({raw_coding_base}); "
|
|
915
|
+
f"reusing chat model {agent_model_id!r} (skipping auto-detect)"
|
|
897
916
|
)
|
|
898
917
|
coding_base_url = agent_base_url
|
|
899
918
|
coding_model_id = agent_model_id
|
|
919
|
+
else:
|
|
920
|
+
# A DIFFERENT dedicated coding server. If no model name was given, ask the
|
|
921
|
+
# server which model it serves (mlx_lm.server otherwise tries to download a
|
|
922
|
+
# mismatched name from HF and 404s).
|
|
923
|
+
detected = raw_coding_model or _detect_model_id(raw_coding_base, agent_api_key)
|
|
924
|
+
if detected:
|
|
925
|
+
coding_base_url = raw_coding_base
|
|
926
|
+
coding_model_id = detected
|
|
927
|
+
else:
|
|
928
|
+
_log(
|
|
929
|
+
f"coding model id unresolved for {raw_coding_base!r}; "
|
|
930
|
+
"falling back to chat model"
|
|
931
|
+
)
|
|
932
|
+
coding_base_url = agent_base_url
|
|
933
|
+
coding_model_id = agent_model_id
|
|
900
934
|
else:
|
|
901
935
|
coding_base_url = agent_base_url
|
|
902
936
|
coding_model_id = agent_model_id
|
|
903
|
-
#
|
|
904
|
-
#
|
|
905
|
-
#
|
|
906
|
-
#
|
|
907
|
-
|
|
937
|
+
# Multi-step ReAct loop (thinking agent): the agent may take several
|
|
938
|
+
# Thought → tool → Observation steps and then call final_answer() itself. This
|
|
939
|
+
# replaced the old strict single-shot (max_steps=1) once a stronger coding model
|
|
940
|
+
# (Qwen3-14B class) made real multi-step reasoning reliable. Default budget 8,
|
|
941
|
+
# overridable via cfg.maxSteps. The provide_final_answer override below is now only
|
|
942
|
+
# the exhaustion fallback (loop ends without final_answer).
|
|
943
|
+
try:
|
|
944
|
+
max_steps = int(cfg.get("maxSteps") or 8)
|
|
945
|
+
except (TypeError, ValueError):
|
|
946
|
+
max_steps = 8
|
|
947
|
+
if max_steps < 1:
|
|
948
|
+
max_steps = 8
|
|
908
949
|
|
|
909
950
|
_log(
|
|
910
951
|
f"start model={agent_model_id!r} base={agent_base_url!r} "
|
|
@@ -1136,9 +1177,12 @@ def run_agent_chat(cfg):
|
|
|
1136
1177
|
"(previews first, then sends on confirm).\n"
|
|
1137
1178
|
) if _EMAIL_AVAILABLE else ""
|
|
1138
1179
|
tool_hint = (
|
|
1139
|
-
"
|
|
1140
|
-
"
|
|
1141
|
-
"
|
|
1180
|
+
f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
|
|
1181
|
+
"brief Thought, then ONE code block that calls a SINGLE tool. You will SEE that "
|
|
1182
|
+
"tool's result (Observation) before the next step — use it to decide what to do "
|
|
1183
|
+
"next (e.g. web_search to find a URL, THEN fetch_url to read it). When you have "
|
|
1184
|
+
"enough to answer, call final_answer(<your answer>) in a code block. Prefer "
|
|
1185
|
+
"FEWER steps, and never repeat a tool call that already succeeded.\n\n"
|
|
1142
1186
|
f"You have {_tool_count_word} tools:\n"
|
|
1143
1187
|
" 1. http_request(method, url, headers='', body='', username='', password='') — "
|
|
1144
1188
|
"call a SPECIFIC known API/URL.\n"
|
|
@@ -1156,15 +1200,13 @@ def run_agent_chat(cfg):
|
|
|
1156
1200
|
"\n"
|
|
1157
1201
|
"http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
|
|
1158
1202
|
"\n"
|
|
1159
|
-
"
|
|
1160
|
-
"
|
|
1161
|
-
"
|
|
1162
|
-
"
|
|
1163
|
-
"
|
|
1164
|
-
"
|
|
1165
|
-
"
|
|
1166
|
-
" response = http_request('GET', url, headers='{\"Authorization\": \"Bearer TOKEN\"}')\n"
|
|
1167
|
-
" final_answer(response)\n"
|
|
1203
|
+
"AUTH — ONLY when the TASK itself provides credentials or a token. If it does "
|
|
1204
|
+
"NOT, call http_request with NO username/password/headers. NEVER invent or copy "
|
|
1205
|
+
"the placeholder values below.\n"
|
|
1206
|
+
" BASIC AUTH — pass username= and password= (auto base64; never build a 'Basic ' header):\n"
|
|
1207
|
+
" http_request('GET', url, username=<username from the task>, password=<password from the task>)\n"
|
|
1208
|
+
" BEARER TOKEN — pass it in headers:\n"
|
|
1209
|
+
" http_request('GET', url, headers='{\"Authorization\": \"Bearer <token from the task>\"}')\n"
|
|
1168
1210
|
"\n"
|
|
1169
1211
|
"CHOOSING A TOOL (match the TASK, not these examples):\n"
|
|
1170
1212
|
"- ONLY a date/time question (e.g. 'what is the date today') -> get_current_datetime().\n"
|
|
@@ -1172,6 +1214,8 @@ def run_agent_chat(cfg):
|
|
|
1172
1214
|
"knowledge -> web_search(query).\n"
|
|
1173
1215
|
"- 'read this page' / 'open this link' / 'what does <URL> say' -> fetch_url(url).\n"
|
|
1174
1216
|
"- any arithmetic, percentage, or 'how much is' math -> calculate(expression).\n"
|
|
1217
|
+
"- a live/current NUMBER with no URL given (price, exchange rate, weather, score, "
|
|
1218
|
+
"stock/crypto) -> web_search(query). Do NOT guess an API URL for these.\n"
|
|
1175
1219
|
"- 'create/make/generate/export a PDF' of some text/data -> create_pdf(text, title).\n"
|
|
1176
1220
|
+ _pdf_read_choose_line + _email_choose_line +
|
|
1177
1221
|
"- A specific known API/URL was given -> http_request().\n"
|
|
@@ -1181,8 +1225,11 @@ def run_agent_chat(cfg):
|
|
|
1181
1225
|
"If nothing works, call final_answer explaining what you need — do NOT make up an answer.\n"
|
|
1182
1226
|
"- Only report what a tool ACTUALLY returned. Never fabricate a response, body, or status code.\n"
|
|
1183
1227
|
"- Pass an http_request response DIRECTLY to final_answer — do NOT split, parse, or index it.\n"
|
|
1184
|
-
"-
|
|
1185
|
-
"
|
|
1228
|
+
"- When a call's result is UNCERTAIN (any network call), do NOT call final_answer "
|
|
1229
|
+
"in the SAME block. Call the tool alone, READ the Observation, THEN answer next step.\n"
|
|
1230
|
+
"- If a response starts with 'HTTP 2' it SUCCEEDED — answer with it next step.\n"
|
|
1231
|
+
"- NEVER pass an 'Error:' / HTTP 4xx / 5xx result to final_answer. On an error, try a "
|
|
1232
|
+
"DIFFERENT approach next step (e.g. web_search); only give up after a few tries.\n"
|
|
1186
1233
|
"- Do NOT put final_answer outside the code block.\n\n"
|
|
1187
1234
|
)
|
|
1188
1235
|
# Lead with the TASK so the weak model anchors on what's actually being asked —
|
|
@@ -1198,8 +1245,8 @@ def run_agent_chat(cfg):
|
|
|
1198
1245
|
# Track URLs that have already failed so we don't retry dead endpoints across steps.
|
|
1199
1246
|
_failed_urls: set = set()
|
|
1200
1247
|
|
|
1201
|
-
# Remember the last tool output so the
|
|
1202
|
-
#
|
|
1248
|
+
# Remember the last tool output so the max-steps fallback can report exactly what a
|
|
1249
|
+
# tool returned (no extra model call) if the loop ends without final_answer.
|
|
1203
1250
|
_last_obs: dict = {"text": ""}
|
|
1204
1251
|
|
|
1205
1252
|
@tool
|
|
@@ -1450,7 +1497,7 @@ def run_agent_chat(cfg):
|
|
|
1450
1497
|
|
|
1451
1498
|
# 2) Render the Markdown to PDF bytes locally (pure-Python xhtml2pdf).
|
|
1452
1499
|
# Catch EVERYTHING (xhtml2pdf can raise arbitrary errors on exotic glyphs /
|
|
1453
|
-
# emoji), so the tool always returns a string and never breaks the
|
|
1500
|
+
# emoji), so the tool always returns a string and never breaks the agent loop.
|
|
1454
1501
|
_emit({"type": "step", "text": "Rendering PDF…"})
|
|
1455
1502
|
try:
|
|
1456
1503
|
pdf_bytes = _render_pdf_bytes(markdown_text, doc_title)
|
|
@@ -1526,6 +1573,21 @@ def run_agent_chat(cfg):
|
|
|
1526
1573
|
# and any step memory it accumulates. completion_kwargs["messages"] here is the
|
|
1527
1574
|
# literal messages array sent to /v1/chat/completions.
|
|
1528
1575
|
class _LoggingModel(OpenAIServerModel):
|
|
1576
|
+
def generate(self, *args, **kwargs):
|
|
1577
|
+
# Strip Qwen3 <think>…</think> reasoning traces from each step's reply
|
|
1578
|
+
# BEFORE smolagents parses it for the code block / final_answer. Thinking
|
|
1579
|
+
# stays on (better reasoning), but the trace never confuses the parser or
|
|
1580
|
+
# leaks to the user.
|
|
1581
|
+
msg = super().generate(*args, **kwargs)
|
|
1582
|
+
try:
|
|
1583
|
+
if isinstance(getattr(msg, "content", None), str) and "</think>" in msg.content.lower():
|
|
1584
|
+
cleaned = _strip_think(msg.content)
|
|
1585
|
+
_log(f"stripped <think> trace ({len(msg.content) - len(cleaned)} chars)")
|
|
1586
|
+
msg.content = cleaned
|
|
1587
|
+
except Exception as e: # noqa: BLE001
|
|
1588
|
+
_log(f"think-strip error: {e}")
|
|
1589
|
+
return msg
|
|
1590
|
+
|
|
1529
1591
|
def _prepare_completion_kwargs(self, *args, **kwargs):
|
|
1530
1592
|
ck = super()._prepare_completion_kwargs(*args, **kwargs)
|
|
1531
1593
|
try:
|
|
@@ -1569,21 +1631,22 @@ def run_agent_chat(cfg):
|
|
|
1569
1631
|
else:
|
|
1570
1632
|
_log("agent tool mode: code (python CodeAgent)")
|
|
1571
1633
|
|
|
1572
|
-
#
|
|
1573
|
-
#
|
|
1634
|
+
# Exhaustion fallback: normally the model calls final_answer() itself within the
|
|
1635
|
+
# multi-step loop and that answer is used. Only if it burns through all max_steps
|
|
1636
|
+
# WITHOUT calling final_answer does smolagents call provide_final_answer to
|
|
1574
1637
|
# synthesize one. We override that to return the last tool observation
|
|
1575
|
-
# deterministically
|
|
1576
|
-
# corrupting exact tool output (dates/numbers)
|
|
1577
|
-
class
|
|
1638
|
+
# deterministically (or a plain reply) rather than dead-ending — and without a weak
|
|
1639
|
+
# model corrupting exact tool output (dates/numbers).
|
|
1640
|
+
class _ToolAgent(_AgentBase):
|
|
1578
1641
|
def provide_final_answer(self, task, *args, **kwargs):
|
|
1579
1642
|
from smolagents.models import ChatMessage, MessageRole
|
|
1580
1643
|
text = (_last_obs.get("text") or "").strip()
|
|
1581
1644
|
if text:
|
|
1582
|
-
_log(f"
|
|
1645
|
+
_log(f"max-steps fallback (last tool obs) → {text[:80]}")
|
|
1583
1646
|
else:
|
|
1584
|
-
# No usable tool output (e.g.
|
|
1647
|
+
# No usable tool output (e.g. every step's code failed to parse).
|
|
1585
1648
|
# Don't dead-end on a canned apology — answer from the conversation.
|
|
1586
|
-
_log("
|
|
1649
|
+
_log("max-steps fallback → plain reply over conversation")
|
|
1587
1650
|
try:
|
|
1588
1651
|
text = _plain_reply(
|
|
1589
1652
|
messages, agent_base_url, agent_api_key, agent_model_id
|
|
@@ -1592,8 +1655,8 @@ def run_agent_chat(cfg):
|
|
|
1592
1655
|
_log(f"plain-reply fallback error: {e}")
|
|
1593
1656
|
text = ""
|
|
1594
1657
|
if not text:
|
|
1595
|
-
text = ("I couldn't complete that
|
|
1596
|
-
"give a specific URL/API to call.")
|
|
1658
|
+
text = ("I couldn't complete that within the step budget. Please "
|
|
1659
|
+
"rephrase, or give a specific URL/API to call.")
|
|
1597
1660
|
return ChatMessage(role=MessageRole.ASSISTANT, content=text)
|
|
1598
1661
|
|
|
1599
1662
|
try:
|
|
@@ -1622,14 +1685,13 @@ def run_agent_chat(cfg):
|
|
|
1622
1685
|
agent_kwargs["additional_authorized_imports"] = [
|
|
1623
1686
|
"json", "base64", "urllib", "urllib.request", "urllib.error"
|
|
1624
1687
|
]
|
|
1625
|
-
agent =
|
|
1688
|
+
agent = _ToolAgent(**agent_kwargs)
|
|
1626
1689
|
with contextlib.redirect_stdout(sys.stderr):
|
|
1627
1690
|
result = agent.run(task_with_hint)
|
|
1628
|
-
#
|
|
1629
|
-
# final_answer (or the
|
|
1630
|
-
#
|
|
1631
|
-
# they don't leak to the user.
|
|
1632
|
-
# weak summarizer model used to introduce.
|
|
1691
|
+
# Final formatting — NO extra summarizer model call. In multi-step mode the
|
|
1692
|
+
# agent's own final_answer() (or the max-steps fallback above) already holds the
|
|
1693
|
+
# synthesized answer; we just strip the internal hint tags we appended to tool
|
|
1694
|
+
# results so they don't leak to the user.
|
|
1633
1695
|
_emit({"type": "step", "text": "Composing answer…"})
|
|
1634
1696
|
final_text = _strip_tool_tags(str(result).strip()) or "[No result]"
|
|
1635
1697
|
_log(f"done (deterministic, no summarizer call): {len(final_text)} chars")
|
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.124",
|
|
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",
|