mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-30 07:00:37 +00:00
fix(uploads): resolve conversion settings in the parser worker (#7642)
This commit is contained in:
parent
9e6ce2f98e
commit
c50cdc5161
@ -2462,6 +2462,8 @@ Unsent Web UI composer drafts survive page reloads and switching between convers
|
||||
|
||||
If an attachment upload reports `success: false`, the Web UI stops the send even when the HTTP response is 200. The upload error is shown and the composer keeps its draft and attachments for correction or retry. Files already accepted by a partially successful upload remain on the server; this does not roll them back.
|
||||
|
||||
Document conversion reads the live PDF-converter setting and parses documents in a background worker, keeping configuration-file checks off the Gateway event loop.
|
||||
|
||||
The Web UI composer supports `@` references at the cursor or through the `@` button. Search enabled skills, documents in the current project, and other conversations in a grouped picker; selected references appear inline alongside the draft text and can be removed with normal editing. Multiple skills can be selected in one message (up to 16 unique skills); selecting a checked skill again removes it, with each selection checked against the current user registry and agent allowlist. Project files are attached only after ingestion succeeds; conversation references require the `read_conversation` capability and retain its per-run limit. If capability discovery fails, saved conversation references remain intact and sending waits for a successful retry. Polishing preserves complete reference labels when instructions are reordered. The file group also offers local uploads. `/goal`, `/compact`, and legacy `/skill-name` input remain supported.
|
||||
|
||||
After picking an inline reference, typing continues immediately after it. Moving the cursor keeps the position you chose, including when editing text beside a reference.
|
||||
|
||||
@ -136,6 +136,13 @@ pins offload, executor isolation, request context, and failure ordering.
|
||||
|
||||
Tool and agent assembly re-enters `get_available_tools()` and may block on MCP discovery, so the four async assembly entry points — `run_agent`'s `agent_factory` call, `task_tool`, durable batch `_execute_item`, and `abuild_checkpoint_state_accessor` — dispatch through `deerflow.utils.assembly_io.run_assembly`, a dedicated ContextVar-preserving bounded executor (`DEER_FLOW_ASSEMBLY_WORKERS`, default 8) rather than the loop's default executor; a hung MCP server therefore parks an assembly worker instead of queueing unrelated default-executor work, and the pool logs a warning when pending assemblies exceed the worker count. `tests/blocking_io/test_tool_assembly_offloop.py` pins all four offloads plus the ContextVar propagation.
|
||||
|
||||
### Document Conversion
|
||||
|
||||
`file_conversion.convert_file_to_markdown` resolves the live PDF converter in
|
||||
its drained parsing worker. Config cache hits still stat/hash files; keep them
|
||||
off-loop with parsing. Cold/cache-hit coverage uses real temporary configs in
|
||||
`tests/blocking_io/test_file_conversion_offloop.py`.
|
||||
|
||||
### Uploaded Document Summaries
|
||||
|
||||
Outlines skip root-level HTML comment blocks (`<!--` with up to three leading
|
||||
|
||||
@ -8,9 +8,9 @@ PDF conversion strategy (auto mode):
|
||||
total when page count is unavailable), treat as image-based and fall back to MarkItDown.
|
||||
3. If pymupdf4llm is not installed, use MarkItDown directly (existing behaviour).
|
||||
|
||||
All document parsing runs in a thread pool via asyncio.to_thread() so compressed
|
||||
Office files and small PDFs cannot block the serving event loop. Source byte
|
||||
size does not bound parsing time.
|
||||
Configuration resolution and all document parsing run in a thread pool via
|
||||
asyncio.to_thread() so config reads, compressed Office files and small PDFs cannot
|
||||
block the serving event loop. Source byte size does not bound parsing time.
|
||||
|
||||
No FastAPI or HTTP dependencies — pure utility functions.
|
||||
"""
|
||||
@ -154,12 +154,17 @@ async def convert_file_to_markdown(file_path: Path, output_path: Path | None = N
|
||||
Returns:
|
||||
Path to the generated .md file, or None if conversion failed.
|
||||
"""
|
||||
|
||||
def convert_source() -> str:
|
||||
return _do_convert(file_path, _get_pdf_converter())
|
||||
|
||||
try:
|
||||
pdf_converter = _get_pdf_converter()
|
||||
# Config resolution also stats and hashes files, including cache hits.
|
||||
# Keep it in the same drained worker as parsing.
|
||||
# Compressed size cannot predict parsing cost (e.g. a small XLSX can
|
||||
# contain many cells). Always offload, and drain across cancellation so
|
||||
# the caller retains its source until the converter finishes reading.
|
||||
text = await await_drained(asyncio.to_thread(_do_convert, file_path, pdf_converter))
|
||||
text = await await_drained(asyncio.to_thread(convert_source))
|
||||
|
||||
md_path = output_path if output_path is not None else file_path.with_suffix(".md")
|
||||
# Drain the write-back across caller cancellation: run_file_io awaits
|
||||
|
||||
@ -93,3 +93,56 @@ async def test_cancelled_conversion_cleanup_failure_preserves_cancellation(
|
||||
finish.set()
|
||||
|
||||
assert any("partial conversion output" in record.getMessage() for record in caplog.records), "cleanup failure should be logged inside the handler, not escaped"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pdf_converter", ["markitdown", "pymupdf4llm"])
|
||||
@pytest.mark.parametrize("warm_cache", [False, True], ids=["cold_config", "cached_config"])
|
||||
async def test_conversion_resolves_real_config_in_the_parser_worker(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, pdf_converter: str, warm_cache: bool) -> None:
|
||||
from deerflow.config.app_config import AppConfig, get_app_config, reset_app_config
|
||||
from deerflow.config.extensions_config import reset_extensions_config
|
||||
from deerflow.utils import file_conversion
|
||||
|
||||
config_path = tmp_path / "config.yaml"
|
||||
extensions_path = tmp_path / "extensions_config.json"
|
||||
source = tmp_path / "document.pdf"
|
||||
|
||||
def seed_files() -> None:
|
||||
config_path.write_text(f"sandbox:\n use: deerflow.sandbox.local:LocalSandboxProvider\nuploads:\n pdf_converter: {pdf_converter}\n", encoding="utf-8")
|
||||
extensions_path.write_text('{"mcpServers": {}, "skills": {}}', encoding="utf-8")
|
||||
source.write_text("synthetic source", encoding="utf-8")
|
||||
|
||||
await asyncio.to_thread(seed_files)
|
||||
monkeypatch.setenv("DEER_FLOW_CONFIG_PATH", str(config_path))
|
||||
monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(extensions_path))
|
||||
reset_app_config()
|
||||
reset_extensions_config()
|
||||
config_threads: list[int] = []
|
||||
parser_threads: list[int] = []
|
||||
selected: list[str] = []
|
||||
loop_thread = threading.get_ident()
|
||||
|
||||
def load_config() -> AppConfig:
|
||||
config_threads.append(threading.get_ident())
|
||||
return get_app_config()
|
||||
|
||||
def parse_document(path: Path, converter: str) -> str:
|
||||
parser_threads.append(threading.get_ident())
|
||||
selected.append(converter)
|
||||
return path.read_text(encoding="utf-8") + " converted"
|
||||
|
||||
monkeypatch.setattr(file_conversion, "get_app_config", load_config)
|
||||
monkeypatch.setattr(file_conversion, "_do_convert", parse_document)
|
||||
try:
|
||||
if warm_cache:
|
||||
await asyncio.to_thread(get_app_config)
|
||||
output = await file_conversion.convert_file_to_markdown(source)
|
||||
finally:
|
||||
reset_app_config()
|
||||
reset_extensions_config()
|
||||
|
||||
assert config_threads == parser_threads
|
||||
assert len(config_threads) == 1 and config_threads[0] != loop_thread
|
||||
assert selected == [pdf_converter]
|
||||
assert output is not None
|
||||
assert output == source.with_suffix(".md")
|
||||
assert output.read_text(encoding="utf-8") == "synthetic source converted"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user