@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.
- package/gonext_agent_chat.py +105 -0
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -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.
|
|
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",
|