feat(rag): agent document tools — list/read tools with env-tuned budgets, SSE tool events + "calling tool" UI
Grounded chat turns now run the agent loop (app/rag/agent.py) instead
of a bare chat_stream: while the per-turn budgets last
(BOR_AGENT_LIST_CALLS / BOR_AGENT_READ_CALLS, default 1 each) the model
gets list_documents (the indexed catalog, /api/docs order) and
read_document (full text, never truncated — A7-revised contract); once
both budgets are spent the tools key is dropped from the request and
the model must answer. Rejected calls (unknown tool, unknown/missing
path, document already in context, spent budget) consume no budget.
Budgets 0/0 make exactly one tools=None request — byte-identical to
the pre-phase path (budgets-as-kill-switch). Deflected turns keep the
direct chat_stream (A8 unchanged; the LOW prompt never carries the
<tools> section).
SSE contract gains {"type":"tool","name":...,"argument":
"source/path"|null} frames ahead of the answer deltas (PLAN §4
extension, owner permission 2026-08-26); done.sources, query_log.sources
and the per-turn log line (gains tool_calls=N) report the retrieval
docs + read docs, deduped. The UI shows a "calling tool"
button/label state and one visible .tool-call line per call above the
answer; the lines persist with the chat record and re-render on
reload. chat_stream passes tools through and accumulates streaming
tool_calls deltas into ToolCallPiece (tools=None stays byte-identical).
E2E: deterministic mock tool flow ("use your tools" + <tools> marker:
list -> read first catalog line -> quoted answer) plus the story suite
(marker flow, reload re-render, plain/deflected no-tool regressions).
Docs: .env.example + README (the two tools, the budgets, the SSE tool
frame, the "calling tool" UI state).
probe: turbo tool_calls=supported 2026-08-26 (uv run python -m
scripts.llm_probe --tools — non-streaming + streaming
finish_reason=tool_calls, indexed delta.tool_calls partials)
This commit is contained in:
@@ -129,6 +129,41 @@ exactly as before.
|
||||
To hide it, set `BOR_STREAM_THINKING=0` — the `thinking` events stop
|
||||
(the per-turn log line still counts `thinking_chars`).
|
||||
|
||||
## Agent document tools (list + read)
|
||||
|
||||
Retrieval only puts the top documents in context. When an answer depends
|
||||
on a file a note *references* ("the exact JSON shape is in
|
||||
example-record-file.json"), the model can extend its own context with two
|
||||
server-side tools — on **grounded** (high-relevance) turns only:
|
||||
|
||||
* **`list_documents`** — lists every indexed document, one
|
||||
`source/path — title` line each (the same order as the Sources page);
|
||||
* **`read_document(source, path)`** — appends the **full** text of
|
||||
exactly one more indexed document to the context (never truncated).
|
||||
|
||||
Each call the model requests is executed against Postgres only (no extra
|
||||
LLM round trip) and streamed as an SSE `tool` frame ahead of the answer —
|
||||
`{"type": "tool", "name": …, "argument": "source/path" | null}`. In the
|
||||
chat, each call shows a **"calling tool" state** in addition to
|
||||
"thinking": the send button keeps its busy state ("Calling tool…") and a
|
||||
visible tool line (`🔎 Listing documents` / `📄 Reading source/path`) lands
|
||||
above the answer, one per call, in order. The tool lines persist with the
|
||||
message, so a reloaded conversation re-renders them. The read document is
|
||||
reflected in the answer's **source chips** and in the `query_log` row.
|
||||
|
||||
The opportunities are budgeted per turn:
|
||||
|
||||
| Env | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `BOR_AGENT_LIST_CALLS` | `1` | `list_documents` calls per turn (0 disables the tool) |
|
||||
| `BOR_AGENT_READ_CALLS` | `1` | `read_document` calls per turn (0 disables the tool) |
|
||||
|
||||
Once both budgets are spent the tools are dropped from the LLM request
|
||||
and the model must answer. `BOR_AGENT_LIST_CALLS=0 BOR_AGENT_READ_CALLS=0`
|
||||
reproduces the pre-agent chat behavior exactly (no `tools` in the
|
||||
request, no `tool` frames). Deflected turns run no tools at all — the
|
||||
low-relevance path is unchanged.
|
||||
|
||||
## Admin & sign-in
|
||||
|
||||
Brain of Reese has exactly **one account: the admin (you)**. Signing in
|
||||
@@ -559,6 +594,8 @@ served locally (no CDN), `BOR_ENVIRONMENT=production`.
|
||||
| `BOR_HYBRID_VECTOR_CANDIDATES` | `100` | cosine list width for the RRF fusion |
|
||||
| `BOR_HYBRID_LEXICAL_CANDIDATES` | `30` | FTS list width for the RRF fusion |
|
||||
| `BOR_RRF_K` | `60` | RRF damping constant (`1/(k + rank)`) |
|
||||
| `BOR_AGENT_LIST_CALLS` | `1` | per-turn `list_documents` tool opportunities on grounded turns (0 disables the tool) |
|
||||
| `BOR_AGENT_READ_CALLS` | `1` | per-turn `read_document` tool opportunities on grounded turns (0 disables the tool) |
|
||||
| `BOR_IMPORT_EXTENSIONS` | `md,markdown,txt,yaml,yml,json,py` | csv of importable formats (may only narrow the A9 set) |
|
||||
| `BOR_GIT_SOURCES` | — (empty) | csv of git repo URLs — **fallback while the admin Git sources page's list (Postgres `git_sources`) is empty**; the page is the primary management surface (see *Git-based sources*) |
|
||||
| `BOR_SOURCES_DIR` | `~/bor-sources` | where the git source repos are cloned/pulled (one subdirectory per repo) |
|
||||
|
||||
Reference in New Issue
Block a user