@tiens.nguyen/gu-cli 1.0.743 → 1.0.745
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 +43 -324
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -3727,46 +3727,6 @@ def _mark_stream_unsupported(base_url, model_id) -> None:
|
|
|
3727
3727
|
"of this process — the server refused the streamed request shape itself")
|
|
3728
3728
|
|
|
3729
3729
|
|
|
3730
|
-
def _synthesize_document(gathered: list, task: str, base_url: str, api_key: str,
|
|
3731
|
-
model_id: str) -> str:
|
|
3732
|
-
"""Turn raw gathered research observations into ONE clean, organized document body
|
|
3733
|
-
for create_pdf. This is a pure REFORMAT (organize/deduplicate facts already found —
|
|
3734
|
-
no new research), so the fast local chat model handles it well. Returns "" on any
|
|
3735
|
-
failure so the caller can fall back to the raw concatenation."""
|
|
3736
|
-
notes = "\n\n---\n\n".join(gathered)[:12000]
|
|
3737
|
-
sys_prompt = (
|
|
3738
|
-
"You are a document editor. You are given a user's request and raw research "
|
|
3739
|
-
"notes gathered to answer it. Produce a single, clean, well-organized document "
|
|
3740
|
-
"that fulfills the request using ONLY facts present in the notes.\n"
|
|
3741
|
-
"- Use a clear title, headings, and bullet lists or a markdown table where it "
|
|
3742
|
-
"fits (e.g. schedules → a table of Teams | Date | Time).\n"
|
|
3743
|
-
"- Deduplicate and order the information sensibly (e.g. by date).\n"
|
|
3744
|
-
"- Do NOT invent facts not in the notes, and do NOT mention the research process, "
|
|
3745
|
-
"steps, tools, or that anything is partial. Output ONLY the document body."
|
|
3746
|
-
)
|
|
3747
|
-
user_prompt = f"USER REQUEST:\n{task}\n\nRESEARCH NOTES:\n{notes}"
|
|
3748
|
-
try:
|
|
3749
|
-
from openai import OpenAI
|
|
3750
|
-
client = OpenAI(base_url=base_url, api_key=api_key or "local",
|
|
3751
|
-
http_client=_proxy_http_client(base_url),
|
|
3752
|
-
max_retries=0, timeout=120)
|
|
3753
|
-
resp = _chat_create(
|
|
3754
|
-
client,
|
|
3755
|
-
model=model_id,
|
|
3756
|
-
messages=[{"role": "system", "content": sys_prompt},
|
|
3757
|
-
{"role": "user", "content": user_prompt}],
|
|
3758
|
-
temperature=0.3,
|
|
3759
|
-
max_tokens=2048,
|
|
3760
|
-
)
|
|
3761
|
-
out = (resp.choices[0].message.content or "").strip()
|
|
3762
|
-
# A reasoning model might wrap output in <think>…</think>; strip it.
|
|
3763
|
-
out = _THINK_BLOCK.sub("", out).strip()
|
|
3764
|
-
return out
|
|
3765
|
-
except Exception as e: # noqa: BLE001
|
|
3766
|
-
_log(f"document synthesis failed ({e}) → will use raw gathered notes")
|
|
3767
|
-
return ""
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
3730
|
def _strip_tool_tags(text: str) -> str:
|
|
3771
3731
|
"""Remove the internal hint tags we append to tool output (e.g. '[SUCCESS …]',
|
|
3772
3732
|
'[NOTE: …]', 'Note: This URL failed …') so they never leak into the user reply."""
|
|
@@ -4607,130 +4567,6 @@ def _format_text_for_pdf(text: str, title: str, base_url: str, api_key: str,
|
|
|
4607
4567
|
return fallback
|
|
4608
4568
|
|
|
4609
4569
|
|
|
4610
|
-
# A clear "make a PDF from this" request. Matched against the raw user message so we
|
|
4611
|
-
# can run the PDF pipeline deterministically — a small model cannot reliably echo a
|
|
4612
|
-
# long document back into a Python string literal for the create_pdf() tool call.
|
|
4613
|
-
_PDF_INTENT = re.compile(
|
|
4614
|
-
r"(create|make|generate|export|build|produce|convert|turn)\b[\s\S]{0,40}\bpdf\b"
|
|
4615
|
-
r"|\bpdf\b[\s\S]{0,40}\b(from|for|of|with|out of)\b",
|
|
4616
|
-
re.IGNORECASE,
|
|
4617
|
-
)
|
|
4618
|
-
|
|
4619
|
-
# Follow-up phrasings that reference an *existing / target* PDF — e.g. "in the pdf
|
|
4620
|
-
# file", "add the teams to the pdf", "put it in the pdf", "update the pdf", "as a pdf".
|
|
4621
|
-
# These carry no "create … pdf" verb so _PDF_INTENT misses them, yet they still mean
|
|
4622
|
-
# "(re)generate the PDF" — previously they fell through to the CodeAgent and crashed
|
|
4623
|
-
# on create_pdf(text="…long literal…"). The document body comes from history (A2).
|
|
4624
|
-
_PDF_FOLLOWUP = re.compile(
|
|
4625
|
-
r"\b(in|into|on|to)\s+(the\s+|a\s+|this\s+|that\s+)?pdf\b"
|
|
4626
|
-
r"|\b(add|include|put|fill|insert|append|update|regenerate|remake|redo)\b[\s\S]{0,40}\bpdf\b"
|
|
4627
|
-
r"|\bpdf\b[\s\S]{0,40}\b(add|include|fill|update|insert|append)\b"
|
|
4628
|
-
r"|\bas\s+(a\s+)?pdf\b"
|
|
4629
|
-
r"|\bthe\s+pdf\s+file\b",
|
|
4630
|
-
re.IGNORECASE,
|
|
4631
|
-
)
|
|
4632
|
-
|
|
4633
|
-
# Interrogatives ABOUT an existing PDF ("what's in the pdf?") must NOT trigger
|
|
4634
|
-
# generation — only imperative (re)build requests should.
|
|
4635
|
-
_PDF_QUESTION = re.compile(
|
|
4636
|
-
r"^\s*(what|why|how|where|who|which|when|does|do|is|are|can|could|should|would)\b[\s\S]*\?\s*$",
|
|
4637
|
-
re.IGNORECASE,
|
|
4638
|
-
)
|
|
4639
|
-
|
|
4640
|
-
# READING an existing PDF ("summarize this pdf", "extract text from the pdf") is the job
|
|
4641
|
-
# of extract_text_from_pdf, NOT the create fast-path. These read verbs must veto _wants_pdf
|
|
4642
|
-
# so a "summarize the pdf file" request doesn't get hijacked into (re)generating a PDF.
|
|
4643
|
-
# Deliberately excludes add/put/fill/update (task #6 follow-ups still (re)generate).
|
|
4644
|
-
_PDF_READ_INTENT = re.compile(
|
|
4645
|
-
r"\b(read|summari[sz]e|extract|analy[sz]e|parse|review|scan)\b[\s\S]{0,40}\bpdf\b"
|
|
4646
|
-
r"|\bpdf\b[\s\S]{0,40}\b(say|says|contain|contains|about|content)\b",
|
|
4647
|
-
re.IGNORECASE,
|
|
4648
|
-
)
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
# A PDF request whose CONTENT must be gathered/researched first (e.g. "get me the world
|
|
4652
|
-
# cup 2026 schedule … then create a pdf"). The deterministic fast-path can only render text
|
|
4653
|
-
# the user already GAVE (or a prior assistant turn) — it cannot web_search, and the model
|
|
4654
|
-
# would just hallucinate live facts. When these retrieval/currency signals appear AND no
|
|
4655
|
-
# document body was provided, we skip the fast-path and let the multi-step agent gather the
|
|
4656
|
-
# data (web_search / fetch_url) and THEN call create_pdf with it.
|
|
4657
|
-
_PDF_RESEARCH_INTENT = re.compile(
|
|
4658
|
-
r"\b(get me|get the|find|research|look ?up|search|fetch|gather|collect|compile|"
|
|
4659
|
-
r"latest|current|today'?s|upcoming|recent|schedules?|fixtures?|standings?|"
|
|
4660
|
-
r"news|weather|prices?|scores?|results?|rankings?|statistics|stats)\b",
|
|
4661
|
-
re.IGNORECASE,
|
|
4662
|
-
)
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
def _pdf_has_explicit_body(user_text: str) -> bool:
|
|
4666
|
-
"""True when the message itself supplies the document body — a quoted span, or text
|
|
4667
|
-
after a 'text:/content:/following:/data:' lead-in. Distinguishes 'make a PDF of THIS
|
|
4668
|
-
text …' (fast-path OK) from 'get me X then make a PDF' (needs the agent to fetch X)."""
|
|
4669
|
-
t = user_text or ""
|
|
4670
|
-
for pat in (r'"([\s\S]+)"', r"“([\s\S]+)”", r"'([\s\S]+)'"):
|
|
4671
|
-
m = re.search(pat, t)
|
|
4672
|
-
if m and len(m.group(1).strip()) >= 40:
|
|
4673
|
-
return True
|
|
4674
|
-
m = re.search(r"\b(texts?|following|below|content|data)\b\s*[:\-]\s*\S", t, re.IGNORECASE)
|
|
4675
|
-
return bool(m)
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
def _wants_pdf(text: str) -> bool:
|
|
4679
|
-
"""True when the user is asking us to (re)generate a PDF — fresh request or a
|
|
4680
|
-
follow-up referencing a target PDF — but NOT when merely asking about one, and NOT
|
|
4681
|
-
when asking us to READ/summarize an existing PDF (that's extract_text_from_pdf)."""
|
|
4682
|
-
t = (text or "").strip()
|
|
4683
|
-
if not t:
|
|
4684
|
-
return False
|
|
4685
|
-
if _PDF_QUESTION.match(t):
|
|
4686
|
-
return False
|
|
4687
|
-
if _PDF_READ_INTENT.search(t):
|
|
4688
|
-
return False
|
|
4689
|
-
return bool(_PDF_INTENT.search(t) or _PDF_FOLLOWUP.search(t))
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
def _extract_pdf_doc_text(user_text: str) -> str:
|
|
4693
|
-
"""Pull the document body out of a 'create a PDF from this text "…"' message.
|
|
4694
|
-
|
|
4695
|
-
Prefers a quoted span; otherwise strips the leading instruction clause.
|
|
4696
|
-
"""
|
|
4697
|
-
t = user_text or ""
|
|
4698
|
-
# 1) Largest quoted span (straight, smart, or single quotes).
|
|
4699
|
-
best = ""
|
|
4700
|
-
for pat in (r'"([\s\S]+)"', r"“([\s\S]+)”", r"'([\s\S]+)'"):
|
|
4701
|
-
m = re.search(pat, t)
|
|
4702
|
-
if m and len(m.group(1).strip()) > len(best):
|
|
4703
|
-
best = m.group(1).strip()
|
|
4704
|
-
if best:
|
|
4705
|
-
return best
|
|
4706
|
-
# 2) Everything after a 'text:'/'following:'/'content:' lead-in.
|
|
4707
|
-
m = re.search(r"\b(texts?|following|below|content|data)\b\s*[:\-]?\s*\n?", t, re.IGNORECASE)
|
|
4708
|
-
if m and t[m.end():].strip():
|
|
4709
|
-
return t[m.end():].strip()
|
|
4710
|
-
# 3) Drop a leading 'please create a pdf (file) from this text:' verb phrase.
|
|
4711
|
-
stripped = re.sub(
|
|
4712
|
-
r"^\s*(please\s+)?(create|make|generate|export|build|produce|convert|turn)\s+"
|
|
4713
|
-
r"(a\s+|an\s+|this\s+|the\s+)?(pdf|document)\s*(file|doc)?\s*"
|
|
4714
|
-
r"(from|for|of|with|using|out of)?\s*(this|the|following)?\s*"
|
|
4715
|
-
r"(text|texts|data|content)?\s*[:\-]?\s*",
|
|
4716
|
-
"",
|
|
4717
|
-
t,
|
|
4718
|
-
flags=re.IGNORECASE,
|
|
4719
|
-
)
|
|
4720
|
-
return stripped.strip() or t.strip()
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
def _derive_pdf_title(doc_text: str) -> str:
|
|
4724
|
-
"""Use the first meaningful line as the document title (strip leading symbols/emoji)."""
|
|
4725
|
-
for line in (doc_text or "").splitlines():
|
|
4726
|
-
s = line.strip().lstrip("#").strip()
|
|
4727
|
-
if not s:
|
|
4728
|
-
continue
|
|
4729
|
-
# Drop leading non-letter symbols/emoji (keep Latin + Vietnamese letters/digits).
|
|
4730
|
-
s2 = re.sub(r"^[^\wÀ-ỹ]+", "", s).strip()
|
|
4731
|
-
return (s2 or s)[:60]
|
|
4732
|
-
return "Document"
|
|
4733
|
-
|
|
4734
4570
|
|
|
4735
4571
|
# ============================ RAG (knowledge base) ============================
|
|
4736
4572
|
# The agent can download a ZIP-at-URL, unzip it locally, index the text files into a
|
|
@@ -7956,106 +7792,6 @@ def run_agent_chat(cfg):
|
|
|
7956
7792
|
_log("router: agent (HTTP tool use needed)")
|
|
7957
7793
|
_emit({"type": "step", "text": "Choosing a tool…"})
|
|
7958
7794
|
|
|
7959
|
-
# True when the user wants a PDF but its content must first be researched by the
|
|
7960
|
-
# agent loop (set below); read by the max-steps fallback to force-deliver a PDF.
|
|
7961
|
-
agent_pdf_requested = False
|
|
7962
|
-
|
|
7963
|
-
# ---- Deterministic PDF fast-path -------------------------------------------------
|
|
7964
|
-
# A small model cannot reliably re-emit a long, emoji/quote-heavy document as a
|
|
7965
|
-
# Python string literal for create_pdf(text="…"), so the CodeAgent call fails before
|
|
7966
|
-
# the tool ever runs. When the user clearly wants a PDF, extract their real text and
|
|
7967
|
-
# run format → render → upload directly. No model string-echoing, and 1 fewer call.
|
|
7968
|
-
# (worker_api_base / worker_api_key are read once near the top of run_agent_chat.)
|
|
7969
|
-
if _wants_pdf(latest_user_text):
|
|
7970
|
-
doc_text = _extract_pdf_doc_text(latest_user_text)
|
|
7971
|
-
# A2: a follow-up like "in the pdf file" / "add the teams to the pdf" carries
|
|
7972
|
-
# no document body — it refers to the last thing the assistant produced. Pull
|
|
7973
|
-
# the most recent substantial assistant turn as the base document and treat the
|
|
7974
|
-
# current user message as an edit instruction the formatter should apply.
|
|
7975
|
-
edit_instruction = ""
|
|
7976
|
-
prior_doc = ""
|
|
7977
|
-
if len(doc_text.strip()) < 40:
|
|
7978
|
-
for m in reversed(messages[:last_user_idx]):
|
|
7979
|
-
if m.get("role") != "assistant":
|
|
7980
|
-
continue
|
|
7981
|
-
cand = think_re.sub("", (m.get("content") or "")).strip()
|
|
7982
|
-
low = cand.lower()
|
|
7983
|
-
# Skip our own PDF-ready confirmations / bare links — not document bodies.
|
|
7984
|
-
if cand.startswith("✅") or low.startswith("http") or "amazonaws.com" in low:
|
|
7985
|
-
continue
|
|
7986
|
-
if len(cand) >= 40:
|
|
7987
|
-
prior_doc = cand
|
|
7988
|
-
break
|
|
7989
|
-
if prior_doc:
|
|
7990
|
-
edit_instruction = latest_user_text.strip()
|
|
7991
|
-
doc_text = prior_doc
|
|
7992
|
-
|
|
7993
|
-
# Guard: if the PDF's content must be RESEARCHED (not provided in the message, not
|
|
7994
|
-
# from a prior turn), don't render the instruction text into a PDF — fall through to
|
|
7995
|
-
# the agent loop so it can web_search/fetch_url the facts and THEN create_pdf them.
|
|
7996
|
-
if (not prior_doc
|
|
7997
|
-
and not _pdf_has_explicit_body(latest_user_text)
|
|
7998
|
-
and _PDF_RESEARCH_INTENT.search(latest_user_text)):
|
|
7999
|
-
_log("PDF fast-path SKIPPED: content needs research (no provided body) "
|
|
8000
|
-
"→ agent loop (web_search → create_pdf)")
|
|
8001
|
-
# Remember the user wants a PDF: if the agent loop exhausts its steps without
|
|
8002
|
-
# calling create_pdf, the max-steps fallback renders the gathered facts anyway.
|
|
8003
|
-
agent_pdf_requested = True
|
|
8004
|
-
else:
|
|
8005
|
-
doc_title = _derive_pdf_title(doc_text)
|
|
8006
|
-
_log(
|
|
8007
|
-
f"PDF fast-path: title={_c(doc_title, r=True)} doc_chars={len(doc_text)}"
|
|
8008
|
-
+ (f" edit={_c(edit_instruction, 60, r=True)}" if edit_instruction else "")
|
|
8009
|
-
)
|
|
8010
|
-
|
|
8011
|
-
_emit({"type": "step", "text": "Formatting document…"})
|
|
8012
|
-
# CHAT model, not the coder (task #74 review): formatting raw text into
|
|
8013
|
-
# Markdown is a language task, and the chat model (local MLX) answers in
|
|
8014
|
-
# seconds where the remote coder can take minutes to first token — inside
|
|
8015
|
-
# the 20s format timeout that difference is "formatted" vs "always falls
|
|
8016
|
-
# back to raw text". Same split as routing/plain replies/summarization.
|
|
8017
|
-
markdown_text = _format_text_for_pdf(
|
|
8018
|
-
doc_text, doc_title,
|
|
8019
|
-
agent_base_url or coding_base_url, agent_api_key,
|
|
8020
|
-
agent_model_id or coding_model_id,
|
|
8021
|
-
instruction=edit_instruction,
|
|
8022
|
-
)
|
|
8023
|
-
|
|
8024
|
-
_emit({"type": "step", "text": "Rendering PDF…"})
|
|
8025
|
-
try:
|
|
8026
|
-
pdf_bytes = _render_pdf_bytes(markdown_text, doc_title)
|
|
8027
|
-
except RuntimeError as e:
|
|
8028
|
-
msg = str(e)
|
|
8029
|
-
_log(f"PDF fast-path render error: {msg[:200]}")
|
|
8030
|
-
_emit({"type": "final", "text": msg})
|
|
8031
|
-
return
|
|
8032
|
-
except Exception as e: # noqa: BLE001
|
|
8033
|
-
_log(f"PDF fast-path render crash: {type(e).__name__}: {e!r}\n{traceback.format_exc()}")
|
|
8034
|
-
_emit({"type": "final", "text": (
|
|
8035
|
-
f"Sorry — I couldn't render that into a PDF ({type(e).__name__}: {str(e)[:160]})."
|
|
8036
|
-
)})
|
|
8037
|
-
return
|
|
8038
|
-
|
|
8039
|
-
if not local_artifacts:
|
|
8040
|
-
_emit({"type": "step", "text": "Uploading PDF…"})
|
|
8041
|
-
try:
|
|
8042
|
-
out = _deliver_artifact(
|
|
8043
|
-
f"{doc_title}.pdf", pdf_bytes, "application/pdf", "PDF",
|
|
8044
|
-
local=local_artifacts, api_base=worker_api_base, worker_key=worker_api_key,
|
|
8045
|
-
ask_s3_setup=_ask_s3_setup,
|
|
8046
|
-
)
|
|
8047
|
-
except Exception as e: # noqa: BLE001
|
|
8048
|
-
_log(f"PDF fast-path delivery error: {type(e).__name__}: {e!r}\n{traceback.format_exc()}")
|
|
8049
|
-
_emit({"type": "final", "text": (
|
|
8050
|
-
f"PDF was created but could not be delivered: {str(e)[:200]}"
|
|
8051
|
-
)})
|
|
8052
|
-
return
|
|
8053
|
-
|
|
8054
|
-
_log(f"PDF fast-path ok title={_c(doc_title, r=True)} bytes={len(pdf_bytes)}")
|
|
8055
|
-
_emit({"type": "final", "text": out})
|
|
8056
|
-
return
|
|
8057
|
-
# ---------------------------------------------------------------------------------
|
|
8058
|
-
|
|
8059
7795
|
# Prepend explicit tool instructions so small models pick the right tool, never
|
|
8060
7796
|
# fabricate URLs/responses, and always terminate with final_answer().
|
|
8061
7797
|
from datetime import datetime as _dt_now
|
|
@@ -8250,10 +7986,6 @@ def run_agent_chat(cfg):
|
|
|
8250
7986
|
"AFTER editing, re-read with grep_repo/read_file_lines (an indexed knowledge base may be "
|
|
8251
7987
|
"stale — do not trust rag_search for freshly edited code).\n"
|
|
8252
7988
|
) if _WS_AVAILABLE else ""
|
|
8253
|
-
_pdf_read_choose_line = (
|
|
8254
|
-
"- 'read'/'summarize'/'what does the PDF say' for an existing PDF URL -> "
|
|
8255
|
-
"extract_text_from_pdf(url).\n"
|
|
8256
|
-
) if _PDF_READ_AVAILABLE else ""
|
|
8257
7989
|
_email_choose_line = (
|
|
8258
7990
|
"- 'email …' / 'send an email to …' -> send_email(to, subject, body) "
|
|
8259
7991
|
"(previews first, then sends on confirm).\n"
|
|
@@ -8345,11 +8077,7 @@ def run_agent_chat(cfg):
|
|
|
8345
8077
|
"- any arithmetic, percentage, or 'how much is' math -> calculate(expression).\n"
|
|
8346
8078
|
"- a live/current NUMBER with no URL given (price, exchange rate, weather, score, "
|
|
8347
8079
|
"stock/crypto) -> web_search(query). Do NOT guess an API URL for these.\n"
|
|
8348
|
-
|
|
8349
|
-
"- a PDF of info you must LOOK UP first (e.g. 'get the world cup schedule then make a "
|
|
8350
|
-
"pdf') -> FIRST web_search/fetch_url to gather the real facts, THEN in a later step "
|
|
8351
|
-
"create_pdf(text=<the gathered facts>, title=...). Never PDF the request wording itself.\n"
|
|
8352
|
-
+ _pdf_read_choose_line + _email_choose_line + _rag_choose_line + _ws_choose_line +
|
|
8080
|
+
+ _email_choose_line + _rag_choose_line + _ws_choose_line +
|
|
8353
8081
|
"- A specific known API/URL was given -> http_request().\n"
|
|
8354
8082
|
"\n"
|
|
8355
8083
|
"RULES:\n"
|
|
@@ -8364,13 +8092,11 @@ def run_agent_chat(cfg):
|
|
|
8364
8092
|
"DIFFERENT approach next step (e.g. web_search); only give up after a few tries.\n"
|
|
8365
8093
|
"- CONVERGE — do NOT keep searching for a 'more complete' or 'perfect' source; a single "
|
|
8366
8094
|
"clean one often does not exist. After 1-2 searches/fetches, COMPILE what you have and "
|
|
8367
|
-
"finish
|
|
8368
|
-
"
|
|
8095
|
+
"finish with the tool that delivers what was asked, or final_answer. NEVER repeat a "
|
|
8096
|
+
"web_search/fetch_url you already ran.\n"
|
|
8369
8097
|
f"- RESEARCH BUDGET — you may make at most {research_budget} web_search/fetch_url calls "
|
|
8370
8098
|
"TOTAL per task (rewording a query still counts). Reserve your remaining steps for "
|
|
8371
|
-
"
|
|
8372
|
-
"- If an earlier Observation already says '✅ Your PDF … is ready', the PDF EXISTS — "
|
|
8373
|
-
"do NOT call create_pdf again. Pass that download link to final_answer.\n"
|
|
8099
|
+
"finishing.\n"
|
|
8374
8100
|
"- Do NOT put final_answer outside the code block.\n\n"
|
|
8375
8101
|
)
|
|
8376
8102
|
# Recover an interrupted prior turn's step trace, if one survives on disk (crash/
|
|
@@ -8503,10 +8229,6 @@ def run_agent_chat(cfg):
|
|
|
8503
8229
|
_research["used"] += 1
|
|
8504
8230
|
return ""
|
|
8505
8231
|
|
|
8506
|
-
# Everything useful the retrieval tools returned this run, so an exhausted loop can
|
|
8507
|
-
# still deterministically deliver (e.g. render a partial PDF) instead of dead-ending.
|
|
8508
|
-
_gathered: list = []
|
|
8509
|
-
|
|
8510
8232
|
# --- Loop / no-progress guards (task #79) -------------------------------------------
|
|
8511
8233
|
# Weak coders thrash on a debug loop: rewrite a file to a previously-tried version,
|
|
8512
8234
|
# re-hit a broken endpoint, and re-read unchanged files — burning the whole step
|
|
@@ -8739,8 +8461,6 @@ def run_agent_chat(cfg):
|
|
|
8739
8461
|
)
|
|
8740
8462
|
_log(f"web_search {_c(query, 60, r=True)} → {_c(result, 80)}")
|
|
8741
8463
|
_last_obs["text"] = result
|
|
8742
|
-
if result and not result.startswith("Error"):
|
|
8743
|
-
_gathered.append(result)
|
|
8744
8464
|
return result
|
|
8745
8465
|
|
|
8746
8466
|
@tool
|
|
@@ -8791,7 +8511,6 @@ def run_agent_chat(cfg):
|
|
|
8791
8511
|
out = f"{url}\n{text}"
|
|
8792
8512
|
_log(f"fetch_url {_u(url)} → {len(text)} chars")
|
|
8793
8513
|
_last_obs["text"] = out
|
|
8794
|
-
_gathered.append(out)
|
|
8795
8514
|
return out
|
|
8796
8515
|
|
|
8797
8516
|
@tool
|
|
@@ -8904,17 +8623,46 @@ def run_agent_chat(cfg):
|
|
|
8904
8623
|
return bool(a and b) and (a == b or a in b or b in a)
|
|
8905
8624
|
|
|
8906
8625
|
@tool
|
|
8907
|
-
def create_pdf(text: str, title: str = "") -> str:
|
|
8908
|
-
"""Create a PDF document
|
|
8626
|
+
def create_pdf(text: str = "", title: str = "", path: str = "") -> str:
|
|
8627
|
+
"""Create a PDF document and return where it is (a download link, or a file on this computer).
|
|
8909
8628
|
|
|
8910
|
-
Use this
|
|
8911
|
-
|
|
8912
|
-
|
|
8629
|
+
Use this when the user asks for a PDF. Give EITHER the content as text, OR the path of a
|
|
8630
|
+
file in the project to turn into a PDF (a .txt/.md/.csv…) — a file is read as it is, so
|
|
8631
|
+
pass its path rather than copying its contents into text. The content is cleaned into
|
|
8632
|
+
Markdown and rendered.
|
|
8913
8633
|
|
|
8914
8634
|
Args:
|
|
8915
|
-
text: The content to put in the PDF (raw text, notes, or data).
|
|
8635
|
+
text: The content to put in the PDF (raw text, notes, or data). Leave empty when giving path.
|
|
8916
8636
|
title: Optional document title shown at the top and used in the file name.
|
|
8637
|
+
path: A text file in the project to turn into a PDF, instead of text.
|
|
8917
8638
|
"""
|
|
8639
|
+
# A FILE, read here (task #269 follow-up): "a PDF of this .txt file" used to be caught by
|
|
8640
|
+
# a keyword fast-path that rendered the REQUEST's own words. The same reach as
|
|
8641
|
+
# read_text_file — registered workspaces, the terminal folder, ~/.gonext, the temp dir.
|
|
8642
|
+
if (path or "").strip():
|
|
8643
|
+
import os as _os_pdf
|
|
8644
|
+
try:
|
|
8645
|
+
_rp = _rag_read_allowed(path.strip())
|
|
8646
|
+
except Exception as _e: # noqa: BLE001
|
|
8647
|
+
msg = f"Error: create_pdf cannot read {path}: {_e}"
|
|
8648
|
+
_last_obs["text"] = msg
|
|
8649
|
+
return msg
|
|
8650
|
+
if not _os_pdf.path.isfile(_rp):
|
|
8651
|
+
msg = f"Error: not a file: {path}"
|
|
8652
|
+
_last_obs["text"] = msg
|
|
8653
|
+
return msg
|
|
8654
|
+
if _os_pdf.path.getsize(_rp) > 2 * 1024 * 1024:
|
|
8655
|
+
msg = f"Error: {path} is larger than 2 MB — too big for one PDF."
|
|
8656
|
+
_last_obs["text"] = msg
|
|
8657
|
+
return msg
|
|
8658
|
+
with open(_rp, "r", encoding="utf-8", errors="replace") as _fh:
|
|
8659
|
+
text = _fh.read()
|
|
8660
|
+
if not (title or "").strip():
|
|
8661
|
+
title = _os_pdf.path.splitext(_os_pdf.path.basename(_rp))[0]
|
|
8662
|
+
if not (text or "").strip():
|
|
8663
|
+
msg = "Error: create_pdf needs the content — text=… or path=<a file in the project>."
|
|
8664
|
+
_last_obs["text"] = msg
|
|
8665
|
+
return msg
|
|
8918
8666
|
doc_title = (title or "").strip() or "Document"
|
|
8919
8667
|
# Retry guard (task #74): identical content is ALWAYS a retry; a merely similar
|
|
8920
8668
|
# title counts only when the previous success was swallowed by an executor
|
|
@@ -8950,7 +8698,8 @@ def run_agent_chat(cfg):
|
|
|
8950
8698
|
_last_obs["text"] = msg
|
|
8951
8699
|
return msg
|
|
8952
8700
|
# 1) Format the raw text into clean Markdown (extra model call, intentional).
|
|
8953
|
-
# CHAT model, not the coder
|
|
8701
|
+
# CHAT model, not the coder: formatting is a plain rewrite, and the coder is the slow,
|
|
8702
|
+
# expensive one (task #74).
|
|
8954
8703
|
_emit({"type": "step", "text": "Formatting document…"})
|
|
8955
8704
|
markdown_text = _format_text_for_pdf(
|
|
8956
8705
|
text or "", doc_title,
|
|
@@ -8981,8 +8730,8 @@ def run_agent_chat(cfg):
|
|
|
8981
8730
|
return msg
|
|
8982
8731
|
|
|
8983
8732
|
# 3) Hand it over: a link via the API-presigned PUT (no AWS creds on the worker), or — in
|
|
8984
|
-
# private mode without the user's own S3 — on this computer (task #252, same path as
|
|
8985
|
-
#
|
|
8733
|
+
# private mode without the user's own S3 — on this computer (task #252, same path as
|
|
8734
|
+
# create_download).
|
|
8986
8735
|
_emit({"type": "step", "text": "Uploading PDF…"})
|
|
8987
8736
|
file_name = f"{doc_title}.pdf"
|
|
8988
8737
|
try:
|
|
@@ -10720,36 +10469,6 @@ def run_agent_chat(cfg):
|
|
|
10720
10469
|
class _ToolAgent(_AgentBase):
|
|
10721
10470
|
def provide_final_answer(self, task, *args, **kwargs):
|
|
10722
10471
|
from smolagents.models import ChatMessage, MessageRole
|
|
10723
|
-
# Deterministic PDF delivery: the user asked for a PDF and the model did the
|
|
10724
|
-
# research but never emitted the create_pdf call itself (weak/slow coders often
|
|
10725
|
-
# keep researching and never converge). Rendering the gathered facts here is the
|
|
10726
|
-
# INTENDED deliverable, not an error — so deliver it cleanly, with NO apologetic
|
|
10727
|
-
# "may be partial / hit my step limit" note (that made a normal, successful PDF
|
|
10728
|
-
# look like a failure to the user).
|
|
10729
|
-
if (agent_pdf_requested and _gathered
|
|
10730
|
-
and not (_last_obs.get("text") or "").startswith("✅")):
|
|
10731
|
-
_log(f"delivering PDF deterministically from {len(_gathered)} "
|
|
10732
|
-
"gathered result(s) (model didn't call create_pdf itself)")
|
|
10733
|
-
try:
|
|
10734
|
-
# Reformat the raw observations into a clean document (headings/tables)
|
|
10735
|
-
# via the fast local chat model; fall back to the raw concatenation if
|
|
10736
|
-
# synthesis fails or returns nothing.
|
|
10737
|
-
compiled = _synthesize_document(
|
|
10738
|
-
_gathered, latest_user_text,
|
|
10739
|
-
agent_base_url, agent_api_key, agent_model_id,
|
|
10740
|
-
)
|
|
10741
|
-
if compiled:
|
|
10742
|
-
_log(f"synthesized document ({len(compiled)} chars) for PDF")
|
|
10743
|
-
else:
|
|
10744
|
-
compiled = "\n\n---\n\n".join(_gathered)[:12000]
|
|
10745
|
-
out = create_pdf(
|
|
10746
|
-
text=compiled, title=_derive_pdf_title(latest_user_text)
|
|
10747
|
-
)
|
|
10748
|
-
if isinstance(out, str) and out.startswith("✅"):
|
|
10749
|
-
return ChatMessage(role=MessageRole.ASSISTANT, content=out)
|
|
10750
|
-
_log(f"deterministic create_pdf did not succeed: {_c(out, 120)}")
|
|
10751
|
-
except Exception as e: # noqa: BLE001
|
|
10752
|
-
_log(f"deterministic create_pdf error: {type(e).__name__}: {e}")
|
|
10753
10472
|
_maxsteps_partial["hit"] = True
|
|
10754
10473
|
text = (_last_obs.get("text") or "").strip()
|
|
10755
10474
|
if text:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.745",
|
|
4
4
|
"description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|