@tiens.nguyen/gonext-local-worker 1.0.160 → 1.0.164
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 +118 -12
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -1156,17 +1156,39 @@ def _rag_workdir(source_key: str) -> str:
|
|
|
1156
1156
|
return base
|
|
1157
1157
|
|
|
1158
1158
|
|
|
1159
|
-
def _rag_download(url: str, dest_path: str = "") ->
|
|
1160
|
-
"""Download a public URL
|
|
1159
|
+
def _rag_download(url: str, dest_path: str = "") -> tuple:
|
|
1160
|
+
"""Download a public URL with SSRF + size guards — IDEMPOTENT per URL. Every URL
|
|
1161
|
+
maps to a deterministic work dir (~/.gonext/rag-work/<urlHash>/); relative
|
|
1162
|
+
dest_paths are contained there (never the process cwd, which is the user's home),
|
|
1163
|
+
and a repeat call for an already-downloaded URL reuses the cached file instantly
|
|
1164
|
+
(follow-up questions re-run the pipeline cheaply). Returns (path, cached)."""
|
|
1161
1165
|
import os
|
|
1162
1166
|
src = _rag_gdrive_direct((url or "").strip())
|
|
1163
1167
|
_rag_assert_safe_url(src)
|
|
1164
1168
|
key = _rag_source_key(url)
|
|
1169
|
+
workdir = _rag_workdir(key)
|
|
1170
|
+
dest_path = os.path.expanduser((dest_path or "").strip())
|
|
1165
1171
|
if not dest_path:
|
|
1166
1172
|
name = os.path.basename(src.split("?")[0]) or "download.bin"
|
|
1167
1173
|
if not name.lower().endswith(".zip") and "zip" in src.lower():
|
|
1168
1174
|
name += ".zip"
|
|
1169
|
-
dest_path = os.path.join(
|
|
1175
|
+
dest_path = os.path.join(workdir, name)
|
|
1176
|
+
elif not os.path.isabs(dest_path):
|
|
1177
|
+
# Contain relative paths in the per-URL work dir — models pass bare names like
|
|
1178
|
+
# "project.zip" which would otherwise land in the worker's cwd (the home dir).
|
|
1179
|
+
dest_path = os.path.join(workdir, dest_path)
|
|
1180
|
+
# Cache hit 1: the exact target already exists.
|
|
1181
|
+
if os.path.isfile(dest_path) and os.path.getsize(dest_path) > 0:
|
|
1182
|
+
return dest_path, True
|
|
1183
|
+
# Cache hit 2: this URL was downloaded before under a different name — the work dir
|
|
1184
|
+
# is per-URL, so any archive already in it IS this URL's file.
|
|
1185
|
+
try:
|
|
1186
|
+
for fn in sorted(os.listdir(workdir)):
|
|
1187
|
+
p = os.path.join(workdir, fn)
|
|
1188
|
+
if os.path.isfile(p) and fn.lower().endswith(".zip") and os.path.getsize(p) > 0:
|
|
1189
|
+
return p, True
|
|
1190
|
+
except OSError:
|
|
1191
|
+
pass
|
|
1170
1192
|
os.makedirs(os.path.dirname(dest_path) or ".", exist_ok=True)
|
|
1171
1193
|
req = urllib.request.Request(src, headers={"User-Agent": "gonext-rag/1.0"})
|
|
1172
1194
|
total = 0
|
|
@@ -1184,15 +1206,48 @@ def _rag_download(url: str, dest_path: str = "") -> str:
|
|
|
1184
1206
|
f"Download exceeds the {_RAG_MAX_DOWNLOAD_BYTES // (1024*1024)} MB cap."
|
|
1185
1207
|
)
|
|
1186
1208
|
out.write(block)
|
|
1187
|
-
return dest_path
|
|
1209
|
+
return dest_path, False
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def _rag_resolve_zip(zip_path: str) -> str:
|
|
1213
|
+
"""Resolve a (possibly relative) zip path. Bare names the model echoes from earlier
|
|
1214
|
+
turns are searched in the rag work dirs so a fresh process still finds them."""
|
|
1215
|
+
import glob as _glob
|
|
1216
|
+
import os
|
|
1217
|
+
zp = os.path.expanduser((zip_path or "").strip())
|
|
1218
|
+
if os.path.isfile(zp):
|
|
1219
|
+
return zp
|
|
1220
|
+
if not os.path.isabs(zp):
|
|
1221
|
+
hits = _glob.glob(os.path.join(
|
|
1222
|
+
os.path.expanduser("~"), ".gonext", "rag-work", "*", os.path.basename(zp)))
|
|
1223
|
+
if hits:
|
|
1224
|
+
return hits[0]
|
|
1225
|
+
return zp
|
|
1188
1226
|
|
|
1189
1227
|
|
|
1190
1228
|
def _rag_unzip(zip_path: str, dest_dir: str = "") -> tuple:
|
|
1191
|
-
"""Extract a local zip with Zip-Slip + zip-bomb guards.
|
|
1229
|
+
"""Extract a local zip with Zip-Slip + zip-bomb guards — IDEMPOTENT. If the target
|
|
1230
|
+
dir already has extracted files, reuse it instead of re-extracting. Relative
|
|
1231
|
+
dest_dirs are placed next to the zip (inside the per-URL work dir), never the cwd.
|
|
1232
|
+
Returns (dir, file_list, cached)."""
|
|
1192
1233
|
import os
|
|
1193
1234
|
import zipfile
|
|
1235
|
+
zip_path = _rag_resolve_zip(zip_path)
|
|
1236
|
+
dest_dir = os.path.expanduser((dest_dir or "").strip())
|
|
1194
1237
|
if not dest_dir:
|
|
1195
1238
|
dest_dir = os.path.splitext(zip_path)[0] + "_unzipped"
|
|
1239
|
+
elif not os.path.isabs(dest_dir):
|
|
1240
|
+
dest_dir = os.path.join(os.path.dirname(zip_path) or ".", dest_dir)
|
|
1241
|
+
# Cache hit: already extracted for this zip → return the existing listing.
|
|
1242
|
+
if os.path.isdir(dest_dir):
|
|
1243
|
+
existing = []
|
|
1244
|
+
for dirpath, _dirnames, filenames in os.walk(dest_dir):
|
|
1245
|
+
for fn in filenames:
|
|
1246
|
+
existing.append(os.path.relpath(os.path.join(dirpath, fn), dest_dir))
|
|
1247
|
+
if len(existing) > _RAG_MAX_FILES:
|
|
1248
|
+
break
|
|
1249
|
+
if existing:
|
|
1250
|
+
return dest_dir, sorted(existing), True
|
|
1196
1251
|
os.makedirs(dest_dir, exist_ok=True)
|
|
1197
1252
|
dest_root = os.path.realpath(dest_dir)
|
|
1198
1253
|
written = 0
|
|
@@ -1216,7 +1271,7 @@ def _rag_unzip(zip_path: str, dest_dir: str = "") -> tuple:
|
|
|
1216
1271
|
dst.write(src.read())
|
|
1217
1272
|
written += 1
|
|
1218
1273
|
names.append(info.filename)
|
|
1219
|
-
return dest_dir, names
|
|
1274
|
+
return dest_dir, names, False
|
|
1220
1275
|
|
|
1221
1276
|
|
|
1222
1277
|
def _rag_iter_text_files(root: str):
|
|
@@ -1776,8 +1831,11 @@ def run_agent_chat(cfg):
|
|
|
1776
1831
|
"THEN list_dir(dir) to find the README, THEN read_text_file(that README) THEN answer from it. "
|
|
1777
1832
|
"This is the FASTEST path for a summary — do NOT index unless deep Q&A across many files is needed.\n"
|
|
1778
1833
|
"- user wants deep Q&A across the WHOLE project -> download_file THEN unzip_file THEN "
|
|
1779
|
-
"rag_index(dir, source_url=url) THEN rag_search(source_url=url, query=<question>) THEN answer
|
|
1780
|
-
"
|
|
1834
|
+
"rag_index(dir, source_url=url) THEN rag_search(source_url=url, query=<question>) THEN answer.\n"
|
|
1835
|
+
"- FOLLOW-UP question about a ZIP url from EARLIER in the conversation -> try "
|
|
1836
|
+
"rag_search(source_url=url, query=<question>) FIRST. If it says no knowledge base exists, "
|
|
1837
|
+
"run download_file(url) + unzip_file(zip) again — both are CACHED per url and return "
|
|
1838
|
+
"instantly — then list_dir/read_text_file or rag_index as needed. "
|
|
1781
1839
|
"If the user adds information, rag_add(source_url=url, text=...).\n"
|
|
1782
1840
|
) if _RAG_AVAILABLE else ""
|
|
1783
1841
|
_pdf_read_tool_line = (
|
|
@@ -2325,6 +2383,26 @@ def run_agent_chat(cfg):
|
|
|
2325
2383
|
in_tok = out_tok = reasoning_len = 0
|
|
2326
2384
|
first_token_at = None
|
|
2327
2385
|
|
|
2386
|
+
# Runaway guard: a degenerate model (seen live: gemma4:26b spewing
|
|
2387
|
+
# "<channel|><thought>" forever) will stream garbage until the client times
|
|
2388
|
+
# out — a multi-minute hang. Abort early if the output either (a) balloons
|
|
2389
|
+
# past a hard char cap, or (b) collapses into a repeating short line. On abort
|
|
2390
|
+
# we return "" so the empty-turn retry re-prompts with the CODE-NOW directive.
|
|
2391
|
+
_MAX_STREAM_CHARS = 16000 # a single agent step never legitimately needs this
|
|
2392
|
+
_stream_buf = [] # every streamed piece (content + reasoning)
|
|
2393
|
+
_stream_chars = 0
|
|
2394
|
+
_runaway = False
|
|
2395
|
+
|
|
2396
|
+
def _looks_runaway() -> bool:
|
|
2397
|
+
tail = "".join(_stream_buf)[-1600:]
|
|
2398
|
+
lines = [ln.strip() for ln in tail.splitlines() if ln.strip()]
|
|
2399
|
+
# 12+ of the last lines are the same short string → degenerate repetition.
|
|
2400
|
+
if len(lines) >= 12:
|
|
2401
|
+
last = lines[-12:]
|
|
2402
|
+
if len(set(last)) <= 2 and all(len(ln) <= 80 for ln in last):
|
|
2403
|
+
return True
|
|
2404
|
+
return False
|
|
2405
|
+
|
|
2328
2406
|
def _mark_first():
|
|
2329
2407
|
# First streamed byte (content OR reasoning). The gap t0→here is pure
|
|
2330
2408
|
# prompt-eval time (server sent nothing before this); everything after is
|
|
@@ -2362,13 +2440,33 @@ def run_agent_chat(cfg):
|
|
|
2362
2440
|
if rpiece:
|
|
2363
2441
|
_mark_first()
|
|
2364
2442
|
reasoning_len += len(rpiece)
|
|
2443
|
+
_stream_buf.append(rpiece)
|
|
2444
|
+
_stream_chars += len(rpiece)
|
|
2365
2445
|
_emit({"type": "stream", "text": rpiece})
|
|
2366
2446
|
piece = getattr(delta, "content", None)
|
|
2367
2447
|
if piece:
|
|
2368
2448
|
_mark_first()
|
|
2369
2449
|
parts.append(piece)
|
|
2450
|
+
_stream_buf.append(piece)
|
|
2451
|
+
_stream_chars += len(piece)
|
|
2370
2452
|
_emit({"type": "stream", "text": piece})
|
|
2453
|
+
# Check the runaway guard periodically (cheap) once enough has streamed.
|
|
2454
|
+
if _stream_chars > 500 and (_stream_chars > _MAX_STREAM_CHARS or _looks_runaway()):
|
|
2455
|
+
_runaway = True
|
|
2456
|
+
reason = "char cap" if _stream_chars > _MAX_STREAM_CHARS else "repeating output"
|
|
2457
|
+
_log(f"streamed generate: ABORTING ({reason}) after {_stream_chars} chars "
|
|
2458
|
+
"— model degenerated; closing stream.")
|
|
2459
|
+
try:
|
|
2460
|
+
stream.close()
|
|
2461
|
+
except Exception: # noqa: BLE001
|
|
2462
|
+
pass
|
|
2463
|
+
break
|
|
2371
2464
|
content = "".join(parts)
|
|
2465
|
+
if _runaway:
|
|
2466
|
+
# Discard the garbage; "" makes _streamed_generate run the CODE-NOW retry.
|
|
2467
|
+
_emit({"type": "step", "text": "Model output ran away — restarting this step…"})
|
|
2468
|
+
_log(f"streamed generate: discarded {len(content)} runaway chars → empty content")
|
|
2469
|
+
return "", role, in_tok, out_tok, reasoning_len
|
|
2372
2470
|
_log(f"streamed generate assembled {len(content)} chars "
|
|
2373
2471
|
f"(in={in_tok} out={out_tok} tokens, reasoning={reasoning_len} chars)")
|
|
2374
2472
|
return content, role, in_tok, out_tok, reasoning_len
|
|
@@ -2678,10 +2776,14 @@ def run_agent_chat(cfg):
|
|
|
2678
2776
|
url: public http(s) URL of the file to download.
|
|
2679
2777
|
dest_path: optional local save path; leave empty for an automatic work-dir path.
|
|
2680
2778
|
"""
|
|
2681
|
-
_emit({"type": "step", "text": f"Downloading → {url[:80]}"})
|
|
2682
2779
|
try:
|
|
2683
2780
|
import os
|
|
2684
|
-
path = _rag_download(url, (dest_path or "").strip())
|
|
2781
|
+
path, cached = _rag_download(url, (dest_path or "").strip())
|
|
2782
|
+
if cached:
|
|
2783
|
+
_emit({"type": "step", "text": "Already downloaded — reusing cached file"})
|
|
2784
|
+
return (f"Already downloaded earlier — reusing {path} "
|
|
2785
|
+
f"({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}').")
|
|
2786
|
+
_emit({"type": "step", "text": f"Downloading → {url[:80]}"})
|
|
2685
2787
|
return f"Downloaded to {path} ({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}') if it is a zip."
|
|
2686
2788
|
except Exception as e: # noqa: BLE001
|
|
2687
2789
|
return f"Error: download failed: {type(e).__name__}: {e}"
|
|
@@ -2694,11 +2796,15 @@ def run_agent_chat(cfg):
|
|
|
2694
2796
|
zip_path: local path to the .zip (from download_file).
|
|
2695
2797
|
dest_dir: optional target directory; leave empty for an automatic one.
|
|
2696
2798
|
"""
|
|
2697
|
-
_emit({"type": "step", "text": "Unzipping…"})
|
|
2698
2799
|
try:
|
|
2699
|
-
d, names = _rag_unzip((zip_path or "").strip(), (dest_dir or "").strip())
|
|
2800
|
+
d, names, cached = _rag_unzip((zip_path or "").strip(), (dest_dir or "").strip())
|
|
2700
2801
|
preview = ", ".join(names[:15])
|
|
2701
2802
|
more = f" …(+{len(names) - 15} more)" if len(names) > 15 else ""
|
|
2803
|
+
if cached:
|
|
2804
|
+
_emit({"type": "step", "text": "Already unzipped — reusing extracted files"})
|
|
2805
|
+
return (f"Already unzipped earlier — {len(names)} files at {d}. "
|
|
2806
|
+
f"Files: {preview}{more}. Use list_dir/read_text_file or rag_index on it.")
|
|
2807
|
+
_emit({"type": "step", "text": "Unzipping…"})
|
|
2702
2808
|
return f"Unzipped {len(names)} files to {d}. Files: {preview}{more}. Next: rag_index(path='{d}', source_url=<the original url>)."
|
|
2703
2809
|
except Exception as e: # noqa: BLE001
|
|
2704
2810
|
return f"Error: unzip failed: {type(e).__name__}: {e}"
|
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.164",
|
|
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",
|