1
-import mimetypes
2
-import os
3
-import asyncio
4
-import json
5
-
6
-from helpers.vector_db import VectorDB
7
-
8
-os.environ["USER_AGENT"] = "@mixedbread-ai/unstructured" # noqa E402
9
-from langchain_unstructured import UnstructuredLoader # noqa E402
10
-
11
-from urllib.parse import urlparse
12
-from typing import Callable, Sequence, List, Optional, Tuple
13
-
14
-from langchain_community.document_loaders.pdf import PyMuPDFLoader
15
-from langchain_community.document_transformers import MarkdownifyTransformer
16
-from langchain_community.document_loaders.parsers.images import TesseractBlobParser
17
-
18
-from langchain_core.documents import Document
19
-from langchain.schema import SystemMessage, HumanMessage
20
-
21
-from helpers.print_style import PrintStyle
22
-from helpers.localization import Localization
23
-from helpers import files, errors
24
-from helpers.network import HttpFetchResult, fetch_public_http_resource
25
-from agent import Agent
26
-
27
-from langchain.text_splitter import RecursiveCharacterTextSplitter
28
-
29
-
30
-DEFAULT_SEARCH_THRESHOLD = 0.5
31
-MAX_REMOTE_DOCUMENT_BYTES = 50 * 1024 * 1024
32
-SMALL_DOCUMENT_QA_FALLBACK_CHARS = 12_000
33
-
34
-
35
-class DocumentQueryStore:
36
- """
37
- FAISS Store for document query results.
38
- Manages documents identified by URI for storage, retrieval, and searching.
39
- """
40
-
41
- # Default chunking parameters
42
- DEFAULT_CHUNK_SIZE = 1000
43
- DEFAULT_CHUNK_OVERLAP = 100
44
-
45
- # Cache for initialized stores
46
- _stores: dict[str, "DocumentQueryStore"] = {}
47
-
48
- @staticmethod
49
- def get(agent: Agent):
50
- """Create a DocumentQueryStore instance for the specified agent."""
51
- if not agent or not agent.config:
52
- raise ValueError("Agent and agent config must be provided")
53
-
54
- # Initialize store
55
- store = DocumentQueryStore(agent)
56
- return store
57
-
58
- def __init__(
59
- self,
60
- agent: Agent,
61
- ):
62
- """Initialize a DocumentQueryStore instance."""
63
- self.agent = agent
64
- self.vector_db: VectorDB | None = None
65
-
66
- @staticmethod
67
- def normalize_uri(uri: str) -> str:
68
- """
69
- Normalize a document URI to ensure consistent lookup.
70
-
71
- Args:
72
- uri: The URI to normalize
73
-
74
- Returns:
75
- Normalized URI
76
- """
77
- # Convert to lowercase
78
- normalized = uri.strip() # uri.lower()
79
-
80
- # Parse the URL to get scheme
81
- parsed = urlparse(normalized)
82
- scheme = parsed.scheme or "file"
83
-
84
- # Normalize based on scheme
85
- if scheme == "file":
86
- path = files.fix_dev_path(
87
- normalized.removeprefix("file://").removeprefix("file:")
88
- )
89
- normalized = f"file://{path}"
90
-
91
- elif scheme in ["http", "https"]:
92
- # Always use https for web URLs
93
- normalized = normalized.replace("http://", "https://")
94
-
95
- return normalized
96
-
97
- def init_vector_db(self):
98
- return VectorDB(self.agent, cache=True)
99
-
100
- async def add_document(
101
- self, text: str, document_uri: str, metadata: dict | None = None
102
- ) -> tuple[bool, list[str]]:
103
- """
104
- Add a document to the store with the given URI.
105
-
106
- Args:
107
- text: The document text content
108
- document_uri: The URI that uniquely identifies this document
109
- metadata: Optional metadata for the document
110
-
111
- Returns:
112
- True if successful, False otherwise
113
- """
114
- # Normalize the URI
115
- document_uri = self.normalize_uri(document_uri)
116
-
117
- # Delete existing document if it exists to avoid duplicates
118
- await self.delete_document(document_uri)
119
-
120
- # Initialize metadata
121
- doc_metadata = metadata or {}
122
- doc_metadata["document_uri"] = document_uri
123
- doc_metadata["timestamp"] = Localization.get().now_iso(timespec="seconds")
124
-
125
- # Split text into chunks
126
- text_splitter = RecursiveCharacterTextSplitter(
127
- chunk_size=self.DEFAULT_CHUNK_SIZE, chunk_overlap=self.DEFAULT_CHUNK_OVERLAP
128
- )
129
- chunks = text_splitter.split_text(text)
130
-
131
- # Create documents
132
- docs = []
133
- for i, chunk in enumerate(chunks):
134
- chunk_metadata = doc_metadata.copy()
135
- chunk_metadata["chunk_index"] = i
136
- chunk_metadata["total_chunks"] = len(chunks)
137
- docs.append(Document(page_content=chunk, metadata=chunk_metadata))
138
-
139
- if not docs:
140
- PrintStyle.error(f"No chunks created for document: {document_uri}")
141
- return False, []
142
-
143
- try:
144
- # Initialize vector db if not already initialized
145
- if not self.vector_db:
146
- self.vector_db = self.init_vector_db()
147
-
148
- ids = await self.vector_db.insert_documents(docs)
149
- PrintStyle.standard(
150
- f"Added document '{document_uri}' with {len(docs)} chunks"
151
- )
152
- return True, ids
153
- except Exception as e:
154
- err_text = errors.format_error(e)
155
- PrintStyle.error(f"Error adding document '{document_uri}': {err_text}")
156
- return False, []
157
-
158
- async def get_document(self, document_uri: str) -> Optional[Document]:
159
- """
160
- Retrieve a document by its URI.
161
-
162
- Args:
163
- document_uri: The URI of the document to retrieve
164
-
165
- Returns:
166
- The complete document if found, None otherwise
167
- """
168
-
169
- # DB not initialized, no documents inside
170
- if not self.vector_db:
171
- return None
172
-
173
- # Normalize the URI
174
- document_uri = self.normalize_uri(document_uri)
175
-
176
- # Get all chunks for this document
177
- docs = await self._get_document_chunks(document_uri)
178
- if not docs:
179
- PrintStyle.error(f"Document not found: {document_uri}")
180
- return None
181
-
182
- # Combine chunks into a single document
183
- chunks = sorted(docs, key=lambda x: x.metadata.get("chunk_index", 0))
184
- full_content = "\n".join(chunk.page_content for chunk in chunks)
185
-
186
- # Use metadata from first chunk
187
- metadata = chunks[0].metadata.copy()
188
- metadata.pop("chunk_index", None)
189
- metadata.pop("total_chunks", None)
190
-
191
- return Document(page_content=full_content, metadata=metadata)
192
-
193
- async def _get_document_chunks(self, document_uri: str) -> List[Document]:
194
- """
195
- Get all chunks for a document.
196
-
197
- Args:
198
- document_uri: The URI of the document
199
-
200
- Returns:
201
- List of document chunks
202
- """
203
-
204
- # DB not initialized, no documents inside
205
- if not self.vector_db:
206
- return []
207
-
208
- # Normalize the URI
209
- document_uri = self.normalize_uri(document_uri)
210
-
211
- # get docs from vector db
212
-
213
- chunks = await self.vector_db.search_by_metadata(
214
- filter=f"document_uri == '{document_uri}'",
215
- )
216
-
217
- PrintStyle.standard(f"Found {len(chunks)} chunks for document: {document_uri}")
218
- return chunks
219
-
220
- async def document_exists(self, document_uri: str) -> bool:
221
- """
222
- Check if a document exists in the store.
223
-
224
- Args:
225
- document_uri: The URI of the document to check
226
-
227
- Returns:
228
- True if the document exists, False otherwise
229
- """
230
-
231
- # DB not initialized, no documents inside
232
- if not self.vector_db:
233
- return False
234
-
235
- # Normalize the URI
236
- document_uri = self.normalize_uri(document_uri)
237
-
238
- chunks = await self._get_document_chunks(document_uri)
239
- return len(chunks) > 0
240
-
241
- async def delete_document(self, document_uri: str) -> bool:
242
- """
243
- Delete a document from the store.
244
-
245
- Args:
246
- document_uri: The URI of the document to delete
247
-
248
- Returns:
249
- True if deleted, False if not found
250
- """
251
-
252
- # DB not initialized, no documents inside
253
- if not self.vector_db:
254
- return False
255
-
256
- # Normalize the URI
257
- document_uri = self.normalize_uri(document_uri)
258
-
259
- chunks = await self.vector_db.search_by_metadata(
260
- filter=f"document_uri == '{document_uri}'",
261
- )
262
- if not chunks:
263
- return False
264
-
265
- # Collect IDs to delete
266
- ids_to_delete = [chunk.metadata["id"] for chunk in chunks]
267
-
268
- # Delete from vector store
269
- if ids_to_delete:
270
- dels = await self.vector_db.delete_documents_by_ids(ids_to_delete)
271
- PrintStyle.standard(
272
- f"Deleted document '{document_uri}' with {len(dels)} chunks"
273
- )
274
- return True
275
-
276
- return False
277
-
278
- async def search_documents(
279
- self, query: str, limit: int = 10, threshold: float = 0.5, filter: str = ""
280
- ) -> List[Document]:
281
- """
282
- Search for documents similar to the query across the entire store.
283
-
284
- Args:
285
- query: The search query string
286
- limit: Maximum number of results to return
287
- threshold: Minimum similarity score threshold (0-1)
288
-
289
- Returns:
290
- List of matching documents
291
- """
292
-
293
- # DB not initialized, no documents inside
294
- if not self.vector_db:
295
- return []
296
-
297
- # Handle empty query
298
- if not query:
299
- return []
300
-
301
- # Perform search
302
- try:
303
- results = await self.vector_db.search_by_similarity_threshold(
304
- query=query, limit=limit, threshold=threshold, filter=filter
305
- )
306
-
307
- PrintStyle.standard(f"Search '{query}' returned {len(results)} results")
308
- return results
309
- except Exception as e:
310
- PrintStyle.error(f"Error searching documents: {str(e)}")
311
- return []
312
-
313
- async def search_document(
314
- self, document_uri: str, query: str, limit: int = 10, threshold: float = 0.5
315
- ) -> List[Document]:
316
- """
317
- Search for content within a specific document.
318
-
319
- Args:
320
- document_uri: The URI of the document to search within
321
- query: The search query string
322
- limit: Maximum number of results to return
323
- threshold: Minimum similarity score threshold (0-1)
324
-
325
- Returns:
326
- List of matching document chunks
327
- """
328
- return await self.search_documents(
329
- query, limit, threshold, f"document_uri == '{document_uri}'"
330
- )
331
-
332
- async def list_documents(self) -> List[str]:
333
- """
334
- Get a list of all document URIs in the store.
335
-
336
- Returns:
337
- List of document URIs
338
- """
339
- # DB not initialized, no documents inside
340
- if not self.vector_db:
341
- return []
342
-
343
- # Extract unique URIs
344
- uris = set()
345
- for doc in self.vector_db.db.get_all_docs().values():
346
- if isinstance(doc.metadata, dict):
347
- uri = doc.metadata.get("document_uri")
348
- if uri:
349
- uris.add(uri)
350
-
351
- return sorted(list(uris))
352
-
353
-
354
-class DocumentQueryHelper:
355
-
356
- def __init__(
357
- self, agent: Agent, progress_callback: Callable[[str], None] | None = None
358
- ):
359
- self.agent = agent
360
- self.store = DocumentQueryStore.get(agent)
361
- self.progress_callback = progress_callback or (lambda x: None)
362
- self.store_lock = asyncio.Lock()
363
-
364
- async def document_qa(
365
- self, document_uris: List[str], questions: Sequence[str]
366
- ) -> Tuple[bool, str]:
367
- self.progress_callback(
368
- f"Starting Q&A process for {len(document_uris)} documents"
369
- )
370
- await self.agent.handle_intervention()
371
-
372
- # index documents
373
- document_contents = await asyncio.gather(
374
- *[self.document_get_content(uri, True) for uri in document_uris]
375
- )
376
- await self.agent.handle_intervention()
377
- selected_chunks = {}
378
- for question in questions:
379
- self.progress_callback(f"Optimizing query: {question}")
380
- await self.agent.handle_intervention()
381
- human_content = f'Search Query: "{question}"'
382
- system_content = self.agent.parse_prompt(
383
- "fw.document_query.optmimize_query.md"
384
- )
385
-
386
- optimized_query = (
387
- await self.agent.call_utility_model(
388
- system=system_content, message=human_content
389
- )
390
- ).strip()
391
-
392
- await self.agent.handle_intervention()
393
- self.progress_callback(f"Searching documents with query: {optimized_query}")
394
-
395
- normalized_uris = [self.store.normalize_uri(uri) for uri in document_uris]
396
- doc_filter = " or ".join(
397
- [f"document_uri == '{uri}'" for uri in normalized_uris]
398
- )
399
-
400
- chunks = await self.store.search_documents(
401
- query=optimized_query,
402
- limit=100,
403
- threshold=DEFAULT_SEARCH_THRESHOLD,
404
- filter=doc_filter,
405
- )
406
-
407
- self.progress_callback(f"Found {len(chunks)} chunks")
408
-
409
- for chunk in chunks:
410
- selected_chunks[chunk.metadata["id"]] = chunk
411
-
412
- if not selected_chunks:
413
- content = self._small_document_fallback_content(
414
- document_uris, document_contents
415
- )
416
- if not content:
417
- self.progress_callback("No relevant content found in the documents")
418
- content = f"!!! No content found for documents: {json.dumps(document_uris)} matching queries: {json.dumps(questions)}"
419
- return False, content
420
- self.progress_callback(
421
- "No matching chunks found; using complete small-document content"
422
- )
423
- else:
424
- content = "\n\n----\n\n".join(
425
- [chunk.page_content for chunk in selected_chunks.values()]
426
- )
427
-
428
- self.progress_callback(
429
- f"Processing {len(questions)} questions in document context"
430
- )
431
- await self.agent.handle_intervention()
432
-
433
- questions_str = "\n".join([f" * {question}" for question in questions])
434
-
435
- qa_system_message = self.agent.parse_prompt(
436
- "fw.document_query.system_prompt.md"
437
- )
438
- qa_user_message = f"# Document:\n{content}\n\n# Queries:\n{questions_str}"
439
-
440
- ai_response, _reasoning = await self.agent.call_chat_model(
441
- messages=[
442
- SystemMessage(content=qa_system_message),
443
- HumanMessage(content=qa_user_message),
444
- ],
445
- explicit_caching=False,
446
- )
447
-
448
- self.progress_callback(f"Q&A process completed")
449
-
450
- return True, str(ai_response)
451
-
452
- @staticmethod
453
- def _small_document_fallback_content(
454
- document_uris: Sequence[str], document_contents: Sequence[str]
455
- ) -> str:
456
- total_chars = 0
457
- sections = []
458
-
459
- for uri, content in zip(document_uris, document_contents):
460
- if not isinstance(content, str) or not content.strip():
461
- continue
462
- total_chars += len(content)
463
- if total_chars > SMALL_DOCUMENT_QA_FALLBACK_CHARS:
464
- return ""
465
- sections.append(f"## {uri}\n\n{content.strip()}")
466
-
467
- return "\n\n----\n\n".join(sections)
468
-
469
- async def document_get_content(
470
- self, document_uri: str, add_to_db: bool = False
471
- ) -> str:
472
- self.progress_callback(f"Fetching document content")
473
- await self.agent.handle_intervention()
474
- url = urlparse(document_uri)
475
- scheme = url.scheme or "file"
476
- mimetype, encoding = mimetypes.guess_type(document_uri)
477
- mimetype = mimetype or "application/octet-stream"
478
- remote_resource: HttpFetchResult | None = None
479
-
480
- if scheme in ["http", "https"]:
481
- remote_resource = await asyncio.to_thread(
482
- fetch_public_http_resource,
483
- document_uri,
484
- max_bytes=MAX_REMOTE_DOCUMENT_BYTES,
485
- )
486
- if (
487
- remote_resource.content_type
488
- and remote_resource.content_type != "application/octet-stream"
489
- ):
490
- mimetype = remote_resource.content_type
491
-
492
- if scheme == "file":
493
- try:
494
- document_uri = files.fix_dev_path(url.path)
495
- except Exception as e:
496
- raise ValueError(f"Invalid document path '{url.path}'") from e
497
-
498
- if encoding:
499
- raise ValueError(
500
- f"Compressed documents are unsupported '{encoding}' ({document_uri})"
501
- )
502
-
503
- if mimetype == "application/octet-stream":
504
- raise ValueError(
505
- f"Unsupported document mimetype '{mimetype}' ({document_uri})"
506
- )
507
-
508
- # Use the store's normalization method
509
- document_uri_norm = self.store.normalize_uri(document_uri)
510
-
511
- await self.agent.handle_intervention()
512
- exists = await self.store.document_exists(document_uri_norm)
513
- document_content = ""
514
- if not exists:
515
- await self.agent.handle_intervention()
516
- if mimetype.startswith("image/"):
517
- document_content = self.handle_image_document(
518
- document_uri, scheme, remote_resource=remote_resource
519
- )
520
- elif mimetype == "text/html":
521
- document_content = self.handle_html_document(
522
- document_uri, scheme, remote_resource=remote_resource
523
- )
524
- elif mimetype.startswith("text/") or mimetype == "application/json":
525
- document_content = self.handle_text_document(
526
- document_uri, scheme, remote_resource=remote_resource
527
- )
528
- elif mimetype == "application/pdf":
529
- document_content = self.handle_pdf_document(
530
- document_uri, scheme, remote_resource=remote_resource
531
- )
532
- else:
533
- document_content = self.handle_unstructured_document(
534
- document_uri, scheme, remote_resource=remote_resource
535
- )
536
- if add_to_db:
537
- self.progress_callback(f"Indexing document")
538
- await self.agent.handle_intervention()
539
- async with self.store_lock:
540
- success, ids = await self.store.add_document(
541
- document_content, document_uri_norm
542
- )
543
- if not success:
544
- self.progress_callback(f"Failed to index document")
545
- raise ValueError(
546
- f"DocumentQueryHelper::document_get_content: Failed to index document: {document_uri_norm}"
547
- )
548
- self.progress_callback(f"Indexed {len(ids)} chunks")
549
- else:
550
- await self.agent.handle_intervention()
551
- doc = await self.store.get_document(document_uri_norm)
552
- if doc:
553
- document_content = doc.page_content
554
- else:
555
- raise ValueError(
556
- f"DocumentQueryHelper::document_get_content: Document not found: {document_uri_norm}"
557
- )
558
- return document_content
559
-
560
- @staticmethod
561
- def _decode_remote_text(remote_resource: HttpFetchResult) -> str:
562
- encoding = remote_resource.encoding or "utf-8"
563
- try:
564
- return remote_resource.content.decode(encoding)
565
- except (LookupError, UnicodeDecodeError):
566
- return remote_resource.content.decode("utf-8", errors="replace")
567
-
568
- @staticmethod
569
- def _get_temp_file_suffix(
570
- document: str, remote_resource: HttpFetchResult | None = None
571
- ) -> str:
572
- parsed = urlparse(document)
573
- _stem, ext = os.path.splitext(parsed.path or document)
574
- if ext:
575
- return ext
576
-
577
- if remote_resource and remote_resource.content_type:
578
- guessed_ext = mimetypes.guess_extension(
579
- remote_resource.content_type, strict=False
580
- )
581
- if guessed_ext:
582
- return guessed_ext
583
-
584
- return ".bin"
585
-
586
- def handle_image_document(
587
- self,
588
- document: str,
589
- scheme: str,
590
- remote_resource: HttpFetchResult | None = None,
591
- ) -> str:
592
- return self.handle_unstructured_document(
593
- document, scheme, remote_resource=remote_resource
594
- )
595
-
596
- def handle_html_document(
597
- self,
598
- document: str,
599
- scheme: str,
600
- remote_resource: HttpFetchResult | None = None,
601
- ) -> str:
602
- if scheme in ["http", "https"]:
603
- if remote_resource is None:
604
- raise ValueError("Missing prefetched remote HTML content")
605
- html_content = self._decode_remote_text(remote_resource)
606
- parts = [Document(page_content=html_content, metadata={"source": document})]
607
- elif scheme == "file":
608
- # Use RFC file operations instead of TextLoader
609
- file_content_bytes = files.read_file_bin(document)
610
- file_content = file_content_bytes.decode("utf-8")
611
- # Create Document manually since we're not using TextLoader
612
- parts = [Document(page_content=file_content, metadata={"source": document})]
613
- else:
614
- raise ValueError(f"Unsupported scheme: {scheme}")
615
-
616
- return "\n".join(
617
- [
618
- element.page_content
619
- for element in MarkdownifyTransformer().transform_documents(parts)
620
- ]
621
- )
622
-
623
- def handle_text_document(
624
- self,
625
- document: str,
626
- scheme: str,
627
- remote_resource: HttpFetchResult | None = None,
628
- ) -> str:
629
- if scheme in ["http", "https"]:
630
- if remote_resource is None:
631
- raise ValueError("Missing prefetched remote text content")
632
- file_content = self._decode_remote_text(remote_resource)
633
- elements = [
634
- Document(page_content=file_content, metadata={"source": document})
635
- ]
636
- elif scheme == "file":
637
- # Use RFC file operations instead of TextLoader
638
- file_content_bytes = files.read_file_bin(document)
639
- file_content = file_content_bytes.decode("utf-8")
640
- # Create Document manually since we're not using TextLoader
641
- elements = [
642
- Document(page_content=file_content, metadata={"source": document})
643
- ]
644
- else:
645
- raise ValueError(f"Unsupported scheme: {scheme}")
646
-
647
- return "\n".join([element.page_content for element in elements])
648
-
649
- def handle_pdf_document(
650
- self,
651
- document: str,
652
- scheme: str,
653
- remote_resource: HttpFetchResult | None = None,
654
- ) -> str:
655
- temp_file_path = ""
656
- if scheme == "file":
657
- # Use RFC file operations to read the PDF file as binary
658
- file_content_bytes = files.read_file_bin(document)
659
- # Create a temporary file for PyMuPDFLoader since it needs a file path
660
- import tempfile
661
-
662
- with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
663
- temp_file.write(file_content_bytes)
664
- temp_file_path = temp_file.name
665
- elif scheme in ["http", "https"]:
666
- import tempfile
667
-
668
- if remote_resource is None:
669
- raise ValueError("Missing prefetched remote PDF content")
670
- with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
671
- temp_file.write(remote_resource.content)
672
- temp_file_path = temp_file.name
673
- else:
674
- raise ValueError(f"Unsupported scheme: {scheme}")
675
-
676
- if not os.path.exists(temp_file_path):
677
- raise ValueError(
678
- f"DocumentQueryHelper::handle_pdf_document: Temporary file not found: {temp_file_path}"
679
- )
680
-
681
- try:
682
- try:
683
- loader = PyMuPDFLoader(
684
- temp_file_path,
685
- mode="single",
686
- extract_tables="markdown",
687
- extract_images=True,
688
- images_inner_format="text",
689
- images_parser=TesseractBlobParser(),
690
- pages_delimiter="\n",
691
- )
692
- elements: list[Document] = loader.load()
693
- contents = "\n".join([element.page_content for element in elements])
694
- except Exception as e:
695
- PrintStyle.error(
696
- f"DocumentQueryHelper::handle_pdf_document: Error loading with PyMuPDF: {e}"
697
- )
698
- contents = ""
699
-
700
- if not contents:
701
- import pdf2image
702
- import pytesseract
703
-
704
- PrintStyle.debug(
705
- f"DocumentQueryHelper::handle_pdf_document: FALLBACK Converting PDF to images: {temp_file_path}"
706
- )
707
-
708
- # Convert PDF to images
709
- pages = pdf2image.convert_from_path(temp_file_path) # type: ignore
710
- for page in pages:
711
- contents += pytesseract.image_to_string(page) + "\n\n"
712
-
713
- return contents
714
- finally:
715
- os.unlink(temp_file_path)
716
-
717
- def handle_unstructured_document(
718
- self,
719
- document: str,
720
- scheme: str,
721
- remote_resource: HttpFetchResult | None = None,
722
- ) -> str:
723
- elements: list[Document] = []
724
- if scheme in ["http", "https"]:
725
- if remote_resource is None:
726
- raise ValueError("Missing prefetched remote document content")
727
- import tempfile
728
-
729
- temp_file_path = ""
730
- suffix = self._get_temp_file_suffix(document, remote_resource)
731
- with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
732
- temp_file.write(remote_resource.content)
733
- temp_file_path = temp_file.name
734
-
735
- try:
736
- loader = UnstructuredLoader(
737
- file_path=temp_file_path,
738
- mode="single",
739
- partition_via_api=False,
740
- # chunking_strategy="by_page",
741
- strategy="hi_res",
742
- )
743
- elements = loader.load()
744
- finally:
745
- os.unlink(temp_file_path)
746
- elif scheme == "file":
747
- # Use RFC file operations to read the file as binary
748
- file_content_bytes = files.read_file_bin(document)
749
- # Create a temporary file for UnstructuredLoader since it needs a file path
750
- import tempfile
751
- import os
752
-
753
- # Get file extension to preserve it for proper processing
754
- _, ext = os.path.splitext(document)
755
- with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as temp_file:
756
- temp_file.write(file_content_bytes)
757
- temp_file_path = temp_file.name
758
-
759
- try:
760
- loader = UnstructuredLoader(
761
- file_path=temp_file_path,
762
- mode="single",
763
- partition_via_api=False,
764
- # chunking_strategy="by_page",
765
- strategy="hi_res",
766
- )
767
- elements = loader.load()
768
- finally:
769
- # Clean up temporary file
770
- os.unlink(temp_file_path)
771
- else:
772
- raise ValueError(f"Unsupported scheme: {scheme}")
773
-
774
- return "\n".join([element.page_content for element in elements])