@tiens.nguyen/gonext-local-worker 1.0.191 → 1.0.193
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 +244 -102
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -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
|
|
2271
|
-
#
|
|
2272
|
-
#
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
-
|
|
2366
|
-
"
|
|
2367
|
-
"
|
|
2368
|
-
|
|
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 =
|
|
3138
|
-
|
|
3139
|
-
|
|
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
|
|
@@ -3257,6 +3288,14 @@ def run_agent_chat(cfg):
|
|
|
3257
3288
|
text = (_last_obs.get("text") or "").strip()
|
|
3258
3289
|
if text:
|
|
3259
3290
|
_log(f"max-steps fallback (last tool obs) → {text[:80]}")
|
|
3291
|
+
# Without this, `text` (a tool's RAW return value — e.g. a bare grep_repo
|
|
3292
|
+
# dump of file:line hits) becomes the entire chat reply with zero framing,
|
|
3293
|
+
# reading as a bizarre non-answer. A short honest note makes clear this is
|
|
3294
|
+
# a partial result from an interrupted run, not the actual answer/edit.
|
|
3295
|
+
text = (
|
|
3296
|
+
"I ran out of steps before finishing this. Here's the last thing I "
|
|
3297
|
+
"found — reply to have me continue from here:\n\n" + text
|
|
3298
|
+
)
|
|
3260
3299
|
else:
|
|
3261
3300
|
# No usable tool output (e.g. every step's code failed to parse).
|
|
3262
3301
|
# Don't dead-end on a canned apology — answer from the conversation.
|
|
@@ -3313,12 +3352,18 @@ def run_agent_chat(cfg):
|
|
|
3313
3352
|
path, cached = _rag_download(url, (dest_path or "").strip())
|
|
3314
3353
|
if cached:
|
|
3315
3354
|
_emit({"type": "step", "text": "Already downloaded — reusing cached file"})
|
|
3316
|
-
|
|
3317
|
-
|
|
3355
|
+
out = (f"Already downloaded earlier — reusing {path} "
|
|
3356
|
+
f"({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}').")
|
|
3357
|
+
_last_obs["text"] = out
|
|
3358
|
+
return out
|
|
3318
3359
|
_emit({"type": "step", "text": f"Downloading → {url[:80]}"})
|
|
3319
|
-
|
|
3360
|
+
out = f"Downloaded to {path} ({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}') if it is a zip."
|
|
3361
|
+
_last_obs["text"] = out
|
|
3362
|
+
return out
|
|
3320
3363
|
except Exception as e: # noqa: BLE001
|
|
3321
|
-
|
|
3364
|
+
msg = f"Error: download failed: {type(e).__name__}: {e}"
|
|
3365
|
+
_last_obs["text"] = msg
|
|
3366
|
+
return msg
|
|
3322
3367
|
|
|
3323
3368
|
@tool
|
|
3324
3369
|
def unzip_file(zip_path: str, dest_dir: str = "") -> str:
|
|
@@ -3334,12 +3379,18 @@ def run_agent_chat(cfg):
|
|
|
3334
3379
|
more = f" …(+{len(names) - 15} more)" if len(names) > 15 else ""
|
|
3335
3380
|
if cached:
|
|
3336
3381
|
_emit({"type": "step", "text": "Already unzipped — reusing extracted files"})
|
|
3337
|
-
|
|
3338
|
-
|
|
3382
|
+
out = (f"Already unzipped earlier — {len(names)} files at {d}. "
|
|
3383
|
+
f"Files: {preview}{more}. Use list_dir/read_text_file or rag_index on it.")
|
|
3384
|
+
_last_obs["text"] = out
|
|
3385
|
+
return out
|
|
3339
3386
|
_emit({"type": "step", "text": "Unzipping…"})
|
|
3340
|
-
|
|
3387
|
+
out = f"Unzipped {len(names)} files to {d}. Files: {preview}{more}. Next: rag_index(path='{d}', source_url=<the original url>)."
|
|
3388
|
+
_last_obs["text"] = out
|
|
3389
|
+
return out
|
|
3341
3390
|
except Exception as e: # noqa: BLE001
|
|
3342
|
-
|
|
3391
|
+
msg = f"Error: unzip failed: {type(e).__name__}: {e}"
|
|
3392
|
+
_last_obs["text"] = msg
|
|
3393
|
+
return msg
|
|
3343
3394
|
|
|
3344
3395
|
@tool
|
|
3345
3396
|
def list_dir(path: str) -> str:
|
|
@@ -3352,7 +3403,9 @@ def run_agent_chat(cfg):
|
|
|
3352
3403
|
import os
|
|
3353
3404
|
root = _rag_read_allowed((path or ".").strip())
|
|
3354
3405
|
if os.path.isfile(root):
|
|
3355
|
-
|
|
3406
|
+
out = f"{root} is a file ({os.path.getsize(root)} bytes)."
|
|
3407
|
+
_last_obs["text"] = out
|
|
3408
|
+
return out
|
|
3356
3409
|
entries = []
|
|
3357
3410
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
3358
3411
|
dirnames[:] = [d for d in dirnames if d not in _RAG_SKIP_DIRS]
|
|
@@ -3367,9 +3420,13 @@ def run_agent_chat(cfg):
|
|
|
3367
3420
|
break
|
|
3368
3421
|
entries.sort()
|
|
3369
3422
|
more = " …(truncated)" if len(entries) >= 300 else ""
|
|
3370
|
-
|
|
3423
|
+
out = f"{len(entries)} files under {root}:\n" + "\n".join(entries) + more
|
|
3424
|
+
_last_obs["text"] = out
|
|
3425
|
+
return out
|
|
3371
3426
|
except Exception as e: # noqa: BLE001
|
|
3372
|
-
|
|
3427
|
+
msg = f"Error: list_dir failed: {type(e).__name__}: {e}"
|
|
3428
|
+
_last_obs["text"] = msg
|
|
3429
|
+
return msg
|
|
3373
3430
|
|
|
3374
3431
|
@tool
|
|
3375
3432
|
def read_text_file(path: str, max_chars: int = 20000) -> str:
|
|
@@ -3383,14 +3440,22 @@ def run_agent_chat(cfg):
|
|
|
3383
3440
|
import os
|
|
3384
3441
|
rp = _rag_read_allowed((path or "").strip())
|
|
3385
3442
|
if not os.path.isfile(rp):
|
|
3386
|
-
|
|
3443
|
+
msg = f"Error: not a file: {path}"
|
|
3444
|
+
_last_obs["text"] = msg
|
|
3445
|
+
return msg
|
|
3387
3446
|
if os.path.getsize(rp) > 5 * 1024 * 1024:
|
|
3388
|
-
|
|
3447
|
+
msg = "Error: file too large to read (> 5 MB)."
|
|
3448
|
+
_last_obs["text"] = msg
|
|
3449
|
+
return msg
|
|
3389
3450
|
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
3390
3451
|
data = fh.read(max(500, min(int(max_chars or 20000), 100000)))
|
|
3391
|
-
|
|
3452
|
+
out = data or "(empty file)"
|
|
3453
|
+
_last_obs["text"] = out
|
|
3454
|
+
return out
|
|
3392
3455
|
except Exception as e: # noqa: BLE001
|
|
3393
|
-
|
|
3456
|
+
msg = f"Error: read_text_file failed: {type(e).__name__}: {e}"
|
|
3457
|
+
_last_obs["text"] = msg
|
|
3458
|
+
return msg
|
|
3394
3459
|
|
|
3395
3460
|
@tool
|
|
3396
3461
|
def rag_index(path: str, source_url: str) -> str:
|
|
@@ -3401,7 +3466,9 @@ def run_agent_chat(cfg):
|
|
|
3401
3466
|
source_url: the original URL the user gave — used as the knowledge-base key.
|
|
3402
3467
|
"""
|
|
3403
3468
|
if not _RAG_AVAILABLE:
|
|
3404
|
-
|
|
3469
|
+
msg = "Error: RAG is not configured (enable it + add AWS credentials in Settings)."
|
|
3470
|
+
_last_obs["text"] = msg
|
|
3471
|
+
return msg
|
|
3405
3472
|
_emit({"type": "step", "text": f"RAG source → {source_url}"})
|
|
3406
3473
|
_emit({"type": "step", "text": "Indexing files for RAG…"})
|
|
3407
3474
|
try:
|
|
@@ -3417,7 +3484,9 @@ def run_agent_chat(cfg):
|
|
|
3417
3484
|
files += 1
|
|
3418
3485
|
records.extend(_rag_chunk_text(text, rel, ext))
|
|
3419
3486
|
if not records:
|
|
3420
|
-
|
|
3487
|
+
msg = "No indexable text files found at that path."
|
|
3488
|
+
_last_obs["text"] = msg
|
|
3489
|
+
return msg
|
|
3421
3490
|
for i in range(0, len(records), 64):
|
|
3422
3491
|
batch = records[i:i + 64]
|
|
3423
3492
|
vecs = _embed(rag_embed_base, rag_embed_model, [r["text"] for r in batch])
|
|
@@ -3438,10 +3507,14 @@ def run_agent_chat(cfg):
|
|
|
3438
3507
|
client.put_object(Bucket=bucket, Key=f"{base}/manifest.json",
|
|
3439
3508
|
Body=json.dumps(manifest).encode("utf-8"),
|
|
3440
3509
|
ContentType="application/json")
|
|
3441
|
-
|
|
3442
|
-
|
|
3510
|
+
out = (f"Indexed {files} files into {len(records)} chunks for {source_url}. "
|
|
3511
|
+
f"Use rag_search(source_url='{source_url}', query=...) to retrieve, then answer the user.")
|
|
3512
|
+
_last_obs["text"] = out
|
|
3513
|
+
return out
|
|
3443
3514
|
except Exception as e: # noqa: BLE001
|
|
3444
|
-
|
|
3515
|
+
msg = f"Error: indexing failed: {type(e).__name__}: {e}"
|
|
3516
|
+
_last_obs["text"] = msg
|
|
3517
|
+
return msg
|
|
3445
3518
|
|
|
3446
3519
|
@tool
|
|
3447
3520
|
def rag_add(source_url: str, text: str) -> str:
|
|
@@ -3452,9 +3525,13 @@ def run_agent_chat(cfg):
|
|
|
3452
3525
|
text: the new information to embed and store.
|
|
3453
3526
|
"""
|
|
3454
3527
|
if not _RAG_AVAILABLE:
|
|
3455
|
-
|
|
3528
|
+
msg = "Error: RAG is not configured."
|
|
3529
|
+
_last_obs["text"] = msg
|
|
3530
|
+
return msg
|
|
3456
3531
|
if not (text or "").strip():
|
|
3457
|
-
|
|
3532
|
+
msg = "Error: no text to add."
|
|
3533
|
+
_last_obs["text"] = msg
|
|
3534
|
+
return msg
|
|
3458
3535
|
_emit({"type": "step", "text": f"RAG source → {source_url}"})
|
|
3459
3536
|
_emit({"type": "step", "text": "Adding info to RAG…"})
|
|
3460
3537
|
try:
|
|
@@ -3468,9 +3545,13 @@ def run_agent_chat(cfg):
|
|
|
3468
3545
|
shard = f"{base}/chunks-add-{int(_t.time())}.jsonl"
|
|
3469
3546
|
body = "\n".join(json.dumps(r, ensure_ascii=False) for r in records).encode("utf-8")
|
|
3470
3547
|
client.put_object(Bucket=bucket, Key=shard, Body=body, ContentType="application/x-ndjson")
|
|
3471
|
-
|
|
3548
|
+
out = f"Added {len(records)} chunk(s) to the knowledge base for {source_url}."
|
|
3549
|
+
_last_obs["text"] = out
|
|
3550
|
+
return out
|
|
3472
3551
|
except Exception as e: # noqa: BLE001
|
|
3473
|
-
|
|
3552
|
+
msg = f"Error: rag_add failed: {type(e).__name__}: {e}"
|
|
3553
|
+
_last_obs["text"] = msg
|
|
3554
|
+
return msg
|
|
3474
3555
|
|
|
3475
3556
|
@tool
|
|
3476
3557
|
def rag_search(source_url: str, query: str, k: int = 0) -> str:
|
|
@@ -3482,7 +3563,9 @@ def run_agent_chat(cfg):
|
|
|
3482
3563
|
k: number of chunks to return (0 = configured default).
|
|
3483
3564
|
"""
|
|
3484
3565
|
if not _RAG_AVAILABLE:
|
|
3485
|
-
|
|
3566
|
+
msg = "Error: RAG is not configured."
|
|
3567
|
+
_last_obs["text"] = msg
|
|
3568
|
+
return msg
|
|
3486
3569
|
_emit({"type": "step", "text": f"RAG source → {source_url}"})
|
|
3487
3570
|
_emit({"type": "step", "text": f"Searching RAG → {query[:60]}"})
|
|
3488
3571
|
try:
|
|
@@ -3490,7 +3573,9 @@ def run_agent_chat(cfg):
|
|
|
3490
3573
|
base = _rag_prefix_for(source_url, prefix)
|
|
3491
3574
|
chunks = _rag_load_chunks(client, bucket, base)
|
|
3492
3575
|
if not chunks:
|
|
3493
|
-
|
|
3576
|
+
msg = f"No knowledge base found for {source_url}. Run rag_index first."
|
|
3577
|
+
_last_obs["text"] = msg
|
|
3578
|
+
return msg
|
|
3494
3579
|
qvec = _embed(rag_embed_base, rag_embed_model, [query])[0]
|
|
3495
3580
|
scored = sorted(
|
|
3496
3581
|
((_cosine(qvec, c.get("embedding") or []), c) for c in chunks),
|
|
@@ -3499,9 +3584,13 @@ def run_agent_chat(cfg):
|
|
|
3499
3584
|
topk = scored[: (k if k and k > 0 else rag_top_k)]
|
|
3500
3585
|
parts = [f"[{c.get('file', '?')}] (score {score:.2f})\n{c.get('text', '')}"
|
|
3501
3586
|
for score, c in topk]
|
|
3502
|
-
|
|
3587
|
+
out = "\n\n---\n\n".join(parts) if parts else "No matches."
|
|
3588
|
+
_last_obs["text"] = out
|
|
3589
|
+
return out
|
|
3503
3590
|
except Exception as e: # noqa: BLE001
|
|
3504
|
-
|
|
3591
|
+
msg = f"Error: rag_search failed: {type(e).__name__}: {e}"
|
|
3592
|
+
_last_obs["text"] = msg
|
|
3593
|
+
return msg
|
|
3505
3594
|
|
|
3506
3595
|
@tool
|
|
3507
3596
|
def grep_repo(pattern: str, path: str = "", glob: str = "") -> str:
|
|
@@ -3542,11 +3631,17 @@ def run_agent_chat(cfg):
|
|
|
3542
3631
|
break
|
|
3543
3632
|
_emit({"type": "step", "text": f"Searching code → {pattern[:60]} ({len(hits)} hits)"})
|
|
3544
3633
|
if not hits:
|
|
3545
|
-
|
|
3634
|
+
msg = f"No matches for {pattern!r} under {root}."
|
|
3635
|
+
_last_obs["text"] = msg
|
|
3636
|
+
return msg
|
|
3546
3637
|
more = "\n…(capped at 100 hits)" if len(hits) >= 100 else ""
|
|
3547
|
-
|
|
3638
|
+
out = f"Matches under {root}:\n" + "\n".join(hits) + more
|
|
3639
|
+
_last_obs["text"] = out
|
|
3640
|
+
return out
|
|
3548
3641
|
except Exception as e: # noqa: BLE001
|
|
3549
|
-
|
|
3642
|
+
msg = f"Error: grep_repo failed: {type(e).__name__}: {e}"
|
|
3643
|
+
_last_obs["text"] = msg
|
|
3644
|
+
return msg
|
|
3550
3645
|
|
|
3551
3646
|
@tool
|
|
3552
3647
|
def read_file_lines(path: str, start_line: int = 1, end_line: int = 0) -> str:
|
|
@@ -3565,9 +3660,13 @@ def run_agent_chat(cfg):
|
|
|
3565
3660
|
e = int(end_line or 0) or len(lines)
|
|
3566
3661
|
e = min(e, len(lines), s + 399)
|
|
3567
3662
|
body = "".join(f"{i}: {lines[i - 1]}" for i in range(s, e + 1))
|
|
3568
|
-
|
|
3663
|
+
out = f"{rp} (lines {s}-{e} of {len(lines)}):\n{body}"
|
|
3664
|
+
_last_obs["text"] = out
|
|
3665
|
+
return out
|
|
3569
3666
|
except Exception as ex: # noqa: BLE001
|
|
3570
|
-
|
|
3667
|
+
msg = f"Error: read_file_lines failed: {type(ex).__name__}: {ex}"
|
|
3668
|
+
_last_obs["text"] = msg
|
|
3669
|
+
return msg
|
|
3571
3670
|
|
|
3572
3671
|
@tool
|
|
3573
3672
|
def edit_lines(path: str, start_line: int, end_line: int, new_content: str) -> str:
|
|
@@ -3583,14 +3682,18 @@ def run_agent_chat(cfg):
|
|
|
3583
3682
|
rp = _ws_write_allowed((path or "").strip())
|
|
3584
3683
|
import os
|
|
3585
3684
|
if not os.path.isfile(rp):
|
|
3586
|
-
|
|
3685
|
+
msg = f"Error: not a file: {path}"
|
|
3686
|
+
_last_obs["text"] = msg
|
|
3687
|
+
return msg
|
|
3587
3688
|
backup = _ws_snapshot(rp)
|
|
3588
3689
|
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
3589
3690
|
lines = fh.readlines()
|
|
3590
3691
|
s, e = int(start_line), int(end_line)
|
|
3591
3692
|
if not (1 <= s <= e <= len(lines)):
|
|
3592
|
-
|
|
3593
|
-
|
|
3693
|
+
msg = (f"Error: line range {s}-{e} is out of bounds "
|
|
3694
|
+
f"(file has {len(lines)} lines). Re-check with read_file_lines.")
|
|
3695
|
+
_last_obs["text"] = msg
|
|
3696
|
+
return msg
|
|
3594
3697
|
new_lines = new_content.splitlines(keepends=True)
|
|
3595
3698
|
if new_lines and not new_lines[-1].endswith("\n"):
|
|
3596
3699
|
new_lines[-1] += "\n"
|
|
@@ -3601,10 +3704,14 @@ def run_agent_chat(cfg):
|
|
|
3601
3704
|
_emit({"type": "step", "text": f"Edited {os.path.basename(rp)} lines {s}-{e}"})
|
|
3602
3705
|
shown = "".join(f"{i}: {lines[i - 1]}" for i in
|
|
3603
3706
|
range(max(1, s - 2), min(len(lines), s + len(new_lines) + 1) + 1))
|
|
3604
|
-
|
|
3605
|
-
|
|
3707
|
+
out = (f"Replaced lines {s}-{e} of {rp} with {len(new_lines)} line(s). "
|
|
3708
|
+
f"Backup saved. Updated region:\n{shown}")
|
|
3709
|
+
_last_obs["text"] = out
|
|
3710
|
+
return out
|
|
3606
3711
|
except Exception as ex: # noqa: BLE001
|
|
3607
|
-
|
|
3712
|
+
msg = f"Error: edit_lines failed: {type(ex).__name__}: {ex}"
|
|
3713
|
+
_last_obs["text"] = msg
|
|
3714
|
+
return msg
|
|
3608
3715
|
|
|
3609
3716
|
@tool
|
|
3610
3717
|
def edit_file(path: str, old_string: str, new_string: str) -> str:
|
|
@@ -3619,23 +3726,33 @@ def run_agent_chat(cfg):
|
|
|
3619
3726
|
rp = _ws_write_allowed((path or "").strip())
|
|
3620
3727
|
import os
|
|
3621
3728
|
if not os.path.isfile(rp):
|
|
3622
|
-
|
|
3729
|
+
msg = f"Error: not a file: {path}"
|
|
3730
|
+
_last_obs["text"] = msg
|
|
3731
|
+
return msg
|
|
3623
3732
|
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
3624
3733
|
text = fh.read()
|
|
3625
3734
|
n = text.count(old_string)
|
|
3626
3735
|
if n == 0:
|
|
3627
|
-
|
|
3628
|
-
|
|
3736
|
+
msg = ("Error: old_string not found — it must match EXACTLY "
|
|
3737
|
+
"(check whitespace with read_file_lines, or use edit_lines).")
|
|
3738
|
+
_last_obs["text"] = msg
|
|
3739
|
+
return msg
|
|
3629
3740
|
if n > 1:
|
|
3630
|
-
|
|
3741
|
+
msg = f"Error: old_string occurs {n} times — add surrounding context to make it unique, or use edit_lines."
|
|
3742
|
+
_last_obs["text"] = msg
|
|
3743
|
+
return msg
|
|
3631
3744
|
backup = _ws_snapshot(rp)
|
|
3632
3745
|
with open(rp, "w", encoding="utf-8") as fh:
|
|
3633
3746
|
fh.write(text.replace(old_string, new_string, 1))
|
|
3634
3747
|
_ws_record_change("edit", rp, backup)
|
|
3635
3748
|
_emit({"type": "step", "text": f"Edited {os.path.basename(rp)}"})
|
|
3636
|
-
|
|
3749
|
+
out = f"Replaced 1 occurrence in {rp}. Backup saved."
|
|
3750
|
+
_last_obs["text"] = out
|
|
3751
|
+
return out
|
|
3637
3752
|
except Exception as ex: # noqa: BLE001
|
|
3638
|
-
|
|
3753
|
+
msg = f"Error: edit_file failed: {type(ex).__name__}: {ex}"
|
|
3754
|
+
_last_obs["text"] = msg
|
|
3755
|
+
return msg
|
|
3639
3756
|
|
|
3640
3757
|
@tool
|
|
3641
3758
|
def create_file(path: str, content: str) -> str:
|
|
@@ -3649,15 +3766,21 @@ def run_agent_chat(cfg):
|
|
|
3649
3766
|
rp = _ws_write_allowed((path or "").strip())
|
|
3650
3767
|
import os
|
|
3651
3768
|
if os.path.exists(rp):
|
|
3652
|
-
|
|
3769
|
+
msg = f"Error: {path} already exists — use edit_lines/edit_file to change it."
|
|
3770
|
+
_last_obs["text"] = msg
|
|
3771
|
+
return msg
|
|
3653
3772
|
os.makedirs(os.path.dirname(rp) or ".", exist_ok=True)
|
|
3654
3773
|
with open(rp, "w", encoding="utf-8") as fh:
|
|
3655
3774
|
fh.write(content)
|
|
3656
3775
|
_ws_record_change("create", rp, None)
|
|
3657
3776
|
_emit({"type": "step", "text": f"Created {os.path.basename(rp)}"})
|
|
3658
|
-
|
|
3777
|
+
out = f"Created {rp} ({len(content)} chars)."
|
|
3778
|
+
_last_obs["text"] = out
|
|
3779
|
+
return out
|
|
3659
3780
|
except Exception as ex: # noqa: BLE001
|
|
3660
|
-
|
|
3781
|
+
msg = f"Error: create_file failed: {type(ex).__name__}: {ex}"
|
|
3782
|
+
_last_obs["text"] = msg
|
|
3783
|
+
return msg
|
|
3661
3784
|
|
|
3662
3785
|
@tool
|
|
3663
3786
|
def run_command(command: str, workdir: str = "", timeout_seconds: int = 180) -> str:
|
|
@@ -3675,14 +3798,20 @@ def run_agent_chat(cfg):
|
|
|
3675
3798
|
wd = _rag_read_allowed((workdir or "").strip() or (_WS_ROOTS[0]["path"] if _WS_ROOTS else "."))
|
|
3676
3799
|
w = _ws_for_path(wd)
|
|
3677
3800
|
if w is None or not w.get("allowRun"):
|
|
3678
|
-
|
|
3679
|
-
|
|
3801
|
+
msg = ("Error: running commands is not enabled for this workspace. "
|
|
3802
|
+
"Re-register it with: gonext-local-worker workspace add <path> --allow-run")
|
|
3803
|
+
_last_obs["text"] = msg
|
|
3804
|
+
return msg
|
|
3680
3805
|
argv = shlex.split(command or "")
|
|
3681
3806
|
if not argv:
|
|
3682
|
-
|
|
3807
|
+
msg = "Error: empty command."
|
|
3808
|
+
_last_obs["text"] = msg
|
|
3809
|
+
return msg
|
|
3683
3810
|
if argv[0] not in _WS_RUN_ALLOWED:
|
|
3684
|
-
|
|
3685
|
-
|
|
3811
|
+
msg = (f"Error: '{argv[0]}' is not an allowed runner. Allowed: "
|
|
3812
|
+
+ ", ".join(sorted(_WS_RUN_ALLOWED)))
|
|
3813
|
+
_last_obs["text"] = msg
|
|
3814
|
+
return msg
|
|
3686
3815
|
_emit({"type": "step", "text": f"Running → {command[:70]}"})
|
|
3687
3816
|
t = max(5, min(int(timeout_seconds or 180), 600))
|
|
3688
3817
|
# Scrubbed env: repo test scripts must never see worker secrets.
|
|
@@ -3693,16 +3822,23 @@ def run_agent_chat(cfg):
|
|
|
3693
3822
|
out = _ws_distill_output(proc.stdout or "")
|
|
3694
3823
|
status = "PASSED (exit 0)" if proc.returncode == 0 else f"FAILED (exit {proc.returncode})"
|
|
3695
3824
|
_emit({"type": "step", "text": f"Command {status.split()[0].lower()} → {command[:50]}"})
|
|
3696
|
-
|
|
3825
|
+
result = f"{status}\n{out}" if out.strip() else status
|
|
3826
|
+
_last_obs["text"] = result
|
|
3827
|
+
return result
|
|
3697
3828
|
except subprocess.TimeoutExpired:
|
|
3698
|
-
|
|
3829
|
+
msg = f"Error: command timed out after {timeout_seconds}s."
|
|
3830
|
+
_last_obs["text"] = msg
|
|
3831
|
+
return msg
|
|
3699
3832
|
except Exception as ex: # noqa: BLE001
|
|
3700
|
-
|
|
3833
|
+
msg = f"Error: run_command failed: {type(ex).__name__}: {ex}"
|
|
3834
|
+
_last_obs["text"] = msg
|
|
3835
|
+
return msg
|
|
3701
3836
|
|
|
3702
3837
|
agent_tools = [http_request, web_search, fetch_url, calculate,
|
|
3703
3838
|
get_current_datetime, create_pdf]
|
|
3704
|
-
# Only register the PDF reader / email / RAG tools when available
|
|
3705
|
-
#
|
|
3839
|
+
# Only register the PDF reader / email / RAG tools when available — the model
|
|
3840
|
+
# never sees a tool it can't run, since the task-hint's tool list (filled in just
|
|
3841
|
+
# below) is generated FROM this exact list, so it's always in sync by construction.
|
|
3706
3842
|
if _PDF_READ_AVAILABLE:
|
|
3707
3843
|
agent_tools.append(extract_text_from_pdf)
|
|
3708
3844
|
if _EMAIL_AVAILABLE:
|
|
@@ -3717,6 +3853,12 @@ def run_agent_chat(cfg):
|
|
|
3717
3853
|
# (S3 indexing) isn't configured.
|
|
3718
3854
|
if not _RAG_AVAILABLE:
|
|
3719
3855
|
agent_tools += [list_dir, read_text_file]
|
|
3856
|
+
# Fill in the numbered tool list now that the REAL tool objects exist — see
|
|
3857
|
+
# _render_numbered_tool_list and the {{TOOL_LIST}} placeholder left in tool_hint.
|
|
3858
|
+
task_with_hint = task_with_hint.replace(
|
|
3859
|
+
"{{TOOL_LIST}}",
|
|
3860
|
+
f"You have {len(agent_tools)} tools:\n" + _render_numbered_tool_list(agent_tools),
|
|
3861
|
+
)
|
|
3720
3862
|
agent_kwargs = dict(
|
|
3721
3863
|
tools=agent_tools,
|
|
3722
3864
|
model=model,
|
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.193",
|
|
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",
|