@tiens.nguyen/gonext-local-worker 1.0.93 → 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 +131 -6
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -23,6 +23,7 @@ import contextlib
|
|
|
23
23
|
import json
|
|
24
24
|
import re
|
|
25
25
|
import sys
|
|
26
|
+
import traceback
|
|
26
27
|
import urllib.request
|
|
27
28
|
import urllib.error
|
|
28
29
|
|
|
@@ -435,6 +436,11 @@ def _pdf_upload_via_api(api_base: str, worker_key: str, file_name: str,
|
|
|
435
436
|
except _ue.HTTPError as e: # noqa: PERF203
|
|
436
437
|
detail = e.read().decode("utf-8", "replace")[:300]
|
|
437
438
|
raise RuntimeError(f"Could not get a PDF upload URL (HTTP {e.code}): {detail}")
|
|
439
|
+
except _ue.URLError as e:
|
|
440
|
+
raise RuntimeError(
|
|
441
|
+
f"Could not reach the API at {base} to presign the upload "
|
|
442
|
+
f"({getattr(e, 'reason', e)}). Is the worker API deployed/online?"
|
|
443
|
+
)
|
|
438
444
|
put_url = ref.get("putUrl")
|
|
439
445
|
get_url = ref.get("getUrl")
|
|
440
446
|
if not put_url or not get_url:
|
|
@@ -451,7 +457,10 @@ def _pdf_upload_via_api(api_base: str, worker_key: str, file_name: str,
|
|
|
451
457
|
if up.status not in (200, 201, 204):
|
|
452
458
|
raise RuntimeError(f"S3 upload failed (HTTP {up.status}).")
|
|
453
459
|
except _ue.HTTPError as e: # noqa: PERF203
|
|
454
|
-
|
|
460
|
+
detail = e.read().decode("utf-8", "replace")[:200]
|
|
461
|
+
raise RuntimeError(f"S3 upload failed (HTTP {e.code}): {detail}")
|
|
462
|
+
except _ue.URLError as e:
|
|
463
|
+
raise RuntimeError(f"S3 upload could not connect ({getattr(e, 'reason', e)}).")
|
|
455
464
|
return get_url
|
|
456
465
|
|
|
457
466
|
|
|
@@ -497,6 +506,59 @@ def _format_text_for_pdf(text: str, title: str, base_url: str, api_key: str,
|
|
|
497
506
|
return fallback
|
|
498
507
|
|
|
499
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
|
+
|
|
500
562
|
def run_agent_chat(cfg):
|
|
501
563
|
try:
|
|
502
564
|
from smolagents import CodeAgent, OpenAIServerModel, tool
|
|
@@ -626,6 +688,58 @@ def run_agent_chat(cfg):
|
|
|
626
688
|
_log("router: agent (HTTP tool use needed)")
|
|
627
689
|
_emit({"type": "step", "text": "Choosing a tool…"})
|
|
628
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
|
+
|
|
629
743
|
# Prepend explicit tool instructions so small models pick the right tool, never
|
|
630
744
|
# fabricate URLs/responses, and always terminate with final_answer().
|
|
631
745
|
from datetime import datetime as _dt_now
|
|
@@ -807,13 +921,24 @@ def run_agent_chat(cfg):
|
|
|
807
921
|
)
|
|
808
922
|
|
|
809
923
|
# 2) Render the Markdown to PDF bytes locally (pure-Python xhtml2pdf).
|
|
924
|
+
# Catch EVERYTHING (xhtml2pdf can raise arbitrary errors on exotic glyphs /
|
|
925
|
+
# emoji), so the tool always returns a string and never breaks the single shot.
|
|
810
926
|
_emit({"type": "step", "text": "Rendering PDF…"})
|
|
811
927
|
try:
|
|
812
928
|
pdf_bytes = _render_pdf_bytes(markdown_text, doc_title)
|
|
813
929
|
except RuntimeError as e:
|
|
814
|
-
# Engine missing or render failure — surface the
|
|
930
|
+
# Engine missing or a clean render failure — surface the message as-is.
|
|
815
931
|
msg = str(e)
|
|
816
|
-
_log(f"create_pdf render error: {msg[:
|
|
932
|
+
_log(f"create_pdf render error: {msg[:200]}")
|
|
933
|
+
_last_obs["text"] = msg
|
|
934
|
+
return msg
|
|
935
|
+
except Exception as e: # noqa: BLE001 — unexpected render crash
|
|
936
|
+
_log(f"create_pdf render crash: {type(e).__name__}: {e!r}\n{traceback.format_exc()}")
|
|
937
|
+
msg = (
|
|
938
|
+
"Sorry — I couldn't render that text into a PDF "
|
|
939
|
+
f"({type(e).__name__}: {str(e)[:160]}). "
|
|
940
|
+
"Try simpler text without unusual symbols/emoji."
|
|
941
|
+
)
|
|
817
942
|
_last_obs["text"] = msg
|
|
818
943
|
return msg
|
|
819
944
|
|
|
@@ -824,9 +949,9 @@ def run_agent_chat(cfg):
|
|
|
824
949
|
download_url = _pdf_upload_via_api(
|
|
825
950
|
pdf_api_base, pdf_worker_key, file_name, pdf_bytes
|
|
826
951
|
)
|
|
827
|
-
except
|
|
828
|
-
|
|
829
|
-
|
|
952
|
+
except Exception as e: # noqa: BLE001 — network / API errors must not crash the tool
|
|
953
|
+
_log(f"create_pdf upload error: {type(e).__name__}: {e!r}\n{traceback.format_exc()}")
|
|
954
|
+
msg = f"PDF was created but could not be uploaded: {str(e)[:200]}"
|
|
830
955
|
_last_obs["text"] = msg
|
|
831
956
|
return msg
|
|
832
957
|
|
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",
|