@tiens.nguyen/gonext-local-worker 1.0.141 → 1.0.143
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 +123 -18
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -482,6 +482,12 @@ def _summarise_step(step_log):
|
|
|
482
482
|
err = str(error)
|
|
483
483
|
if "Import of" in err and "not allowed" in err:
|
|
484
484
|
parts.append("→ (import blocked — using http_request tool instead)")
|
|
485
|
+
elif "reached max steps" in err.lower() or "max steps" in err.lower():
|
|
486
|
+
# NOT a user-facing failure: hitting the step budget triggers our
|
|
487
|
+
# provide_final_answer override, which ALWAYS delivers a synthesized answer
|
|
488
|
+
# (a rendered PDF, the last tool result, or a plain reply). Surfacing the raw
|
|
489
|
+
# "Error: Reached max steps." made a successful run look failed (task #10).
|
|
490
|
+
parts.append("→ Wrapping up (reached step budget)…")
|
|
485
491
|
else:
|
|
486
492
|
parts.append(f"→ Error: {err[:120]}")
|
|
487
493
|
|
|
@@ -634,14 +640,23 @@ def _strip_tool_tags(text: str) -> str:
|
|
|
634
640
|
|
|
635
641
|
_THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
|
636
642
|
|
|
637
|
-
#
|
|
638
|
-
# emit "<code >" (trailing space), "< code >", "</ code>",
|
|
639
|
-
#
|
|
640
|
-
#
|
|
641
|
-
#
|
|
642
|
-
#
|
|
643
|
-
|
|
643
|
+
# Malformed variants of smolagents' code-block tags. Weak coding models routinely
|
|
644
|
+
# emit "<code >" (trailing space), "< code >", "</ code>", "<CODE>", OR — worst —
|
|
645
|
+
# "<code print(x) </code>" where the OPENING tag is missing its ">" entirely. None
|
|
646
|
+
# match smolagents' exact "<code>(.*?)</code>" parser, so EVERY step fails with
|
|
647
|
+
# "regex pattern <code>(.*?)</code> was not found" and the run burns all its steps.
|
|
648
|
+
# Normalizing the tags to canonical form before the parser sees them turns that
|
|
649
|
+
# spiral into a normal, parseable step.
|
|
644
650
|
_CODE_CLOSE_TAG = re.compile(r"<\s*/\s*code\s*>", re.IGNORECASE)
|
|
651
|
+
# A well-formed-ISH open tag that DOES have a ">": "<code>", "<code >", "< code >",
|
|
652
|
+
# "<CODE>", "<code class=py>". Critically the attr span is [^<>] (NOT [^>]) so it can
|
|
653
|
+
# never swallow across the "<" of a following "</code>" — the bug that collapsed
|
|
654
|
+
# "<code print(x) </code>" to a bare "<code>" and destroyed the code.
|
|
655
|
+
_CODE_OPEN_TAG = re.compile(r"<\s*code\b[^<>]*>", re.IGNORECASE)
|
|
656
|
+
# An open tag MISSING its ">": "<code" not immediately followed by ">". \b keeps it
|
|
657
|
+
# from matching "<codeblock"/"<coder"; the lookahead skips the already-canonical
|
|
658
|
+
# "<code>" so this pass only rescues the no-">" case.
|
|
659
|
+
_CODE_OPEN_NO_GT = re.compile(r"<\s*code\b(?!>)", re.IGNORECASE)
|
|
645
660
|
# Fallback: a markdown code fence (```py / ```python / ```) when the model ignored
|
|
646
661
|
# the tag convention entirely. Captures the fenced body so we can re-wrap it.
|
|
647
662
|
_MD_FENCE_BLOCK = re.compile(
|
|
@@ -652,18 +667,25 @@ _MD_FENCE_BLOCK = re.compile(
|
|
|
652
667
|
def _normalize_code_tags(text: str) -> str:
|
|
653
668
|
"""Coerce a model reply's code-block delimiters to smolagents' exact <code>…</code>.
|
|
654
669
|
|
|
655
|
-
Handles the
|
|
656
|
-
1. tag
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
670
|
+
Handles the failure modes seen live with weak coding models:
|
|
671
|
+
1. close-tag variants: "</ code>", "< / code >" → "</code>" (done FIRST so the
|
|
672
|
+
open-tag pass has a canonical "<" boundary to stop at).
|
|
673
|
+
2. open-tag-with-">": "<code >", "< code >", "<CODE>", "<code attr>" → "<code>".
|
|
674
|
+
3. open-tag-missing-">": "<code print(x) </code>" → "<code> print(x) </code>".
|
|
675
|
+
4. markdown fences instead of tags: ```py … ``` → <code>…</code> (only when no
|
|
676
|
+
<code> tag is present, so we don't touch backticks inside a real <code> block).
|
|
677
|
+
5. opened-but-never-closed (exactly one "<code>", no "</code>"): append "</code>"
|
|
678
|
+
so the parser can still extract the code instead of failing the whole step.
|
|
679
|
+
Idempotent — already-correct "<code>…</code>" is returned unchanged."""
|
|
661
680
|
if not text:
|
|
662
681
|
return text
|
|
663
|
-
fixed =
|
|
664
|
-
fixed =
|
|
682
|
+
fixed = _CODE_CLOSE_TAG.sub("</code>", text)
|
|
683
|
+
fixed = _CODE_OPEN_TAG.sub("<code>", fixed)
|
|
684
|
+
fixed = _CODE_OPEN_NO_GT.sub("<code>", fixed)
|
|
665
685
|
if "<code>" not in fixed:
|
|
666
686
|
fixed = _MD_FENCE_BLOCK.sub(lambda m: f"<code>\n{m.group(1)}</code>", fixed, count=1)
|
|
687
|
+
if fixed.count("<code>") == 1 and "</code>" not in fixed:
|
|
688
|
+
fixed = fixed + "\n</code>"
|
|
667
689
|
return fixed
|
|
668
690
|
|
|
669
691
|
|
|
@@ -1805,6 +1827,85 @@ def run_agent_chat(cfg):
|
|
|
1805
1827
|
# and any step memory it accumulates. completion_kwargs["messages"] here is the
|
|
1806
1828
|
# literal messages array sent to /v1/chat/completions.
|
|
1807
1829
|
class _LoggingModel(OpenAIServerModel):
|
|
1830
|
+
def __init__(self, *a, stream_agent=False, **kw):
|
|
1831
|
+
# stream_agent: request the completion with stream=True and reassemble the
|
|
1832
|
+
# deltas into one message. Enabled for Ollama, where a long single-shot
|
|
1833
|
+
# (non-stream) generation sends nothing until the very end — a reverse proxy
|
|
1834
|
+
# in front of the box (ollama1.gomarsic.cc) then hits its idle read-timeout
|
|
1835
|
+
# and kills the request mid-generation. Streaming keeps tokens flowing so the
|
|
1836
|
+
# idle timer never fires. smolagents still receives the COMPLETE text, so its
|
|
1837
|
+
# <code>/final_answer parsing is unaffected.
|
|
1838
|
+
self._stream_agent = bool(stream_agent)
|
|
1839
|
+
super().__init__(*a, **kw)
|
|
1840
|
+
|
|
1841
|
+
def _generate_once(self, *args, **kwargs):
|
|
1842
|
+
"""One model call. Streams + reassembles when _stream_agent, else the normal
|
|
1843
|
+
non-streaming path. Streaming is a pure transport optimization: on ANY failure
|
|
1844
|
+
it falls back to the non-streaming call, and it's skipped for tool-calling
|
|
1845
|
+
mode (tools_to_call_from), whose tool_call deltas we don't reassemble."""
|
|
1846
|
+
tools_to_call_from = kwargs.get("tools_to_call_from")
|
|
1847
|
+
if not self._stream_agent or tools_to_call_from:
|
|
1848
|
+
return super().generate(*args, **kwargs)
|
|
1849
|
+
try:
|
|
1850
|
+
return self._streamed_generate(*args, **kwargs)
|
|
1851
|
+
except Exception as e: # noqa: BLE001
|
|
1852
|
+
_log(f"streamed generate failed ({e}) → falling back to non-streaming")
|
|
1853
|
+
return super().generate(*args, **kwargs)
|
|
1854
|
+
|
|
1855
|
+
def _streamed_generate(
|
|
1856
|
+
self, messages, stop_sequences=None, response_format=None,
|
|
1857
|
+
tools_to_call_from=None, **kwargs,
|
|
1858
|
+
):
|
|
1859
|
+
from smolagents.models import ChatMessage, TokenUsage # local: version-safe
|
|
1860
|
+
completion_kwargs = self._prepare_completion_kwargs(
|
|
1861
|
+
messages=messages,
|
|
1862
|
+
stop_sequences=stop_sequences,
|
|
1863
|
+
response_format=response_format,
|
|
1864
|
+
tools_to_call_from=tools_to_call_from,
|
|
1865
|
+
model=self.model_id,
|
|
1866
|
+
custom_role_conversions=self.custom_role_conversions,
|
|
1867
|
+
convert_images_to_image_urls=True,
|
|
1868
|
+
**kwargs,
|
|
1869
|
+
)
|
|
1870
|
+
completion_kwargs["stream"] = True
|
|
1871
|
+
# Ask for a usage summary in the final chunk (OpenAI streaming convention;
|
|
1872
|
+
# Ollama honors it). Harmless if the server ignores it — usage stays 0.
|
|
1873
|
+
completion_kwargs["stream_options"] = {"include_usage": True}
|
|
1874
|
+
self._apply_rate_limit()
|
|
1875
|
+
stream = self.client.chat.completions.create(**completion_kwargs)
|
|
1876
|
+
parts = []
|
|
1877
|
+
role = "assistant"
|
|
1878
|
+
in_tok = out_tok = 0
|
|
1879
|
+
for chunk in stream:
|
|
1880
|
+
usage = getattr(chunk, "usage", None)
|
|
1881
|
+
if usage is not None:
|
|
1882
|
+
in_tok = getattr(usage, "prompt_tokens", 0) or in_tok
|
|
1883
|
+
out_tok = getattr(usage, "completion_tokens", 0) or out_tok
|
|
1884
|
+
choices = getattr(chunk, "choices", None)
|
|
1885
|
+
if not choices:
|
|
1886
|
+
continue
|
|
1887
|
+
delta = getattr(choices[0], "delta", None)
|
|
1888
|
+
if delta is None:
|
|
1889
|
+
continue
|
|
1890
|
+
if getattr(delta, "role", None):
|
|
1891
|
+
role = delta.role
|
|
1892
|
+
piece = getattr(delta, "content", None)
|
|
1893
|
+
if piece:
|
|
1894
|
+
parts.append(piece)
|
|
1895
|
+
content = "".join(parts)
|
|
1896
|
+
if stop_sequences and not self.supports_stop_parameter:
|
|
1897
|
+
from smolagents.models import remove_content_after_stop_sequences
|
|
1898
|
+
content = remove_content_after_stop_sequences(content, stop_sequences)
|
|
1899
|
+
_log(f"streamed generate assembled {len(content)} chars "
|
|
1900
|
+
f"(in={in_tok} out={out_tok} tokens)")
|
|
1901
|
+
return ChatMessage(
|
|
1902
|
+
role=role,
|
|
1903
|
+
content=content,
|
|
1904
|
+
tool_calls=None,
|
|
1905
|
+
raw=None,
|
|
1906
|
+
token_usage=TokenUsage(input_tokens=in_tok, output_tokens=out_tok),
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1808
1909
|
def generate(self, *args, **kwargs):
|
|
1809
1910
|
# Two safeguards on each step's reply BEFORE smolagents parses it for the
|
|
1810
1911
|
# code block / final_answer:
|
|
@@ -1843,7 +1944,7 @@ def run_agent_chat(cfg):
|
|
|
1843
1944
|
try:
|
|
1844
1945
|
for attempt in range(3):
|
|
1845
1946
|
try:
|
|
1846
|
-
msg =
|
|
1947
|
+
msg = self._generate_once(*args, **kwargs)
|
|
1847
1948
|
break
|
|
1848
1949
|
except Exception as e: # noqa: BLE001
|
|
1849
1950
|
emsg = str(e)
|
|
@@ -2014,13 +2115,17 @@ def run_agent_chat(cfg):
|
|
|
2014
2115
|
# the live test — minutes per step on a slow GPU — so disable it explicitly.
|
|
2015
2116
|
# Sniffed per-server because MLX might reject the unknown param.
|
|
2016
2117
|
extra_model_kwargs = {}
|
|
2017
|
-
|
|
2018
|
-
|
|
2118
|
+
coding_is_ollama = _is_ollama_server(coding_base_url)
|
|
2119
|
+
if coding_is_ollama:
|
|
2120
|
+
_log("coding server is Ollama → reasoning_effort='none' (disable thinking) + streaming")
|
|
2019
2121
|
extra_model_kwargs["reasoning_effort"] = "none"
|
|
2020
2122
|
model = _LoggingModel(
|
|
2021
2123
|
model_id=coding_model_id,
|
|
2022
2124
|
api_base=coding_base_url,
|
|
2023
2125
|
api_key=agent_api_key,
|
|
2126
|
+
# Stream for Ollama so a long generation keeps the connection warm and can't
|
|
2127
|
+
# trip a reverse-proxy idle read-timeout. smolagents still gets the full text.
|
|
2128
|
+
stream_agent=coding_is_ollama,
|
|
2024
2129
|
# Cap each HTTP attempt at 600s so a genuinely slow single call (a
|
|
2025
2130
|
# remote M6000/Vulkan box doing a cold ~4-5min prompt-eval) can finish
|
|
2026
2131
|
# on the first try, while a truly hung request still fails into OUR
|
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.143",
|
|
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",
|