@tiens.nguyen/gonext-local-worker 1.0.181 → 1.0.182

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.
Files changed (2) hide show
  1. package/gonext_agent_chat.py +161 -18
  2. package/package.json +1 -1
@@ -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) -> str:
645
- """Plain chat completion using the full conversation history."""
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,33 @@ 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
- try:
659
- from openai import OpenAI
660
- client = OpenAI(base_url=base_url, api_key=api_key or "local",
661
- max_retries=0, timeout=60)
662
- resp = client.chat.completions.create(
663
- model=model_id,
664
- messages=chat_messages,
665
- temperature=0.7,
666
- max_tokens=512,
667
- )
668
- return (resp.choices[0].message.content or "").strip()
669
- except Exception as e: # noqa: BLE001
670
- return f"[Error: {e}]"
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
+ _log(f"plain reply: primary model failed ({e}); retrying on fallback "
787
+ f"{mid} @ {b}")
788
+ return f"[Error: {last_err}]"
671
789
 
672
790
 
673
791
  def _synthesize_document(gathered: list, task: str, base_url: str, api_key: str,
@@ -1903,8 +2021,16 @@ def run_agent_chat(cfg):
1903
2021
  return bool(_EMAIL_AVAILABLE and _EMAIL_CONFIRM.search(latest_user_text or "")
1904
2022
  and _prior_email_preview())
1905
2023
 
1906
- # Route: ask the model if this task needs HTTP tool use.
1907
- needs_agent = _route(latest_user_text, agent_base_url, agent_api_key, agent_model_id)
2024
+ # Route: does this task need HTTP tool use? A trivial greeting/thank-you/"who are you"
2025
+ # is caught LOCALLY (no model classifier call, no tool preamble) → straight to a fast
2026
+ # small-prompt reply. Otherwise ask the model classifier.
2027
+ if _is_trivial_chat(latest_user_text) and not _AGENT_KEYWORDS.search(latest_user_text or ""):
2028
+ _log("router → NO (trivial greeting/smalltalk — local fast-path, no tool preamble)")
2029
+ _emit({"type": "step", "text": "Routing your request…"})
2030
+ _emit({"type": "step", "text": "→ Chat reply"})
2031
+ needs_agent = False
2032
+ else:
2033
+ needs_agent = _route(latest_user_text, agent_base_url, agent_api_key, agent_model_id)
1908
2034
  # A pending email awaiting 'confirm' must reach the agent even though a bare
1909
2035
  # 'confirm' / 'send it' is not a network keyword — otherwise the send never fires.
1910
2036
  if not needs_agent and _email_confirm_pending():
@@ -1915,7 +2041,9 @@ def run_agent_chat(cfg):
1915
2041
  if not needs_agent:
1916
2042
  _log("router: plain chat (no HTTP needed)")
1917
2043
  _emit({"type": "step", "text": "Composing a reply…"})
1918
- answer = _plain_reply(messages, agent_base_url, agent_api_key, agent_model_id)
2044
+ # Small prompt fast. Fall back to the coding model if the chat model is down.
2045
+ answer = _plain_reply(messages, agent_base_url, agent_api_key, agent_model_id,
2046
+ coding_base_url, coding_model_id)
1919
2047
  _log(f"plain reply: {len(answer)} chars")
1920
2048
  _emit({"type": "final", "text": answer})
1921
2049
  return
@@ -3478,6 +3606,21 @@ def run_agent_chat(cfg):
3478
3606
  agent_kwargs["additional_authorized_imports"] = [
3479
3607
  "json", "base64", "urllib", "urllib.request", "urllib.error"
3480
3608
  ]
3609
+ # Trim smolagents' ~6k-char generic few-shot examples from the system prompt
3610
+ # (they dominate prompt-eval on a slow coding model) while keeping the tool
3611
+ # rendering + format rules intact. Load the stock templates and swap only the
3612
+ # system_prompt; fall back to the default on any error.
3613
+ try:
3614
+ import yaml as _yaml
3615
+ import os as _os
3616
+ import smolagents as _sm
3617
+ _tpl = _yaml.safe_load(open(_os.path.join(
3618
+ _os.path.dirname(_sm.__file__), "prompts", "code_agent.yaml")))
3619
+ _tpl["system_prompt"] = _COMPACT_CODE_SYSTEM_PROMPT
3620
+ agent_kwargs["prompt_templates"] = _tpl
3621
+ _log("using compact CodeAgent system prompt (trimmed few-shot examples)")
3622
+ except Exception as _e: # noqa: BLE001
3623
+ _log(f"compact system prompt unavailable ({_e}); using smolagents default")
3481
3624
  agent = _ToolAgent(**agent_kwargs)
3482
3625
  with contextlib.redirect_stdout(sys.stderr):
3483
3626
  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.181",
3
+ "version": "1.0.182",
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",