@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.
@@ -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
- raise RuntimeError(f"S3 upload failed (HTTP {e.code}).")
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 (self-explaining) message.
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[:120]}")
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 RuntimeError as e:
828
- msg = f"PDF was created but could not be uploaded: {e}"
829
- _log(f"create_pdf upload error: {str(e)[:120]}")
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.93",
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",