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:
@@ -30,6 +30,15 @@ discipline rules stay byte-identical): the teaching refusals in
|
||||
:mod:`app.rag.agent` re-state the same contract; the ``<tools>``
|
||||
marker keying (HIGH only) is unchanged.
|
||||
|
||||
And the phase-182 ``show_text`` clause (task 04, LOCKED A1–A3/A6): the
|
||||
verbatim-display teaching appended INSIDE ``<tools>`` when
|
||||
``BOR_ANSWER_TEXT`` is on — the user copies from what is DISPLAYED, never
|
||||
from the model's retype, and the inline ```bor <source/path> [range]```
|
||||
fence places a server-resolved block. The clause composes with the
|
||||
phase-124 ``show_image`` clause through one mechanism, and every build
|
||||
the flags do not select stays byte-identical (the constant's own sha pin
|
||||
lives in :mod:`tests.unit.test_prompt_lock`).
|
||||
|
||||
And the phase-119 cite discipline (task 04, LOCKED A5): the intro's
|
||||
final sentence ("cite the document(s) you used, by path", phase 118)
|
||||
is REPLACED — the HIGH prompt carries the discipline sentence exactly
|
||||
@@ -40,6 +49,7 @@ the LOW prompt and ``PERSONA`` / ``TOOLS_SECTION`` stay byte-identical
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import re
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
@@ -52,6 +62,7 @@ from app.rag.prompts import (
|
||||
PERSONA,
|
||||
SEARCH_DISCIPLINE_RULE,
|
||||
SHOW_IMAGE_CLAUSE,
|
||||
SHOW_TEXT_CLAUSE,
|
||||
SUGGEST_INTRO,
|
||||
TOOLS_SECTION,
|
||||
_base,
|
||||
@@ -1281,6 +1292,170 @@ def test_show_image_clause_is_last_for_every_high_shape() -> None:
|
||||
assert SHOW_IMAGE_CLAUSE not in off # the toggle-off twin carries none of it
|
||||
|
||||
|
||||
# ---------- phase 182 (task 04, LOCKED A1–A3/A6): the show_text clause ----------
|
||||
#
|
||||
# The clause is the fix for the retype bug: nothing in the pre-phase prompt
|
||||
# ever said the user copies from what is DISPLAYED, so the model retyped
|
||||
# files from context and every retype differed from the stored bytes by a
|
||||
# character. Pinned three ways: the locked sentence order (the behaviour
|
||||
# the E2E and the persona depend on), the byte-identity of every build the
|
||||
# flag does NOT select, and the composition of the two display clauses
|
||||
# through the one "section minus its final </tools>" mechanism.
|
||||
|
||||
#: The clause's sentence anchors, in the locked order (task 04's work
|
||||
#: list): display contract → when to call → do-NOT-retype → the ```bor```
|
||||
#: placement syntax → the numbers belong to the server → the failure rule
|
||||
#: → the image pointer.
|
||||
SHOW_TEXT_ANCHORS: tuple[str, ...] = (
|
||||
"displays part of a document to the user EXACTLY as stored",
|
||||
"the user copies from what is displayed, not from your prose",
|
||||
"Call it whenever the user wants to SEE, COPY or RUN a file",
|
||||
"a quadlet, a config, a script, a unit file",
|
||||
"do NOT repeat the displayed lines in your answer",
|
||||
"may differ from the file by a character, and the user cannot tell",
|
||||
"write a locator line on its own: three backticks followed by `bor`",
|
||||
"the combined `source/path`, and optionally the line range (` 12-40`)",
|
||||
"Leave its body EMPTY — the app replaces it with the exact stored text",
|
||||
"Never type the file's content inside a `bor` block",
|
||||
"Never guess line numbers — use the numbers `grep` and `read` reported",
|
||||
"OMIT the range and display the whole document",
|
||||
"the numbers you display are the server's, never yours",
|
||||
"If `show_text` refuses, say what you could not display",
|
||||
"do not reconstruct it from memory",
|
||||
"For an image use `show_image` instead",
|
||||
)
|
||||
|
||||
|
||||
def test_show_text_clause_carries_every_locked_rule_in_order() -> None:
|
||||
"""Every sentence of the contract is present, in the locked order, and
|
||||
the clause is a clause — one paragraph, no ``<tools>`` markup of its
|
||||
own (the ``SHOW_IMAGE_CLAUSE`` precedent)."""
|
||||
positions = [SHOW_TEXT_CLAUSE.index(anchor) for anchor in SHOW_TEXT_ANCHORS]
|
||||
assert positions == sorted(positions), SHOW_TEXT_ANCHORS
|
||||
assert "\n" not in SHOW_TEXT_CLAUSE
|
||||
assert SHOW_TEXT_CLAUSE.startswith("`show_text`")
|
||||
assert SHOW_TEXT_CLAUSE.endswith("instead.")
|
||||
assert "<tools>" not in SHOW_TEXT_CLAUSE and "</tools>" not in SHOW_TEXT_CLAUSE
|
||||
# The fence language is named as a token, never as a stray substring.
|
||||
assert re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", SHOW_TEXT_CLAUSE)
|
||||
|
||||
|
||||
def test_high_prompt_default_stays_byte_identical_without_show_text() -> None:
|
||||
"""Completion pin: ``show_text`` defaults to ``False`` — the default
|
||||
build and the explicit ``show_text=False`` are byte-identical to each
|
||||
other and end with the untouched ``TOOLS_SECTION``, with no trace of
|
||||
the tool or the fence language (the sha pins in
|
||||
:mod:`tests.unit.test_prompt_lock` pass UNMODIFIED)."""
|
||||
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
|
||||
plain = build_high_prompt([doc])
|
||||
assert build_high_prompt([doc], show_text=False) == plain
|
||||
assert plain.endswith(TOOLS_SECTION)
|
||||
assert SHOW_TEXT_CLAUSE not in plain
|
||||
assert "show_text" not in plain
|
||||
assert not re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", plain)
|
||||
empty = build_high_prompt([])
|
||||
assert build_high_prompt([], show_text=False) == empty
|
||||
assert empty.endswith(TOOLS_SECTION)
|
||||
assert SHOW_TEXT_CLAUSE not in empty
|
||||
|
||||
|
||||
def test_high_prompt_show_text_clause_lands_inside_tools_after_the_copy() -> None:
|
||||
"""``show_text=True`` builds EXACTLY the toggle-off prompt with its
|
||||
final ``</tools>`` replaced by the clause (on its own line, after the
|
||||
existing three-tool copy) + the closing tag — the three-tool copy is
|
||||
byte-identical, ``TOOLS_SECTION`` is unedited, and the clause is the
|
||||
last thing the model reads."""
|
||||
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
|
||||
off = build_high_prompt([doc])
|
||||
on = build_high_prompt([doc], show_text=True)
|
||||
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
|
||||
clause_start = on.index(SHOW_TEXT_CLAUSE)
|
||||
assert on[:clause_start] == off[: -len("</tools>")]
|
||||
assert on[on.index("<tools>") : clause_start] == TOOLS_SECTION[: -len("</tools>")]
|
||||
assert "Answer as soon as you have what you need." in on[:clause_start]
|
||||
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
|
||||
assert on.index("Answer as soon as you have what you need.") < clause_start
|
||||
assert clause_start < on.index("</tools>")
|
||||
assert on.count(SHOW_TEXT_CLAUSE) == 1
|
||||
assert on.count("<tools>") == 1 and on.count("</tools>") == 1
|
||||
|
||||
|
||||
def test_high_prompt_display_clauses_compose_independently() -> None:
|
||||
"""The two display flags are independent (LOCKED A1): ``show_image``
|
||||
only reproduces the phase-124 bytes exactly, ``show_text`` only is the
|
||||
three-tool copy plus the text clause, both puts the image clause
|
||||
FIRST and the text clause last, and neither is byte-identical to
|
||||
pre-phase — no combination leaks the other's clause."""
|
||||
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
|
||||
neither = build_high_prompt([doc])
|
||||
head = neither[: -len("</tools>")]
|
||||
image = build_high_prompt([doc], show_image=True)
|
||||
text = build_high_prompt([doc], show_text=True)
|
||||
both = build_high_prompt([doc], show_image=True, show_text=True)
|
||||
assert neither.endswith(TOOLS_SECTION)
|
||||
assert image == head + SHOW_IMAGE_CLAUSE + "\n</tools>"
|
||||
assert text == head + SHOW_TEXT_CLAUSE + "\n</tools>"
|
||||
assert both == head + SHOW_IMAGE_CLAUSE + "\n\n" + SHOW_TEXT_CLAUSE + "\n</tools>"
|
||||
assert SHOW_TEXT_CLAUSE not in image and SHOW_IMAGE_CLAUSE not in text
|
||||
assert both.index(SHOW_IMAGE_CLAUSE) < both.index(SHOW_TEXT_CLAUSE)
|
||||
for on in (image, text, both):
|
||||
assert on[: on.index("<tools>")] == neither[: on.index("<tools>")]
|
||||
assert on.count("<tools>") == 1 and on.count("</tools>") == 1
|
||||
|
||||
|
||||
def test_high_prompt_empty_documents_show_text_clause_still_in_tools() -> None:
|
||||
"""The "(no documents matched…)" body + ``show_text=True``: the clause
|
||||
still lands inside ``<tools>`` — the grounded turn may still ``ls`` /
|
||||
``grep`` its way to a file and display it."""
|
||||
off = build_high_prompt([])
|
||||
on = build_high_prompt([], show_text=True)
|
||||
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
|
||||
assert "(no documents matched — do not invent specifics)" in on
|
||||
assert on.index("<tools>") < on.index(SHOW_TEXT_CLAUSE) < on.index("</tools>")
|
||||
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
|
||||
|
||||
|
||||
def test_show_text_clause_is_last_for_every_high_shape() -> None:
|
||||
"""Steering + KB overview + folder lines + documents all present: the
|
||||
section order is UNCHANGED (``<relevance>`` → ``<knowledge_base>`` →
|
||||
``<tuning>`` → ``<documents>`` → ``<tools>``) and the text clause is
|
||||
still the last thing inside ``<tools>`` before the closing tag."""
|
||||
doc = _doc("deploy/k8s.md", "CONTENT_SENTINEL", "K8s Deploy", summary="K8s deploy notes.")
|
||||
lines = ["Homelab/deploy/: k8s.md (1 doc)"]
|
||||
off = build_high_prompt([doc], notes=["be concise"], kb_overview=OVERVIEW, folder_lines=lines)
|
||||
on = build_high_prompt(
|
||||
[doc],
|
||||
notes=["be concise"],
|
||||
kb_overview=OVERVIEW,
|
||||
folder_lines=lines,
|
||||
show_text=True,
|
||||
)
|
||||
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
|
||||
clause_start = on.index(SHOW_TEXT_CLAUSE)
|
||||
assert on[on.index("<tools>") : clause_start] == TOOLS_SECTION[: -len("</tools>")]
|
||||
for section in ("<relevance>", "<knowledge_base>", "<tuning>", "<documents>"):
|
||||
assert on.index(section) < on.index("<tools>")
|
||||
assert on.index("K8s deploy notes.") < on.index("<tools>")
|
||||
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
|
||||
assert SHOW_TEXT_CLAUSE not in off
|
||||
|
||||
|
||||
def test_show_text_clause_rides_only_the_high_prompt() -> None:
|
||||
"""Neither the tool name nor the fence language ever reaches the
|
||||
deflection prompt (phase 182's D9: a deflected turn has no tools, and
|
||||
resolving a locator there would leak KB text through a turn that just
|
||||
disclaimed it) — and ``build_deflect_prompt`` takes no such flag."""
|
||||
assert "show_text" not in inspect.signature(build_deflect_prompt).parameters
|
||||
for prompt in (
|
||||
build_deflect_prompt(["T1", "T2"]),
|
||||
build_deflect_prompt(["T1"], notes=["be concise"], kb_overview=OVERVIEW),
|
||||
build_deflect_prompt([]),
|
||||
):
|
||||
assert SHOW_TEXT_CLAUSE not in prompt
|
||||
assert "show_text" not in prompt
|
||||
assert not re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", prompt)
|
||||
|
||||
|
||||
# ---------- phase 125 (task 05, LOCKED A4): the image context line ----------
|
||||
# An enriched image doc (tasks 03/04: non-NULL ``image_context`` in the
|
||||
# "Referenced in …" block shape) leads its ``<document>`` suggestion
|
||||
|
||||
Reference in New Issue
Block a user