mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-08 13:28:43 +03:00
fix(agent): read document attachments on demand (#5122)
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""Tests for context builder media handling.
|
||||
|
||||
The ContextBuilder._build_user_content method should ONLY handle images.
|
||||
Document text extraction is the responsibility of the processing layer
|
||||
(AgentLoop._process_message and _drain_pending).
|
||||
The ContextBuilder.build_user_content method should ONLY handle images.
|
||||
The processing layer turns non-image media into attachment path references;
|
||||
document contents are read on demand through ``read_file``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -10,7 +10,6 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
from nanobot.agent.context import ContextBuilder
|
||||
from nanobot.utils.document import extract_documents
|
||||
|
||||
|
||||
def _make_builder(tmp_path: Path) -> ContextBuilder:
|
||||
@@ -20,7 +19,7 @@ def _make_builder(tmp_path: Path) -> ContextBuilder:
|
||||
|
||||
def test_build_user_content_with_no_media_returns_string(tmp_path: Path) -> None:
|
||||
builder = _make_builder(tmp_path)
|
||||
result = builder._build_user_content("hello", None)
|
||||
result = builder.build_user_content("hello", None)
|
||||
assert result == "hello"
|
||||
|
||||
|
||||
@@ -29,7 +28,7 @@ def test_build_user_content_with_image_returns_list(tmp_path: Path) -> None:
|
||||
builder = _make_builder(tmp_path)
|
||||
png = tmp_path / "test.png"
|
||||
png.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
||||
result = builder._build_user_content("describe this", [str(png)])
|
||||
result = builder.build_user_content("describe this", [str(png)])
|
||||
assert isinstance(result, list)
|
||||
types = [b["type"] for b in result]
|
||||
assert "image_url" in types
|
||||
@@ -41,7 +40,7 @@ def test_build_user_content_ignores_non_image_files(tmp_path: Path) -> None:
|
||||
builder = _make_builder(tmp_path)
|
||||
txt = tmp_path / "notes.txt"
|
||||
txt.write_text("some text", encoding="utf-8")
|
||||
result = builder._build_user_content("summarize", [str(txt)])
|
||||
result = builder.build_user_content("summarize", [str(txt)])
|
||||
assert result == "summarize"
|
||||
|
||||
|
||||
@@ -53,81 +52,8 @@ def test_build_user_content_mixed_image_and_non_image(tmp_path: Path) -> None:
|
||||
txt = tmp_path / "report.txt"
|
||||
txt.write_text("report text", encoding="utf-8")
|
||||
|
||||
result = builder._build_user_content("analyze", [str(png), str(txt)])
|
||||
result = builder.build_user_content("analyze", [str(png), str(txt)])
|
||||
assert isinstance(result, list)
|
||||
assert any(b["type"] == "image_url" for b in result)
|
||||
text_parts = [b.get("text", "") for b in result if b.get("type") == "text"]
|
||||
assert all("report text" not in t for t in text_parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bug detection: extract_documents must be called BEFORE _build_user_content
|
||||
# to prevent document media from being silently dropped.
|
||||
# This simulates the _drain_pending code path.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_drain_pending_path_preserves_document_text(tmp_path: Path) -> None:
|
||||
"""Simulates the _drain_pending path: a pending follow-up message
|
||||
with a document attachment must have its text extracted before being
|
||||
passed to _build_user_content. Without extract_documents, the
|
||||
document is silently dropped."""
|
||||
from docx import Document
|
||||
|
||||
doc = Document()
|
||||
doc.add_paragraph("Quarterly revenue is $5M")
|
||||
docx_path = tmp_path / "report.docx"
|
||||
doc.save(docx_path)
|
||||
|
||||
content = "summarize"
|
||||
media = [str(docx_path)]
|
||||
|
||||
# Step 1: extract_documents separates docs from images
|
||||
new_content, image_only = extract_documents(content, media)
|
||||
|
||||
# Step 2: _build_user_content handles only images (none left here)
|
||||
builder = _make_builder(tmp_path)
|
||||
result = builder._build_user_content(new_content, image_only if image_only else None)
|
||||
|
||||
# The document text should be present in the final content
|
||||
assert "Quarterly revenue" in result
|
||||
assert "summarize" in result
|
||||
|
||||
|
||||
def test_drain_pending_path_preserves_docx_table_text(tmp_path: Path) -> None:
|
||||
"""Uploaded Word forms must retain content stored in table cells."""
|
||||
from docx import Document
|
||||
|
||||
doc = Document()
|
||||
table = doc.add_table(rows=2, cols=2)
|
||||
table.cell(0, 0).text = "Applicant"
|
||||
table.cell(0, 1).text = "Ada Lovelace"
|
||||
table.cell(1, 0).text = "Research area"
|
||||
table.cell(1, 1).text = "Analytical engines"
|
||||
docx_path = tmp_path / "application.docx"
|
||||
doc.save(docx_path)
|
||||
|
||||
content, image_only = extract_documents("summarize", [str(docx_path)])
|
||||
|
||||
assert image_only == []
|
||||
assert "Applicant\tAda Lovelace" in content
|
||||
assert "Research area\tAnalytical engines" in content
|
||||
|
||||
|
||||
def test_drain_pending_path_without_extract_loses_document(tmp_path: Path) -> None:
|
||||
"""Demonstrates the BUG: if _drain_pending calls _build_user_content
|
||||
directly without extract_documents, document content is lost."""
|
||||
from docx import Document
|
||||
|
||||
doc = Document()
|
||||
doc.add_paragraph("Secret data in document")
|
||||
docx_path = tmp_path / "report.docx"
|
||||
doc.save(docx_path)
|
||||
|
||||
builder = _make_builder(tmp_path)
|
||||
|
||||
# Bug path: call _build_user_content directly with document media
|
||||
result = builder._build_user_content("summarize", [str(docx_path)])
|
||||
|
||||
# The document text is LOST — _build_user_content ignores non-images
|
||||
assert result == "summarize" # only the original text, no doc content
|
||||
assert "Secret data" not in result
|
||||
|
||||
Reference in New Issue
Block a user