feat(agent): search_documents tool — the model can grep the indexed documents for an exact string
This commit is contained in:
+39
-30
@@ -49,25 +49,30 @@ outline (0 when absent) and the per-turn log line records
|
||||
|
||||
Agent document tools (phase 37, PLAN §4 extension, owner permission
|
||||
2026-08-26; phase 45 removed the per-tool budgets — owner permission
|
||||
2026-08-27): a **grounded** turn (``not plan.deflected``) no longer
|
||||
streams a bare ``chat_stream`` — it runs the agent loop
|
||||
(``app.rag.agent.run_agent``), which offers the model the two
|
||||
server-side tools ``list_documents`` / ``read_document`` for the whole
|
||||
turn (as many calls as the model wants, re-lists included) until it
|
||||
answers or the round cap (``BOR_AGENT_MAX_ROUNDS``, default 10) forces
|
||||
one final no-tools answer. Each model-requested call streams as an SSE
|
||||
``tool`` event — ``{"type": "tool", "name": …, "argument":
|
||||
"source/path" | null}`` — ahead of the answer's ``delta`` frames.
|
||||
``done.sources``, ``query_log.sources`` and the per-turn log line all
|
||||
report the same combined source list (retrieval docs + the agent's
|
||||
read docs, deduped by ``(source, path)``, order preserved), and the log
|
||||
line records ``tool_calls=N`` after ``thinking_chars=N`` (PLAN §9 line
|
||||
extension — ``N`` counts executed tool calls; rejected calls do not
|
||||
count). **Deflected turns keep the direct ``chat_stream`` —
|
||||
byte-identical to the pre-phase path (A8):** the LOW prompt never
|
||||
carries tools, and with ``agent_max_rounds`` at **0** ``run_agent``
|
||||
makes exactly one ``tools=None`` request, reproducing the pre-phase
|
||||
behavior (the kill switch).
|
||||
2026-08-27; phase 68 added the ``search_documents`` grep): a
|
||||
**grounded** turn (``not plan.deflected``) no longer streams a bare
|
||||
``chat_stream`` — it runs the agent loop (``app.rag.agent.run_agent``),
|
||||
which offers the model the three server-side tools
|
||||
``list_documents`` / ``read_document`` / ``search_documents`` for the
|
||||
whole turn (as many calls as the model wants, re-lists and re-searches
|
||||
included) until it answers or the round cap (``BOR_AGENT_MAX_ROUNDS``,
|
||||
default 10) forces one final no-tools answer. Each model-requested call
|
||||
streams as an SSE ``tool`` event — ``{"type": "tool", "name": …,
|
||||
"argument": "source/path" | pattern | null}`` — ahead of the answer's
|
||||
``delta`` frames: ``argument`` is the read document's path for
|
||||
``read_document``, the raw search pattern for ``search_documents``
|
||||
(a non-string pattern — a model error the backend refuses — yields
|
||||
null), and null for ``list_documents``. ``done.sources``,
|
||||
``query_log.sources`` and the per-turn log line all report the same
|
||||
combined source list (retrieval docs + the agent's read docs, deduped
|
||||
by ``(source, path)``, order preserved — a search adds no source; it is
|
||||
a locator, locked A5), and the log line records ``tool_calls=N`` after
|
||||
``thinking_chars=N`` (PLAN §9 line extension — ``N`` counts executed
|
||||
tool calls; rejected calls do not count). **Deflected turns keep the
|
||||
direct ``chat_stream`` — byte-identical to the pre-phase path (A8):**
|
||||
the LOW prompt never carries tools, and with ``agent_max_rounds`` at
|
||||
**0** ``run_agent`` makes exactly one ``tools=None`` request,
|
||||
reproducing the pre-phase behavior (the kill switch).
|
||||
|
||||
LLM retries (phase 67, ``BOR_LLM_RETRIES`` / ``BOR_LLM_RETRY_DELAY``,
|
||||
owner-locked 2026-09-01): when the aipi endpoint dies before a request
|
||||
@@ -396,18 +401,22 @@ async def chat(
|
||||
async for piece in answer_stream: # StreamPiece | ToolCallPiece | RetryPiece
|
||||
if isinstance(piece, ToolCallPiece):
|
||||
# Phase 37 (PLAN §4 extension): one SSE ``tool``
|
||||
# frame per model-requested call; ``argument`` is
|
||||
# the read_document "source/path" (null
|
||||
# otherwise).
|
||||
# frame per model-requested call. ``argument`` is
|
||||
# the read_document "source/path"; phase 68
|
||||
# extends it with the search_documents pattern
|
||||
# (a non-string pattern — a model error the
|
||||
# backend refuses — is null); null otherwise.
|
||||
if piece.name == "read_document":
|
||||
argument = (
|
||||
f"{piece.arguments.get('source')}/{piece.arguments.get('path')}"
|
||||
)
|
||||
elif piece.name == "search_documents":
|
||||
pattern = piece.arguments.get("pattern")
|
||||
argument = pattern if isinstance(pattern, str) else None
|
||||
else:
|
||||
argument = None
|
||||
yield sse_event(
|
||||
ChatToolEvent(
|
||||
name=piece.name,
|
||||
argument=(
|
||||
f"{piece.arguments.get('source')}/{piece.arguments.get('path')}"
|
||||
if piece.name == "read_document"
|
||||
else None
|
||||
),
|
||||
).model_dump()
|
||||
ChatToolEvent(name=piece.name, argument=argument).model_dump()
|
||||
)
|
||||
continue
|
||||
if isinstance(piece, RetryPiece):
|
||||
|
||||
Reference in New Issue
Block a user