@tiens.nguyen/gonext-local-worker 1.0.144 → 1.0.145

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.
@@ -626,6 +626,44 @@ def _plain_reply(messages: list, base_url: str, api_key: str, model_id: str) ->
626
626
  return f"[Error: {e}]"
627
627
 
628
628
 
629
+ def _synthesize_document(gathered: list, task: str, base_url: str, api_key: str,
630
+ model_id: str) -> str:
631
+ """Turn raw gathered research observations into ONE clean, organized document body
632
+ for create_pdf. This is a pure REFORMAT (organize/deduplicate facts already found —
633
+ no new research), so the fast local chat model handles it well. Returns "" on any
634
+ failure so the caller can fall back to the raw concatenation."""
635
+ notes = "\n\n---\n\n".join(gathered)[:12000]
636
+ sys_prompt = (
637
+ "You are a document editor. You are given a user's request and raw research "
638
+ "notes gathered to answer it. Produce a single, clean, well-organized document "
639
+ "that fulfills the request using ONLY facts present in the notes.\n"
640
+ "- Use a clear title, headings, and bullet lists or a markdown table where it "
641
+ "fits (e.g. schedules → a table of Teams | Date | Time).\n"
642
+ "- Deduplicate and order the information sensibly (e.g. by date).\n"
643
+ "- Do NOT invent facts not in the notes, and do NOT mention the research process, "
644
+ "steps, tools, or that anything is partial. Output ONLY the document body."
645
+ )
646
+ user_prompt = f"USER REQUEST:\n{task}\n\nRESEARCH NOTES:\n{notes}"
647
+ try:
648
+ from openai import OpenAI
649
+ client = OpenAI(base_url=base_url, api_key=api_key or "local",
650
+ max_retries=0, timeout=120)
651
+ resp = client.chat.completions.create(
652
+ model=model_id,
653
+ messages=[{"role": "system", "content": sys_prompt},
654
+ {"role": "user", "content": user_prompt}],
655
+ temperature=0.3,
656
+ max_tokens=2048,
657
+ )
658
+ out = (resp.choices[0].message.content or "").strip()
659
+ # A reasoning model might wrap output in <think>…</think>; strip it.
660
+ out = _THINK_BLOCK.sub("", out).strip()
661
+ return out
662
+ except Exception as e: # noqa: BLE001
663
+ _log(f"document synthesis failed ({e}) → will use raw gathered notes")
664
+ return ""
665
+
666
+
629
667
  def _strip_tool_tags(text: str) -> str:
630
668
  """Remove the internal hint tags we append to tool output (e.g. '[SUCCESS …]',
631
669
  '[NOTE: …]', 'Note: This URL failed …') so they never leak into the user reply."""
@@ -1444,10 +1482,12 @@ def run_agent_chat(cfg):
1444
1482
  return " ".join(sorted(tokens))
1445
1483
 
1446
1484
  # Hard research budget: at most N retrieval calls (web_search + fetch_url) per run.
1447
- # Past it, retrieval tools refuse and steer to create_pdf/final_answer. Keeps ≥2 of
1448
- # the 5 steps free to actually FINISH — the live failure mode was burning ALL steps
1449
- # on slightly-different searches and never producing the asked-for PDF.
1450
- _research = {"used": 0, "max": 3}
1485
+ # Past it, retrieval tools refuse and steer to create_pdf/final_answer. Keeps steps
1486
+ # free to actually FINISH — the live failure mode was burning ALL steps on
1487
+ # slightly-different searches and never producing the asked-for PDF. PDF tasks get a
1488
+ # TIGHTER budget (2) so the model is steered to create_pdf a step sooner and has more
1489
+ # steps left to actually emit that call itself (avoids leaning on the fallback).
1490
+ _research = {"used": 0, "max": 2 if agent_pdf_requested else 3}
1451
1491
 
1452
1492
  def _research_spend(tool_name: str) -> str:
1453
1493
  """Return '' if under budget (and spend 1), else a refusal steering to finish."""
@@ -2081,28 +2121,36 @@ def run_agent_chat(cfg):
2081
2121
  class _ToolAgent(_AgentBase):
2082
2122
  def provide_final_answer(self, task, *args, **kwargs):
2083
2123
  from smolagents.models import ChatMessage, MessageRole
2084
- # PDF-on-exhaustion guarantee: the user asked for a PDF and the loop burned
2085
- # all steps researching without ever calling create_pdf (seen live: 5 steps
2086
- # of search/fetch, zero deliverable). Render whatever WAS gathered into a
2087
- # (possibly partial) PDF instead of returning raw page text.
2124
+ # Deterministic PDF delivery: the user asked for a PDF and the model did the
2125
+ # research but never emitted the create_pdf call itself (weak/slow coders often
2126
+ # keep researching and never converge). Rendering the gathered facts here is the
2127
+ # INTENDED deliverable, not an error so deliver it cleanly, with NO apologetic
2128
+ # "may be partial / hit my step limit" note (that made a normal, successful PDF
2129
+ # look like a failure to the user).
2088
2130
  if (agent_pdf_requested and _gathered
2089
2131
  and not (_last_obs.get("text") or "").startswith("✅")):
2090
- _log(f"max-steps fallback forcing create_pdf from {len(_gathered)} "
2091
- "gathered observation(s)")
2132
+ _log(f"delivering PDF deterministically from {len(_gathered)} "
2133
+ "gathered result(s) (model didn't call create_pdf itself)")
2092
2134
  try:
2093
- compiled = "\n\n---\n\n".join(_gathered)[:12000]
2135
+ # Reformat the raw observations into a clean document (headings/tables)
2136
+ # via the fast local chat model; fall back to the raw concatenation if
2137
+ # synthesis fails or returns nothing.
2138
+ compiled = _synthesize_document(
2139
+ _gathered, latest_user_text,
2140
+ agent_base_url, agent_api_key, agent_model_id,
2141
+ )
2142
+ if compiled:
2143
+ _log(f"synthesized document ({len(compiled)} chars) for PDF")
2144
+ else:
2145
+ compiled = "\n\n---\n\n".join(_gathered)[:12000]
2094
2146
  out = create_pdf(
2095
2147
  text=compiled, title=_derive_pdf_title(latest_user_text)
2096
2148
  )
2097
2149
  if isinstance(out, str) and out.startswith("✅"):
2098
- out += (
2099
- "\n\n(Note: I hit my research step limit, so this PDF contains "
2100
- "what I could gather — it may be partial.)"
2101
- )
2102
2150
  return ChatMessage(role=MessageRole.ASSISTANT, content=out)
2103
- _log(f"forced create_pdf did not succeed: {str(out)[:120]}")
2151
+ _log(f"deterministic create_pdf did not succeed: {str(out)[:120]}")
2104
2152
  except Exception as e: # noqa: BLE001
2105
- _log(f"forced create_pdf error: {type(e).__name__}: {e}")
2153
+ _log(f"deterministic create_pdf error: {type(e).__name__}: {e}")
2106
2154
  text = (_last_obs.get("text") or "").strip()
2107
2155
  if text:
2108
2156
  _log(f"max-steps fallback (last tool obs) → {text[:80]}")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.144",
3
+ "version": "1.0.145",
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",