@tiens.nguyen/gonext-local-worker 1.0.181 → 1.0.183
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 +162 -18
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -558,6 +558,96 @@ _AGENT_KEYWORDS = re.compile(
|
|
|
558
558
|
re.IGNORECASE,
|
|
559
559
|
)
|
|
560
560
|
|
|
561
|
+
# Pure conversational openers/closers that never need a tool — a greeting, a thank-you, a
|
|
562
|
+
# "who are you". Matching the WHOLE message (anchored) means "hi, fetch https://…" won't
|
|
563
|
+
# match. These skip BOTH the model router call AND the heavy tool preamble → an instant
|
|
564
|
+
# small-prompt plain reply, instead of a multi-minute prompt-eval on the coding model.
|
|
565
|
+
_TRIVIAL_CHAT = re.compile(
|
|
566
|
+
r"^\s*(?:"
|
|
567
|
+
r"h(?:i+|ello+|ey+|iya|owdy)|yo|sup|wass?up|"
|
|
568
|
+
r"good\s*(?:morning|afternoon|evening|night|day)|greetings|"
|
|
569
|
+
r"thanks?(?:\s*you)?|thank\s*you|thx|ty|cheers|much\s*appreciated|"
|
|
570
|
+
r"bye+|goodbye|see\s*(?:ya|you)|cya|later|good\s*night|"
|
|
571
|
+
r"ok(?:ay)?|k|cool|nice|great|awesome|perfect|sounds\s*good|got\s*it|"
|
|
572
|
+
r"how\s*(?:are|r)\s*(?:you|u|ya)(?:\s*doing)?|how'?s\s*it\s*going|what'?s\s*up|"
|
|
573
|
+
r"who\s*(?:are|r)\s*(?:you|u)|what\s*(?:can|do)\s*you\s*do|what\s*are\s*you|"
|
|
574
|
+
r"test(?:ing)?|ping"
|
|
575
|
+
r")"
|
|
576
|
+
r"(?:\s+(?:there|everyone|all|bot|assistant|gonext|man|dude|buddy))?"
|
|
577
|
+
r"[\s!.?,'\"]*$",
|
|
578
|
+
re.IGNORECASE,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _is_trivial_chat(text: str) -> bool:
|
|
583
|
+
"""True for a short, pure-conversational message (greeting, thanks, 'who are you')
|
|
584
|
+
that clearly needs no tools — so we can answer directly and fast."""
|
|
585
|
+
t = (text or "").strip()
|
|
586
|
+
if not t or len(t) > 60:
|
|
587
|
+
return False
|
|
588
|
+
return bool(_TRIVIAL_CHAT.match(t))
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# Compact replacement for smolagents' default CodeAgent system prompt. The stock template
|
|
592
|
+
# is ~9.9k chars, dominated by ~6k chars of GENERIC few-shot examples (image captions,
|
|
593
|
+
# Wikipedia, etc.) that are irrelevant to our HTTP/file tools and cost real prompt-eval
|
|
594
|
+
# time every step on a slow coding model. This keeps everything FUNCTIONALLY required —
|
|
595
|
+
# the Thought→Code→Observation loop, the code-blob format + closing tag, the {{tools}}
|
|
596
|
+
# rendering (so exact signatures survive), authorized imports, custom_instructions — plus
|
|
597
|
+
# ONE short worked example, and trims the rest. Jinja vars must match the stock template.
|
|
598
|
+
_COMPACT_CODE_SYSTEM_PROMPT = (
|
|
599
|
+
"You are an expert assistant who solves the task by writing Python code that calls "
|
|
600
|
+
"tools. You work in a loop of Thought → Code → Observation.\n\n"
|
|
601
|
+
"At EACH step:\n"
|
|
602
|
+
"- Write one 'Thought:' line: what you'll do and which tool.\n"
|
|
603
|
+
"- Then a code block that OPENS with {{code_block_opening_tag}} and CLOSES with "
|
|
604
|
+
"{{code_block_closing_tag}}, containing simple Python that calls ONE tool and print()s "
|
|
605
|
+
"anything you need next.\n"
|
|
606
|
+
"- You then receive that tool's 'Observation:'. Use it to decide the next step. When "
|
|
607
|
+
"you have the answer, call final_answer(answer) inside a code block.\n\n"
|
|
608
|
+
"Example:\n"
|
|
609
|
+
"Thought: I'll read the page the user gave.\n"
|
|
610
|
+
"{{code_block_opening_tag}}\n"
|
|
611
|
+
'text = fetch_url("https://example.com")\n'
|
|
612
|
+
"print(text)\n"
|
|
613
|
+
"{{code_block_closing_tag}}\n"
|
|
614
|
+
"Observation: \"Example Domain … \"\n"
|
|
615
|
+
"Thought: I have the content, so I'll answer.\n"
|
|
616
|
+
"{{code_block_opening_tag}}\n"
|
|
617
|
+
'final_answer("The page is Example Domain, a placeholder site.")\n'
|
|
618
|
+
"{{code_block_closing_tag}}\n\n"
|
|
619
|
+
"You have access to these tools — call them as plain Python functions with the exact "
|
|
620
|
+
"signatures shown:\n"
|
|
621
|
+
"{{code_block_opening_tag}}\n"
|
|
622
|
+
"{%- for tool in tools.values() %}\n"
|
|
623
|
+
"{{ tool.to_code_prompt() }}\n"
|
|
624
|
+
"{% endfor %}\n"
|
|
625
|
+
"{{code_block_closing_tag}}\n"
|
|
626
|
+
"{%- if managed_agents and managed_agents.values() | list %}\n"
|
|
627
|
+
"You can also delegate to team members by calling them like a tool with a 'task' "
|
|
628
|
+
"string argument:\n"
|
|
629
|
+
"{{code_block_opening_tag}}\n"
|
|
630
|
+
"{%- for agent in managed_agents.values() %}\n"
|
|
631
|
+
"def {{ agent.name }}(task: str) -> str:\n"
|
|
632
|
+
' """{{ agent.description }}"""\n'
|
|
633
|
+
"{% endfor %}\n"
|
|
634
|
+
"{{code_block_closing_tag}}\n"
|
|
635
|
+
"{%- endif %}\n\n"
|
|
636
|
+
"Rules:\n"
|
|
637
|
+
"1. ALWAYS write a 'Thought:' line, then a code block opening with "
|
|
638
|
+
"{{code_block_opening_tag}} and closing with {{code_block_closing_tag}} — or you fail.\n"
|
|
639
|
+
"2. Pass tool arguments directly: calculate(expression=\"2+2\"), NOT as a dict.\n"
|
|
640
|
+
"3. Use only variables you have defined; state persists between steps.\n"
|
|
641
|
+
"4. Never re-run a tool call with the exact same parameters.\n"
|
|
642
|
+
"5. Don't name a variable after a tool (e.g. 'final_answer').\n"
|
|
643
|
+
"6. Imports are allowed ONLY from: {{authorized_imports}}\n"
|
|
644
|
+
"7. Don't give up — you are in charge of solving the task.\n"
|
|
645
|
+
"{%- if custom_instructions %}\n"
|
|
646
|
+
"{{custom_instructions}}\n"
|
|
647
|
+
"{%- endif %}\n\n"
|
|
648
|
+
"Now Begin!"
|
|
649
|
+
)
|
|
650
|
+
|
|
561
651
|
|
|
562
652
|
def _route(task_text: str, base_url: str, api_key: str, model_id: str) -> bool:
|
|
563
653
|
"""Decide if the task needs the HTTP agent (True) or a plain chat reply (False).
|
|
@@ -603,6 +693,16 @@ def _route(task_text: str, base_url: str, api_key: str, model_id: str) -> bool:
|
|
|
603
693
|
_emit({"type": "step", "text": "→ Agent mode (needs tools)" if is_agent else "→ Chat reply"})
|
|
604
694
|
return is_agent
|
|
605
695
|
except Exception as e: # noqa: BLE001
|
|
696
|
+
# Classifier unreachable (e.g. the local chat server is down). No agent keyword
|
|
697
|
+
# matched above, so a SHORT message is almost certainly plain conversation — send
|
|
698
|
+
# it to a fast small-prompt reply instead of the heavy tool loop (which would
|
|
699
|
+
# prompt-eval a huge preamble for minutes). Only longer/ambiguous tasks fall back
|
|
700
|
+
# to the agent.
|
|
701
|
+
short = len((task_text or "").split()) <= 12
|
|
702
|
+
if short:
|
|
703
|
+
_log(f"router error: {e} — short message, defaulting to plain chat reply")
|
|
704
|
+
_emit({"type": "step", "text": "→ Chat reply"})
|
|
705
|
+
return False
|
|
606
706
|
_log(f"router error: {e} — defaulting to agent")
|
|
607
707
|
_emit({"type": "step", "text": "→ Agent mode (needs tools)"})
|
|
608
708
|
return True
|
|
@@ -641,8 +741,12 @@ def _summarize_result(task_text: str, agent_output: str,
|
|
|
641
741
|
return agent_output
|
|
642
742
|
|
|
643
743
|
|
|
644
|
-
def _plain_reply(messages: list, base_url: str, api_key: str, model_id: str
|
|
645
|
-
|
|
744
|
+
def _plain_reply(messages: list, base_url: str, api_key: str, model_id: str,
|
|
745
|
+
fallback_base_url: str = "", fallback_model_id: str = "") -> str:
|
|
746
|
+
"""Plain chat completion using the full conversation history — a SMALL prompt (no tool
|
|
747
|
+
preamble), so it returns fast. If the primary model is unreachable (e.g. the local MLX
|
|
748
|
+
server is down) and a distinct fallback model is given (the coding model), retry there
|
|
749
|
+
so a greeting still gets answered instead of erroring."""
|
|
646
750
|
_THINK_RE_LOCAL = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
|
647
751
|
chat_messages = [{"role": "system", "content": "You are a helpful assistant."}]
|
|
648
752
|
for m in messages:
|
|
@@ -655,19 +759,34 @@ def _plain_reply(messages: list, base_url: str, api_key: str, model_id: str) ->
|
|
|
655
759
|
if not content:
|
|
656
760
|
continue
|
|
657
761
|
chat_messages.append({"role": role, "content": content})
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
762
|
+
|
|
763
|
+
targets = [(base_url, model_id)]
|
|
764
|
+
if fallback_base_url and fallback_model_id and (
|
|
765
|
+
fallback_base_url.rstrip("/") != (base_url or "").rstrip("/")
|
|
766
|
+
or fallback_model_id != model_id
|
|
767
|
+
):
|
|
768
|
+
targets.append((fallback_base_url, fallback_model_id))
|
|
769
|
+
|
|
770
|
+
last_err = None
|
|
771
|
+
for i, (b, mid) in enumerate(targets):
|
|
772
|
+
try:
|
|
773
|
+
from openai import OpenAI
|
|
774
|
+
client = OpenAI(base_url=b, api_key=api_key or "local",
|
|
775
|
+
max_retries=0, timeout=60)
|
|
776
|
+
resp = client.chat.completions.create(
|
|
777
|
+
model=mid,
|
|
778
|
+
messages=chat_messages,
|
|
779
|
+
temperature=0.7,
|
|
780
|
+
max_tokens=512,
|
|
781
|
+
)
|
|
782
|
+
return (resp.choices[0].message.content or "").strip()
|
|
783
|
+
except Exception as e: # noqa: BLE001
|
|
784
|
+
last_err = e
|
|
785
|
+
if i + 1 < len(targets):
|
|
786
|
+
nb, nmid = targets[i + 1] # the model we're about to try next
|
|
787
|
+
_log(f"plain reply: {mid} @ {b} failed ({e}); "
|
|
788
|
+
f"falling back to {nmid} @ {nb}")
|
|
789
|
+
return f"[Error: {last_err}]"
|
|
671
790
|
|
|
672
791
|
|
|
673
792
|
def _synthesize_document(gathered: list, task: str, base_url: str, api_key: str,
|
|
@@ -1903,8 +2022,16 @@ def run_agent_chat(cfg):
|
|
|
1903
2022
|
return bool(_EMAIL_AVAILABLE and _EMAIL_CONFIRM.search(latest_user_text or "")
|
|
1904
2023
|
and _prior_email_preview())
|
|
1905
2024
|
|
|
1906
|
-
# Route:
|
|
1907
|
-
|
|
2025
|
+
# Route: does this task need HTTP tool use? A trivial greeting/thank-you/"who are you"
|
|
2026
|
+
# is caught LOCALLY (no model classifier call, no tool preamble) → straight to a fast
|
|
2027
|
+
# small-prompt reply. Otherwise ask the model classifier.
|
|
2028
|
+
if _is_trivial_chat(latest_user_text) and not _AGENT_KEYWORDS.search(latest_user_text or ""):
|
|
2029
|
+
_log("router → NO (trivial greeting/smalltalk — local fast-path, no tool preamble)")
|
|
2030
|
+
_emit({"type": "step", "text": "Routing your request…"})
|
|
2031
|
+
_emit({"type": "step", "text": "→ Chat reply"})
|
|
2032
|
+
needs_agent = False
|
|
2033
|
+
else:
|
|
2034
|
+
needs_agent = _route(latest_user_text, agent_base_url, agent_api_key, agent_model_id)
|
|
1908
2035
|
# A pending email awaiting 'confirm' must reach the agent even though a bare
|
|
1909
2036
|
# 'confirm' / 'send it' is not a network keyword — otherwise the send never fires.
|
|
1910
2037
|
if not needs_agent and _email_confirm_pending():
|
|
@@ -1915,7 +2042,9 @@ def run_agent_chat(cfg):
|
|
|
1915
2042
|
if not needs_agent:
|
|
1916
2043
|
_log("router: plain chat (no HTTP needed)")
|
|
1917
2044
|
_emit({"type": "step", "text": "Composing a reply…"})
|
|
1918
|
-
|
|
2045
|
+
# Small prompt → fast. Fall back to the coding model if the chat model is down.
|
|
2046
|
+
answer = _plain_reply(messages, agent_base_url, agent_api_key, agent_model_id,
|
|
2047
|
+
coding_base_url, coding_model_id)
|
|
1919
2048
|
_log(f"plain reply: {len(answer)} chars")
|
|
1920
2049
|
_emit({"type": "final", "text": answer})
|
|
1921
2050
|
return
|
|
@@ -3478,6 +3607,21 @@ def run_agent_chat(cfg):
|
|
|
3478
3607
|
agent_kwargs["additional_authorized_imports"] = [
|
|
3479
3608
|
"json", "base64", "urllib", "urllib.request", "urllib.error"
|
|
3480
3609
|
]
|
|
3610
|
+
# Trim smolagents' ~6k-char generic few-shot examples from the system prompt
|
|
3611
|
+
# (they dominate prompt-eval on a slow coding model) while keeping the tool
|
|
3612
|
+
# rendering + format rules intact. Load the stock templates and swap only the
|
|
3613
|
+
# system_prompt; fall back to the default on any error.
|
|
3614
|
+
try:
|
|
3615
|
+
import yaml as _yaml
|
|
3616
|
+
import os as _os
|
|
3617
|
+
import smolagents as _sm
|
|
3618
|
+
_tpl = _yaml.safe_load(open(_os.path.join(
|
|
3619
|
+
_os.path.dirname(_sm.__file__), "prompts", "code_agent.yaml")))
|
|
3620
|
+
_tpl["system_prompt"] = _COMPACT_CODE_SYSTEM_PROMPT
|
|
3621
|
+
agent_kwargs["prompt_templates"] = _tpl
|
|
3622
|
+
_log("using compact CodeAgent system prompt (trimmed few-shot examples)")
|
|
3623
|
+
except Exception as _e: # noqa: BLE001
|
|
3624
|
+
_log(f"compact system prompt unavailable ({_e}); using smolagents default")
|
|
3481
3625
|
agent = _ToolAgent(**agent_kwargs)
|
|
3482
3626
|
with contextlib.redirect_stdout(sys.stderr):
|
|
3483
3627
|
result = agent.run(task_with_hint)
|
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.183",
|
|
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",
|