@tiens.nguyen/gonext-local-worker 1.0.143 → 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.
- package/gonext_agent_chat.py +79 -17
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -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
|
|
1448
|
-
#
|
|
1449
|
-
#
|
|
1450
|
-
|
|
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."""
|
|
@@ -1872,10 +1912,13 @@ def run_agent_chat(cfg):
|
|
|
1872
1912
|
# Ollama honors it). Harmless if the server ignores it — usage stays 0.
|
|
1873
1913
|
completion_kwargs["stream_options"] = {"include_usage": True}
|
|
1874
1914
|
self._apply_rate_limit()
|
|
1915
|
+
t0 = time.monotonic()
|
|
1916
|
+
_log("streamed generate: request issued (stream=True), awaiting first token…")
|
|
1875
1917
|
stream = self.client.chat.completions.create(**completion_kwargs)
|
|
1876
1918
|
parts = []
|
|
1877
1919
|
role = "assistant"
|
|
1878
1920
|
in_tok = out_tok = 0
|
|
1921
|
+
first_token_at = None
|
|
1879
1922
|
for chunk in stream:
|
|
1880
1923
|
usage = getattr(chunk, "usage", None)
|
|
1881
1924
|
if usage is not None:
|
|
@@ -1891,6 +1934,17 @@ def run_agent_chat(cfg):
|
|
|
1891
1934
|
role = delta.role
|
|
1892
1935
|
piece = getattr(delta, "content", None)
|
|
1893
1936
|
if piece:
|
|
1937
|
+
if first_token_at is None:
|
|
1938
|
+
# First streamed byte. The gap t0→here is pure prompt-eval time
|
|
1939
|
+
# (server sent nothing before this); everything after is token
|
|
1940
|
+
# generation with bytes flowing, so the proxy idle timer resets.
|
|
1941
|
+
# If this line never appears in the log, the request died in
|
|
1942
|
+
# prompt-eval BEFORE any token — streaming can't help there
|
|
1943
|
+
# (shrink the model / warm the prompt cache instead).
|
|
1944
|
+
first_token_at = time.monotonic()
|
|
1945
|
+
_log(f"streamed generate: FIRST token after {first_token_at - t0:.1f}s "
|
|
1946
|
+
"(prompt-eval done; tokens now flowing)")
|
|
1947
|
+
_emit({"type": "step", "text": "Model is responding…"})
|
|
1894
1948
|
parts.append(piece)
|
|
1895
1949
|
content = "".join(parts)
|
|
1896
1950
|
if stop_sequences and not self.supports_stop_parameter:
|
|
@@ -2067,28 +2121,36 @@ def run_agent_chat(cfg):
|
|
|
2067
2121
|
class _ToolAgent(_AgentBase):
|
|
2068
2122
|
def provide_final_answer(self, task, *args, **kwargs):
|
|
2069
2123
|
from smolagents.models import ChatMessage, MessageRole
|
|
2070
|
-
# PDF
|
|
2071
|
-
#
|
|
2072
|
-
#
|
|
2073
|
-
#
|
|
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).
|
|
2074
2130
|
if (agent_pdf_requested and _gathered
|
|
2075
2131
|
and not (_last_obs.get("text") or "").startswith("✅")):
|
|
2076
|
-
_log(f"
|
|
2077
|
-
"gathered
|
|
2132
|
+
_log(f"delivering PDF deterministically from {len(_gathered)} "
|
|
2133
|
+
"gathered result(s) (model didn't call create_pdf itself)")
|
|
2078
2134
|
try:
|
|
2079
|
-
|
|
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]
|
|
2080
2146
|
out = create_pdf(
|
|
2081
2147
|
text=compiled, title=_derive_pdf_title(latest_user_text)
|
|
2082
2148
|
)
|
|
2083
2149
|
if isinstance(out, str) and out.startswith("✅"):
|
|
2084
|
-
out += (
|
|
2085
|
-
"\n\n(Note: I hit my research step limit, so this PDF contains "
|
|
2086
|
-
"what I could gather — it may be partial.)"
|
|
2087
|
-
)
|
|
2088
2150
|
return ChatMessage(role=MessageRole.ASSISTANT, content=out)
|
|
2089
|
-
_log(f"
|
|
2151
|
+
_log(f"deterministic create_pdf did not succeed: {str(out)[:120]}")
|
|
2090
2152
|
except Exception as e: # noqa: BLE001
|
|
2091
|
-
_log(f"
|
|
2153
|
+
_log(f"deterministic create_pdf error: {type(e).__name__}: {e}")
|
|
2092
2154
|
text = (_last_obs.get("text") or "").strip()
|
|
2093
2155
|
if text:
|
|
2094
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.
|
|
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",
|