Give the model full retrieved chunks instead of 400-character snippets
Compaction replaces a tool's text with its artifact, and the search artifact only carries UI snippets, so the full chunk the retriever found never reached the model. Search tools now attach the full text as model_context, which compaction prefers and shortens under budget pressure while keeping source ids; the persisted artifact and UI are unchanged. The per-chunk header drops the raw metadata dict (about a fifth of search output tokens) for title, date and source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,29 @@ class TestCreateArtifact:
|
||||
assert result is None
|
||||
assert await Artifact.all().count() == 0
|
||||
|
||||
async def test_drops_model_context_before_persisting(self, owner):
|
||||
"""model_context carries the full chunk text for the model's own view;
|
||||
it must never reach the DB (or, from there, the UI)."""
|
||||
user, conversation, message = owner
|
||||
artifact = await create_artifact_from_output(
|
||||
output=_output(
|
||||
{
|
||||
"kind": "search.documents",
|
||||
"title": "Search: vet",
|
||||
"data": {"results": [{"snippet": "short"}]},
|
||||
"model_context": {"results": [{"content": "x" * 5000}]},
|
||||
}
|
||||
),
|
||||
tool="simba_search",
|
||||
user=user,
|
||||
conversation=conversation,
|
||||
message=message,
|
||||
)
|
||||
assert artifact is not None
|
||||
saved = await Artifact.get(id=artifact.id)
|
||||
assert saved.data == {"results": [{"snippet": "short"}]}
|
||||
assert "model_context" not in saved.data
|
||||
|
||||
async def test_plain_string_output(self, owner):
|
||||
user, conversation, message = owner
|
||||
result = await create_artifact_from_output(
|
||||
|
||||
@@ -464,6 +464,64 @@ def test_focused_note_heading_index_uses_absolute_content_lines():
|
||||
assert msg.artifact["data"]["markdown"] == "full original"
|
||||
|
||||
|
||||
def _search_artifact(chunk_text: str) -> dict:
|
||||
return {
|
||||
"kind": "search.documents",
|
||||
"title": "Search: vet",
|
||||
"data": {
|
||||
"query": "vet",
|
||||
"source": "paperless",
|
||||
"results": [
|
||||
{
|
||||
"snippet": chunk_text[:50],
|
||||
"title": "vet_invoice",
|
||||
"source_id": "chunk-1",
|
||||
"url": "/sources/chunk-1",
|
||||
}
|
||||
],
|
||||
},
|
||||
"model_context": {
|
||||
"results": [
|
||||
{
|
||||
"content": chunk_text,
|
||||
"title": "vet_invoice",
|
||||
"source_id": "chunk-1",
|
||||
"url": "/sources/chunk-1",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_search_result_compaction_keeps_full_chunk_text_under_a_generous_limit():
|
||||
import json
|
||||
|
||||
chunk_text = "Rabies vaccine administered. " + "detail " * 200
|
||||
msg = ToolMessage(
|
||||
content="display", artifact=_search_artifact(chunk_text), tool_call_id="a"
|
||||
)
|
||||
projected = context.compact_tool_result(msg, 100_000)
|
||||
parsed = json.loads(projected.content)
|
||||
(result,) = parsed["data"]["results"]
|
||||
assert result["content"] == chunk_text
|
||||
assert "snippet" not in result
|
||||
|
||||
|
||||
def test_search_result_compaction_shortens_content_but_keeps_source_ids_under_pressure():
|
||||
import json
|
||||
|
||||
chunk_text = "Rabies vaccine administered. " + "detail " * 200
|
||||
msg = ToolMessage(
|
||||
content="display", artifact=_search_artifact(chunk_text), tool_call_id="a"
|
||||
)
|
||||
projected = context.compact_tool_result(msg, 700)
|
||||
parsed = json.loads(projected.content)
|
||||
(result,) = parsed["data"]["results"]
|
||||
assert len(result["content"]) < len(chunk_text)
|
||||
assert result["source_id"] == "chunk-1"
|
||||
assert result["url"] == "/sources/chunk-1"
|
||||
|
||||
|
||||
def _conversation(turns, size=1000):
|
||||
messages = []
|
||||
for i in range(turns):
|
||||
@@ -542,3 +600,18 @@ def test_trim_drops_to_low_watermark_and_prefix_stays_stable():
|
||||
# ...so the retained prefix is unchanged for several turns in between.
|
||||
assert all(b - a >= 3 for a, b in zip(cuts, cuts[1:]))
|
||||
assert len(cuts) <= 5
|
||||
|
||||
|
||||
def test_search_result_compaction_keeps_source_ids_when_no_rows_fit():
|
||||
import json
|
||||
|
||||
chunk_text = "Rabies vaccine administered. " + "detail " * 200
|
||||
msg = ToolMessage(
|
||||
content="display", artifact=_search_artifact(chunk_text), tool_call_id="a"
|
||||
)
|
||||
projected = json.loads(context.compact_tool_result(msg, 300).content)
|
||||
data = projected["data"]
|
||||
assert data["results"] == []
|
||||
assert data["results_compaction"]["omitted_source_refs"] == [
|
||||
{"source_id": "chunk-1", "url": "/sources/chunk-1"}
|
||||
]
|
||||
|
||||
@@ -94,6 +94,32 @@ def test_artifact_citations_identify_exact_retrieved_chunk(monkeypatch):
|
||||
assert result["headings"] == ["Budget", "Savings"]
|
||||
|
||||
|
||||
async def test_serialized_chunk_header_is_compact_not_raw_metadata(monkeypatch):
|
||||
identifier = str(uuid4())
|
||||
doc = Document(
|
||||
id=identifier,
|
||||
page_content="Rabies vaccine administered",
|
||||
metadata={
|
||||
"source": "paperless",
|
||||
"filename": "vet_invoice.pdf",
|
||||
"created_date": datetime(2026, 9, 1, tzinfo=timezone.utc).timestamp(),
|
||||
"document_type": "Invoice",
|
||||
},
|
||||
)
|
||||
store = SimpleNamespace(
|
||||
amax_marginal_relevance_search=AsyncMock(return_value=[doc])
|
||||
)
|
||||
monkeypatch.setattr(logic, "_get_vector_store", lambda: store)
|
||||
text, _ = await logic.query_vector_store("vet", source="paperless")
|
||||
assert "{'source'" not in text
|
||||
assert str(doc.metadata) not in text
|
||||
assert "Title: vet_invoice" in text
|
||||
assert "Date: 2026-09-01" in text
|
||||
assert "Source: paperless" in text
|
||||
assert f"Citation: /sources/{identifier}" in text
|
||||
assert "Content: Rabies vaccine administered" in text
|
||||
|
||||
|
||||
def test_document_date_and_unknown_citations():
|
||||
assert (
|
||||
document_date(
|
||||
|
||||
@@ -521,6 +521,13 @@ async def test_simba_search(monkeypatch):
|
||||
assert result["snippet"].startswith("Rabies vaccine administered. ")
|
||||
assert len(result["snippet"]) <= SNIPPET_MAX_CHARS
|
||||
|
||||
(model_result,) = art["model_context"]["results"]
|
||||
assert model_result["content"] == PAPERLESS_DOCS[0].page_content
|
||||
assert len(model_result["content"]) > SNIPPET_MAX_CHARS
|
||||
assert model_result["title"] == result["title"]
|
||||
assert model_result["filename"] == result["filename"]
|
||||
assert "snippet" not in model_result
|
||||
|
||||
|
||||
async def test_simba_search_empty(monkeypatch):
|
||||
monkeypatch.setattr(agents, "query_vector_store", AsyncMock(return_value=("", [])))
|
||||
@@ -560,6 +567,14 @@ async def test_obsidian_search_notes(obsidian, monkeypatch):
|
||||
"date": "2026-01-02",
|
||||
}
|
||||
]
|
||||
assert art["model_context"]["results"] == [
|
||||
{
|
||||
"content": "Simba's vet is Dr Smith.",
|
||||
"title": "simba",
|
||||
"path": "pets/simba.md",
|
||||
"date": "2026-01-02",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def test_obsidian_search_notes_error(obsidian, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user