@tiens.nguyen/gonext-local-worker 1.0.138 → 1.0.141
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 +9 -8
- package/gonext_agent_chat.py +75 -4
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1526,14 +1526,15 @@ async function runAgentChatJob(job) {
|
|
|
1526
1526
|
emailFrom: payload?.emailFrom ?? "",
|
|
1527
1527
|
emailAllowList: payload?.emailAllowList ?? "",
|
|
1528
1528
|
});
|
|
1529
|
-
//
|
|
1530
|
-
// or a remote Ollama box cold-loading a big model — can
|
|
1531
|
-
// stay BELOW the web client's hard WS deadline
|
|
1532
|
-
// reaches the user instead of a generic
|
|
1533
|
-
// SIGKILLed, which also shows up as a
|
|
1534
|
-
// (expected). Liveness during long model
|
|
1535
|
-
// (a keepalive step chunk every 45s), not
|
|
1536
|
-
|
|
1529
|
+
// 30 min max for an agent run: multi-step ReAct on a 14B/31B with a cold prompt
|
|
1530
|
+
// cache — or a remote Ollama box on slow GPU cold-loading a big model — can run
|
|
1531
|
+
// for many minutes per step. Must stay BELOW the web client's hard WS deadline
|
|
1532
|
+
// (1_860_000 ms) so our clean timeout error reaches the user instead of a generic
|
|
1533
|
+
// WS timeout. On expiry the python child is SIGKILLed, which also shows up as a
|
|
1534
|
+
// BrokenPipeError in the model server log (expected). Liveness during long model
|
|
1535
|
+
// calls comes from the python heartbeat (a keepalive step chunk every 45s), not
|
|
1536
|
+
// from this cap.
|
|
1537
|
+
const timeoutMs = 1_800_000;
|
|
1537
1538
|
|
|
1538
1539
|
let inThink = false;
|
|
1539
1540
|
let finalText = "";
|
package/gonext_agent_chat.py
CHANGED
|
@@ -398,6 +398,24 @@ def _list_model_ids(base_url, api_key=""):
|
|
|
398
398
|
if isinstance(d, dict) and isinstance(d.get("id"), str) and d["id"].strip()]
|
|
399
399
|
|
|
400
400
|
|
|
401
|
+
def _is_ollama_server(base_url):
|
|
402
|
+
"""True if the OpenAI-compatible base URL is an Ollama server.
|
|
403
|
+
|
|
404
|
+
Ollama's root path answers GET / with the plain-text banner "Ollama is
|
|
405
|
+
running". Used to gate Ollama-only request params (reasoning_effort) that an
|
|
406
|
+
MLX server might reject.
|
|
407
|
+
"""
|
|
408
|
+
root = re.sub(r"/v1/?$", "", (base_url or "").rstrip("/"))
|
|
409
|
+
if not root:
|
|
410
|
+
return False
|
|
411
|
+
req = urllib.request.Request(root, headers={"User-Agent": "gonext-agent/1.0"})
|
|
412
|
+
try:
|
|
413
|
+
with urllib.request.urlopen(req, timeout=6, context=_ssl_context()) as resp:
|
|
414
|
+
return b"ollama" in resp.read(200).lower()
|
|
415
|
+
except Exception: # noqa: BLE001
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
|
|
401
419
|
def _detect_model_id(base_url, api_key=""):
|
|
402
420
|
"""Ask an OpenAI-compatible server which model it serves (first reported id).
|
|
403
421
|
|
|
@@ -616,6 +634,38 @@ def _strip_tool_tags(text: str) -> str:
|
|
|
616
634
|
|
|
617
635
|
_THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
|
618
636
|
|
|
637
|
+
# Whitespace/attr variants of smolagents' code-block tags. Weak models routinely
|
|
638
|
+
# emit "<code >" (trailing space), "< code >", "</ code>", or an uppercase form —
|
|
639
|
+
# none of which match smolagents' exact "<code>(.*?)</code>" parser, so EVERY step
|
|
640
|
+
# fails with "regex pattern <code>(.*?)</code> was not found" and the run burns all
|
|
641
|
+
# its steps. Normalizing the tags to the canonical form before the parser sees them
|
|
642
|
+
# turns that spiral into a normal, parseable step.
|
|
643
|
+
_CODE_OPEN_TAG = re.compile(r"<\s*code(?:\s[^>]*)?\s*>", re.IGNORECASE)
|
|
644
|
+
_CODE_CLOSE_TAG = re.compile(r"<\s*/\s*code\s*>", re.IGNORECASE)
|
|
645
|
+
# Fallback: a markdown code fence (```py / ```python / ```) when the model ignored
|
|
646
|
+
# the tag convention entirely. Captures the fenced body so we can re-wrap it.
|
|
647
|
+
_MD_FENCE_BLOCK = re.compile(
|
|
648
|
+
r"```(?:py|python)?[ \t]*\r?\n(.*?)```", re.DOTALL | re.IGNORECASE
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _normalize_code_tags(text: str) -> str:
|
|
653
|
+
"""Coerce a model reply's code-block delimiters to smolagents' exact <code>…</code>.
|
|
654
|
+
|
|
655
|
+
Handles the two failure modes seen live with weak coding models:
|
|
656
|
+
1. tag whitespace/case: "<code >", "< code >", "</ code>", "<CODE>" → "<code>".
|
|
657
|
+
2. markdown fences instead of tags: ```py … ``` → <code> … </code> (only when
|
|
658
|
+
no <code> tag is present after step 1, so we don't touch code that legitimately
|
|
659
|
+
contains backticks inside a real <code> block).
|
|
660
|
+
Returns text unchanged when it already parses cleanly."""
|
|
661
|
+
if not text:
|
|
662
|
+
return text
|
|
663
|
+
fixed = _CODE_OPEN_TAG.sub("<code>", text)
|
|
664
|
+
fixed = _CODE_CLOSE_TAG.sub("</code>", fixed)
|
|
665
|
+
if "<code>" not in fixed:
|
|
666
|
+
fixed = _MD_FENCE_BLOCK.sub(lambda m: f"<code>\n{m.group(1)}</code>", fixed, count=1)
|
|
667
|
+
return fixed
|
|
668
|
+
|
|
619
669
|
|
|
620
670
|
def _strip_think(text: str) -> str:
|
|
621
671
|
"""Remove Qwen3-style <think>…</think> reasoning traces from a model reply.
|
|
@@ -1823,10 +1873,20 @@ def run_agent_chat(cfg):
|
|
|
1823
1873
|
if content is None:
|
|
1824
1874
|
_log("model returned content=None (empty/thinking-only turn) → coercing to ''")
|
|
1825
1875
|
msg.content = ""
|
|
1876
|
+
content = ""
|
|
1826
1877
|
elif isinstance(content, str) and "</think>" in content.lower():
|
|
1827
1878
|
cleaned = _strip_think(content)
|
|
1828
1879
|
_log(f"stripped <think> trace ({len(content) - len(cleaned)} chars)")
|
|
1829
1880
|
msg.content = cleaned
|
|
1881
|
+
content = cleaned
|
|
1882
|
+
# Normalize code-block delimiters so smolagents' <code>…</code> parser
|
|
1883
|
+
# matches even when the model wrote "<code >" / a markdown fence. Without
|
|
1884
|
+
# this, tag-whitespace variants fail EVERY step → "Reached max steps".
|
|
1885
|
+
if isinstance(content, str) and content:
|
|
1886
|
+
normalized = _normalize_code_tags(content)
|
|
1887
|
+
if normalized != content:
|
|
1888
|
+
_log("normalized code-block tags (<code >/fence → <code>)")
|
|
1889
|
+
msg.content = normalized
|
|
1830
1890
|
except Exception as e: # noqa: BLE001
|
|
1831
1891
|
_log(f"content-normalize error: {e}")
|
|
1832
1892
|
return msg
|
|
@@ -1948,16 +2008,27 @@ def run_agent_chat(cfg):
|
|
|
1948
2008
|
return ChatMessage(role=MessageRole.ASSISTANT, content=text)
|
|
1949
2009
|
|
|
1950
2010
|
try:
|
|
2011
|
+
# Ollama's OpenAI-compat endpoint ignores Qwen3's /no_think soft-switch (its
|
|
2012
|
+
# template forces thinking mode; "think": false is also ignored there) but DOES
|
|
2013
|
+
# honor reasoning_effort="none". Thinking burned ~430 of 487 output tokens in
|
|
2014
|
+
# the live test — minutes per step on a slow GPU — so disable it explicitly.
|
|
2015
|
+
# Sniffed per-server because MLX might reject the unknown param.
|
|
2016
|
+
extra_model_kwargs = {}
|
|
2017
|
+
if _is_ollama_server(coding_base_url):
|
|
2018
|
+
_log("coding server is Ollama → sending reasoning_effort='none' to disable thinking")
|
|
2019
|
+
extra_model_kwargs["reasoning_effort"] = "none"
|
|
1951
2020
|
model = _LoggingModel(
|
|
1952
2021
|
model_id=coding_model_id,
|
|
1953
2022
|
api_base=coding_base_url,
|
|
1954
2023
|
api_key=agent_api_key,
|
|
1955
|
-
# Cap each HTTP attempt at
|
|
1956
|
-
#
|
|
1957
|
-
# the
|
|
2024
|
+
# Cap each HTTP attempt at 600s so a genuinely slow single call (a
|
|
2025
|
+
# remote M6000/Vulkan box doing a cold ~4-5min prompt-eval) can finish
|
|
2026
|
+
# on the first try, while a truly hung request still fails into OUR
|
|
2027
|
+
# retry loop rather than eating the whole 30min worker budget.
|
|
1958
2028
|
# max_retries=0: the openai client's own silent retries would otherwise
|
|
1959
2029
|
# stack multiplicatively with ours (3 × 3 attempts).
|
|
1960
|
-
client_kwargs={"timeout":
|
|
2030
|
+
client_kwargs={"timeout": 600.0, "max_retries": 0},
|
|
2031
|
+
**extra_model_kwargs,
|
|
1961
2032
|
)
|
|
1962
2033
|
agent_tools = [http_request, web_search, fetch_url, calculate,
|
|
1963
2034
|
get_current_datetime, create_pdf]
|
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.141",
|
|
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",
|