@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.
@@ -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.94",
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",