@tiens.nguyen/gonext-local-worker 1.0.94 → 1.0.95

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.
@@ -506,6 +506,59 @@ def _format_text_for_pdf(text: str, title: str, base_url: str, api_key: str,
506
506
  return fallback
507
507
 
508
508
 
509
+ # A clear "make a PDF from this" request. Matched against the raw user message so we
510
+ # can run the PDF pipeline deterministically — a small model cannot reliably echo a
511
+ # long document back into a Python string literal for the create_pdf() tool call.
512
+ _PDF_INTENT = re.compile(
513
+ r"(create|make|generate|export|build|produce|convert|turn)\b[\s\S]{0,40}\bpdf\b"
514
+ r"|\bpdf\b[\s\S]{0,40}\b(from|for|of|with|out of)\b",
515
+ re.IGNORECASE,
516
+ )
517
+
518
+
519
+ def _extract_pdf_doc_text(user_text: str) -> str:
520
+ """Pull the document body out of a 'create a PDF from this text "…"' message.
521
+
522
+ Prefers a quoted span; otherwise strips the leading instruction clause.
523
+ """
524
+ t = user_text or ""
525
+ # 1) Largest quoted span (straight, smart, or single quotes).
526
+ best = ""
527
+ for pat in (r'"([\s\S]+)"', r"“([\s\S]+)”", r"'([\s\S]+)'"):
528
+ m = re.search(pat, t)
529
+ if m and len(m.group(1).strip()) > len(best):
530
+ best = m.group(1).strip()
531
+ if best:
532
+ return best
533
+ # 2) Everything after a 'text:'/'following:'/'content:' lead-in.
534
+ m = re.search(r"\b(texts?|following|below|content|data)\b\s*[:\-]?\s*\n?", t, re.IGNORECASE)
535
+ if m and t[m.end():].strip():
536
+ return t[m.end():].strip()
537
+ # 3) Drop a leading 'please create a pdf (file) from this text:' verb phrase.
538
+ stripped = re.sub(
539
+ r"^\s*(please\s+)?(create|make|generate|export|build|produce|convert|turn)\s+"
540
+ r"(a\s+|an\s+|this\s+|the\s+)?(pdf|document)\s*(file|doc)?\s*"
541
+ r"(from|for|of|with|using|out of)?\s*(this|the|following)?\s*"
542
+ r"(text|texts|data|content)?\s*[:\-]?\s*",
543
+ "",
544
+ t,
545
+ flags=re.IGNORECASE,
546
+ )
547
+ return stripped.strip() or t.strip()
548
+
549
+
550
+ def _derive_pdf_title(doc_text: str) -> str:
551
+ """Use the first meaningful line as the document title (strip leading symbols/emoji)."""
552
+ for line in (doc_text or "").splitlines():
553
+ s = line.strip().lstrip("#").strip()
554
+ if not s:
555
+ continue
556
+ # Drop leading non-letter symbols/emoji (keep Latin + Vietnamese letters/digits).
557
+ s2 = re.sub(r"^[^\wÀ-ỹ]+", "", s).strip()
558
+ return (s2 or s)[:60]
559
+ return "Document"
560
+
561
+
509
562
  def run_agent_chat(cfg):
510
563
  try:
511
564
  from smolagents import CodeAgent, OpenAIServerModel, tool
@@ -635,6 +688,58 @@ def run_agent_chat(cfg):
635
688
  _log("router: agent (HTTP tool use needed)")
636
689
  _emit({"type": "step", "text": "Choosing a tool…"})
637
690
 
691
+ # ---- Deterministic PDF fast-path -------------------------------------------------
692
+ # A small model cannot reliably re-emit a long, emoji/quote-heavy document as a
693
+ # Python string literal for create_pdf(text="…"), so the CodeAgent call fails before
694
+ # the tool ever runs. When the user clearly wants a PDF, extract their real text and
695
+ # run format → render → upload directly. No model string-echoing, and 1 fewer call.
696
+ # (pdf_api_base / pdf_worker_key are read once near the top of run_agent_chat.)
697
+ if _PDF_INTENT.search(latest_user_text or ""):
698
+ doc_text = _extract_pdf_doc_text(latest_user_text)
699
+ doc_title = _derive_pdf_title(doc_text)
700
+ _log(f"PDF fast-path: title={doc_title!r} doc_chars={len(doc_text)}")
701
+
702
+ _emit({"type": "step", "text": "Formatting document…"})
703
+ markdown_text = _format_text_for_pdf(
704
+ doc_text, doc_title, coding_base_url, agent_api_key, coding_model_id
705
+ )
706
+
707
+ _emit({"type": "step", "text": "Rendering PDF…"})
708
+ try:
709
+ pdf_bytes = _render_pdf_bytes(markdown_text, doc_title)
710
+ except RuntimeError as e:
711
+ msg = str(e)
712
+ _log(f"PDF fast-path render error: {msg[:200]}")
713
+ _emit({"type": "final", "text": msg})
714
+ return
715
+ except Exception as e: # noqa: BLE001
716
+ _log(f"PDF fast-path render crash: {type(e).__name__}: {e!r}\n{traceback.format_exc()}")
717
+ _emit({"type": "final", "text": (
718
+ f"Sorry — I couldn't render that into a PDF ({type(e).__name__}: {str(e)[:160]})."
719
+ )})
720
+ return
721
+
722
+ _emit({"type": "step", "text": "Uploading PDF…"})
723
+ try:
724
+ download_url = _pdf_upload_via_api(
725
+ pdf_api_base, pdf_worker_key, f"{doc_title}.pdf", pdf_bytes
726
+ )
727
+ except Exception as e: # noqa: BLE001
728
+ _log(f"PDF fast-path upload error: {type(e).__name__}: {e!r}\n{traceback.format_exc()}")
729
+ _emit({"type": "final", "text": (
730
+ f"PDF was created but could not be uploaded: {str(e)[:200]}"
731
+ )})
732
+ return
733
+
734
+ out = (
735
+ f"✅ Your PDF \"{doc_title}\" is ready.\n"
736
+ f"Download it here (link valid for a limited time):\n{download_url}"
737
+ )
738
+ _log(f"PDF fast-path ok title={doc_title!r} bytes={len(pdf_bytes)}")
739
+ _emit({"type": "final", "text": out})
740
+ return
741
+ # ---------------------------------------------------------------------------------
742
+
638
743
  # Prepend explicit tool instructions so small models pick the right tool, never
639
744
  # fabricate URLs/responses, and always terminate with final_answer().
640
745
  from datetime import datetime as _dt_now
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.94",
3
+ "version": "1.0.95",
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",