feat(rag): unbounded agent tool calls behind a round cap (owner revision)
Phase 45 (owner permission 2026-08-27, TODO.md L8: "allow the LLM
to make as many tool calls as it wants"): the phase-37 per-turn tool
budgets (BOR_AGENT_LIST_CALLS / BOR_AGENT_READ_CALLS, default 1 each)
and their exhaustion refusals are removed — a grounded turn now offers
list_documents / read_document for the whole turn (re-lists included),
bounded only by the round cap:
- app/config.py: agent_max_rounds (BOR_AGENT_MAX_ROUNDS, default 10,
negative rejected) replaces agent_list_calls / agent_read_calls;
.env.example + README document the single knob; app/rag/prompts.py
docstrings follow.
- app/rag/agent.py: the loop runs tools until the model answers or
rounds >= max_rounds, at which point it forces one final no-tools
answer (the cap is the only forced exit); 0 = no tools — exactly one
tools=None request, byte-identical to the pre-phase-37 path (the
kill switch). Rejected calls (unknown tool / missing args /
already-in-context / unknown path) still consume a round, so
pathological rejected-call streams are bounded by the cap. The
per-call log line is now tool/args/round=N/M; the per-turn
tool_calls=N field and the tool SSE event are unchanged.
- tests/e2e/mock_llm.py: MULTI_READ_TRIGGER ("read two documents") —
the deterministic list -> read #1 -> read #2 -> forced-answer flow
(byte-stable "I read <sp1> and <sp2>." line), classified by the
count of tool-role read results; the phase-37 single-read flow stays
byte-identical (unit-pinned in tests/unit/test_mock_tool_flow.py).
- tests/e2e/test_agent_unlimited_tools.py (new, story suite,
mock-only): three tool frames/lines in order (one list, two reads —
the second read is what the old read budget refused) + the
both-named non-deflected answer; done.sources + chips = retrieval
doc + both reads, deduped; no budget refusal rendered; the
single-read marker flow regression (exactly one read, single tool
pair).
- .agent/PLAN.md: the phase-45 SSE revision note (owner-locked, R2) —
the only PLAN edit this phase; the phase-37 note's budget clause is
marked removed.
Unit/integration rewrites (test_agent.py round-cap matrix incl. the
kill switch and rejected-call spam, test_config.py, test_chat_api.py
agent_max_rounds=0 fixtures) landed with the server core so every gate
stays green.
uv run pytest: 756 passed, app/ coverage 99%; ruff + pyright clean;
story E2E 4/4 in isolation (ran twice); regression E2E suites
(agent_document_tools unmodified, chat_rag, smoke) green in isolation.
Also records the 45_agent_unlimited_tools todo/ -> complete/ task-file
moves (00/01/02 pending in the working tree, task 03 moves on success).
This commit is contained in:
+17
-7
@@ -87,13 +87,15 @@ class Settings(BaseSettings):
|
||||
#: ``app.rag.overview``). Overflow is cut at the cap and the shared
|
||||
#: ``[…truncated…]`` marker is appended (summarizer convention).
|
||||
overview_input_max_chars: int = 40_000
|
||||
#: Per-turn opportunities to call the ``list_documents`` agent tool
|
||||
#: (phase 37, ``app.rag.agent``); 0 disables the tool entirely
|
||||
#: (pre-phase behavior with both budgets at 0).
|
||||
agent_list_calls: int = 1
|
||||
#: Per-turn opportunities to call the ``read_document`` agent tool
|
||||
#: (phase 37, ``app.rag.agent``); 0 disables the tool entirely.
|
||||
agent_read_calls: int = 1
|
||||
#: Hard cap on the agent tool rounds per grounded turn (phase 45,
|
||||
#: revising phase 37's per-tool budgets — owner permission
|
||||
#: 2026-08-27, TODO L8: "allow the LLM to make as many tool calls
|
||||
#: as it wants"). Every tool call the model emits consumes a
|
||||
#: round; at the cap the loop forces one final no-tools answer.
|
||||
#: ``0`` disables the tools entirely — the turn is a single
|
||||
#: request with ``tools=None`` (the pre-phase-37 path — the kill
|
||||
#: switch). Negative values are rejected at startup (validator).
|
||||
agent_max_rounds: int = 10
|
||||
|
||||
# --- Hybrid retrieval (A7, revised 2026-08-21) ---
|
||||
# cosine top-N ∪ Postgres FTS top-N, fused with Reciprocal Rank Fusion
|
||||
@@ -159,6 +161,14 @@ class Settings(BaseSettings):
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("agent_max_rounds")
|
||||
@classmethod
|
||||
def _agent_max_rounds_non_negative(cls, v: int) -> int:
|
||||
"""``0`` is the no-tools kill switch — a negative value is a typo."""
|
||||
if v < 0:
|
||||
raise ValueError("agent_max_rounds must be >= 0 (0 = no tools)")
|
||||
return v
|
||||
|
||||
# Suggested questions (onboarding + empty state).
|
||||
suggestions: list[str] = [
|
||||
"How is my Kubernetes cluster set up?",
|
||||
|
||||
Reference in New Issue
Block a user