phase: 182_verbatim_show_text
**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**
- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
This commit is contained in:
+568
-34
@@ -73,9 +73,17 @@ from app.models import Document, GitSource
|
||||
from app.rag import agent
|
||||
from app.rag.agent import (
|
||||
AGENT_TOOLS,
|
||||
MISSING_SHOW_TEXT_ARGS,
|
||||
READ_TRUNCATION_NOTICE,
|
||||
SHOW_TEXT_ALREADY_SHOWN,
|
||||
SHOW_TEXT_BLOCK_CAP,
|
||||
SHOW_TEXT_NOT_A_TEXT_DOC,
|
||||
SHOW_TEXT_TOOL,
|
||||
SHOWN_TEXT_TRUNCATION_NOTICE,
|
||||
UNKNOWN_TOOL,
|
||||
AgentHolder,
|
||||
MalformedReplyError,
|
||||
agent_tools,
|
||||
run_agent,
|
||||
)
|
||||
from app.rag.llm import (
|
||||
@@ -98,6 +106,20 @@ def _settings(**kwargs: Any) -> Settings:
|
||||
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
|
||||
|
||||
|
||||
#: Phase 182 (task 03) pin update, deliberate: ``answer_text``
|
||||
#: (``BOR_ANSWER_TEXT``) defaults ON, so the default grounded offer is
|
||||
#: the three base tools (element-for-element ``AGENT_TOOLS``, the
|
||||
#: byte-identity pinned separately by ``test_agent_tools_names_and_
|
||||
#: parameters``) plus the ``show_text`` display tool — every
|
||||
#: "tools stay offered" pin in this module now expects this list. The
|
||||
#: pre-phase offer is still exactly ``AGENT_TOOLS`` with
|
||||
#: ``answer_text=False`` and ``answer_images=False`` (pinned in the
|
||||
#: phase-182 offer-matrix tests below). With ``answer_images=True`` the
|
||||
#: offer is ``[*AGENT_TOOLS, SHOW_IMAGE_TOOL, SHOW_TEXT_TOOL]`` (pinned
|
||||
#: in ``tests/unit/test_image_chips_and_answers.py``).
|
||||
_OFFER = [*AGENT_TOOLS, SHOW_TEXT_TOOL]
|
||||
|
||||
|
||||
#: The fixture documents' fixed creation date (phase 106, D5): the
|
||||
#: agent formats ``doc.created_at`` on the ``read`` result's second
|
||||
#: line — the detached fixture rows carry it exactly as the NOT NULL
|
||||
@@ -547,9 +569,9 @@ def test_ls_then_read_then_answer(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Phase 45: no per-tool budgets — the tools stay offered on every
|
||||
# request (the round cap, not spent budgets, bounds the loop), so
|
||||
# the answer request still carries them (2 rounds < default cap 10).
|
||||
assert llm.requests[0][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[2][1] == AGENT_TOOLS
|
||||
assert llm.requests[0][1] == _OFFER
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
assert llm.requests[2][1] == _OFFER
|
||||
assert len(llm.requests) == 3
|
||||
|
||||
# read splits the combined form at the FIRST slash — one exact
|
||||
@@ -855,7 +877,7 @@ def test_ls_scoped_known_source_with_zero_docs_counts(
|
||||
asyncio.run(_run(llm, holder, _settings()))
|
||||
assert llm.requests[1][0][3]["content"].startswith("Homelab — 0 documents, 0 folders:")
|
||||
assert holder.tool_calls == 1 # an executed ls, not a refusal
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
|
||||
|
||||
def test_ls_scoped_unknown_source_refused(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -873,7 +895,7 @@ def test_ls_scoped_unknown_source_refused(monkeypatch: pytest.MonkeyPatch) -> No
|
||||
assert llm.requests[1][0][3]["content"].startswith(
|
||||
agent.NO_SOURCE_NOT_A_DIRECTORY.format(scope="Ghost")
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
def test_ls_path_like_scope_unknown_source_gets_no_source_refusal(
|
||||
@@ -900,7 +922,7 @@ def test_ls_path_like_scope_unknown_source_gets_no_source_refusal(
|
||||
assert llm.requests[1][0][3]["content"].startswith(
|
||||
agent.NO_SOURCE_NOT_A_DIRECTORY.format(scope="app")
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
def test_ls_dot_scope_gets_not_a_directory_teaching_refusal(
|
||||
@@ -921,7 +943,7 @@ def test_ls_dot_scope_gets_not_a_directory_teaching_refusal(
|
||||
assert llm.requests[1][0][3]["content"].startswith(
|
||||
agent.NO_SOURCE_NOT_A_DIRECTORY.format(scope=".")
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
def test_ls_unknown_top_level_folder_gets_not_a_folder_teaching(
|
||||
@@ -958,7 +980,7 @@ def test_ls_unknown_top_level_folder_gets_not_a_folder_teaching(
|
||||
"'Homelab/netwoking' is not a folder — Homelab has: "
|
||||
"backups/ containers/ networking/"
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
def test_ls_unknown_nested_folder_gets_not_a_folder_with_nested_parent(
|
||||
@@ -1527,7 +1549,7 @@ def test_read_bare_source_name_refused_without_db(monkeypatch: pytest.MonkeyPatc
|
||||
assert llm.requests[1][0][3]["content"].startswith(
|
||||
"No document at 'Homelab' — check the ls output."
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
|
||||
|
||||
def test_read_unknown_path_refused_echoing_argument(
|
||||
@@ -1549,7 +1571,7 @@ def test_read_unknown_path_refused_echoing_argument(
|
||||
assert llm.requests[1][0][3]["content"].startswith(
|
||||
"No document at 'S/ghost.md' — check the ls output."
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # tools stay offered (cap bounds)
|
||||
assert llm.requests[1][1] == _OFFER # tools stay offered (cap bounds)
|
||||
|
||||
|
||||
# ---------- read/grep: the "did you mean …?" suggestions (phase 72, task 02) ----------
|
||||
@@ -1649,7 +1671,7 @@ def test_read_bare_path_exact_match_gets_did_you_mean(
|
||||
"No document at 'active/container_caddy/caddy.md' — "
|
||||
"did you mean 'Homelab/active/container_caddy/caddy.md'?"
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
def test_read_bare_path_suffix_match_gets_did_you_mean(
|
||||
@@ -1767,7 +1789,7 @@ def test_read_bare_filename_without_slash_keeps_no_db_refusal(
|
||||
assert llm.requests[1][0][3]["content"].startswith(
|
||||
"No document at 'caddy.md' — check the ls output."
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
|
||||
|
||||
def test_read_bare_path_of_suggested_doc_gets_suggestion_then_succeeds(
|
||||
@@ -1850,7 +1872,7 @@ def test_read_missing_arguments_refused(
|
||||
asyncio.run(_run(llm, holder, _settings()))
|
||||
assert holder.read_docs == [] and holder.tool_calls == 0
|
||||
assert llm.requests[1][0][3]["content"].startswith(agent.MISSING_READ_ARGS)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
|
||||
|
||||
def test_reading_a_suggested_seed_doc_adds_its_full_text(
|
||||
@@ -1887,7 +1909,7 @@ def test_reading_a_suggested_seed_doc_adds_its_full_text(
|
||||
assert llm.requests[1][0][3]["content"].startswith(
|
||||
"Document Homelab/kubernetes.md:\ndate: 2024-06-15\nK8S-CONTENT"
|
||||
)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
|
||||
|
||||
def test_re_reading_a_suggested_doc_is_deduped(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -1938,7 +1960,7 @@ def test_re_reading_a_suggested_doc_is_deduped(monkeypatch: pytest.MonkeyPatch)
|
||||
assert llm.requests[2][0][5]["content"].startswith(agent.ALREADY_IN_CONTEXT)
|
||||
# Rejected → the tools are still offered on the next request (the
|
||||
# round cap is the only bound).
|
||||
assert llm.requests[2][1] == AGENT_TOOLS
|
||||
assert llm.requests[2][1] == _OFFER
|
||||
|
||||
|
||||
def test_reading_an_already_read_doc_is_deduped(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -1960,7 +1982,7 @@ def test_reading_an_already_read_doc_is_deduped(monkeypatch: pytest.MonkeyPatch)
|
||||
)
|
||||
assert llm.requests[2][0][5]["content"].startswith(agent.ALREADY_IN_CONTEXT)
|
||||
# Rejected → the tools are still offered on the next request…
|
||||
assert llm.requests[2][1] == AGENT_TOOLS
|
||||
assert llm.requests[2][1] == _OFFER
|
||||
|
||||
|
||||
# ---------- phase 95: the read cap (bounded reads, honest truncation) ----------
|
||||
@@ -2359,7 +2381,7 @@ def test_bad_range_args_refused_before_any_lookup(
|
||||
assert llm.requests[1][0][3]["content"].startswith(agent.BAD_RANGE_ARGS.format(
|
||||
offset=2.5, limit=None
|
||||
))
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
|
||||
|
||||
def test_bad_range_args_beats_the_dedupe(
|
||||
@@ -3678,7 +3700,7 @@ def test_unknown_tool_name_refused(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
asyncio.run(_run(llm, holder, _settings()))
|
||||
assert holder.read_docs == [] and holder.tool_calls == 0
|
||||
assert llm.requests[1][0][3]["content"].startswith(agent.UNKNOWN_TOOL)
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
# ---------- phase 145 (task 01): the origin/branch lines in the read result ----------
|
||||
@@ -4062,7 +4084,7 @@ def test_grep_whole_kb_grep_style_output(monkeypatch: pytest.MonkeyPatch) -> Non
|
||||
)
|
||||
assert holder.tool_calls == 1
|
||||
assert holder.read_docs == [] # locked A5: a grep adds no context
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # tools stay offered
|
||||
|
||||
|
||||
def test_grep_capped_at_20_matches_in_catalog_order(
|
||||
@@ -4370,7 +4392,7 @@ def test_grep_scoped_missing_document_refused(monkeypatch: pytest.MonkeyPatch) -
|
||||
"No document at 'S/ghost.md' — check the ls output."
|
||||
)
|
||||
assert holder.tool_calls == 0 and holder.read_docs == [] # a refusal
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
def test_grep_scoped_missing_path_like_doc_gets_did_you_mean(
|
||||
@@ -4408,7 +4430,7 @@ def test_grep_scoped_missing_path_like_doc_gets_did_you_mean(
|
||||
)
|
||||
)
|
||||
assert holder.tool_calls == 0 and holder.read_docs == [] # a refusal
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
def test_grep_scoped_bare_source_name_refused_without_db(
|
||||
@@ -4467,7 +4489,7 @@ def test_grep_missing_arguments_refused(
|
||||
asyncio.run(_run(llm, holder, _settings()))
|
||||
assert llm.requests[1][0][3]["content"].startswith(agent.MISSING_SEARCH_ARGS)
|
||||
assert holder.tool_calls == 0 and holder.read_docs == []
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
assert llm.requests[1][1] == _OFFER # rejected → tools stay offered
|
||||
|
||||
|
||||
def test_grep_no_matches_whole_kb(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -4577,7 +4599,7 @@ def test_relist_executes_and_counts(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# after round 1, 8 left after round 2 (10-round budget).
|
||||
assert llm.requests[2][0][3]["content"] == listing + agent.TOOL_BUDGET_REMAINING.format(n=9)
|
||||
assert llm.requests[2][0][5]["content"] == listing + agent.TOOL_BUDGET_REMAINING.format(n=8)
|
||||
assert llm.requests[2][1] == AGENT_TOOLS # still offered (no budgets)
|
||||
assert llm.requests[2][1] == _OFFER # still offered (no budgets)
|
||||
|
||||
|
||||
def test_multi_read_executes_without_budgets(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -4604,7 +4626,7 @@ def test_multi_read_executes_without_budgets(monkeypatch: pytest.MonkeyPatch) ->
|
||||
assert llm.requests[2][0][5]["content"].startswith(
|
||||
"Document S/b.md:\ndate: 2024-06-15\nB-CONTENT"
|
||||
)
|
||||
assert llm.requests[2][1] == AGENT_TOOLS # the second read was still offered
|
||||
assert llm.requests[2][1] == _OFFER # the second read was still offered
|
||||
|
||||
|
||||
# ---------- round cap (phase 45: replaces the per-tool budgets) ----------
|
||||
@@ -4632,9 +4654,9 @@ def test_always_ls_bounded_by_round_cap(monkeypatch: pytest.MonkeyPatch) -> None
|
||||
]
|
||||
assert len(llm.requests) == 4 # 3 tool rounds + the forced answer
|
||||
# The three tool rounds were offered the tools…
|
||||
assert llm.requests[0][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[2][1] == AGENT_TOOLS
|
||||
assert llm.requests[0][1] == _OFFER
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
assert llm.requests[2][1] == _OFFER
|
||||
# …and the forced final request carries no tools, whatever is left.
|
||||
assert llm.requests[3][1] is None
|
||||
# Every re-list executed and counted.
|
||||
@@ -4686,9 +4708,9 @@ def test_rejected_read_spam_runs_to_round_cap(monkeypatch: pytest.MonkeyPatch) -
|
||||
)
|
||||
asyncio.run(_run(llm, holder, _settings(agent_max_rounds=3)))
|
||||
assert len(llm.requests) == 4 # 3 rejected rounds + the forced answer
|
||||
assert llm.requests[0][1] == AGENT_TOOLS
|
||||
assert llm.requests[1][1] == AGENT_TOOLS
|
||||
assert llm.requests[2][1] == AGENT_TOOLS
|
||||
assert llm.requests[0][1] == _OFFER
|
||||
assert llm.requests[1][1] == _OFFER
|
||||
assert llm.requests[2][1] == _OFFER
|
||||
assert llm.requests[3][1] is None # the forced final request: no tools
|
||||
assert holder.read_docs == [] and holder.tool_calls == 0 # nothing executed
|
||||
refusal = "No document at 'S/ghost.md' — check the ls output."
|
||||
@@ -5101,8 +5123,8 @@ def test_round_retried_before_first_piece(
|
||||
# The restart is byte-identical: same messages, same tools offered.
|
||||
assert len(llm.requests) == 3
|
||||
assert llm.requests[0] == llm.requests[1]
|
||||
assert llm.requests[0][1] == AGENT_TOOLS
|
||||
assert llm.requests[2][1] == AGENT_TOOLS # the answer round still offered
|
||||
assert llm.requests[0][1] == _OFFER
|
||||
assert llm.requests[2][1] == _OFFER # the answer round still offered
|
||||
# The flat delay was awaited exactly once, before the retry.
|
||||
assert sleeps == [2.5]
|
||||
tool_logs = [r for r in caplog.records if r.getMessage().startswith("agent tool=")]
|
||||
@@ -5397,7 +5419,7 @@ def test_scaffolding_only_round_gets_exactly_one_recovery(
|
||||
assert pieces == [StreamPiece("content", "The clean recovery answer.")]
|
||||
assert len(llm.requests) == 2 # the round + the one recovery
|
||||
# The round: the original system prompt, the tools offered.
|
||||
assert llm.requests[0][1] == AGENT_TOOLS
|
||||
assert llm.requests[0][1] == _OFFER
|
||||
assert llm.requests[0][0] == [
|
||||
{"role": "system", "content": "SYSTEM_PROMPT"},
|
||||
{"role": "user", "content": "QUESTION"},
|
||||
@@ -5534,7 +5556,7 @@ def test_scaffolding_round_with_tool_calls_needs_no_recovery(
|
||||
StreamPiece("content", "the answer after the tool"),
|
||||
]
|
||||
assert len(llm.requests) == 2
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # a normal round, not a recovery
|
||||
assert llm.requests[1][1] == _OFFER # a normal round, not a recovery
|
||||
for messages, _tools in llm.requests:
|
||||
assert all(
|
||||
m["content"] is None or agent.CORRECTION_INSTRUCTION not in m["content"]
|
||||
@@ -5615,3 +5637,515 @@ def test_forced_final_scaffolding_only_yields_the_budget_fallback(
|
||||
# calls — this is the fallback path, not the rejection path.
|
||||
assert holder.rejected_calls == 0
|
||||
assert holder.tool_calls == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 182 (task 03) — the 5th agent tool: ``show_text(path, offset?, limit?)``
|
||||
#
|
||||
# The display sibling of ``show_image`` (the phase-124 family in
|
||||
# ``tests/unit/test_image_chips_and_answers.py`` is the template): the model
|
||||
# NAMES a location, the SERVER shows the bytes (LOCKED A1–A3/A6). The suite
|
||||
# pins the offer matrix (``agent_tools``, four base + two display tools, each
|
||||
# behind its own flag), the successful result shape (the ``read``/
|
||||
# ``show_image`` header contract, the SERVER's extent in the ``lines:``
|
||||
# header), ``holder.shown_texts`` (the read-after-stream signal task 05 puts
|
||||
# on the ``done`` frame), and EVERY refusal — each counts in nothing,
|
||||
# appends nothing, and consumes a round like every other refusal.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_SHOW_SRC = "showsrc"
|
||||
_SHOW_PATH = "quadlets/gufo.container"
|
||||
_SHOW_IDENTITY = f"{_SHOW_SRC}/{_SHOW_PATH}"
|
||||
_SHOW_CONTENT = "L1-one\nL2-two\nL3-three\nL4-four\nL5-five"
|
||||
|
||||
|
||||
def _show_doc(content: str = _SHOW_CONTENT, *, is_image: bool = False) -> Document:
|
||||
"""The fixture row ``show_text`` resolves (detached, like every
|
||||
agent-suite fixture); ``is_image`` flips the A6 refusal case."""
|
||||
doc = _doc(_SHOW_SRC, _SHOW_PATH, "Gufo quadlet", content)
|
||||
doc.is_image = is_image
|
||||
return doc
|
||||
|
||||
|
||||
def _patch_show_doc(
|
||||
monkeypatch: pytest.MonkeyPatch, doc: Document | None
|
||||
) -> None:
|
||||
"""One resolvable document for ``_resolve_path`` (the same
|
||||
monkeypatch style as the read family: the DB accessors are the seam)."""
|
||||
|
||||
def _find(_db: Session, source: str, path: str) -> Document | None:
|
||||
if doc is not None and (source, path) == (doc.source, doc.path):
|
||||
return doc
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(agent, "find_document", _find)
|
||||
|
||||
|
||||
def _show_text(
|
||||
arguments: dict[str, Any],
|
||||
holder: AgentHolder,
|
||||
settings: Settings | None = None,
|
||||
) -> str:
|
||||
"""One ``show_text`` call through the pure ``_execute_tool`` seam (the
|
||||
budget trailer is appended by ``run_agent``, never here — the raw
|
||||
result strings are what this suite pins)."""
|
||||
return agent._execute_tool(
|
||||
_mock_session(),
|
||||
ToolCallPiece(id="c1", name="show_text", arguments=arguments),
|
||||
[],
|
||||
holder,
|
||||
settings or _settings(),
|
||||
)
|
||||
|
||||
|
||||
# --- the offer matrix (the task-03 completion pin: 4 base + 2 display) ---
|
||||
|
||||
|
||||
def test_show_text_offer_matrix_agent_tools_by_flags() -> None:
|
||||
"""``agent_tools`` offers FOUR tools with ``answer_text=False`` (the
|
||||
pre-phase-182 offer byte-identical — ``show_image`` last when
|
||||
offered) and FIVE with it ON (``show_text`` present, correct order
|
||||
``ls, read, grep, show_image, show_text``); both flags off is
|
||||
element-for-element ``AGENT_TOOLS`` — a fresh list, never the
|
||||
constant."""
|
||||
pre182 = agent_tools(_settings(answer_images=True, answer_text=False))
|
||||
assert [t["function"]["name"] for t in pre182] == [
|
||||
"ls",
|
||||
"read",
|
||||
"grep",
|
||||
"show_image",
|
||||
]
|
||||
assert pre182[:3] == AGENT_TOOLS # the base three byte-identical
|
||||
|
||||
both = agent_tools(_settings(answer_images=True, answer_text=True))
|
||||
assert [t["function"]["name"] for t in both] == [
|
||||
"ls",
|
||||
"read",
|
||||
"grep",
|
||||
"show_image",
|
||||
"show_text",
|
||||
]
|
||||
assert both[:3] == AGENT_TOOLS
|
||||
|
||||
pre124 = agent_tools(_settings(answer_images=False, answer_text=False))
|
||||
assert pre124 == AGENT_TOOLS # element-for-element
|
||||
assert pre124 is not AGENT_TOOLS # a fresh list, never the constant
|
||||
pre124.append({"junk": True}) # mutating the offer must not touch it
|
||||
assert len(AGENT_TOOLS) == 3
|
||||
|
||||
# The shipped default: ``answer_text`` is ON (LOCKED A8), so the
|
||||
# default grounded offer already carries the 5th tool.
|
||||
default = agent_tools(_settings())
|
||||
assert [t["function"]["name"] for t in default] == [
|
||||
"ls",
|
||||
"read",
|
||||
"grep",
|
||||
"show_text",
|
||||
]
|
||||
assert default == _OFFER # the module-wide offer pin stays honest
|
||||
|
||||
|
||||
def test_show_text_tool_definition_is_the_behavior_contract() -> None:
|
||||
"""The same OpenAI function shape as the other tools (one required
|
||||
string ``path`` + the OPTIONAL ``offset``/``limit`` integers), and
|
||||
the description IS the behavior contract (the phase-124 discipline):
|
||||
verbatim display, the do-NOT-retype instruction, the server-owned
|
||||
extent (clamp + past-end teaching), the ``show_image`` pointer
|
||||
(A6), the bare-path teaching mirrored from ``read``, and the
|
||||
one-call-at-a-time discipline."""
|
||||
assert SHOW_TEXT_TOOL["type"] == "function"
|
||||
fn = SHOW_TEXT_TOOL["function"]
|
||||
assert fn["name"] == "show_text"
|
||||
params = fn["parameters"]
|
||||
assert params["type"] == "object"
|
||||
assert params["required"] == ["path"]
|
||||
props = params["properties"]
|
||||
assert set(props) == {"path", "offset", "limit"}
|
||||
assert props["path"]["type"] == "string"
|
||||
assert props["offset"]["type"] == "integer"
|
||||
assert props["limit"]["type"] == "integer"
|
||||
# The ``read`` bare-path teaching reused verbatim (one grammar).
|
||||
assert (
|
||||
"A bare document path (without the source name) will not resolve."
|
||||
in props["path"]["description"]
|
||||
)
|
||||
desc = fn["description"]
|
||||
assert "EXACTLY as stored" in desc
|
||||
assert "do NOT retype the content yourself" in desc
|
||||
assert "`offset` (1-based first line) and `limit` (line count)" in desc
|
||||
assert "overshooting `limit` is clamped" in desc
|
||||
assert "refused with the true line" in desc
|
||||
assert "Only text documents — for an image use `show_image`" in desc
|
||||
assert "Call one tool at a time" in desc
|
||||
|
||||
|
||||
def test_show_text_refusal_texts_are_pinned() -> None:
|
||||
"""The four refusal texts are pinned (the model's self-correction
|
||||
relies on stable copy), and the range problems REUSE the read
|
||||
constants — one copy of each string in the codebase."""
|
||||
assert MISSING_SHOW_TEXT_ARGS == "show_text requires a string argument 'path'."
|
||||
assert SHOW_TEXT_NOT_A_TEXT_DOC.format(arg="s/p.png") == (
|
||||
"'s/p.png' is a standalone image document — use show_image to "
|
||||
"display images."
|
||||
)
|
||||
assert SHOW_TEXT_BLOCK_CAP.format(max=5) == (
|
||||
"5 text blocks are already displayed this answer — describe them "
|
||||
"instead of showing more."
|
||||
)
|
||||
assert SHOW_TEXT_ALREADY_SHOWN.format(locator="s/p.md#12-40") == (
|
||||
"s/p.md#12-40 is already on screen this answer — say so instead "
|
||||
"of showing it again."
|
||||
)
|
||||
# The reuse pins: the branch formats THESE constants, not copies —
|
||||
# the exact same teaching strings (and echoed values) ``read`` uses.
|
||||
assert agent.BAD_RANGE_ARGS.format(offset=0, limit=None) == (
|
||||
"`offset` and `limit` are whole line numbers — pass integers ≥ 1 "
|
||||
"(you passed offset=0, limit=None)."
|
||||
)
|
||||
assert agent.OFFSET_PAST_END.format(
|
||||
source=_SHOW_SRC, path=_SHOW_PATH, total_lines=5, offset=9
|
||||
).startswith(f"{_SHOW_IDENTITY} is 5 lines long — `offset` 9 is past the end.")
|
||||
|
||||
|
||||
# --- the successful show: whole document, ranged, capped ---
|
||||
|
||||
|
||||
def test_show_text_whole_document_success_shape(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Whole-document show (the taught default): result headers in the
|
||||
exact ``read``/``show_image`` order (a NEW first line, then
|
||||
``Document …``, ``date:``, the ``lines:`` extent, the VERBATIM
|
||||
slice), ``shown_texts`` carries the one block (canonical whole-doc
|
||||
locator), ``tool_calls`` counts the executed call, and the READ
|
||||
bookkeeping stays untouched (a show is not a context-adder)."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
result = _show_text({"path": _SHOW_IDENTITY}, holder)
|
||||
assert result == (
|
||||
"Text shown to the user — these lines are displayed verbatim; "
|
||||
"describe them, do NOT retype them.\n"
|
||||
f"Document {_SHOW_IDENTITY}:\n"
|
||||
"date: 2024-06-15\n"
|
||||
"lines: 1-5 of 5\n"
|
||||
f"{_SHOW_CONTENT}"
|
||||
)
|
||||
assert result.splitlines()[1].startswith("Document ") # the mock's contract
|
||||
assert len(holder.shown_texts) == 1
|
||||
block = holder.shown_texts[0]
|
||||
assert block.locator == _SHOW_IDENTITY # whole doc → no range part
|
||||
assert (block.start_line, block.end_line, block.total_lines) == (1, 5, 5)
|
||||
assert block.truncated is False
|
||||
assert holder.tool_calls == 1 # executed-only counting
|
||||
# A show NEVER touches the read paths (it is not a context-adder).
|
||||
assert holder.read_docs == []
|
||||
assert holder.read_coverage == []
|
||||
assert holder.read_truncations == []
|
||||
|
||||
|
||||
def test_show_text_ranged_show_clamps_limit_in_the_lines_header(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Ranged show with an OVERSHOOTING ``limit``: the header carries the
|
||||
SERVER's clamped extent (``lines: 4-5 of 5`` — the caption never
|
||||
lies), the slice is exactly the displayed lines, and the canonical
|
||||
locator carries the clamped range (the D7 matching key)."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
result = _show_text({"path": _SHOW_IDENTITY, "offset": 4, "limit": 99}, holder)
|
||||
assert result == (
|
||||
"Text shown to the user — these lines are displayed verbatim; "
|
||||
"describe them, do NOT retype them.\n"
|
||||
f"Document {_SHOW_IDENTITY}:\n"
|
||||
"date: 2024-06-15\n"
|
||||
"lines: 4-5 of 5\n"
|
||||
"L4-four\nL5-five"
|
||||
)
|
||||
block = holder.shown_texts[0]
|
||||
assert block.locator == f"{_SHOW_IDENTITY}#4-5"
|
||||
assert (block.start_line, block.end_line, block.total_lines) == (4, 5, 5)
|
||||
assert holder.tool_calls == 1
|
||||
|
||||
|
||||
def test_show_text_capped_block_carries_marker_and_notice(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Capped show (``settings.show_text_max_chars``): the slice is cut
|
||||
at a LINE boundary, the shared ``TRUNCATION_MARKER`` rides the block,
|
||||
the ``lines:`` extent SHRINKS to the last displayed line (never the
|
||||
asked extent), and the pinned notice names that the rest is NOT
|
||||
displayed and the user opens the document (no re-show loop implied)."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
result = _show_text(
|
||||
{"path": _SHOW_IDENTITY},
|
||||
holder,
|
||||
_settings(show_text_max_chars=13),
|
||||
)
|
||||
assert TRUNCATION_MARKER in result
|
||||
assert result.endswith(
|
||||
SHOWN_TEXT_TRUNCATION_NOTICE.format(source=_SHOW_SRC, path=_SHOW_PATH)
|
||||
)
|
||||
block = holder.shown_texts[0]
|
||||
assert block.truncated is True
|
||||
# The budget (13 chars) lands exactly after line 2 — the caption
|
||||
# shrinks to the lines ACTUALLY shown (never the asked 1-5).
|
||||
assert block.end_line == 2
|
||||
assert block.locator == f"{_SHOW_IDENTITY}#1-2" # cap-derived range
|
||||
assert f"lines: 1-2 of 5\nL1-one\nL2-two\n{TRUNCATION_MARKER}" in result
|
||||
assert holder.tool_calls == 1 # a capped show is still a successful call
|
||||
|
||||
|
||||
def test_show_text_origin_lines_ridge_the_header_like_read(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The phase-145 origin contract: a git-sourced show carries the
|
||||
``origin:``/``branch:`` lines immediately after ``date:`` and BEFORE
|
||||
``lines:`` (the same ``_origin_lines`` block ``read`` renders; the
|
||||
second line stays ``Document …:``)."""
|
||||
doc = _show_doc()
|
||||
_patch_show_doc(monkeypatch, doc)
|
||||
|
||||
def _factory() -> Session:
|
||||
from app.models import GitSource
|
||||
|
||||
rows = [GitSource(url="https://github.com/owner/showsrc", kind="git", branch="main")]
|
||||
mock = cast("Session", MagicMock())
|
||||
mock.__enter__ = MagicMock(return_value=mock)
|
||||
mock.__exit__ = MagicMock(return_value=False)
|
||||
mock.scalars = MagicMock(return_value=MagicMock(all=MagicMock(return_value=rows)))
|
||||
return mock
|
||||
|
||||
holder = AgentHolder()
|
||||
result = agent._execute_tool(
|
||||
_factory(),
|
||||
ToolCallPiece(id="c1", name="show_text", arguments={"path": _SHOW_IDENTITY}),
|
||||
[],
|
||||
holder,
|
||||
_settings(),
|
||||
)
|
||||
lines = result.splitlines()
|
||||
assert lines[1] == f"Document {_SHOW_IDENTITY}:"
|
||||
assert lines[2] == "date: 2024-06-15"
|
||||
assert lines[3] == "origin: https://github.com/owner/showsrc"
|
||||
assert lines[4] == "branch: main"
|
||||
assert lines[5] == "lines: 1-5 of 5"
|
||||
|
||||
|
||||
# --- the refusal matrix: each counts in nothing, appends nothing ---
|
||||
|
||||
|
||||
def _assert_refusal(holder: AgentHolder) -> None:
|
||||
assert holder.tool_calls == 0
|
||||
assert holder.shown_texts == []
|
||||
assert holder.read_docs == []
|
||||
assert holder.read_coverage == []
|
||||
|
||||
|
||||
def test_show_text_missing_or_non_string_path_refused() -> None:
|
||||
"""Missing, blank and non-string ``path`` → ``MISSING_SHOW_TEXT_ARGS``
|
||||
(the ``MISSING_SHOW_ARGS`` shape); counts in nothing."""
|
||||
holder = AgentHolder()
|
||||
for arguments in ({}, {"path": " "}, {"path": None}, {"path": 42}):
|
||||
assert _show_text(arguments, holder) == MISSING_SHOW_TEXT_ARGS
|
||||
_assert_refusal(holder)
|
||||
|
||||
|
||||
def test_show_text_unknown_path_gets_the_did_you_mean_teaching(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The phase-72 teaching, REUSED unchanged: a bare argument keeps the
|
||||
no-DB line, a path-like argument matching one indexed ``path`` gets
|
||||
the combined-identity suggestion — nothing displayed either way."""
|
||||
_patch_show_doc(monkeypatch, None)
|
||||
doc = _show_doc()
|
||||
monkeypatch.setattr(
|
||||
agent,
|
||||
"find_path_candidates",
|
||||
lambda db, arg: [(_SHOW_SRC, _SHOW_PATH, doc.title)],
|
||||
)
|
||||
holder = AgentHolder()
|
||||
result = _show_text({"path": "quadlets/gufo.container"}, holder)
|
||||
assert result == agent.NO_DOCUMENT_DID_YOU_MEAN.format(
|
||||
arg="quadlets/gufo.container", source=_SHOW_SRC, path=_SHOW_PATH
|
||||
)
|
||||
assert _show_text({"path": "nope"}, holder) == (
|
||||
"No document at 'nope' — check the ls output."
|
||||
)
|
||||
_assert_refusal(holder)
|
||||
|
||||
|
||||
def test_show_text_on_an_image_doc_is_refused_never_converted(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""LOCKED A6: a standalone IMAGE row is refused with the
|
||||
``show_image`` pointer — never silently converted (the exact mirror
|
||||
of ``show_image``'s ``NOT_AN_IMAGE_DOC``); counts in nothing."""
|
||||
_patch_show_doc(monkeypatch, _show_doc(is_image=True))
|
||||
holder = AgentHolder()
|
||||
result = _show_text({"path": _SHOW_IDENTITY}, holder)
|
||||
assert result == SHOW_TEXT_NOT_A_TEXT_DOC.format(arg=_SHOW_IDENTITY)
|
||||
_assert_refusal(holder)
|
||||
|
||||
|
||||
def test_show_text_bad_range_args_reuse_the_read_teaching(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A non-whole/``< 1`` ``offset``/``limit`` → :data:`BAD_RANGE_ARGS`
|
||||
with the values echoed — the SAME constant the ``read`` branch
|
||||
formats (one copy of the string), fired before any display."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
assert _show_text({"path": _SHOW_IDENTITY, "offset": 0}, holder) == (
|
||||
agent.BAD_RANGE_ARGS.format(offset=0, limit=None)
|
||||
)
|
||||
assert _show_text({"path": _SHOW_IDENTITY, "offset": 1, "limit": "x"}, holder) == (
|
||||
agent.BAD_RANGE_ARGS.format(offset=1, limit="x")
|
||||
)
|
||||
_assert_refusal(holder)
|
||||
|
||||
|
||||
def test_show_text_offset_past_end_names_the_true_total(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""An ``offset`` past the last line → :data:`OFFSET_PAST_END` (the
|
||||
SAME teaching as ``read``) naming the document's TRUE line count;
|
||||
counts in nothing, a corrected retry still proceeds."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
result = _show_text({"path": _SHOW_IDENTITY, "offset": 9}, holder)
|
||||
assert result == agent.OFFSET_PAST_END.format(
|
||||
source=_SHOW_SRC, path=_SHOW_PATH, total_lines=5, offset=9
|
||||
)
|
||||
assert "is 5 lines long" in result
|
||||
_assert_refusal(holder)
|
||||
|
||||
|
||||
def test_show_text_empty_document_is_refused_like_past_end(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""An EMPTY document has nothing to display: the resolver's
|
||||
past-the-end refusal carries the true total (``0 lines long``) —
|
||||
never an empty block, never a crash."""
|
||||
_patch_show_doc(monkeypatch, _show_doc(content=""))
|
||||
holder = AgentHolder()
|
||||
result = _show_text({"path": _SHOW_IDENTITY}, holder)
|
||||
assert result == agent.OFFSET_PAST_END.format(
|
||||
source=_SHOW_SRC, path=_SHOW_PATH, total_lines=0, offset=1
|
||||
)
|
||||
_assert_refusal(holder)
|
||||
|
||||
|
||||
def test_show_text_block_cap_refuses_second_show(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The per-answer block budget (server-enforced): with
|
||||
``show_text_max_blocks=1`` a SECOND, DIFFERENT show is refused with
|
||||
the cap copy — the first block stays displayed, the refusal counts
|
||||
in nothing."""
|
||||
doc_a = _show_doc()
|
||||
doc_b = _doc(_SHOW_SRC, "quadlets/other.container", "Other", "B1\nB2")
|
||||
docs = {doc_a.path: doc_a, doc_b.path: doc_b}
|
||||
|
||||
def _find(_db: Session, source: str, path: str) -> Document | None:
|
||||
return docs.get(path) if source == _SHOW_SRC else None
|
||||
|
||||
monkeypatch.setattr(agent, "find_document", _find)
|
||||
holder = AgentHolder()
|
||||
settings = _settings(show_text_max_blocks=1)
|
||||
first = _show_text({"path": _SHOW_IDENTITY}, holder, settings)
|
||||
assert first.startswith("Text shown to the user —")
|
||||
second = _show_text({"path": f"{_SHOW_SRC}/quadlets/other.container"}, holder, settings)
|
||||
assert second == SHOW_TEXT_BLOCK_CAP.format(max=1)
|
||||
assert holder.tool_calls == 1 # the refusal counts in nothing
|
||||
assert len(holder.shown_texts) == 1 # still exactly the first block
|
||||
|
||||
|
||||
def test_show_text_zero_knobs_refuse_the_first_call(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The ``0`` kill switches (D8): ``show_text_max_blocks=0`` refuses
|
||||
the FIRST call with the cap copy, and ``show_text_max_chars=0`` (the
|
||||
inert per-block budget — nothing could display) refuses with the very
|
||||
same copy instead of reaching the resolver's raise. The tool stays
|
||||
OFFERED either way (the offer is ``answer_text``-gated only); counts
|
||||
in nothing."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
assert _show_text(
|
||||
{"path": _SHOW_IDENTITY}, holder, _settings(show_text_max_blocks=0)
|
||||
) == SHOW_TEXT_BLOCK_CAP.format(max=0)
|
||||
assert _show_text(
|
||||
{"path": _SHOW_IDENTITY}, holder, _settings(show_text_max_chars=0)
|
||||
) == SHOW_TEXT_BLOCK_CAP.format(max=0)
|
||||
_assert_refusal(holder)
|
||||
# The offer is untouched by the budgets (the gate is the flag).
|
||||
assert [t["function"]["name"] for t in agent_tools(
|
||||
_settings(show_text_max_blocks=0, show_text_max_chars=0)
|
||||
)] == ["ls", "read", "grep", "show_text"]
|
||||
|
||||
|
||||
def test_show_text_duplicate_identical_show_refused(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The canonical-locator dedupe: showing the EXACT same block twice
|
||||
is refused (``SHOW_TEXT_ALREADY_SHOWN`` names the locator) — the
|
||||
first block stays on screen, the refusal counts in nothing."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
first = _show_text({"path": _SHOW_IDENTITY, "offset": 2, "limit": 2}, holder)
|
||||
assert first.startswith("Text shown to the user —")
|
||||
second = _show_text({"path": _SHOW_IDENTITY, "offset": 2, "limit": 2}, holder)
|
||||
assert second == SHOW_TEXT_ALREADY_SHOWN.format(locator=f"{_SHOW_IDENTITY}#2-3")
|
||||
assert holder.tool_calls == 1
|
||||
assert len(holder.shown_texts) == 1
|
||||
|
||||
|
||||
def test_show_text_refused_when_answer_text_flag_off(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Defensive posture: with ``answer_text=False`` the tool was never
|
||||
OFFERED, so a call anyway is answered like an un-offered name (the
|
||||
tools-off posture — :data:`UNKNOWN_TOOL`) — counts in nothing,
|
||||
displays nothing (the escape hatch stays byte-identical everywhere)."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
result = _show_text({"path": _SHOW_IDENTITY}, holder, _settings(answer_text=False))
|
||||
assert result == UNKNOWN_TOOL
|
||||
_assert_refusal(holder)
|
||||
|
||||
|
||||
# --- the scripted turn (the completion-criteria wiring pin) ---
|
||||
|
||||
|
||||
def test_show_text_scripted_turn_returns_the_slice_to_the_model(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Completion pin: with ``answer_text=True`` a scripted agent turn
|
||||
returns a result whose 2nd line starts ``Document `` and whose
|
||||
``lines:`` header matches the slice actually returned; the block
|
||||
rides the holder for the done frame (task 05) and the tool-offer
|
||||
on the request carried the 5th tool."""
|
||||
_patch_show_doc(monkeypatch, _show_doc())
|
||||
holder = AgentHolder()
|
||||
llm = ScriptedLLM(
|
||||
[ToolCallPiece(id="c1", name="show_text", arguments={"path": _SHOW_IDENTITY, "offset": 3})],
|
||||
[StreamPiece("content", "ans")],
|
||||
)
|
||||
asyncio.run(_run(llm, holder, _settings())) # answer_text default ON
|
||||
assert llm.requests[0][1] == _OFFER # the grounded turn offers show_text
|
||||
content = llm.requests[1][0][3]["content"]
|
||||
assert content.endswith(agent._budget_note(9)) # the trailer rides too
|
||||
result = content.removesuffix(agent._budget_note(9))
|
||||
lines = result.splitlines()
|
||||
assert lines[0] == (
|
||||
"Text shown to the user — these lines are displayed verbatim; "
|
||||
"describe them, do NOT retype them."
|
||||
)
|
||||
assert lines[1].startswith("Document ")
|
||||
assert lines[3] == "lines: 3-5 of 5" # offset-only: to the END, clamped
|
||||
assert result.endswith("L3-three\nL4-four\nL5-five") # the slice verbatim
|
||||
assert len(holder.shown_texts) == 1
|
||||
assert holder.shown_texts[0].locator == f"{_SHOW_IDENTITY}#3-5"
|
||||
assert holder.tool_calls == 1
|
||||
|
||||
Reference in New Issue
Block a user