|
12 | 12 | from agentstack_server.configuration import DoclingExtractionConfiguration |
13 | 13 | from agentstack_server.domain.models.file import AsyncFile, ExtractionFormat, TextExtractionSettings |
14 | 14 | from agentstack_server.domain.repositories.file import ITextExtractionBackend |
15 | | -from agentstack_server.utils.utils import extract_multiple_values_stream |
| 15 | +from agentstack_server.utils.utils import StreamingExtractor, build_stream_reader |
16 | 16 |
|
17 | 17 | logger = logging.getLogger(__name__) |
18 | 18 |
|
@@ -58,6 +58,34 @@ async def extract_text( |
58 | 58 | timeout: timedelta | None = None, # noqa: ASYNC109 |
59 | 59 | settings: TextExtractionSettings | None = None, |
60 | 60 | ) -> AsyncIterator[list[tuple[AsyncFile, ExtractionFormat]]]: |
| 61 | + """ |
| 62 | + Extract text from a file using the Docling service. |
| 63 | +
|
| 64 | + IMPORTANT: This method MUST be used as an async context manager. The returned AsyncFile |
| 65 | + objects stream data directly from the HTTP response and must be consumed INSIDE the |
| 66 | + context block: |
| 67 | +
|
| 68 | + async with backend.extract_text(file_url) as files: |
| 69 | + for async_file, format in files: |
| 70 | + # Read and process files HERE, inside the context |
| 71 | + data = await async_file.read() |
| 72 | + # Files are no longer readable after context exits |
| 73 | +
|
| 74 | + Do NOT store AsyncFile references outside the context - they will fail to read with |
| 75 | + "StreamingExtractor is closed" error. |
| 76 | +
|
| 77 | + Args: |
| 78 | + file_url: URL of the file to extract text from |
| 79 | + timeout: Maximum time to wait for extraction (default: 5 minutes) |
| 80 | + settings: Optional extraction settings (formats to extract) |
| 81 | +
|
| 82 | + Yields: |
| 83 | + List of (AsyncFile, ExtractionFormat) tuples containing extracted content |
| 84 | +
|
| 85 | + Raises: |
| 86 | + RuntimeError: If Docling extraction backend is not enabled |
| 87 | + HTTPError: If Docling service returns an error |
| 88 | + """ |
61 | 89 | if not self._enabled: |
62 | 90 | raise RuntimeError( |
63 | 91 | "Docling extraction backend is not enabled, please check the documentation how to enable it" |
@@ -92,34 +120,19 @@ async def extract_text( |
92 | 120 | # Build a dict of keys to extract with their types |
93 | 121 | keys_to_extract = {info.field_key: "object" if info.is_object else "string" for _, info in format_pairs} |
94 | 122 |
|
95 | | - # Extract all requested formats in a single pass through the stream |
96 | | - extracted_contents = await extract_multiple_values_stream(response.aiter_text, keys_to_extract) |
97 | | - |
98 | | - # Create (AsyncFile, ExtractionFormat) tuples for each format |
99 | | - files = [] |
100 | | - for extraction_format, info in format_pairs: |
101 | | - content = extracted_contents[info.field_key] |
102 | | - |
103 | | - # Create a read function that returns the buffered content |
104 | | - def create_read_fn(buffered_content: bytes): |
105 | | - position = 0 |
106 | | - |
107 | | - async def read(chunk_size: int = 1024) -> bytes: |
108 | | - nonlocal position |
109 | | - if position >= len(buffered_content): |
110 | | - return b"" |
111 | | - chunk = buffered_content[position : position + chunk_size] |
112 | | - position += len(chunk) |
113 | | - return chunk |
114 | | - |
115 | | - return read |
116 | | - |
117 | | - async_file = AsyncFile( |
118 | | - filename=f"extracted_response.{info.format_value}", |
119 | | - content_type=info.content_type, |
120 | | - read=create_read_fn(content), |
121 | | - size=len(content), |
122 | | - ) |
123 | | - files.append((async_file, extraction_format)) |
124 | | - |
125 | | - yield files |
| 123 | + # Create streaming extractor with context manager for proper cleanup |
| 124 | + async with StreamingExtractor(response.aiter_text, keys_to_extract) as extractor: |
| 125 | + iterators = extractor.get_iterators() |
| 126 | + |
| 127 | + # Create (AsyncFile, ExtractionFormat) tuples for each format |
| 128 | + files = [] |
| 129 | + for extraction_format, info in format_pairs: |
| 130 | + async_file = AsyncFile( |
| 131 | + filename=f"extracted_response.{info.format_value}", |
| 132 | + content_type=info.content_type, |
| 133 | + read=build_stream_reader(iterators[info.field_key]), |
| 134 | + size=None, # Size unknown when streaming |
| 135 | + ) |
| 136 | + files.append((async_file, extraction_format)) |
| 137 | + |
| 138 | + yield files |
0 commit comments