@tiens.nguyen/gonext-local-worker 1.0.278 → 1.0.281
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-local-worker.mjs +0 -3
- package/gonext_agent_chat.py +165 -157
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1893,9 +1893,6 @@ async function runAgentChatJob(job) {
|
|
|
1893
1893
|
agentModelId: payload?.agentModelId ?? "",
|
|
1894
1894
|
codingBaseURL: payload?.codingBaseURL ?? "",
|
|
1895
1895
|
codingModelId: payload?.codingModelId ?? "",
|
|
1896
|
-
// Optional search model (task #105): synthesizes web_search results. Empty = no synthesis.
|
|
1897
|
-
searchBaseURL: payload?.searchBaseURL ?? "",
|
|
1898
|
-
searchModelId: payload?.searchModelId ?? "",
|
|
1899
1896
|
tools: payload?.tools ?? ["http_request"],
|
|
1900
1897
|
// 0 = "not explicitly set": python defaults to 5, but may auto-raise to 12 when
|
|
1901
1898
|
// workspaces are registered. Passing a literal 5 here would look user-set and
|
package/gonext_agent_chat.py
CHANGED
|
@@ -413,15 +413,23 @@ def _norm_url(u):
|
|
|
413
413
|
|
|
414
414
|
|
|
415
415
|
def _bk_ddg_html(q, timeout=6):
|
|
416
|
-
"""DuckDuckGo HTML endpoint → real web results [{title,snippet,url}] (keyless).
|
|
416
|
+
"""DuckDuckGo HTML endpoint → real web results [{title,snippet,url}] (keyless).
|
|
417
|
+
|
|
418
|
+
Parses each result as a UNIT (task #104 fix): for every result__a anchor we take the
|
|
419
|
+
snippet that belongs to IT — the result__snippet appearing before the NEXT anchor —
|
|
420
|
+
instead of zipping two independent global lists. The old zip silently misaligned
|
|
421
|
+
snippets onto the wrong title/URL the moment any result lacked a snippet (ads, news,
|
|
422
|
+
or snippet-less rows), so the model read evidence attached to the wrong page.
|
|
423
|
+
"""
|
|
417
424
|
from urllib.parse import quote, unquote
|
|
418
425
|
html_body = _get_text(f"https://html.duckduckgo.com/html/?q={quote(q)}", timeout=timeout)
|
|
419
426
|
if not html_body:
|
|
420
427
|
return []
|
|
421
428
|
out = []
|
|
422
|
-
anchors = re.
|
|
423
|
-
|
|
424
|
-
for
|
|
429
|
+
anchors = list(re.finditer(
|
|
430
|
+
r'<a\b[^>]*class="[^"]*result__a[^"]*"[^>]*>.*?</a>', html_body, re.S))
|
|
431
|
+
for idx, am in enumerate(anchors):
|
|
432
|
+
a = am.group(0)
|
|
425
433
|
hm = re.search(r'href="([^"]*)"', a)
|
|
426
434
|
if not hm:
|
|
427
435
|
continue
|
|
@@ -433,16 +441,25 @@ def _bk_ddg_html(q, timeout=6):
|
|
|
433
441
|
if not real.startswith("http"):
|
|
434
442
|
continue
|
|
435
443
|
title = _search_strip(a)
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
444
|
+
if not title:
|
|
445
|
+
continue
|
|
446
|
+
# Snippet = the result__snippet that lies BETWEEN this anchor and the next one
|
|
447
|
+
# (i.e. inside this result's block). Missing → "" with no shift onto the next row.
|
|
448
|
+
seg_end = anchors[idx + 1].start() if idx + 1 < len(anchors) else len(html_body)
|
|
449
|
+
seg = html_body[am.end():seg_end]
|
|
450
|
+
sm = re.search(
|
|
451
|
+
r'<a\b[^>]*class="[^"]*result__snippet[^"]*"[^>]*>(.*?)</a>', seg, re.S)
|
|
452
|
+
snip = _search_strip(sm.group(1)) if sm else ""
|
|
453
|
+
out.append({"title": title[:120], "snippet": snip, "url": real})
|
|
439
454
|
return out
|
|
440
455
|
|
|
441
456
|
|
|
442
457
|
def _bk_searxng(q, base, timeout=6):
|
|
443
|
-
"""Self-hosted SearXNG JSON API
|
|
444
|
-
|
|
445
|
-
|
|
458
|
+
"""Self-hosted SearXNG JSON API → {results:[{title,snippet,url}], answers:[str]} (keyless).
|
|
459
|
+
|
|
460
|
+
'answers' + 'infoboxes' are SearXNG's aggregated SERP answer box / knowledge panel — a
|
|
461
|
+
DIRECT answer we can surface without reading any page (task #104). Returns the dict shape
|
|
462
|
+
so _web_search_impl can promote answers into the 'Answer:' block."""
|
|
446
463
|
from urllib.parse import quote
|
|
447
464
|
base = (base or "").strip().rstrip("/")
|
|
448
465
|
if not base:
|
|
@@ -459,15 +476,16 @@ def _bk_searxng(q, base, timeout=6):
|
|
|
459
476
|
"snippet": _search_strip(r.get("content") or ""),
|
|
460
477
|
"url": url,
|
|
461
478
|
})
|
|
479
|
+
# 'answers' entries are plain strings on some versions, {answer: ...} dicts on others.
|
|
462
480
|
for a in (data.get("answers") or []):
|
|
463
|
-
txt = _search_strip(a if isinstance(a,
|
|
481
|
+
txt = _search_strip(str((a.get("answer") if isinstance(a, dict) else a) or ""))
|
|
464
482
|
if txt:
|
|
465
|
-
answers.append(txt
|
|
483
|
+
answers.append(txt)
|
|
466
484
|
for ib in (data.get("infoboxes") or []):
|
|
467
485
|
if isinstance(ib, dict):
|
|
468
|
-
txt = _search_strip(ib.get("content") or "")
|
|
486
|
+
txt = _search_strip(str(ib.get("content") or ""))
|
|
469
487
|
if txt:
|
|
470
|
-
answers.append(txt
|
|
488
|
+
answers.append(txt)
|
|
471
489
|
return {"results": results, "answers": answers}
|
|
472
490
|
|
|
473
491
|
|
|
@@ -508,91 +526,92 @@ def _bk_ddg_ia_summary(q, timeout=6):
|
|
|
508
526
|
return ("", "")
|
|
509
527
|
|
|
510
528
|
|
|
511
|
-
def _condense_for_query(text, query,
|
|
512
|
-
"""Keep the
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
529
|
+
def _condense_for_query(text, query, budget=1500):
|
|
530
|
+
"""Keep the query-relevant + STRUCTURAL lines of a page's text, bounded to `budget`
|
|
531
|
+
chars, in original order (task #104). Structural lines — table rows (which _html_to_text
|
|
532
|
+
renders with ' | '), list items, or lines carrying a year / clock time — are ALWAYS kept
|
|
533
|
+
even at zero prose-score, so schedules / fixtures / specs survive instead of being
|
|
534
|
+
summarized away (the flatten-to-summary failure). Model-free: pure text selection."""
|
|
535
|
+
if not text:
|
|
536
|
+
return ""
|
|
537
|
+
terms = {w for w in re.findall(r"\w+", (query or "").lower()) if len(w) > 2}
|
|
538
|
+
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
|
539
|
+
picked = []
|
|
540
|
+
for ln in lines:
|
|
541
|
+
low = ln.lower()
|
|
542
|
+
score = sum(1 for t in terms if t in low)
|
|
543
|
+
structural = (
|
|
544
|
+
"|" in ln
|
|
545
|
+
or bool(re.match(r"^([-*•]|\d+[.)])\s", ln))
|
|
546
|
+
or bool(re.search(r"\b(19|20)\d{2}\b", ln))
|
|
547
|
+
or bool(re.search(r"\d{1,2}:\d{2}", ln))
|
|
548
|
+
)
|
|
549
|
+
if score > 0 or structural:
|
|
550
|
+
picked.append(ln)
|
|
551
|
+
if not picked: # nothing matched — fall back to the page lead so we return SOMETHING
|
|
552
|
+
picked = lines[:20]
|
|
553
|
+
kept, total = [], 0
|
|
554
|
+
for ln in picked:
|
|
555
|
+
if total + len(ln) + 1 > budget:
|
|
556
|
+
continue
|
|
557
|
+
kept.append(ln)
|
|
558
|
+
total += len(ln) + 1
|
|
559
|
+
return "\n".join(kept)
|
|
533
560
|
|
|
534
561
|
|
|
535
|
-
def _read_pages(
|
|
536
|
-
"""
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
from
|
|
562
|
+
def _read_pages(ranked, query, per_page_chars=1500, per_timeout=5, overall=6):
|
|
563
|
+
"""Read the given result pages IN PARALLEL → {url: condensed_text} (task #104).
|
|
564
|
+
|
|
565
|
+
Model-free: HTTP GET (_fetch_page_impl) + _html_to_text + _condense_for_query. Each URL
|
|
566
|
+
is SSRF-guarded (_rag_assert_safe_url) BEFORE fetching — these come from an untrusted
|
|
567
|
+
SERP, and this reads them automatically (not a URL the model explicitly chose), so an
|
|
568
|
+
entry pointing at an internal/link-local host must never be fetched. PDFs/binaries and
|
|
569
|
+
any per-page failure are skipped (the caller falls back to that result's snippet)."""
|
|
570
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
540
571
|
import time as _t
|
|
572
|
+
if not ranked:
|
|
573
|
+
return {}
|
|
541
574
|
|
|
542
|
-
def _read_one(
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
575
|
+
def _read_one(url):
|
|
576
|
+
try:
|
|
577
|
+
_rag_assert_safe_url(url)
|
|
578
|
+
except Exception: # noqa: BLE001 — SSRF/scheme refusal → skip this page
|
|
579
|
+
return url, ""
|
|
580
|
+
status, ctype, raw = _fetch_page_impl(url, timeout=per_timeout, max_bytes=300000)
|
|
581
|
+
if status is None or (isinstance(status, int) and status >= 400):
|
|
582
|
+
return url, ""
|
|
583
|
+
if "application/pdf" in ctype or raw[:5].lstrip().startswith(b"%PDF"):
|
|
584
|
+
return url, "" # binary — snippet fallback (fetch_url can't read PDFs either)
|
|
585
|
+
text = _html_to_text(raw.decode("utf-8", errors="replace"), limit=per_page_chars * 3)
|
|
586
|
+
return url, _condense_for_query(text, query, per_page_chars)
|
|
550
587
|
|
|
551
588
|
out = {}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
589
|
+
# Manual shutdown (not `with`): a slow page must not hold the whole search past `overall`
|
|
590
|
+
# via shutdown(wait=True). We collect what finishes within the deadline, then return.
|
|
591
|
+
ex = ThreadPoolExecutor(max_workers=len(ranked))
|
|
592
|
+
futs = {ex.submit(_read_one, e["url"]): e["url"] for e in ranked}
|
|
593
|
+
end = _t.time() + overall
|
|
594
|
+
try:
|
|
595
|
+
for fut in as_completed(futs, timeout=overall):
|
|
558
596
|
try:
|
|
559
|
-
|
|
560
|
-
if txt:
|
|
561
|
-
out[uu] = txt
|
|
597
|
+
url, txt = fut.result(timeout=max(0.0, end - _t.time()))
|
|
562
598
|
except Exception: # noqa: BLE001
|
|
563
599
|
continue
|
|
600
|
+
if txt:
|
|
601
|
+
out[url] = txt
|
|
602
|
+
except Exception: # noqa: BLE001 — overall deadline hit; return what finished
|
|
603
|
+
pass
|
|
604
|
+
finally:
|
|
605
|
+
ex.shutdown(wait=False, cancel_futures=True)
|
|
564
606
|
return out
|
|
565
607
|
|
|
566
608
|
|
|
567
|
-
def
|
|
568
|
-
"""
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
resp = _chat_create(
|
|
574
|
-
client,
|
|
575
|
-
model=model_id or "default_model",
|
|
576
|
-
messages=[
|
|
577
|
-
{"role": "system", "content": (
|
|
578
|
-
"You are a research assistant. Using ONLY the search results provided, write a "
|
|
579
|
-
"concise, factual answer to the user's query. Cite sources inline as [n] using "
|
|
580
|
-
"their numbers. If the results do not contain the answer, say so plainly — never "
|
|
581
|
-
"invent facts, dates, names, or URLs."
|
|
582
|
-
)},
|
|
583
|
-
{"role": "user", "content": f"Query: {query}\n\nSearch results:\n{context}\n\nAnswer:"},
|
|
584
|
-
],
|
|
585
|
-
max_tokens=700,
|
|
586
|
-
temperature=0.2,
|
|
587
|
-
)
|
|
588
|
-
return (resp.choices[0].message.content or "").strip()
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
def _web_search_impl(query, max_results=5, searxng_url="", search_base_url="", search_model_id=""):
|
|
592
|
-
"""Parallel keyless web search (task #103): run DuckDuckGo-HTML + SearXNG (if configured)
|
|
593
|
-
+ Wikipedia CONCURRENTLY with a short deadline, merge/dedupe/rank by cross-engine
|
|
594
|
-
agreement, and return a short SUMMARY (when available) + a numbered list of the top
|
|
595
|
-
PAGES (title — snippet — URL) so the model can fetch_url() the best one. Never fabricates.
|
|
609
|
+
def _web_search_impl(query, max_results=5, searxng_url=""):
|
|
610
|
+
"""Keyless web search that READS for the model (tasks #103 + #104): run DuckDuckGo-HTML
|
|
611
|
+
+ SearXNG (if configured) + Wikipedia CONCURRENTLY, merge/dedupe/rank by cross-engine
|
|
612
|
+
agreement, surface any SERP answer box, then (unless GONEXT_SEARCH_READ=0) READ the top
|
|
613
|
+
pages in parallel and return answer-ready condensed content — so the model usually does
|
|
614
|
+
NOT need a slow fetch_url round-trip. Model-free; never fabricates.
|
|
596
615
|
"""
|
|
597
616
|
import os
|
|
598
617
|
import time as _t
|
|
@@ -603,29 +622,38 @@ def _web_search_impl(query, max_results=5, searxng_url="", search_base_url="", s
|
|
|
603
622
|
return "web_search: empty query."
|
|
604
623
|
searxng = (searxng_url or os.environ.get("GONEXT_SEARXNG_URL") or "").strip()
|
|
605
624
|
|
|
606
|
-
# Short TTL cache (identical query within a turn).
|
|
607
|
-
|
|
608
|
-
ckey = (q.lower(), searxng, search_base_url, search_model_id)
|
|
625
|
+
# Short TTL cache (identical query within a turn).
|
|
626
|
+
ckey = (q.lower(), searxng)
|
|
609
627
|
hit = _WEB_SEARCH_CACHE.get(ckey)
|
|
610
628
|
if hit and (_t.time() - hit[0]) < _WEB_SEARCH_TTL:
|
|
611
629
|
return hit[1]
|
|
612
630
|
|
|
613
631
|
per_timeout = 6 # per-backend socket timeout
|
|
614
|
-
overall =
|
|
632
|
+
overall = 5 # overall deadline; a straggler is abandoned (shutdown is non-blocking)
|
|
615
633
|
|
|
616
634
|
# Result-producing backends (run in parallel). Order = tie-break priority in the merge.
|
|
617
635
|
tasks = [("ddg", lambda: _bk_ddg_html(q, per_timeout))]
|
|
618
636
|
if searxng:
|
|
637
|
+
# SearXNG aggregates Wikipedia (and everything else) server-side, so the standalone
|
|
638
|
+
# Wikipedia backend is redundant here — and its full-text API is the slow straggler
|
|
639
|
+
# (~7s) that otherwise dominates the whole search. Only pay for it as a keyless
|
|
640
|
+
# fallback when SearXNG isn't configured.
|
|
619
641
|
tasks.append(("searxng", lambda: _bk_searxng(q, searxng, per_timeout)))
|
|
620
|
-
|
|
642
|
+
else:
|
|
643
|
+
tasks.append(("wikipedia", lambda: _bk_wikipedia(q, per_timeout, max_results)))
|
|
621
644
|
tasks.append(("summary", lambda: _bk_ddg_ia_summary(q, per_timeout)))
|
|
622
645
|
|
|
623
646
|
results_by = {}
|
|
624
|
-
answers = []
|
|
647
|
+
answers = [] # #104: direct answer-box text (SearXNG answers/infoboxes)
|
|
625
648
|
summary, summary_src = "", ""
|
|
626
|
-
with
|
|
627
|
-
|
|
628
|
-
|
|
649
|
+
# NOT a `with` block: the context manager's shutdown(wait=True) would block on straggler
|
|
650
|
+
# threads PAST the deadline (a slow SearXNG/engine could add 10s+). We read results up to
|
|
651
|
+
# `overall`, then shutdown(wait=False) so the function returns on time; leftover backend
|
|
652
|
+
# threads finish harmlessly in the background.
|
|
653
|
+
ex = ThreadPoolExecutor(max_workers=len(tasks))
|
|
654
|
+
futs = {ex.submit(fn): name for name, fn in tasks}
|
|
655
|
+
end = _t.time() + overall
|
|
656
|
+
try:
|
|
629
657
|
for fut, name in futs.items():
|
|
630
658
|
try:
|
|
631
659
|
r = fut.result(timeout=max(0.0, end - _t.time()))
|
|
@@ -633,13 +661,13 @@ def _web_search_impl(query, max_results=5, searxng_url="", search_base_url="", s
|
|
|
633
661
|
continue
|
|
634
662
|
if name == "summary":
|
|
635
663
|
summary, summary_src = r
|
|
636
|
-
elif name == "searxng":
|
|
637
|
-
results_by[
|
|
664
|
+
elif name == "searxng": # dict shape: {results, answers}
|
|
665
|
+
results_by[name] = r.get("results", [])
|
|
638
666
|
answers.extend(r.get("answers", []))
|
|
639
667
|
else:
|
|
640
668
|
results_by[name] = r
|
|
641
|
-
|
|
642
|
-
|
|
669
|
+
finally:
|
|
670
|
+
ex.shutdown(wait=False, cancel_futures=True)
|
|
643
671
|
|
|
644
672
|
# Merge + dedupe by normalized URL; rank by cross-engine AGREEMENT then first-seen order.
|
|
645
673
|
merged = {}
|
|
@@ -661,55 +689,45 @@ def _web_search_impl(query, max_results=5, searxng_url="", search_base_url="", s
|
|
|
661
689
|
order += 1
|
|
662
690
|
ranked = sorted(merged.values(), key=lambda e: (-e["hits"], e["order"]))[:max_results]
|
|
663
691
|
|
|
664
|
-
if not
|
|
692
|
+
if not summary and not ranked and not answers:
|
|
665
693
|
return (
|
|
666
694
|
f"No results found for '{q}'. Tell the user you couldn't find this — "
|
|
667
695
|
"do NOT invent an answer or a URL."
|
|
668
696
|
)
|
|
669
697
|
|
|
670
|
-
#
|
|
671
|
-
# answer-ready content
|
|
672
|
-
#
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
page_texts = {}
|
|
683
|
-
if read_enabled and read_k and ranked:
|
|
684
|
-
page_texts = _read_pages([e["url"] for e in ranked[:read_k]], q,
|
|
685
|
-
per_page_chars=page_chars, per_timeout=5)
|
|
698
|
+
# #104 read step: unless disabled, READ the top pages inline so the model gets
|
|
699
|
+
# answer-ready content in ONE call (no slow fetch_url round-trips). GONEXT_SEARCH_READ=0
|
|
700
|
+
# is the kill switch → link+snippet behaviour of #103.
|
|
701
|
+
def _env_int(name, default):
|
|
702
|
+
try:
|
|
703
|
+
return max(1, int(os.environ.get(name, "").strip()))
|
|
704
|
+
except (ValueError, TypeError):
|
|
705
|
+
return default
|
|
706
|
+
read_on = os.environ.get("GONEXT_SEARCH_READ", "1").strip().lower() not in ("0", "false", "no")
|
|
707
|
+
read_k = _env_int("GONEXT_SEARCH_READ_K", 3)
|
|
708
|
+
page_chars = _env_int("GONEXT_SEARCH_PAGE_CHARS", 1500)
|
|
709
|
+
page_texts = _read_pages(ranked[:read_k], q, page_chars) if (read_on and ranked) else {}
|
|
686
710
|
|
|
687
711
|
parts = []
|
|
688
|
-
|
|
689
|
-
|
|
712
|
+
# Answer box(es) first — a direct answer often needs no page read at all.
|
|
713
|
+
answer_lines = [f"• {a[:600]}" for a in answers[:3]]
|
|
714
|
+
if summary:
|
|
715
|
+
answer_lines.append(f"• {summary[:600]}" + (f" (source: {summary_src})" if summary_src else ""))
|
|
716
|
+
if answer_lines:
|
|
717
|
+
parts.append("Answer:\n" + "\n".join(answer_lines))
|
|
718
|
+
|
|
690
719
|
if ranked:
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
720
|
+
if page_texts:
|
|
721
|
+
lines = ["Sources — already READ for you (you usually do NOT need fetch_url; "
|
|
722
|
+
"call it only to read a specific page in full):"]
|
|
723
|
+
else:
|
|
724
|
+
lines = ["Top pages (call fetch_url on the most relevant to read it in full):"]
|
|
694
725
|
for i, e in enumerate(ranked, 1):
|
|
695
726
|
head = f"[{i}] {e['title']}\n {e['url']}"
|
|
696
727
|
body = page_texts.get(e["url"]) or (e["snippet"][:200] if e["snippet"] else "")
|
|
697
|
-
|
|
698
|
-
parts.append("\n
|
|
728
|
+
lines.append(head + (f"\n{body}" if body else ""))
|
|
729
|
+
parts.append("\n".join(lines))
|
|
699
730
|
out = "\n\n".join(parts)
|
|
700
|
-
|
|
701
|
-
# Search model (task #105): if configured, have it write the answer FROM the results we
|
|
702
|
-
# just read — so the code model gets a clean cited answer, not raw pages. Best-effort:
|
|
703
|
-
# any failure falls back to the read output above.
|
|
704
|
-
if search_base_url and search_model_id and (answers or ranked):
|
|
705
|
-
try:
|
|
706
|
-
synth = _synthesize_search(q, out, search_base_url, search_model_id)
|
|
707
|
-
if synth:
|
|
708
|
-
srcs = "\n".join(f"[{i}] {e['url']}" for i, e in enumerate(ranked, 1))
|
|
709
|
-
out = f"Answer:\n{synth}" + (f"\n\nSources:\n{srcs}" if srcs else "")
|
|
710
|
-
except Exception as e: # noqa: BLE001
|
|
711
|
-
_log(f"search-model synthesis failed ({type(e).__name__}: {e}); using read output")
|
|
712
|
-
|
|
713
731
|
_WEB_SEARCH_CACHE[ckey] = (_t.time(), out)
|
|
714
732
|
return out
|
|
715
733
|
|
|
@@ -3073,15 +3091,6 @@ def run_agent_chat(cfg):
|
|
|
3073
3091
|
f"start model={agent_model_id!r} base={agent_base_url!r} "
|
|
3074
3092
|
f"codeModel={coding_model_id!r} codeBase={coding_base_url!r} maxSteps={max_steps}"
|
|
3075
3093
|
)
|
|
3076
|
-
# Search model (task #105): who synthesizes web_search answers. Logged so a run shows at a
|
|
3077
|
-
# glance whether it's wired.
|
|
3078
|
-
_search_base = (cfg.get("searchBaseURL") or "").strip()
|
|
3079
|
-
_search_model = (cfg.get("searchModelId") or "").strip()
|
|
3080
|
-
_log(
|
|
3081
|
-
f"search model: {_search_model!r} @ {_search_base}"
|
|
3082
|
-
if _search_base
|
|
3083
|
-
else "search model: none (web_search returns read page content, not a synthesized answer)"
|
|
3084
|
-
)
|
|
3085
3094
|
|
|
3086
3095
|
# Build the task from the conversation history. We include the FULL conversation
|
|
3087
3096
|
# (both user AND assistant turns) so the agent remembers what it already did —
|
|
@@ -4147,13 +4156,16 @@ def run_agent_chat(cfg):
|
|
|
4147
4156
|
|
|
4148
4157
|
@tool
|
|
4149
4158
|
def web_search(query: str) -> str:
|
|
4150
|
-
"""Search the web AND read the top
|
|
4159
|
+
"""Search the web AND read the top pages for you, in one call.
|
|
4151
4160
|
|
|
4152
|
-
Use this INSTEAD of guessing a URL
|
|
4153
|
-
|
|
4161
|
+
Use this INSTEAD of guessing a URL when the user asks to 'find' something or asks a
|
|
4162
|
+
general-knowledge question. Returns any direct answer box plus the top sources with
|
|
4163
|
+
their CONTENT ALREADY READ (HTML stripped, tables kept) — so you can usually answer
|
|
4164
|
+
or call final_answer/create_pdf straight away WITHOUT calling fetch_url again. Only
|
|
4165
|
+
call fetch_url when you specifically need a single page in full.
|
|
4154
4166
|
|
|
4155
4167
|
Args:
|
|
4156
|
-
query: What to look up, e.g. '
|
|
4168
|
+
query: What to look up, e.g. 'capital of France' or 'world cup 2026 schedule'.
|
|
4157
4169
|
"""
|
|
4158
4170
|
dup, nudge = _dedup("web_search", query,
|
|
4159
4171
|
"Use the facts you have ALREADY gathered: if the task asks for a "
|
|
@@ -4166,13 +4178,9 @@ def run_agent_chat(cfg):
|
|
|
4166
4178
|
if blocked:
|
|
4167
4179
|
return blocked
|
|
4168
4180
|
_emit({"type": "step", "text": f"Searching the web → {query[:80]}"})
|
|
4169
|
-
# SearXNG URL (task #103)
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
searxng_url=(cfg.get("searxngUrl") or "").strip(),
|
|
4173
|
-
search_base_url=(cfg.get("searchBaseURL") or "").strip(),
|
|
4174
|
-
search_model_id=(cfg.get("searchModelId") or "").strip(),
|
|
4175
|
-
)
|
|
4181
|
+
# SearXNG URL (task #103): from Settings (cfg.searxngUrl) if the API sends it, else
|
|
4182
|
+
# the GONEXT_SEARXNG_URL env fallback inside _web_search_impl. Keyless either way.
|
|
4183
|
+
result = _web_search_impl(query, searxng_url=(cfg.get("searxngUrl") or "").strip())
|
|
4176
4184
|
_log(f"web_search {query[:60]!r} → {result[:80]}")
|
|
4177
4185
|
_last_obs["text"] = result
|
|
4178
4186
|
if result and not result.startswith("Error"):
|
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.281",
|
|
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",
|