@tiens.nguyen/gonext-local-worker 1.0.191 → 1.0.192

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.
@@ -476,6 +476,28 @@ def _clip(text: str, limit: int = 280) -> str:
476
476
  return f"{cut}…"
477
477
 
478
478
 
479
+ def _render_numbered_tool_list(tools) -> str:
480
+ """Render ' N. name(args) — description' lines from the REAL registered smolagents
481
+ tool objects — their actual name/inputs/docstring — instead of a hand-maintained
482
+ duplicate of the same information. This is the single source of truth for what's
483
+ ACTUALLY callable, so the list can never drift out of sync with the tool set (e.g.
484
+ when RAG or a workspace is enabled/disabled) and the numbering is always exactly
485
+ correct, unlike the old manually-incremented counter it replaces. Kept intentionally
486
+ terse — first docstring line only, bare arg names — since the system prompt's own
487
+ `tool.to_code_prompt()` rendering (see _COMPACT_CODE_SYSTEM_PROMPT) already shows the
488
+ full typed signature on every model call; this is just a quick-reference index."""
489
+ lines = []
490
+ for i, t in enumerate(tools, 1):
491
+ args = []
492
+ for pname, pinfo in (getattr(t, "inputs", None) or {}).items():
493
+ optional = isinstance(pinfo, dict) and pinfo.get("nullable")
494
+ args.append(f"{pname}=''" if optional and pinfo.get("type") == "string" else pname)
495
+ desc_full = (getattr(t, "description", "") or "").strip()
496
+ desc = desc_full.splitlines()[0] if desc_full else ""
497
+ lines.append(f" {i}. {t.name}({', '.join(args)}) — {desc}")
498
+ return ("\n".join(lines) + "\n") if lines else ""
499
+
500
+
479
501
  def _summarise_step(step_log):
480
502
  """Return a short human-readable description of an agent step."""
481
503
  tool_calls = getattr(step_log, "tool_calls", None) or []
@@ -598,6 +620,31 @@ def _is_trivial_chat(text: str) -> bool:
598
620
  return bool(_TRIVIAL_CHAT.match(t))
599
621
 
600
622
 
623
+ # A weak/confused coding model sometimes "thinks out loud" in plain prose instead of
624
+ # emitting the next tool call — e.g. a rambling, self-correcting scratchpad ("Wait,
625
+ # actually... Let's try... Actually, let's just...") that can run thousands of chars.
626
+ # The prose-final-answer heuristic below (long text with list-like structure) can't tell
627
+ # this apart from a genuine, complete answer on structure alone — both use headings/
628
+ # numbered lists. These two signals catch the scratchpad case specifically: it either
629
+ # self-labels as "Thought"/"Thinking Process" up front (a real answer doesn't open by
630
+ # naming itself as the model's own reasoning), or it's dense with self-correction
631
+ # language a finished answer wouldn't contain.
632
+ _SCRATCHPAD_START_RE = re.compile(r"^\s*thought\b\s*[:\n]", re.I)
633
+ _DELIBERATION_RE = re.compile(
634
+ r"\b(wait,|actually,|let'?s (try|just|use|list|see|go)|"
635
+ r"i (?:need to|should) (?:check|verify|confirm))\b",
636
+ re.I,
637
+ )
638
+
639
+
640
+ def _looks_like_scratchpad(stripped: str) -> bool:
641
+ """True if `stripped` reads like unresolved internal deliberation rather than a
642
+ finished, user-facing answer — see module comment above for the two signals used."""
643
+ if _SCRATCHPAD_START_RE.match(stripped):
644
+ return True
645
+ return len(_DELIBERATION_RE.findall(stripped)) >= 3
646
+
647
+
601
648
  # Compact replacement for smolagents' default CodeAgent system prompt. The stock template
602
649
  # is ~9.9k chars, dominated by ~6k chars of GENERIC few-shot examples (image captions,
603
650
  # Wikipedia, etc.) that are irrelevant to our HTTP/file tools and cost real prompt-eval
@@ -2267,25 +2314,12 @@ def run_agent_chat(cfg):
2267
2314
  # fabricate URLs/responses, and always terminate with final_answer().
2268
2315
  from datetime import datetime as _dt_now
2269
2316
  now_str = _dt_now.now().astimezone().strftime("%A, %d %B %Y, %H:%M %Z")
2270
- # The PDF reader and send_email are only advertised (and registered) when available
2271
- # here never offer the model a tool it cannot run. Entry numbers are dynamic so the
2272
- # list reads cleanly whether 6, 7, or 8 tools are present.
2273
- _tool_num = 6
2274
- _pdf_num = 0
2275
- if _PDF_READ_AVAILABLE:
2276
- _tool_num += 1
2277
- _pdf_num = _tool_num
2278
- _email_num = 0
2279
- if _EMAIL_AVAILABLE:
2280
- _tool_num += 1
2281
- _email_num = _tool_num
2282
- if _RAG_AVAILABLE:
2283
- _tool_num += 7 # download_file, unzip_file, list_dir, read_text_file, rag_index/add/search
2284
- if _WS_AVAILABLE:
2285
- _tool_num += 6 # grep_repo, read_file_lines, edit_lines, edit_file, create_file, run_command
2286
- if not _RAG_AVAILABLE:
2287
- _tool_num += 2 # list_dir + read_text_file registered for workspace browsing
2288
- _tool_count_word = {6: "SIX", 7: "SEVEN", 8: "EIGHT"}.get(_tool_num, str(_tool_num))
2317
+ # The numbered tool list itself is generated LATER from the real registered tool
2318
+ # objects (see _render_numbered_tool_list, spliced into task_with_hint below via the
2319
+ # {{TOOL_LIST}} placeholder) it can never drift or miscount, unlike a hand-written
2320
+ # list with a manually-incremented number. The PDF reader / send_email / RAG /
2321
+ # workspace tools are only ever REGISTERED (further below) when available, so an
2322
+ # unavailable tool is never advertised.
2289
2323
  _rag_tool_block = (
2290
2324
  " FILES & KNOWLEDGE BASE — to SUMMARIZE or ANSWER questions about a ZIP of files at a URL:\n"
2291
2325
  " - download_file(url) — download a file (e.g. a .zip) to this machine.\n"
@@ -2339,18 +2373,10 @@ def run_agent_chat(cfg):
2339
2373
  "AFTER editing, re-read with grep_repo/read_file_lines (an S3 knowledge base may be "
2340
2374
  "stale — do not trust rag_search for freshly edited code).\n"
2341
2375
  ) if _WS_AVAILABLE else ""
2342
- _pdf_read_tool_line = (
2343
- f" {_pdf_num}. extract_text_from_pdf(url) — READ/summarize an EXISTING PDF file "
2344
- "at a URL. This does NOT create a PDF (use create_pdf for that).\n"
2345
- ) if _PDF_READ_AVAILABLE else ""
2346
2376
  _pdf_read_choose_line = (
2347
2377
  "- 'read'/'summarize'/'what does the PDF say' for an existing PDF URL -> "
2348
2378
  "extract_text_from_pdf(url).\n"
2349
2379
  ) if _PDF_READ_AVAILABLE else ""
2350
- _email_tool_line = (
2351
- f" {_email_num}. send_email(to, subject, body) — send an email via the configured "
2352
- "email API. It PREVIEWS first and only sends after the user replies 'confirm'.\n"
2353
- ) if _EMAIL_AVAILABLE else ""
2354
2380
  _email_choose_line = (
2355
2381
  "- 'email …' / 'send an email to …' -> send_email(to, subject, body) "
2356
2382
  "(previews first, then sends on confirm).\n"
@@ -2362,20 +2388,10 @@ def run_agent_chat(cfg):
2362
2388
  "next (e.g. web_search to find a URL, THEN fetch_url to read it). When you have "
2363
2389
  "enough to answer, call final_answer(<your answer>) in a code block. Prefer "
2364
2390
  "FEWER steps, and never repeat a tool call that already succeeded.\n\n"
2365
- f"You have {_tool_count_word} tools:\n"
2366
- " 1. http_request(method, url, headers='', body='', username='', password='') "
2367
- "call a SPECIFIC known API/URL.\n"
2368
- " 2. web_search(query) look up facts about a person, place, thing, or topic "
2369
- "when you do NOT already have a real URL. Returns a summary + source.\n"
2370
- " 3. fetch_url(url) — READ the full text of a specific web page (e.g. a link the "
2371
- "user gave, or a source URL from web_search). Returns clean text, not raw HTML.\n"
2372
- " 4. calculate(expression) — do math: arithmetic, percentages, powers "
2373
- "(e.g. '15% of 80', '(3+4)*2'). ALWAYS use this instead of computing yourself.\n"
2374
- f" 5. get_current_datetime(timezone='') — current date/time ONLY (now: {now_str}). "
2375
- "Use this ONLY when the task explicitly asks for the date or time.\n"
2376
- " 6. create_pdf(text, title='') — make a PDF document from text/data and return a "
2377
- "download link. Use ONLY when the task asks to create/make/generate/export a PDF.\n"
2378
- + _pdf_read_tool_line + _email_tool_line + _rag_tool_block + _ws_tool_block +
2391
+ "{{TOOL_LIST}}\n" # filled in below, once from the REAL registered tool objects
2392
+ f"(Current date/time: {now_str} pass a timezone to get_current_datetime() only "
2393
+ "if the task needs a DIFFERENT one.)\n"
2394
+ + _rag_tool_block + _ws_tool_block +
2379
2395
  "\n"
2380
2396
  "http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
2381
2397
  "\n"
@@ -3134,9 +3150,11 @@ def run_agent_chat(cfg):
3134
3150
  msg.content = normalized
3135
3151
  else:
3136
3152
  stripped = content.strip()
3137
- looks_final = len(stripped) >= 400 or re.search(
3138
- r"(^|\n)#{1,4}\s|\n\d+\.\s|\n[-*]\s|\|.+\|", stripped
3139
- ) is not None
3153
+ looks_final = (
3154
+ len(stripped) >= 400 or re.search(
3155
+ r"(^|\n)#{1,4}\s|\n\d+\.\s|\n[-*]\s|\|.+\|", stripped
3156
+ ) is not None
3157
+ ) and not _looks_like_scratchpad(stripped)
3140
3158
  if stripped and looks_final:
3141
3159
  _log(
3142
3160
  f"prose-only turn ({len(stripped)} chars, no tool call) "
@@ -3145,6 +3163,19 @@ def run_agent_chat(cfg):
3145
3163
  msg.content = (
3146
3164
  stripped + "\n<code>\nfinal_answer(" + repr(stripped) + ")\n</code>"
3147
3165
  )
3166
+ elif stripped and _looks_like_scratchpad(stripped):
3167
+ # Looks like unresolved deliberation, not a finished answer — do
3168
+ # NOT wrap it as final_answer (that would dump this rambling
3169
+ # scratchpad into the user-facing reply AND permanently pollute
3170
+ # every future turn's conversation history with it, seen live:
3171
+ # a 6000+ char "Thinking Process:" dump derailed the next two
3172
+ # unrelated questions). Leave content as-is: smolagents' own
3173
+ # code-block parser will fail to find one and burn this step as
3174
+ # a normal parse-error retry, giving the model another attempt.
3175
+ _log(
3176
+ f"scratchpad-like prose ({len(stripped)} chars, no tool call) "
3177
+ "→ NOT auto-wrapping; letting the step fail normally"
3178
+ )
3148
3179
  except Exception as e: # noqa: BLE001
3149
3180
  _log(f"content-normalize error: {e}")
3150
3181
  return msg
@@ -3701,8 +3732,9 @@ def run_agent_chat(cfg):
3701
3732
 
3702
3733
  agent_tools = [http_request, web_search, fetch_url, calculate,
3703
3734
  get_current_datetime, create_pdf]
3704
- # Only register the PDF reader / email / RAG tools when available (kept in sync
3705
- # with the gated hint entries above so the model never sees a tool it can't run).
3735
+ # Only register the PDF reader / email / RAG tools when available the model
3736
+ # never sees a tool it can't run, since the task-hint's tool list (filled in just
3737
+ # below) is generated FROM this exact list, so it's always in sync by construction.
3706
3738
  if _PDF_READ_AVAILABLE:
3707
3739
  agent_tools.append(extract_text_from_pdf)
3708
3740
  if _EMAIL_AVAILABLE:
@@ -3717,6 +3749,12 @@ def run_agent_chat(cfg):
3717
3749
  # (S3 indexing) isn't configured.
3718
3750
  if not _RAG_AVAILABLE:
3719
3751
  agent_tools += [list_dir, read_text_file]
3752
+ # Fill in the numbered tool list now that the REAL tool objects exist — see
3753
+ # _render_numbered_tool_list and the {{TOOL_LIST}} placeholder left in tool_hint.
3754
+ task_with_hint = task_with_hint.replace(
3755
+ "{{TOOL_LIST}}",
3756
+ f"You have {len(agent_tools)} tools:\n" + _render_numbered_tool_list(agent_tools),
3757
+ )
3720
3758
  agent_kwargs = dict(
3721
3759
  tools=agent_tools,
3722
3760
  model=model,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.191",
3
+ "version": "1.0.192",
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",