feat(agent): search_documents tool — the model can grep the indexed documents for an exact string
Build and Push Containers / build-and-push-db (push) Canceled after 0s
Build and Push Containers / build-and-push-app (push) Canceled after 1m11s

This commit is contained in:
2026-09-02 12:04:34 -04:00
parent 88293ed02f
commit 8cf3a827ee
26 changed files with 1780 additions and 85 deletions
+95
View File
@@ -17,7 +17,10 @@ from typing import Any
from tests.e2e.mock_llm import (
MULTI_READ_TRIGGER,
SEARCH_PATTERN,
SEARCH_TRIGGER,
TOOLS_TRIGGER,
_search_flow,
_tool_flow,
)
@@ -256,3 +259,95 @@ def test_multi_trigger_without_tools_trigger_is_none() -> None:
def test_multi_flow_requires_tools_section() -> None:
assert _tool_flow(_body(MULTI_USER, system=SYSTEM_LOW)) is None
# --------------------------------------------------------------------------
# Phase-68 search flow (task 03)
# --------------------------------------------------------------------------
#: Carries ONLY the search trigger (never ``use your tools`` — the
#: phase-68 suite's live question shape, regression-safe by assertion).
SEARCH_USER = (
"Search your documents for the vault passphrase marker in my homelab "
"kubernetes backup notes?"
)
assert SEARCH_TRIGGER in SEARCH_USER.lower()
assert TOOLS_TRIGGER not in SEARCH_USER.lower()
#: The agent's ``search_documents`` result for the e2e fixture
#: (``app/rag/agent.py`` ``_execute_tool``): one ``source/path:LINE: text``
#: match line (the sentinel line, 200-char-capped server-side).
SEARCH_RESULT = (
f"search_docs/reese-notes.md:6: The offsite vault passphrase marker "
f"is {SEARCH_PATTERN}."
)
#: The agent's no-match line quotes the pattern — the sentinel-only
#: shape ``_search_result_line`` also recognizes (degenerate path).
SEARCH_NO_MATCH = f"No matches for '{SEARCH_PATTERN}' in the knowledge base."
def test_search_flow_search_step() -> None:
# tools offered, no search result yet: the model greps.
assert _search_flow(_body(SEARCH_USER)) == ("search",)
def test_search_flow_search_step_requires_tools_offered() -> None:
# agent_max_rounds=0 path: trigger + <tools> prompt, but no tools
# and no search result — regular answer, not a flow.
assert _search_flow(_body(SEARCH_USER, tools=None)) is None
def test_search_flow_found_step_quotes_first_match_line() -> None:
flow = _search_flow(_body(SEARCH_USER, (SEARCH_RESULT,)))
assert flow == ("found", f"The offsite vault passphrase marker is {SEARCH_PATTERN}.")
def test_search_flow_found_step_with_nested_path() -> None:
# A nested path (``/`` in it) stays intact in the match-line parse.
result = f"search_docs/deep/nested-note.md:12: line with {SEARCH_PATTERN} inside"
flow = _search_flow(_body(SEARCH_USER, (result,)))
assert flow == ("found", f"line with {SEARCH_PATTERN} inside")
def test_search_flow_found_step_without_tools_offered() -> None:
# The answer is content, not a tool call — it must not be gated on
# the ``tools`` parameter (phase 45 keeps the tools offered until
# the round cap, but the no-tools final request must still answer).
flow = _search_flow(_body(SEARCH_USER, (SEARCH_RESULT,), tools=None))
assert flow == ("found", f"The offsite vault passphrase marker is {SEARCH_PATTERN}.")
def test_search_flow_ignores_catalog_and_read_results() -> None:
# A catalog (labeled lines) and a read result ("Document …" prefix)
# are NOT search results — the flow stays at the search step.
flow = _search_flow(_body(SEARCH_USER, (CATALOG_2, _read_result(DOC1_SP, DOC1_CONTENT))))
assert flow == ("search",)
def test_search_flow_sentinel_only_result_is_a_search_result() -> None:
# The no-match line quotes the pattern — sentinel-only recognition
# (degenerate path; the e2e fixture always matches).
flow = _search_flow(_body(SEARCH_USER, (SEARCH_NO_MATCH,)))
assert flow == ("found", SEARCH_NO_MATCH)
def test_search_flow_requires_tools_section() -> None:
# Deflected turns never carry the <tools> section.
assert _search_flow(_body(SEARCH_USER, system=SYSTEM_LOW)) is None
def test_search_flow_plain_question_is_none() -> None:
assert _search_flow(_body(PLAIN_USER)) is None
def test_search_trigger_does_not_shadow_the_tool_flow() -> None:
# The search question carries no ``use your tools`` — the phase-37
# classifier must stay inert on it (regression-safe marker).
assert _tool_flow(_body(SEARCH_USER)) is None
def test_tool_trigger_does_not_shadow_the_search_flow() -> None:
# The phase-37/45 questions carry no ``search your documents`` —
# the search classifier must stay inert on them.
assert _search_flow(_body(SINGLE_USER)) is None
assert _search_flow(_body(MULTI_USER)) is None