@tiens.nguyen/gonext-local-worker 1.0.140 → 1.0.142
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 +128 -3
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -634,6 +634,38 @@ def _strip_tool_tags(text: str) -> str:
|
|
|
634
634
|
|
|
635
635
|
_THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
|
636
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
|
+
|
|
637
669
|
|
|
638
670
|
def _strip_think(text: str) -> str:
|
|
639
671
|
"""Remove Qwen3-style <think>…</think> reasoning traces from a model reply.
|
|
@@ -1773,6 +1805,85 @@ def run_agent_chat(cfg):
|
|
|
1773
1805
|
# and any step memory it accumulates. completion_kwargs["messages"] here is the
|
|
1774
1806
|
# literal messages array sent to /v1/chat/completions.
|
|
1775
1807
|
class _LoggingModel(OpenAIServerModel):
|
|
1808
|
+
def __init__(self, *a, stream_agent=False, **kw):
|
|
1809
|
+
# stream_agent: request the completion with stream=True and reassemble the
|
|
1810
|
+
# deltas into one message. Enabled for Ollama, where a long single-shot
|
|
1811
|
+
# (non-stream) generation sends nothing until the very end — a reverse proxy
|
|
1812
|
+
# in front of the box (ollama1.gomarsic.cc) then hits its idle read-timeout
|
|
1813
|
+
# and kills the request mid-generation. Streaming keeps tokens flowing so the
|
|
1814
|
+
# idle timer never fires. smolagents still receives the COMPLETE text, so its
|
|
1815
|
+
# <code>/final_answer parsing is unaffected.
|
|
1816
|
+
self._stream_agent = bool(stream_agent)
|
|
1817
|
+
super().__init__(*a, **kw)
|
|
1818
|
+
|
|
1819
|
+
def _generate_once(self, *args, **kwargs):
|
|
1820
|
+
"""One model call. Streams + reassembles when _stream_agent, else the normal
|
|
1821
|
+
non-streaming path. Streaming is a pure transport optimization: on ANY failure
|
|
1822
|
+
it falls back to the non-streaming call, and it's skipped for tool-calling
|
|
1823
|
+
mode (tools_to_call_from), whose tool_call deltas we don't reassemble."""
|
|
1824
|
+
tools_to_call_from = kwargs.get("tools_to_call_from")
|
|
1825
|
+
if not self._stream_agent or tools_to_call_from:
|
|
1826
|
+
return super().generate(*args, **kwargs)
|
|
1827
|
+
try:
|
|
1828
|
+
return self._streamed_generate(*args, **kwargs)
|
|
1829
|
+
except Exception as e: # noqa: BLE001
|
|
1830
|
+
_log(f"streamed generate failed ({e}) → falling back to non-streaming")
|
|
1831
|
+
return super().generate(*args, **kwargs)
|
|
1832
|
+
|
|
1833
|
+
def _streamed_generate(
|
|
1834
|
+
self, messages, stop_sequences=None, response_format=None,
|
|
1835
|
+
tools_to_call_from=None, **kwargs,
|
|
1836
|
+
):
|
|
1837
|
+
from smolagents.models import ChatMessage, TokenUsage # local: version-safe
|
|
1838
|
+
completion_kwargs = self._prepare_completion_kwargs(
|
|
1839
|
+
messages=messages,
|
|
1840
|
+
stop_sequences=stop_sequences,
|
|
1841
|
+
response_format=response_format,
|
|
1842
|
+
tools_to_call_from=tools_to_call_from,
|
|
1843
|
+
model=self.model_id,
|
|
1844
|
+
custom_role_conversions=self.custom_role_conversions,
|
|
1845
|
+
convert_images_to_image_urls=True,
|
|
1846
|
+
**kwargs,
|
|
1847
|
+
)
|
|
1848
|
+
completion_kwargs["stream"] = True
|
|
1849
|
+
# Ask for a usage summary in the final chunk (OpenAI streaming convention;
|
|
1850
|
+
# Ollama honors it). Harmless if the server ignores it — usage stays 0.
|
|
1851
|
+
completion_kwargs["stream_options"] = {"include_usage": True}
|
|
1852
|
+
self._apply_rate_limit()
|
|
1853
|
+
stream = self.client.chat.completions.create(**completion_kwargs)
|
|
1854
|
+
parts = []
|
|
1855
|
+
role = "assistant"
|
|
1856
|
+
in_tok = out_tok = 0
|
|
1857
|
+
for chunk in stream:
|
|
1858
|
+
usage = getattr(chunk, "usage", None)
|
|
1859
|
+
if usage is not None:
|
|
1860
|
+
in_tok = getattr(usage, "prompt_tokens", 0) or in_tok
|
|
1861
|
+
out_tok = getattr(usage, "completion_tokens", 0) or out_tok
|
|
1862
|
+
choices = getattr(chunk, "choices", None)
|
|
1863
|
+
if not choices:
|
|
1864
|
+
continue
|
|
1865
|
+
delta = getattr(choices[0], "delta", None)
|
|
1866
|
+
if delta is None:
|
|
1867
|
+
continue
|
|
1868
|
+
if getattr(delta, "role", None):
|
|
1869
|
+
role = delta.role
|
|
1870
|
+
piece = getattr(delta, "content", None)
|
|
1871
|
+
if piece:
|
|
1872
|
+
parts.append(piece)
|
|
1873
|
+
content = "".join(parts)
|
|
1874
|
+
if stop_sequences and not self.supports_stop_parameter:
|
|
1875
|
+
from smolagents.models import remove_content_after_stop_sequences
|
|
1876
|
+
content = remove_content_after_stop_sequences(content, stop_sequences)
|
|
1877
|
+
_log(f"streamed generate assembled {len(content)} chars "
|
|
1878
|
+
f"(in={in_tok} out={out_tok} tokens)")
|
|
1879
|
+
return ChatMessage(
|
|
1880
|
+
role=role,
|
|
1881
|
+
content=content,
|
|
1882
|
+
tool_calls=None,
|
|
1883
|
+
raw=None,
|
|
1884
|
+
token_usage=TokenUsage(input_tokens=in_tok, output_tokens=out_tok),
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1776
1887
|
def generate(self, *args, **kwargs):
|
|
1777
1888
|
# Two safeguards on each step's reply BEFORE smolagents parses it for the
|
|
1778
1889
|
# code block / final_answer:
|
|
@@ -1811,7 +1922,7 @@ def run_agent_chat(cfg):
|
|
|
1811
1922
|
try:
|
|
1812
1923
|
for attempt in range(3):
|
|
1813
1924
|
try:
|
|
1814
|
-
msg =
|
|
1925
|
+
msg = self._generate_once(*args, **kwargs)
|
|
1815
1926
|
break
|
|
1816
1927
|
except Exception as e: # noqa: BLE001
|
|
1817
1928
|
emsg = str(e)
|
|
@@ -1841,10 +1952,20 @@ def run_agent_chat(cfg):
|
|
|
1841
1952
|
if content is None:
|
|
1842
1953
|
_log("model returned content=None (empty/thinking-only turn) → coercing to ''")
|
|
1843
1954
|
msg.content = ""
|
|
1955
|
+
content = ""
|
|
1844
1956
|
elif isinstance(content, str) and "</think>" in content.lower():
|
|
1845
1957
|
cleaned = _strip_think(content)
|
|
1846
1958
|
_log(f"stripped <think> trace ({len(content) - len(cleaned)} chars)")
|
|
1847
1959
|
msg.content = cleaned
|
|
1960
|
+
content = cleaned
|
|
1961
|
+
# Normalize code-block delimiters so smolagents' <code>…</code> parser
|
|
1962
|
+
# matches even when the model wrote "<code >" / a markdown fence. Without
|
|
1963
|
+
# this, tag-whitespace variants fail EVERY step → "Reached max steps".
|
|
1964
|
+
if isinstance(content, str) and content:
|
|
1965
|
+
normalized = _normalize_code_tags(content)
|
|
1966
|
+
if normalized != content:
|
|
1967
|
+
_log("normalized code-block tags (<code >/fence → <code>)")
|
|
1968
|
+
msg.content = normalized
|
|
1848
1969
|
except Exception as e: # noqa: BLE001
|
|
1849
1970
|
_log(f"content-normalize error: {e}")
|
|
1850
1971
|
return msg
|
|
@@ -1972,13 +2093,17 @@ def run_agent_chat(cfg):
|
|
|
1972
2093
|
# the live test — minutes per step on a slow GPU — so disable it explicitly.
|
|
1973
2094
|
# Sniffed per-server because MLX might reject the unknown param.
|
|
1974
2095
|
extra_model_kwargs = {}
|
|
1975
|
-
|
|
1976
|
-
|
|
2096
|
+
coding_is_ollama = _is_ollama_server(coding_base_url)
|
|
2097
|
+
if coding_is_ollama:
|
|
2098
|
+
_log("coding server is Ollama → reasoning_effort='none' (disable thinking) + streaming")
|
|
1977
2099
|
extra_model_kwargs["reasoning_effort"] = "none"
|
|
1978
2100
|
model = _LoggingModel(
|
|
1979
2101
|
model_id=coding_model_id,
|
|
1980
2102
|
api_base=coding_base_url,
|
|
1981
2103
|
api_key=agent_api_key,
|
|
2104
|
+
# Stream for Ollama so a long generation keeps the connection warm and can't
|
|
2105
|
+
# trip a reverse-proxy idle read-timeout. smolagents still gets the full text.
|
|
2106
|
+
stream_agent=coding_is_ollama,
|
|
1982
2107
|
# Cap each HTTP attempt at 600s so a genuinely slow single call (a
|
|
1983
2108
|
# remote M6000/Vulkan box doing a cold ~4-5min prompt-eval) can finish
|
|
1984
2109
|
# 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.142",
|
|
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",
|