phase: 124_image_chips_and_answers
All green. Final report: **Phase 124 final verification pass — all criteria verified, no defects found (nothing to fix)** - Verified the implemented phase (all 7 tasks done, changes in working tree): `BOR_ANSWER_IMAGES` toggle, `show_image` tool + refusals + `holder.shown_images`, toggle-gated prompt clause, `answer_image` frame field (None-omitting serializer) + `ChatMessage` persistence, `merge_cited_docs` auto-cite bug fix, answer-figure UI (live/restore/shared/CSS), mock triggers + E2E suite. Also confirmed the bytes route → public change matches the phase-123 shared-chat precedent (task 06 design). - `uv run pytest --cov=app --cov-report=term-missing` → **2848 passed**, TOTAL **99%** (>90% gate) - `bash .agents/validate.sh` → **validation OK** (tests + coverage + ruff + pyright) - `uv run ruff check . && uv run pyright` → clean, 0 errors - `uv run pytest tests/e2e/test_answer_images.py -v --no-cov` (isolated) → **5/5 passed** - Mock-inert spot checks: `tests/e2e/test_image_documents.py` 4/4, `tests/e2e/test_agent_document_tools.py` 4/4 **Completion criteria:** ① zero-read suggested-tier image doc chips with figure — verified (unit matrix, integration `test_zero_read_grounded_turn_auto_cites_suggested_image_doc`, E2E bug-fix flows); ② show_image → answer figure below text/above chips, second call refused, persists + restore + shared — verified (unit handler matrix, integration round-trip, E2E feature flow); ③ default-off byte-identical (3 tools, no clause, no key) — verified (unit + integration + E2E toggle-off); ④ full suite green / >90% / lint clean — verified above; ⑤ commit + phase move — harness responsibility (changes left uncommitted in working tree, no live-infrastructure changes made). **Next pending phase:** none — `todo/` holds only this phase; the pipeline is clear once the harness moves it.
This commit is contained in:
+67
-2
@@ -258,6 +258,43 @@ TOOLS_SECTION: str = (
|
||||
"</tools>"
|
||||
)
|
||||
|
||||
#: The ``show_image`` contract clause (phase 124, task 03, LOCKED A5) —
|
||||
#: appended INSIDE the ``<tools>`` section of the HIGH prompt, after the
|
||||
#: existing three-tool copy and before the closing tag, ONLY when
|
||||
#: :func:`build_high_prompt` is called with ``show_image=True`` (the
|
||||
#: ``BOR_ANSWER_IMAGES`` toggle, via ``app.api.chat``). It teaches the
|
||||
#: same relevance gate as the ``show_image`` tool description in
|
||||
#: :mod:`app.rag.agent` (LOCKED A5: the gate is the LLM's decision — no
|
||||
#: server-side image scoring), plus the image-doc nature the tool
|
||||
#: description cannot fully carry: the text of an image document (in a
|
||||
#: ``<documents>`` block or a ``read`` result) is a description
|
||||
#: GENERATED from the image, not the image's own words. The four gate
|
||||
#: elements, all stated here: (1) the image-doc nature; (2) the
|
||||
#: "contains the answer" bar — call it only when THAT image is what the
|
||||
#: user actually wants to see, never a merely related image, never to
|
||||
#: pad an answer; (3) at most ONE image per answer (also enforced
|
||||
#: server-side, task 02's ``ALREADY_SHOWING`` refusal); (4) the text
|
||||
#: answer is still provided in full — the image supplements, never
|
||||
#: replaces. ``TOOLS_SECTION`` itself stays BYTE-IDENTICAL (the
|
||||
#: pre-phase pins in :mod:`tests.unit.test_prompt_lock` hold); a
|
||||
#: ``show_image=False`` build (the default — every pre-phase call site)
|
||||
#: is byte-identical to the pre-task output. The ``<tools>`` marker the
|
||||
#: E2E mock keys on is untouched either way — the clause rides on its
|
||||
#: presence, not the wording. The LOW/deflection prompt never carries
|
||||
#: the clause (a deflected turn has no tools and never shows an image —
|
||||
#: the phase's locked design).
|
||||
SHOW_IMAGE_CLAUSE: str = (
|
||||
"`show_image` displays a standalone image document as part of your "
|
||||
"answer. The text of an image document — in a <documents> block or "
|
||||
"a `read` result — is a description GENERATED from the image, not "
|
||||
"the image's own words. Call `show_image` only when that image is "
|
||||
"what the user actually wants to see: a diagram, screenshot, or "
|
||||
"photo of the very thing you are answering about — never for a "
|
||||
"merely related image, and never to pad an answer. Show at most one "
|
||||
"image per answer. The image supplements your text — answer fully "
|
||||
"in text either way."
|
||||
)
|
||||
|
||||
|
||||
def history_to_messages(
|
||||
history: Sequence[HistoryTurn],
|
||||
@@ -426,6 +463,7 @@ def build_high_prompt(
|
||||
notes: Sequence[str] | None = None,
|
||||
kb_overview: str | None = None,
|
||||
folder_lines: Sequence[str] = (),
|
||||
show_image: bool = False,
|
||||
) -> str:
|
||||
"""Grounded turn: locked persona (+ steering, + KB overview) + the
|
||||
top-ranked documents' SUMMARY blocks + the ``<tools>`` instructions
|
||||
@@ -434,7 +472,8 @@ def build_high_prompt(
|
||||
phase 118, LOCKED A6 re-revising A7: the ``<documents>`` section
|
||||
seeds SUMMARIES, never full texts — full text enters the context
|
||||
only through the capped ``read`` tool; phase 119, LOCKED A4: the
|
||||
suggested-folder context lines, below).
|
||||
suggested-folder context lines, below; phase 124, task 03, LOCKED
|
||||
A5: the optional :data:`SHOW_IMAGE_CLAUSE`, below).
|
||||
|
||||
Section order: ``<relevance>`` → ``<knowledge_base>`` → ``<tuning>``
|
||||
→ ``<documents>`` → ``<tools>``; empty steering/overview omit their
|
||||
@@ -442,6 +481,23 @@ def build_high_prompt(
|
||||
cap — not the prompt — decides whether the tools are actually
|
||||
offered to the model, see :mod:`app.rag.agent`).
|
||||
|
||||
*show_image* (phase 124, task 03, LOCKED A5): when true (the
|
||||
caller passes the ``BOR_ANSWER_IMAGES`` toggle —
|
||||
``app.api.chat``'s grounded plan is the only call site that does),
|
||||
the :data:`SHOW_IMAGE_CLAUSE` rides INSIDE the ``<tools>`` section
|
||||
— appended after the existing three-tool copy and before the
|
||||
closing ``</tools>`` tag, so the ``<tools>`` marker the E2E mock
|
||||
keys on is untouched and the clause reaches the model together with
|
||||
the tool teaching. The section order is otherwise UNCHANGED and
|
||||
:data:`TOOLS_SECTION` itself is never edited: the built section is
|
||||
``TOOLS_SECTION``'s text minus its final ``</tools>`` + the clause
|
||||
+ the closing tag. The default ``False`` keeps EVERY pre-existing
|
||||
call byte-identical (the pre-task pins in
|
||||
:mod:`tests.unit.test_prompts` / :mod:`tests.unit.test_prompt_lock`
|
||||
hold untouched). The deflection prompt (:func:`build_deflect_prompt`)
|
||||
never carries the clause — a deflected turn has no tools and never
|
||||
shows an image.
|
||||
|
||||
When at least one block is present, the section leads with the
|
||||
start-here :data:`SUGGEST_INTRO` line (before the first block — the
|
||||
phase-15 ``_STEERING_INTRO`` / phase-31 ``_KB_INTRO`` precedent): the
|
||||
@@ -506,7 +562,16 @@ def build_high_prompt(
|
||||
for part in (build_kb_section(kb_overview or ""), build_steering_section(notes or [])):
|
||||
if part:
|
||||
prompt += "\n" + part
|
||||
return prompt + "\n<documents>\n" + body + "\n</documents>\n" + TOOLS_SECTION
|
||||
if show_image:
|
||||
# Phase 124 (task 03, LOCKED A5): the clause rides inside
|
||||
# <tools> — TOOLS_SECTION's text minus its final </tools>, the
|
||||
# clause (on its own line, after the existing three-tool copy),
|
||||
# then the closing tag. TOOLS_SECTION itself is unedited and the
|
||||
# show_image=False path keeps the exact pre-task concatenation.
|
||||
tools_section = TOOLS_SECTION[: -len("</tools>")] + SHOW_IMAGE_CLAUSE + "\n</tools>"
|
||||
else:
|
||||
tools_section = TOOLS_SECTION
|
||||
return prompt + "\n<documents>\n" + body + "\n</documents>\n" + tools_section
|
||||
|
||||
|
||||
def build_deflect_prompt(
|
||||
|
||||
Reference in New Issue
Block a user