@tiens.nguyen/gu-cli 1.0.743 → 1.0.744

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.
Files changed (2) hide show
  1. package/gonext_agent_chat.py +43 -324
  2. package/package.json +1 -1
@@ -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
- "- 'create/make/generate/export a PDF' of some text/data -> create_pdf(text, title).\n"
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: if a PDF was asked for, call create_pdf(text=<your compiled text>, title=...); "
8368
- "otherwise call final_answer. NEVER repeat a web_search/fetch_url you already ran.\n"
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
- "create_pdf / final_answer.\n"
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 from text/data and return a download link.
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 ONLY when the user explicitly asks to make/create/generate/export a PDF.
8911
- The text is first cleaned into well-formed Markdown, then rendered to a PDF on the
8912
- worker and uploaded to cloud storage; the returned message contains the download URL.
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 — see the fast-path call site for why (task #74).
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 the
8985
- # PDF fast-path and create_download).
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.743",
3
+ "version": "1.0.744",
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",