@tiens.nguyen/gonext-local-worker 1.0.94 → 1.0.96
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 +123 -2
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -407,6 +407,21 @@ def _render_pdf_bytes(markdown_text: str, title: str = "") -> bytes:
|
|
|
407
407
|
return out.getvalue()
|
|
408
408
|
|
|
409
409
|
|
|
410
|
+
def _ssl_context():
|
|
411
|
+
"""SSL context backed by certifi's CA bundle.
|
|
412
|
+
|
|
413
|
+
macOS Python (python.org / Homebrew) doesn't use the system keychain, so urllib's
|
|
414
|
+
default verification fails with CERTIFICATE_VERIFY_FAILED. certifi ships with the
|
|
415
|
+
openai/httpx stack the worker already depends on.
|
|
416
|
+
"""
|
|
417
|
+
import ssl
|
|
418
|
+
try:
|
|
419
|
+
import certifi
|
|
420
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
421
|
+
except Exception: # noqa: BLE001 — fall back to system defaults
|
|
422
|
+
return ssl.create_default_context()
|
|
423
|
+
|
|
424
|
+
|
|
410
425
|
def _pdf_upload_via_api(api_base: str, worker_key: str, file_name: str,
|
|
411
426
|
pdf_bytes: bytes) -> str:
|
|
412
427
|
"""Ask the API to presign an S3 upload, PUT the PDF bytes, return the download URL.
|
|
@@ -416,6 +431,7 @@ def _pdf_upload_via_api(api_base: str, worker_key: str, file_name: str,
|
|
|
416
431
|
import urllib.request as _u
|
|
417
432
|
import urllib.error as _ue
|
|
418
433
|
|
|
434
|
+
ctx = _ssl_context()
|
|
419
435
|
base = (api_base or "").rstrip("/")
|
|
420
436
|
if not base or not worker_key:
|
|
421
437
|
raise RuntimeError("PDF upload is not available: worker API base/key missing.")
|
|
@@ -431,7 +447,7 @@ def _pdf_upload_via_api(api_base: str, worker_key: str, file_name: str,
|
|
|
431
447
|
method="POST",
|
|
432
448
|
)
|
|
433
449
|
try:
|
|
434
|
-
with _u.urlopen(req, timeout=30) as resp:
|
|
450
|
+
with _u.urlopen(req, timeout=30, context=ctx) as resp:
|
|
435
451
|
ref = json.loads(resp.read().decode("utf-8"))
|
|
436
452
|
except _ue.HTTPError as e: # noqa: PERF203
|
|
437
453
|
detail = e.read().decode("utf-8", "replace")[:300]
|
|
@@ -453,7 +469,7 @@ def _pdf_upload_via_api(api_base: str, worker_key: str, file_name: str,
|
|
|
453
469
|
method="PUT",
|
|
454
470
|
)
|
|
455
471
|
try:
|
|
456
|
-
with _u.urlopen(put_req, timeout=60) as up:
|
|
472
|
+
with _u.urlopen(put_req, timeout=60, context=ctx) as up:
|
|
457
473
|
if up.status not in (200, 201, 204):
|
|
458
474
|
raise RuntimeError(f"S3 upload failed (HTTP {up.status}).")
|
|
459
475
|
except _ue.HTTPError as e: # noqa: PERF203
|
|
@@ -506,6 +522,59 @@ def _format_text_for_pdf(text: str, title: str, base_url: str, api_key: str,
|
|
|
506
522
|
return fallback
|
|
507
523
|
|
|
508
524
|
|
|
525
|
+
# A clear "make a PDF from this" request. Matched against the raw user message so we
|
|
526
|
+
# can run the PDF pipeline deterministically — a small model cannot reliably echo a
|
|
527
|
+
# long document back into a Python string literal for the create_pdf() tool call.
|
|
528
|
+
_PDF_INTENT = re.compile(
|
|
529
|
+
r"(create|make|generate|export|build|produce|convert|turn)\b[\s\S]{0,40}\bpdf\b"
|
|
530
|
+
r"|\bpdf\b[\s\S]{0,40}\b(from|for|of|with|out of)\b",
|
|
531
|
+
re.IGNORECASE,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _extract_pdf_doc_text(user_text: str) -> str:
|
|
536
|
+
"""Pull the document body out of a 'create a PDF from this text "…"' message.
|
|
537
|
+
|
|
538
|
+
Prefers a quoted span; otherwise strips the leading instruction clause.
|
|
539
|
+
"""
|
|
540
|
+
t = user_text or ""
|
|
541
|
+
# 1) Largest quoted span (straight, smart, or single quotes).
|
|
542
|
+
best = ""
|
|
543
|
+
for pat in (r'"([\s\S]+)"', r"“([\s\S]+)”", r"'([\s\S]+)'"):
|
|
544
|
+
m = re.search(pat, t)
|
|
545
|
+
if m and len(m.group(1).strip()) > len(best):
|
|
546
|
+
best = m.group(1).strip()
|
|
547
|
+
if best:
|
|
548
|
+
return best
|
|
549
|
+
# 2) Everything after a 'text:'/'following:'/'content:' lead-in.
|
|
550
|
+
m = re.search(r"\b(texts?|following|below|content|data)\b\s*[:\-]?\s*\n?", t, re.IGNORECASE)
|
|
551
|
+
if m and t[m.end():].strip():
|
|
552
|
+
return t[m.end():].strip()
|
|
553
|
+
# 3) Drop a leading 'please create a pdf (file) from this text:' verb phrase.
|
|
554
|
+
stripped = re.sub(
|
|
555
|
+
r"^\s*(please\s+)?(create|make|generate|export|build|produce|convert|turn)\s+"
|
|
556
|
+
r"(a\s+|an\s+|this\s+|the\s+)?(pdf|document)\s*(file|doc)?\s*"
|
|
557
|
+
r"(from|for|of|with|using|out of)?\s*(this|the|following)?\s*"
|
|
558
|
+
r"(text|texts|data|content)?\s*[:\-]?\s*",
|
|
559
|
+
"",
|
|
560
|
+
t,
|
|
561
|
+
flags=re.IGNORECASE,
|
|
562
|
+
)
|
|
563
|
+
return stripped.strip() or t.strip()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _derive_pdf_title(doc_text: str) -> str:
|
|
567
|
+
"""Use the first meaningful line as the document title (strip leading symbols/emoji)."""
|
|
568
|
+
for line in (doc_text or "").splitlines():
|
|
569
|
+
s = line.strip().lstrip("#").strip()
|
|
570
|
+
if not s:
|
|
571
|
+
continue
|
|
572
|
+
# Drop leading non-letter symbols/emoji (keep Latin + Vietnamese letters/digits).
|
|
573
|
+
s2 = re.sub(r"^[^\wÀ-ỹ]+", "", s).strip()
|
|
574
|
+
return (s2 or s)[:60]
|
|
575
|
+
return "Document"
|
|
576
|
+
|
|
577
|
+
|
|
509
578
|
def run_agent_chat(cfg):
|
|
510
579
|
try:
|
|
511
580
|
from smolagents import CodeAgent, OpenAIServerModel, tool
|
|
@@ -635,6 +704,58 @@ def run_agent_chat(cfg):
|
|
|
635
704
|
_log("router: agent (HTTP tool use needed)")
|
|
636
705
|
_emit({"type": "step", "text": "Choosing a tool…"})
|
|
637
706
|
|
|
707
|
+
# ---- Deterministic PDF fast-path -------------------------------------------------
|
|
708
|
+
# A small model cannot reliably re-emit a long, emoji/quote-heavy document as a
|
|
709
|
+
# Python string literal for create_pdf(text="…"), so the CodeAgent call fails before
|
|
710
|
+
# the tool ever runs. When the user clearly wants a PDF, extract their real text and
|
|
711
|
+
# run format → render → upload directly. No model string-echoing, and 1 fewer call.
|
|
712
|
+
# (pdf_api_base / pdf_worker_key are read once near the top of run_agent_chat.)
|
|
713
|
+
if _PDF_INTENT.search(latest_user_text or ""):
|
|
714
|
+
doc_text = _extract_pdf_doc_text(latest_user_text)
|
|
715
|
+
doc_title = _derive_pdf_title(doc_text)
|
|
716
|
+
_log(f"PDF fast-path: title={doc_title!r} doc_chars={len(doc_text)}")
|
|
717
|
+
|
|
718
|
+
_emit({"type": "step", "text": "Formatting document…"})
|
|
719
|
+
markdown_text = _format_text_for_pdf(
|
|
720
|
+
doc_text, doc_title, coding_base_url, agent_api_key, coding_model_id
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
_emit({"type": "step", "text": "Rendering PDF…"})
|
|
724
|
+
try:
|
|
725
|
+
pdf_bytes = _render_pdf_bytes(markdown_text, doc_title)
|
|
726
|
+
except RuntimeError as e:
|
|
727
|
+
msg = str(e)
|
|
728
|
+
_log(f"PDF fast-path render error: {msg[:200]}")
|
|
729
|
+
_emit({"type": "final", "text": msg})
|
|
730
|
+
return
|
|
731
|
+
except Exception as e: # noqa: BLE001
|
|
732
|
+
_log(f"PDF fast-path render crash: {type(e).__name__}: {e!r}\n{traceback.format_exc()}")
|
|
733
|
+
_emit({"type": "final", "text": (
|
|
734
|
+
f"Sorry — I couldn't render that into a PDF ({type(e).__name__}: {str(e)[:160]})."
|
|
735
|
+
)})
|
|
736
|
+
return
|
|
737
|
+
|
|
738
|
+
_emit({"type": "step", "text": "Uploading PDF…"})
|
|
739
|
+
try:
|
|
740
|
+
download_url = _pdf_upload_via_api(
|
|
741
|
+
pdf_api_base, pdf_worker_key, f"{doc_title}.pdf", pdf_bytes
|
|
742
|
+
)
|
|
743
|
+
except Exception as e: # noqa: BLE001
|
|
744
|
+
_log(f"PDF fast-path upload error: {type(e).__name__}: {e!r}\n{traceback.format_exc()}")
|
|
745
|
+
_emit({"type": "final", "text": (
|
|
746
|
+
f"PDF was created but could not be uploaded: {str(e)[:200]}"
|
|
747
|
+
)})
|
|
748
|
+
return
|
|
749
|
+
|
|
750
|
+
out = (
|
|
751
|
+
f"✅ Your PDF \"{doc_title}\" is ready.\n"
|
|
752
|
+
f"Download it here (link valid for a limited time):\n{download_url}"
|
|
753
|
+
)
|
|
754
|
+
_log(f"PDF fast-path ok title={doc_title!r} bytes={len(pdf_bytes)}")
|
|
755
|
+
_emit({"type": "final", "text": out})
|
|
756
|
+
return
|
|
757
|
+
# ---------------------------------------------------------------------------------
|
|
758
|
+
|
|
638
759
|
# Prepend explicit tool instructions so small models pick the right tool, never
|
|
639
760
|
# fabricate URLs/responses, and always terminate with final_answer().
|
|
640
761
|
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.96",
|
|
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",
|