chore: import upstream snapshot with attribution
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:28 +08:00
commit c56bef871b
9296 changed files with 1854228 additions and 0 deletions
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,694 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import asyncio
import os
from unittest.mock import AsyncMock, Mock, patch
import pytest
from haystack import Document, Pipeline
from haystack.components.converters.image.document_to_image import DocumentToImageContent
from haystack.components.extractors.image import LLMDocumentContentExtractor
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.writers import DocumentWriter
from haystack.core.serialization import component_to_dict
from haystack.dataclasses.chat_message import ChatMessage, ImageContent
class TestLLMDocumentContentExtractor:
def test_init(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_generator = OpenAIChatGenerator(generation_kwargs={"temperature": 0.5})
extractor = LLMDocumentContentExtractor(
chat_generator=chat_generator,
prompt="Extract content from this image",
file_path_meta_field="file_path",
root_path="/test/path",
detail="high",
size=(800, 600),
raise_on_failure=True,
max_workers=5,
)
assert isinstance(extractor._chat_generator, OpenAIChatGenerator)
# Not testing specific model name, just that it's set
assert extractor._chat_generator.model is not None
assert extractor._chat_generator.generation_kwargs == {"temperature": 0.5}
assert extractor.prompt == "Extract content from this image"
assert extractor.file_path_meta_field == "file_path"
assert extractor.root_path == "/test/path"
assert extractor.detail == "high"
assert extractor.size == (800, 600)
assert extractor.raise_on_failure is True
assert extractor.max_workers == 5
def test_init_with_defaults(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_generator = OpenAIChatGenerator()
extractor = LLMDocumentContentExtractor(chat_generator=chat_generator)
assert extractor.prompt.startswith("\nYou are part of an information extraction pipeline")
assert extractor.file_path_meta_field == "file_path"
assert extractor.root_path == ""
assert extractor.detail is None
assert extractor.size is None
assert extractor.raise_on_failure is False
assert extractor.max_workers == 3
def test_init_with_variables_in_prompt(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_generator = OpenAIChatGenerator()
with pytest.raises(ValueError, match="The prompt must not have any variables"):
LLMDocumentContentExtractor(
chat_generator=chat_generator, prompt="Extract content from {{document.content}}"
)
def test_init_fails_without_chat_generator(self):
with pytest.raises(TypeError):
LLMDocumentContentExtractor()
def test_to_dict_openai(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_generator = OpenAIChatGenerator(generation_kwargs={"temperature": 0.5})
extractor = LLMDocumentContentExtractor(
chat_generator=chat_generator,
prompt="Custom extraction prompt",
file_path_meta_field="custom_path",
root_path="/custom/root",
detail="low",
size=(1024, 768),
raise_on_failure=True,
max_workers=4,
)
extractor_dict = extractor.to_dict()
assert extractor_dict == {
"type": "haystack.components.extractors.image.llm_document_content_extractor.LLMDocumentContentExtractor",
"init_parameters": {
"chat_generator": component_to_dict(chat_generator, "chat_generator"),
"prompt": "Custom extraction prompt",
"file_path_meta_field": "custom_path",
"root_path": "/custom/root",
"detail": "low",
"size": (1024, 768),
"raise_on_failure": True,
"max_workers": 4,
},
}
def test_from_dict_openai(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_generator = OpenAIChatGenerator(generation_kwargs={"temperature": 0.5})
extractor_dict = {
"type": "haystack.components.extractors.image.llm_document_content_extractor.LLMDocumentContentExtractor",
"init_parameters": {
"chat_generator": component_to_dict(chat_generator, "chat_generator"),
"prompt": "Custom extraction prompt",
"file_path_meta_field": "custom_path",
"root_path": "/custom/root",
"detail": "low",
"size": (1024, 768),
"raise_on_failure": True,
"max_workers": 4,
},
}
extractor = LLMDocumentContentExtractor.from_dict(extractor_dict)
assert extractor.prompt == "Custom extraction prompt"
assert extractor.file_path_meta_field == "custom_path"
assert extractor.root_path == "/custom/root"
assert extractor.detail == "low"
assert extractor.size == (1024, 768)
assert extractor.raise_on_failure is True
assert extractor.max_workers == 4
assert component_to_dict(extractor._chat_generator, "name") == component_to_dict(chat_generator, "name")
def test_run_no_documents(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_generator = OpenAIChatGenerator()
extractor = LLMDocumentContentExtractor(chat_generator=chat_generator)
result = extractor.run(documents=[])
assert result["documents"] == []
assert result["failed_documents"] == []
@patch.object(DocumentToImageContent, "run")
def test_run_with_failed_image_conversion(self, mock_doc_to_image_run, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
# Mock DocumentToImageContent to return None (failed conversion)
mock_doc_to_image_run.return_value = {"image_contents": [None]}
doc = Document(content="", meta={"file_path": "/path/to/image.pdf"})
docs = [doc]
result = extractor.run(documents=docs)
# Document should be in failed_documents
assert len(result["documents"]) == 0
assert len(result["failed_documents"]) == 1
failed_doc = result["failed_documents"][0]
assert failed_doc.id == doc.id
assert "extraction_error" in failed_doc.meta
assert failed_doc.meta["extraction_error"] == "Document has no content, skipping LLM call."
# Ensure no attempt was made to call the LLM
mock_chat_generator.run.assert_not_called()
@patch.object(DocumentToImageContent, "run")
def test_run_with_llm_success(self, mock_doc_to_image_run):
# Mock successful LLM response (JSON with document_content)
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.return_value = {
"replies": [ChatMessage.from_assistant(text='{"document_content": "Extracted content from the image"}')]
}
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
docs = [Document(content="", meta={"file_path": "/path/to/image.pdf"})]
result = extractor.run(documents=docs)
assert len(result["documents"]) == 1
assert len(result["failed_documents"]) == 0
processed_doc = result["documents"][0]
assert processed_doc.id == docs[0].id
assert processed_doc.content == "Extracted content from the image"
assert "extraction_error" not in processed_doc.meta
mock_chat_generator.run.assert_called_once()
@patch.object(DocumentToImageContent, "run")
def test_run_plain_string_response_goes_to_content(self, mock_doc_to_image_run):
"""When LLM returns plain string (non-JSON), it is written to document content."""
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.return_value = {"replies": [ChatMessage.from_assistant(text="Plain text, not JSON")]}
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
docs = [Document(content="", meta={"file_path": "/path/to/image.pdf"})]
result = extractor.run(documents=docs)
assert len(result["documents"]) == 1
assert len(result["failed_documents"]) == 0
assert result["documents"][0].content == "Plain text, not JSON"
@patch.object(DocumentToImageContent, "run")
def test_run_valid_json_not_object_reports_error(self, mock_doc_to_image_run):
"""When LLM returns valid JSON that is not an object (e.g. array or primitive), report error."""
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.return_value = {
"replies": [ChatMessage.from_assistant(text='["array", "not", "object"]')]
}
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
docs = [Document(content="", meta={"file_path": "/path/to/image.pdf"})]
result = extractor.run(documents=docs)
assert len(result["documents"]) == 0
assert len(result["failed_documents"]) == 1
assert "extraction_error" in result["failed_documents"][0].meta
assert "JSON object" in result["failed_documents"][0].meta["extraction_error"]
@patch.object(DocumentToImageContent, "run")
def test_run_with_content_and_metadata_extraction(self, mock_doc_to_image_run):
"""When content mode gets JSON with document_content and other keys, other keys are merged into metadata."""
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.return_value = {
"replies": [
ChatMessage.from_assistant(text='{"document_content": "Main text", "title": "Doc Title", "page": "1"}')
]
}
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
docs = [Document(content="", meta={"file_path": "/path/to/image.pdf"})]
result = extractor.run(documents=docs)
assert len(result["documents"]) == 1
processed = result["documents"][0]
assert processed.content == "Main text"
assert processed.meta["title"] == "Doc Title"
assert processed.meta["page"] == "1"
@patch.object(DocumentToImageContent, "run")
def test_run_with_llm_failure_raise_on_failure_false(self, mock_doc_to_image_run, caplog):
# Mock LLM failure
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.side_effect = Exception("LLM API error")
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator, raise_on_failure=False)
# Mock DocumentToImageContent to return valid image content
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
docs = [Document(content="", meta={"file_path": "/path/to/image.pdf"})]
result = extractor.run(documents=docs)
# Document should be in failed_documents
assert len(result["documents"]) == 0
assert len(result["failed_documents"]) == 1
failed_doc = result["failed_documents"][0]
assert failed_doc.id == docs[0].id
assert "extraction_error" in failed_doc.meta
assert "LLM failed with exception: LLM API error" in failed_doc.meta["extraction_error"]
# Check that error was logged
assert "LLM" in caplog.text
assert "execution failed" in caplog.text
@patch.object(DocumentToImageContent, "run")
def test_run_with_llm_failure_raise_on_failure_true(self, mock_doc_to_image_run):
# Mock LLM failure
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.side_effect = Exception("LLM API error")
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator, raise_on_failure=True)
# Mock DocumentToImageContent to return valid image content
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
with pytest.raises(Exception, match="LLM API error"):
extractor.run(documents=[Document(content="", meta={"file_path": "/path/to/image.pdf"})])
@patch.object(DocumentToImageContent, "run")
def test_run_removes_extraction_error_from_previous_runs(self, mock_doc_to_image_run):
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.return_value = {
"replies": [ChatMessage.from_assistant(text='{"document_content": "Successfully extracted content"}')]
}
# Mock DocumentToImageContent to return valid image content
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
# Document with previous extraction error
docs = [
Document(
content="",
meta={
"file_path": "/path/to/image.pdf",
"extraction_error": "Previous error",
"other_meta": "should_remain",
},
)
]
result = extractor.run(documents=docs)
# Document should be successfully processed
assert len(result["documents"]) == 1
assert len(result["failed_documents"]) == 0
processed_doc = result["documents"][0]
assert processed_doc.content == "Successfully extracted content"
assert "extraction_error" not in processed_doc.meta
assert processed_doc.meta["other_meta"] == "should_remain"
@patch.object(DocumentToImageContent, "run")
def test_run_with_mixed_success_and_failure(self, mock_doc_to_image_run):
# Mock successful LLM response for first call, failure for second
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.side_effect = [
{"replies": [ChatMessage.from_assistant(text='{"document_content": "Successfully extracted content"}')]},
Exception("LLM API error"),
]
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator, raise_on_failure=False)
# Mock DocumentToImageContent - first succeeds, second fails
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg"), None]
}
doc1 = Document(content="", meta={"file_path": "./test/test_files/images/apple.jpg"})
doc2 = Document(content="", meta={"file_path": "/path/to/image.jpg"})
docs = [doc1, doc2]
result = extractor.run(documents=docs)
# One document should succeed, one should fail
assert len(result["documents"]) == 1
assert len(result["failed_documents"]) == 1
successful_doc = result["documents"][0]
assert successful_doc.id == doc1.id
assert successful_doc.content == "Successfully extracted content"
failed_doc = result["failed_documents"][0]
assert failed_doc.id == doc2.id
assert "extraction_error" in failed_doc.meta
@patch.object(DocumentToImageContent, "run")
def test_run_json_multiple_keys_metadata_merged(self, mock_doc_to_image_run):
"""When LLM returns JSON with multiple keys and no document_content, all keys are merged into metadata."""
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run.return_value = {
"replies": [
ChatMessage.from_assistant(text='{"title": "Sample Doc", "author": "Test", "document_type": "report"}')
]
}
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
original_content = "Original content"
docs = [Document(content=original_content, meta={"file_path": "/path/to/image.pdf"})]
result = extractor.run(documents=docs)
assert len(result["documents"]) == 1
assert len(result["failed_documents"]) == 0
processed = result["documents"][0]
assert processed.content == original_content
assert processed.meta["title"] == "Sample Doc"
assert processed.meta["author"] == "Test"
assert processed.meta["document_type"] == "report"
def test_run_on_thread_with_none_prompt(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
extractor = LLMDocumentContentExtractor(chat_generator=OpenAIChatGenerator())
result = extractor._run_on_thread(None)
assert "error" in result
assert result["error"] == "Document has no content, skipping LLM call."
@pytest.mark.integration
@pytest.mark.skipif(
not os.environ.get("OPENAI_API_KEY", None),
reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.",
)
def test_live_run(self, in_memory_doc_store):
docs = [Document(content="", meta={"file_path": "./test/test_files/images/apple.jpg"})]
extractor = LLMDocumentContentExtractor(chat_generator=OpenAIChatGenerator(model="gpt-4.1-nano"))
writer = DocumentWriter(document_store=in_memory_doc_store)
pipeline = Pipeline()
pipeline.add_component("extractor", extractor)
pipeline.add_component("doc_writer", writer)
pipeline.connect("extractor.documents", "doc_writer.documents")
pipeline.run(data={"documents": docs})
doc_store_docs = in_memory_doc_store.filter_documents()
assert len(doc_store_docs) >= 1
assert len(doc_store_docs[0].content) > 0
@pytest.mark.integration
@pytest.mark.skipif(
not os.environ.get("OPENAI_API_KEY", None),
reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.",
)
def test_live_run_on_image_with_metadata(self, in_memory_doc_store):
"""
Live test using image_metadata.png: single prompt; LLM can return JSON with document_content
and metadata keys (author, date, document_type, topic) in one response.
"""
prompt = """
You are part of an information extraction pipeline that extracts the content of image-based documents.
Extract the content from the provided image.
You need to extract the content exactly.
Format everything as markdown.
Make sure to retain the reading order of the document.
**Visual Elements**
Do not extract figures, drawings, maps, graphs or any other visual elements.
Instead, add a caption that describes briefly what you see in the visual element.
You must describe each visual element.
If you only see a visual element without other content, you must describe this visual element.
Enclose each image caption with [img-caption][/img-caption]
**Tables**
Make sure to format the table in markdown.
Add a short caption below the table that describes the table's content.
Enclose each table caption with [table-caption][/table-caption].
The caption must be placed below the extracted table.
**Forms**
Reproduce checkbox selections with markdown.
Return a single JSON object. It must contain the key "document_content" with the extracted text as value.
Include all other extracted information as keys for metadata. All metadata should be returned as separate keys
in the JSON object. For example, if you extract the document type and date, you should return:
{"title": "Example Document", "author": "John Doe", "date": "2024-01-15", "document_type": "invoice"}
Don't include any metadata in the "document_content" field. The "document_content" field should only contain the
image description and any possible text extracted from the image.
No markdown, no code fence, only raw JSON.
Document:"""
image_path = "./test/test_files/images/image_metadata.png"
docs = [Document(content="", meta={"file_path": image_path})]
extractor = LLMDocumentContentExtractor(
prompt=prompt,
chat_generator=OpenAIChatGenerator(
model="gpt-4.1-nano",
generation_kwargs={
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "entity_extraction",
"schema": {
"type": "object",
"properties": {
"author": {"type": "string"},
"date": {"type": "string"},
"document_type": {"type": "string"},
"title": {"type": "string"},
},
"additionalProperties": False,
},
},
}
},
),
)
writer = DocumentWriter(document_store=in_memory_doc_store)
pipeline = Pipeline()
pipeline.add_component("extractor", extractor)
pipeline.add_component("doc_writer", writer)
pipeline.connect("extractor.documents", "doc_writer.documents")
pipeline.run(data={"documents": docs})
doc_store_docs = in_memory_doc_store.filter_documents()
assert len(doc_store_docs) >= 1
doc = doc_store_docs[0]
assert len(doc.content) > 0, "Expected non-empty content (image/document description)"
assert "extraction_error" not in doc.meta
assert "author" in doc.meta, "Expected 'author' key in metadata"
assert "date" in doc.meta, "Expected 'date' key in metadata"
assert "document_type" in doc.meta, "Expected 'document_type' key in metadata"
assert "title" in doc.meta, "Expected 'title' key in metadata"
class TestLLMDocumentContentExtractorAsync:
@pytest.mark.asyncio
async def test_run_async_no_documents(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_generator = OpenAIChatGenerator()
extractor = LLMDocumentContentExtractor(chat_generator=chat_generator)
result = await extractor.run_async(documents=[])
assert result["documents"] == []
assert result["failed_documents"] == []
@pytest.mark.asyncio
@patch.object(DocumentToImageContent, "run")
async def test_run_async_success(self, mock_doc_to_image_run):
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run_async = AsyncMock(
return_value={
"replies": [ChatMessage.from_assistant(text='{"document_content": "Extracted content from the image"}')]
}
)
mock_doc_to_image_run.return_value = {
"image_contents": [
ImageContent.from_file_path("./test/test_files/images/apple.jpg"),
ImageContent.from_file_path("./test/test_files/images/apple.jpg"),
]
}
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
docs = [
Document(content="", meta={"file_path": "/path/to/image1.pdf"}),
Document(content="", meta={"file_path": "/path/to/image2.pdf"}),
]
result = await extractor.run_async(documents=docs)
assert len(result["documents"]) == 2
assert len(result["failed_documents"]) == 0
for processed_doc in result["documents"]:
assert processed_doc.content == "Extracted content from the image"
assert "extraction_error" not in processed_doc.meta
# one async LLM call per document
assert mock_chat_generator.run_async.await_count == 2
@pytest.mark.asyncio
@patch.object(DocumentToImageContent, "run")
async def test_run_async_with_llm_failure_raise_on_failure_false(self, mock_doc_to_image_run):
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run_async = AsyncMock(side_effect=Exception("LLM API error"))
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator, raise_on_failure=False)
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
docs = [Document(content="", meta={"file_path": "/path/to/image.pdf"})]
result = await extractor.run_async(documents=docs)
assert len(result["documents"]) == 0
assert len(result["failed_documents"]) == 1
failed_doc = result["failed_documents"][0]
assert failed_doc.id == docs[0].id
assert "extraction_error" in failed_doc.meta
assert "LLM failed with exception: LLM API error" in failed_doc.meta["extraction_error"]
@pytest.mark.asyncio
@patch.object(DocumentToImageContent, "run")
async def test_run_async_with_llm_failure_raise_on_failure_true(self, mock_doc_to_image_run):
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
mock_chat_generator.run_async = AsyncMock(side_effect=Exception("LLM API error"))
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator, raise_on_failure=True)
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
with pytest.raises(Exception, match="LLM API error"):
await extractor.run_async(documents=[Document(content="", meta={"file_path": "/path/to/image.pdf"})])
@pytest.mark.asyncio
@patch.object(DocumentToImageContent, "run")
async def test_run_async_falls_back_to_sync_run(self, mock_doc_to_image_run):
"""If the chat generator has no run_async, the sync run is used and output is still correct."""
class SyncOnlyChatGenerator:
def run(self, messages, **kwargs):
return {"replies": [ChatMessage.from_assistant(text='{"document_content": "Extracted via sync run"}')]}
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg")]
}
extractor = LLMDocumentContentExtractor(chat_generator=SyncOnlyChatGenerator())
docs = [Document(content="", meta={"file_path": "/path/to/image.pdf"})]
result = await extractor.run_async(documents=docs)
assert len(result["documents"]) == 1
assert len(result["failed_documents"]) == 0
assert result["documents"][0].content == "Extracted via sync run"
@pytest.mark.asyncio
@patch.object(DocumentToImageContent, "run")
async def test_run_async_respects_max_workers(self, mock_doc_to_image_run):
max_workers = 2
in_flight = 0
peak_in_flight = 0
mock_chat_generator = Mock(spec=OpenAIChatGenerator)
async def fake_run_async(messages, **kwargs):
nonlocal in_flight, peak_in_flight
in_flight += 1
peak_in_flight = max(peak_in_flight, in_flight)
try:
await asyncio.sleep(0.01)
return {"replies": [ChatMessage.from_assistant(text='{"document_content": "content"}')]}
finally:
in_flight -= 1
mock_chat_generator.run_async = fake_run_async
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator, max_workers=max_workers)
mock_doc_to_image_run.return_value = {
"image_contents": [ImageContent.from_file_path("./test/test_files/images/apple.jpg") for _ in range(10)]
}
docs = [Document(content="", meta={"file_path": f"/path/to/image{i}.pdf"}) for i in range(10)]
result = await extractor.run_async(documents=docs)
assert len(result["documents"]) == 10
assert peak_in_flight <= max_workers
@pytest.mark.integration
@pytest.mark.skipif(
not os.environ.get("OPENAI_API_KEY", None),
reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.",
)
@pytest.mark.asyncio
async def test_live_run_async(self):
docs = [Document(content="", meta={"file_path": "./test/test_files/images/apple.jpg"})]
extractor = LLMDocumentContentExtractor(chat_generator=OpenAIChatGenerator(model="gpt-4.1-nano"))
result = await extractor.run_async(documents=docs)
assert len(result["failed_documents"]) == 0
assert len(result["documents"]) == 1
assert len(result["documents"][0].content) > 0
class TestComponentLifecycle:
def test_warm_up_delegates_to_chat_generator(self):
mock_chat_generator = Mock(spec=["run", "warm_up"])
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
extractor.warm_up()
mock_chat_generator.warm_up.assert_called_once()
async def test_warm_up_async_delegates_to_chat_generator(self):
mock_chat_generator = Mock(spec=["run", "warm_up_async"])
mock_chat_generator.warm_up_async = AsyncMock()
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
await extractor.warm_up_async()
mock_chat_generator.warm_up_async.assert_awaited_once()
async def test_warm_up_async_falls_back_to_sync_warm_up(self):
mock_chat_generator = Mock(spec=["run", "warm_up"])
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
await extractor.warm_up_async()
mock_chat_generator.warm_up.assert_called_once()
def test_close_delegates_to_chat_generator(self):
mock_chat_generator = Mock(spec=["run", "close"])
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
extractor.close()
mock_chat_generator.close.assert_called_once()
async def test_close_async_delegates_to_chat_generator(self):
mock_chat_generator = Mock(spec=["run", "close_async"])
mock_chat_generator.close_async = AsyncMock()
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
await extractor.close_async()
mock_chat_generator.close_async.assert_awaited_once()
async def test_close_async_falls_back_to_sync_close(self):
mock_chat_generator = Mock(spec=["run", "close"])
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
await extractor.close_async()
mock_chat_generator.close.assert_called_once()
async def test_lifecycle_is_safe_when_chat_generator_lacks_methods(self):
mock_chat_generator = Mock(spec=["run"])
extractor = LLMDocumentContentExtractor(chat_generator=mock_chat_generator)
extractor.warm_up()
await extractor.warm_up_async()
extractor.close()
await extractor.close_async()