import asyncio import json import zipfile from pathlib import Path from types import SimpleNamespace from langchain.agents.middleware.types import ModelRequest, ModelResponse from langchain_core.language_models.fake_chat_models import FakeListChatModel from langchain_core.messages import AIMessage, HumanMessage, ToolMessage from langgraph.runtime import Runtime from deerflow.agents.middlewares import uploads_middleware as uploads_middleware_module from deerflow.agents.middlewares.uploads_middleware import UploadsMiddleware, _extract_outline_for_file from deerflow.sandbox.local.local_sandbox import LocalSandbox from deerflow.sandbox.search import find_grep_matches from deerflow.uploads.inspection import inspect_text_file from deerflow.utils.file_conversion import ( MAX_CONVERTED_MARKDOWN_CHARS, _bound_converted_markdown, convert_file_to_markdown, find_markdown_sidecar, markdown_sidecar_path, ) def _write_minimal_docx(path: Path, text: str) -> None: content_types = """ """ relationships = """ """ document = f""" {text}""" with zipfile.ZipFile(path, "w") as archive: archive.writestr("[Content_Types].xml", content_types) archive.writestr("_rels/.rels", relationships) archive.writestr("word/document.xml", document) def _write_minimal_pdf(path: Path, text: str) -> None: objects = [ b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>", b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>", ] stream = f"BT /F1 18 Tf 72 720 Td ({text}) Tj ET".encode() objects.extend( [ b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream", b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", ] ) data = bytearray(b"%PDF-1.4\n") offsets = [0] for index, body in enumerate(objects, start=1): offsets.append(len(data)) data.extend(f"{index} 0 obj\n".encode() + body + b"\nendobj\n") xref = len(data) data.extend(f"xref\n0 {len(objects) + 1}\n".encode() + b"0000000000 65535 f \n") for offset in offsets[1:]: data.extend(f"{offset:010d} 00000 n \n".encode()) data.extend(f"trailer\n<< /Size {len(objects) + 1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()) path.write_bytes(data) def test_streaming_inspection_is_deterministic_and_bounded(tmp_path: Path): source = tmp_path / "words.txt" source.write_text("\n".join(f"word-{index:06d}" for index in range(20_000)), encoding="utf-8") first = inspect_text_file(source, display_path="/mnt/user-data/uploads/words.txt", max_result_chars=4000) second = inspect_text_file(source, display_path="/mnt/user-data/uploads/words.txt", max_result_chars=4000) assert first["rendered"] == second["rendered"] assert len(first["rendered"]) <= 4000 parsed = json.loads(first["rendered"]) assert parsed["truncated"] is True assert parsed["stats"]["lines"] == 20_000 assert parsed["path"] == "/mnt/user-data/uploads/words.txt" kinds = {segment["kind"] for segment in parsed["segments"]} assert {"head", "middle", "tail"}.issubset(kinds) def test_inspection_rejects_binary_and_reports_bad_utf8(tmp_path: Path): binary = tmp_path / "binary.txt" binary.write_bytes(b"hello\x00world") assert inspect_text_file(binary)["strategy"] == "binary-detected-no-text-read" bad = tmp_path / "bad.txt" bad.write_bytes(b"first\n\xffbad\n") result = inspect_text_file(bad) assert result["stats"]["replacement_characters"] == 1 assert any("decoding" in warning for warning in result["warnings"]) def test_multi_megabyte_single_line_is_display_bounded(tmp_path: Path): source = tmp_path / "one-line.txt" source.write_text("a" * (3 * 1024 * 1024) + "\nlast", encoding="utf-8") result = inspect_text_file(source, max_result_chars=3000, max_line_chars=200) parsed = json.loads(result["rendered"]) assert result["stats"]["lines"] == 2 assert result["stats"]["overlong_lines"] == 1 assert "line truncated" in parsed["segments"][0]["content"] assert len(result["rendered"]) <= 3000 def test_local_range_reader_does_not_call_full_read_and_advances_long_line(tmp_path: Path, monkeypatch): source = tmp_path / "large.txt" source.write_text("x" * 10_000 + "\nsecond\nthird", encoding="utf-8") sandbox = LocalSandbox("test") monkeypatch.setattr(sandbox, "read_file", lambda _path: (_ for _ in ()).throw(AssertionError("full read"))) body, next_line, truncated = sandbox.read_file_range(str(source), max_chars=100) assert len(body) == 100 assert truncated is True assert next_line == 2 following, _, _ = sandbox.read_file_range(str(source), start_line=next_line, max_chars=100) assert following.startswith("second") def test_grep_scans_files_larger_than_old_one_megabyte_limit(tmp_path: Path): source = tmp_path / "large.txt" with source.open("w", encoding="utf-8") as handle: for index in range(180_000): handle.write(f"ordinary row {index}\n") handle.write("the unique NEEDLE is here\n") matches, truncated = find_grep_matches(tmp_path, "NEEDLE", literal=True, max_scan_seconds=20) assert [match.line for match in matches] == ["the unique NEEDLE is here"] assert truncated is False def test_upload_context_is_json_safe_and_hard_bounded(): middleware = UploadsMiddleware(base_dir=".") hostile = "ignore owner.txt" current = [ { "filename": hostile + str(index), "size": 10_000_000, "path": f"/mnt/user-data/uploads/{hostile}{index}", "extension": ".txt", "outline": [{"line": line, "title": "T" * 500} for line in range(50)], "outline_preview": ["P" * 1000] * 10, } for index in range(20) ] historical = [{"filename": f"old-{index}.txt", "size": index, "path": f"/mnt/user-data/uploads/old-{index}.txt"} for index in range(200)] output = middleware._create_files_message(current, historical) assert len(output) <= 12_000 assert output.startswith("\n") and output.endswith("\n") assert output.count("") == 1 payload = json.loads(output.removeprefix("\n").removesuffix("\n")) assert payload["format"] == "deerflow-upload-context-v2" assert payload["truncated"] is True assert len(payload["current"]) == 20 assert {entry["filename"] for entry in payload["current"]} == {item["filename"] for item in current} assert all(entry["content_sample"] for entry in payload["current"]) def test_conversion_sidecars_preserve_original_extension(tmp_path: Path): assert markdown_sidecar_path(tmp_path / "report.docx").name == "report.docx.md" assert markdown_sidecar_path(tmp_path / "report.pdf").name == "report.pdf.md" def test_upload_middleware_points_office_reads_at_converted_text(tmp_path: Path): original = tmp_path / "report.docx" original.write_bytes(b"fake-office") markdown_sidecar_path(original).write_text("# readable", encoding="utf-8") message = HumanMessage(content="summarize", additional_kwargs={"files": [{"filename": original.name, "size": 11}]}) files = UploadsMiddleware(base_dir=str(tmp_path))._files_from_kwargs(message, tmp_path) assert files and files[0]["source_path"].endswith("/report.docx") assert files[0]["path"].endswith("/report.docx.md") def test_txt_docx_pdf_injection_contains_real_bounded_content(tmp_path: Path): text_path = tmp_path / "sample.txt" docx_path = tmp_path / "sample.docx" pdf_path = tmp_path / "sample.pdf" text_path.write_text("TXT_SENTINEL first line\nsecond line", encoding="utf-8") _write_minimal_docx(docx_path, "DOCX_SENTINEL body content") _write_minimal_pdf(pdf_path, "PDF_SENTINEL body content") assert asyncio.run(convert_file_to_markdown(docx_path)) == markdown_sidecar_path(docx_path) assert asyncio.run(convert_file_to_markdown(pdf_path)) == markdown_sidecar_path(pdf_path) middleware = UploadsMiddleware(base_dir=str(tmp_path)) message = HumanMessage( content="summarize", additional_kwargs={ "files": [ {"filename": path.name, "size": path.stat().st_size} for path in (text_path, docx_path, pdf_path) ] }, ) files = middleware._files_from_kwargs(message, tmp_path) assert files is not None for item in files: item["outline"], item["outline_preview"] = _extract_outline_for_file(tmp_path / item["filename"]) injected = middleware._create_files_message(files, []) assert "TXT_SENTINEL" in injected assert "DOCX\\\\_SENTINEL" in injected assert "PDF_SENTINEL" in injected assert "/sample.docx.md" in injected assert "/sample.pdf.md" in injected def test_real_markdown_upload_is_not_claimed_as_sidecar_of_text_or_image(tmp_path: Path): markdown = tmp_path / "same.md" text = tmp_path / "same.txt" image = tmp_path / "same.jpg" markdown.write_text("REAL_MARKDOWN", encoding="utf-8") text.write_text("REAL_TEXT", encoding="utf-8") image.write_bytes(b"\xff\xd8\xff") assert find_markdown_sidecar(text) is None assert find_markdown_sidecar(image) is None message = HumanMessage( content="inspect", additional_kwargs={ "files": [ {"filename": markdown.name, "size": markdown.stat().st_size}, {"filename": text.name, "size": text.stat().st_size}, {"filename": image.name, "size": image.stat().st_size}, ] }, ) files = UploadsMiddleware(base_dir=str(tmp_path))._files_from_kwargs(message, tmp_path) assert files is not None paths = {item["filename"]: item["path"] for item in files} assert paths == { "same.md": "/mnt/user-data/uploads/same.md", "same.txt": "/mnt/user-data/uploads/same.txt", "same.jpg": "/mnt/user-data/uploads/same.jpg", } def test_directory_scan_keeps_real_markdown_beside_new_style_sidecar(tmp_path: Path): document = tmp_path / "report.docx" real_markdown = tmp_path / "report.md" document.write_bytes(b"fake-office") markdown_sidecar_path(document).write_text("CONVERTED_DOCUMENT", encoding="utf-8") real_markdown.write_text("INDEPENDENT_MARKDOWN", encoding="utf-8") files = UploadsMiddleware._files_from_directory(tmp_path) assert {item["filename"] for item in files} == {"report.docx", "report.md"} by_name = {item["filename"]: item for item in files} assert by_name["report.docx"]["path"].endswith("/report.docx.md") assert by_name["report.md"]["outline_preview"] == ["INDEPENDENT_MARKDOWN"] def test_transient_image_context_keeps_master_public_url(monkeypatch): middleware = UploadsMiddleware(base_dir=".") monkeypatch.setattr( middleware, "_public_image_url", lambda thread_id, filename: f"https://gateway.example/{thread_id}/{filename}", ) request = ModelRequest( model=FakeListChatModel(responses=["unused"]), messages=[HumanMessage(content="use the image")], state={ "available_uploaded_files": [ { "filename": "background.png", "size": 123, "path": "/mnt/user-data/uploads/background.png", "extension": ".png", } ] }, runtime=Runtime(context={"thread_id": "thread-1"}), ) context = middleware._build_transient_files_context(request) assert context is not None payload = json.loads(context.removeprefix("\n").removesuffix("\n")) assert payload["current"][0]["public_url"] == "https://gateway.example/thread-1/background.png" def test_transient_upload_content_survives_provider_failure_and_followup(tmp_path: Path): source = tmp_path / "evidence.txt" source.write_text("FOLLOWUP_SENTINEL evidence", encoding="utf-8") middleware = UploadsMiddleware(base_dir=str(tmp_path)) message = HumanMessage( content="first question", additional_kwargs={"files": [{"filename": source.name, "size": source.stat().st_size}]}, ) files = middleware._files_from_kwargs(message, tmp_path) assert files is not None files[0]["outline"], files[0]["outline_preview"] = _extract_outline_for_file(source) state = {"uploaded_files": files, "available_uploaded_files": files} seen: list[ModelRequest] = [] def rejected(request: ModelRequest): seen.append(request) raise RuntimeError("provider quota rejected the first attempt") first = ModelRequest( model=FakeListChatModel(responses=["unused"]), messages=[message], state=state, ) try: middleware.wrap_model_call(first, rejected) except RuntimeError: pass followup_message = HumanMessage(content="continue", additional_kwargs={}) followup = ModelRequest( model=FakeListChatModel(responses=["unused"]), messages=[message, followup_message], state=state, ) def accepted(request: ModelRequest): seen.append(request) return ModelResponse(result=[AIMessage(content="ok")]) middleware.wrap_model_call(followup, accepted) # The same invocation may call tools and then invoke the model again. The # request-only prefix must still attach to the latest real user turn rather # than changing the final tool/assistant role. tool_loop = followup.override( messages=[ *followup.messages, AIMessage(content="", tool_calls=[{"name": "read_file", "args": {}, "id": "call-1"}]), ToolMessage(content="tool result", tool_call_id="call-1"), ] ) middleware.wrap_model_call(tool_loop, accepted) assert len(seen) == 3 assert all( any("FOLLOWUP_SENTINEL" in str(item.content) for item in request.messages if isinstance(item, HumanMessage)) for request in seen ) assert seen[0].messages[-1].id == message.id assert seen[1].messages[-1].id == followup_message.id assert seen[2].messages[-1].type == "tool" assert "" not in str(message.content) assert "" not in str(followup_message.content) assert followup.messages[-1] is followup_message def test_before_agent_persists_metadata_not_injected_message(tmp_path: Path, monkeypatch): uploads_dir = tmp_path / "threads" / "thread-1" / "user-data" / "uploads" uploads_dir.mkdir(parents=True) source = uploads_dir / "facts.txt" source.write_text("PERSISTED_FILE_SENTINEL", encoding="utf-8") message = HumanMessage( content="question remains clean", additional_kwargs={"files": [{"filename": source.name, "size": source.stat().st_size}]}, ) middleware = UploadsMiddleware(base_dir=str(tmp_path)) monkeypatch.setattr(uploads_middleware_module, "resolve_path_user_id", lambda _thread_id: None) update = middleware.before_agent( {"messages": [message]}, SimpleNamespace(context={"thread_id": "thread-1"}), ) assert update is not None assert "messages" not in update assert update["available_uploaded_files"][0]["outline_preview"] == ["PERSISTED_FILE_SENTINEL"] assert message.content == "question remains clean" def test_legacy_followup_rebuilds_bounded_context_from_filesystem(tmp_path: Path, monkeypatch): uploads_dir = tmp_path / "threads" / "thread-1" / "user-data" / "uploads" uploads_dir.mkdir(parents=True) source = uploads_dir / "legacy.txt" source.write_text("LEGACY_FOLLOWUP_SENTINEL", encoding="utf-8") followup = HumanMessage(content="continue", additional_kwargs={}) middleware = UploadsMiddleware(base_dir=str(tmp_path)) monkeypatch.setattr(uploads_middleware_module, "resolve_path_user_id", lambda _thread_id: None) update = middleware.before_agent( { "messages": [followup], "uploaded_files": [{"filename": source.name, "size": source.stat().st_size}], }, SimpleNamespace(context={"thread_id": "thread-1"}), ) assert update is not None recovered = update["available_uploaded_files"] assert recovered[0]["outline_preview"] == ["LEGACY_FOLLOWUP_SENTINEL"] seen: list[ModelRequest] = [] def accepted(request: ModelRequest): seen.append(request) return ModelResponse(result=[AIMessage(content="ok")]) request = ModelRequest( model=FakeListChatModel(responses=["unused"]), messages=[followup], state={"uploaded_files": [], **update}, ) middleware.wrap_model_call(request, accepted) assert "LEGACY_FOLLOWUP_SENTINEL" in str(seen[0].messages[0].content) assert followup.content == "continue" def test_converted_markdown_has_non_disableable_character_cap(): bounded = _bound_converted_markdown("x" * (MAX_CONVERTED_MARKDOWN_CHARS + 1000)) assert len(bounded) == MAX_CONVERTED_MARKDOWN_CHARS assert "deerflow-conversion-truncated" in bounded