feat(agent): search_documents tool — the model can grep the indexed documents for an exact string
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user