rag tool finalizing
frdel committed
Jun 15, 2025 at 22:00 UTC
9c8703cd7c7f97221e7420863e0dcc32eeac0ed5
4 files changed
+255
-316
python/api/message.py
+1
-1
@@ -31,7 +31,7 @@ class Message(ApiHandler):
31
attachment_paths = []
32
33
upload_folder_int = "/a0/tmp/uploads"
34
- upload_folder_ext = files.get_abs_path("tmp/uploads")
34
+ upload_folder_ext = files.get_abs_path("tmp/uploads") # for development environment
35
36
if attachments:
37
os.makedirs(upload_folder_ext, exist_ok=True)
python/helpers/document_query.py
+193
-298
@@ -4,6 +4,8 @@ import asyncio
4
import aiohttp
5
import json
6
7
+from python.helpers.vector_db import VectorDB
8
+
9
os.environ["USER_AGENT"] = "@mixedbread-ai/unstructured" # noqa E402
10
from langchain_unstructured import UnstructuredLoader # noqa E402
11
@@ -32,15 +34,14 @@ from langchain_community.vectorstores.utils import (
34
from langchain_core.embeddings import Embeddings
35
36
from python.helpers.print_style import PrintStyle
35
-from python.helpers import files #, rfc_files
36
-rfc_files = files # TODO: fix
37
-
37
+from python.helpers import files
38
from agent import Agent
39
-import models
39
40
from langchain.text_splitter import RecursiveCharacterTextSplitter
41
42
43
+DEFAULT_SEARCH_THRESHOLD = 0.6
44
+
45
class DocumentQueryStore:
46
"""
47
FAISS Store for document query results.
@@ -55,107 +56,25 @@ class DocumentQueryStore:
56
_stores: dict[str, "DocumentQueryStore"] = {}
57
58
@staticmethod
58
- async def get(agent: Agent):
59
- """Get or create a DocumentQueryStore instance for the specified agent."""
59
+ def get(agent: Agent):
60
+ """Create a DocumentQueryStore instance for the specified agent."""
61
if not agent or not agent.config:
62
raise ValueError("Agent and agent config must be provided")
63
63
- memory_subdir = agent.config.memory_subdir or "default"
64
- store_key = f"{memory_subdir}/document_query"
65
-
66
- if store_key not in DocumentQueryStore._stores:
67
- # Initialize embeddings model from agent config
68
- embeddings_model = agent.get_embedding_model()
69
-
70
- # Initialize store
71
- store = DocumentQueryStore(agent, embeddings_model, memory_subdir)
72
- DocumentQueryStore._stores[store_key] = store
73
- return store
74
- else:
75
- return DocumentQueryStore._stores[store_key]
76
-
77
- @staticmethod
78
- async def reload(agent: Agent):
79
- """Reload the DocumentQueryStore for the specified agent."""
80
- memory_subdir = agent.config.memory_subdir or "default"
81
- store_key = f"{memory_subdir}/document_query"
82
-
83
- if store_key in DocumentQueryStore._stores:
84
- del DocumentQueryStore._stores[store_key]
85
-
86
- return await DocumentQueryStore.get(agent)
64
+ # Initialize store
65
+ store = DocumentQueryStore(agent)
66
+ return store
67
68
def __init__(
69
self,
70
agent: Agent,
91
- embeddings_model: Embeddings,
92
- memory_subdir: str,
71
):
72
"""Initialize a DocumentQueryStore instance."""
73
self.agent = agent
96
- self.memory_subdir = memory_subdir
97
-
98
- # Get directory paths
99
- db_dir = self._get_db_dir()
100
- em_dir = os.path.join(db_dir, "embeddings")
101
-
102
- # Create directories
103
- os.makedirs(db_dir, exist_ok=True)
104
- os.makedirs(em_dir, exist_ok=True)
105
-
106
- # Setup embeddings cache
107
- store = LocalFileStore(em_dir)
108
- self.embeddings = CacheBackedEmbeddings.from_bytes_store(
109
- embeddings_model,
110
- store,
111
- namespace=f"document_query_{getattr(embeddings_model, 'model', getattr(embeddings_model, 'model_name', 'default'))}"
112
- )
113
-
114
- # Initialize vector store
115
- index_path = os.path.join(db_dir, "index.faiss")
116
- docstore_path = os.path.join(db_dir, "docstore.json")
117
-
118
- if os.path.exists(index_path) and os.path.exists(docstore_path):
119
- PrintStyle.standard(f"Loading existing vector store from {db_dir}")
120
- try:
121
- self.vectorstore = FAISS.load_local(
122
- folder_path=db_dir,
123
- embeddings=self.embeddings,
124
- allow_dangerous_deserialization=True,
125
- distance_strategy=DistanceStrategy.COSINE,
126
- )
127
- except Exception as e:
128
- PrintStyle.error(f"Error loading vector store: {str(e)}")
129
- self._initialize_new_vectorstore()
130
- else:
131
- PrintStyle.standard(f"Creating new vector store in '{db_dir}'")
132
- self._initialize_new_vectorstore()
133
-
134
- def _initialize_new_vectorstore(self):
135
- """Initialize a new vector store."""
136
- dimension = len(self.embeddings.embed_query("test"))
137
- index = faiss.IndexFlatIP(dimension)
138
- self.vectorstore = FAISS(
139
- embedding_function=self.embeddings,
140
- index=index,
141
- docstore=InMemoryDocstore(),
142
- index_to_docstore_id={},
143
- distance_strategy=DistanceStrategy.COSINE,
144
- )
145
-
146
- def _get_db_dir(self) -> str:
147
- """Get the absolute path to the database directory."""
148
- return files.get_abs_path("memory", self.memory_subdir, "document_query")
149
-
150
- def _save_vectorstore(self):
151
- """Save the vector store to disk."""
152
- db_dir = self._get_db_dir()
153
- PrintStyle.standard(f"Saving vector store to {db_dir}")
154
- self.vectorstore.save_local(folder_path=db_dir)
155
- PrintStyle.standard(f"Vector store saved with {len(self.vectorstore.index_to_docstore_id)} documents")
74
+ self.vector_db: VectorDB | None = None
75
76
@staticmethod
158
- def _normalize_uri(uri: str) -> str:
77
+ def normalize_uri(uri: str) -> str:
78
"""
79
Normalize a document URI to ensure consistent lookup.
80
@@ -166,7 +85,7 @@ class DocumentQueryStore:
85
Normalized URI
86
"""
87
# Convert to lowercase
169
- normalized = uri.lower()
88
+ normalized = uri.strip() # uri.lower()
89
90
# Parse the URL to get scheme
91
parsed = urlparse(normalized)
@@ -174,17 +93,23 @@ class DocumentQueryStore:
93
94
# Normalize based on scheme
95
if scheme == "file":
177
- if not normalized.startswith("file:"):
178
- normalized = "file:" + normalized
179
- if normalized.startswith("file://"):
180
- normalized = normalized.replace("file://", "file:")
96
+ path = files.fix_dev_path(
97
+ normalized.removeprefix("file://").removeprefix("file:")
98
+ )
99
+ normalized = f"file://{path}"
100
+
101
elif scheme in ["http", "https"]:
102
# Always use https for web URLs
183
- normalized = normalized.replace("http://", "https://")
103
+ normalized = normalized.replace("http://", "https://") # TODO why?
104
105
return normalized
106
187
- async def add_document(self, text: str, document_uri: str, metadata: dict | None = None) -> bool:
107
+ def init_vector_db(self):
108
+ return VectorDB(self.agent)
109
+
110
+ async def add_document(
111
+ self, text: str, document_uri: str, metadata: dict | None = None
112
+ ) -> bool:
113
"""
114
Add a document to the store with the given URI.
115
@@ -197,7 +122,7 @@ class DocumentQueryStore:
122
True if successful, False otherwise
123
"""
124
# Normalize the URI
200
- document_uri = self._normalize_uri(document_uri)
125
+ document_uri = self.normalize_uri(document_uri)
126
127
# Delete existing document if it exists to avoid duplicates
128
await self.delete_document(document_uri)
@@ -209,8 +134,7 @@ class DocumentQueryStore:
134
135
# Split text into chunks
136
text_splitter = RecursiveCharacterTextSplitter(
212
- chunk_size=self.DEFAULT_CHUNK_SIZE,
213
- chunk_overlap=self.DEFAULT_CHUNK_OVERLAP
137
+ chunk_size=self.DEFAULT_CHUNK_SIZE, chunk_overlap=self.DEFAULT_CHUNK_OVERLAP
138
)
139
chunks = text_splitter.split_text(text)
140
@@ -230,14 +154,17 @@ class DocumentQueryStore:
154
try:
155
docs_text = "".join(chunk.page_content for chunk in docs)
156
await self.agent.rate_limiter(
233
- model_config=self.agent.config.embeddings_model,
234
- input=docs_text
157
+ model_config=self.agent.config.embeddings_model, input=docs_text
158
)
159
237
- # Add documents to vector store
238
- self.vectorstore.add_documents(documents=docs)
239
- self._save_vectorstore()
240
- PrintStyle.standard(f"Added document '{document_uri}' with {len(docs)} chunks")
160
+ # Initialize vector db if not already initialized
161
+ if not self.vector_db:
162
+ self.vector_db = self.init_vector_db()
163
+
164
+ ids = await self.vector_db.insert_documents(docs)
165
+ PrintStyle.standard(
166
+ f"Added document '{document_uri}' with {len(docs)} chunks"
167
+ )
168
return True
169
except Exception as e:
170
PrintStyle.error(f"Error adding document '{document_uri}': {str(e)}")
@@ -253,8 +180,13 @@ class DocumentQueryStore:
180
Returns:
181
The complete document if found, None otherwise
182
"""
183
+
184
+ # DB not initialized, no documents inside
185
+ if not self.vector_db:
186
+ return None
187
+
188
# Normalize the URI
257
- document_uri = self._normalize_uri(document_uri)
189
+ document_uri = self.normalize_uri(document_uri)
190
191
# Get all chunks for this document
192
docs = await self._get_document_chunks(document_uri)
@@ -283,14 +215,19 @@ class DocumentQueryStore:
215
Returns:
216
List of document chunks
217
"""
218
+
219
+ # DB not initialized, no documents inside
220
+ if not self.vector_db:
221
+ return []
222
+
223
# Normalize the URI
287
- document_uri = self._normalize_uri(document_uri)
224
+ document_uri = self.normalize_uri(document_uri)
225
289
- # Access docstore directly
290
- chunks = []
291
- for doc_id, doc in self.vectorstore.docstore._dict.items(): # type: ignore
292
- if isinstance(doc.metadata, dict) and doc.metadata.get("document_uri") == document_uri:
293
- chunks.append(doc)
226
+ # get docs from vector db
227
+
228
+ chunks = await self.vector_db.search_by_metadata(
229
+ filter=f"document_uri == '{document_uri}'",
230
+ )
231
232
PrintStyle.standard(f"Found {len(chunks)} chunks for document: {document_uri}")
233
return chunks
@@ -305,8 +242,13 @@ class DocumentQueryStore:
242
Returns:
243
True if the document exists, False otherwise
244
"""
245
+
246
+ # DB not initialized, no documents inside
247
+ if not self.vector_db:
248
+ return False
249
+
250
# Normalize the URI
309
- document_uri = self._normalize_uri(document_uri)
251
+ document_uri = self.normalize_uri(document_uri)
252
253
chunks = await self._get_document_chunks(document_uri)
254
return len(chunks) > 0
@@ -321,84 +263,36 @@ class DocumentQueryStore:
263
Returns:
264
True if deleted, False if not found
265
"""
266
+
267
+ # DB not initialized, no documents inside
268
+ if not self.vector_db:
269
+ return False
270
+
271
# Normalize the URI
325
- document_uri = self._normalize_uri(document_uri)
272
+ document_uri = self.normalize_uri(document_uri)
273
327
- chunks = await self._get_document_chunks(document_uri)
274
+ chunks = await self.vector_db.search_by_metadata(
275
+ filter=f"document_uri == '{document_uri}'",
276
+ )
277
if not chunks:
278
return False
279
280
# Collect IDs to delete
332
- ids_to_delete = []
333
- for chunk in chunks:
334
- for doc_id, doc_ref in self.vectorstore.docstore._dict.items(): # type: ignore
335
- if doc_ref == chunk:
336
- ids_to_delete.append(doc_id)
281
+ ids_to_delete = [chunk.metadata["id"] for chunk in chunks]
282
283
# Delete from vector store
284
if ids_to_delete:
340
- self.vectorstore.delete(ids_to_delete)
341
- self._save_vectorstore()
342
- PrintStyle.standard(f"Deleted document '{document_uri}' with {len(ids_to_delete)} chunks")
285
+ dels = await self.vector_db.delete_documents_by_ids(ids_to_delete)
286
+ PrintStyle.standard(
287
+ f"Deleted document '{document_uri}' with {len(dels)} chunks"
288
+ )
289
return True
290
291
return False
292
347
- async def expire_documents(self, older_than_days: float) -> int:
348
- """
349
- Delete documents older than the specified number of days.
350
-
351
- Args:
352
- older_than_days: Number of days (can be fractional) before current time
353
-
354
- Returns:
355
- Number of documents deleted
356
- """
357
- if older_than_days <= 0:
358
- return 0
359
-
360
- # Calculate cutoff timestamp
361
- cutoff_date = datetime.now().timestamp() - (older_than_days * 24 * 60 * 60)
362
-
363
- # Find expired documents
364
- expired_uris = set()
365
-
366
- # Check all documents in the store
367
- for doc_id, doc in self.vectorstore.docstore._dict.items(): # type: ignore
368
- if not isinstance(doc.metadata, dict):
369
- continue
370
-
371
- # Only process each document once (first chunk)
372
- if doc.metadata.get("chunk_index", 0) != 0:
373
- continue
374
-
375
- doc_uri = doc.metadata.get("document_uri")
376
- if not doc_uri:
377
- continue
378
-
379
- try:
380
- # Check timestamp
381
- timestamp_str = doc.metadata.get("timestamp")
382
- if not timestamp_str:
383
- continue
384
-
385
- doc_timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S").timestamp()
386
- if doc_timestamp < cutoff_date:
387
- expired_uris.add(doc_uri)
388
- except (ValueError, TypeError):
389
- # Skip documents with invalid timestamps
390
- continue
391
-
392
- # Delete expired documents
393
- deleted_count = 0
394
- for uri in expired_uris:
395
- if await self.delete_document(uri):
396
- deleted_count += 1
397
-
398
- PrintStyle.standard(f"Expired {deleted_count} documents older than {older_than_days} days")
399
- return deleted_count
400
-
401
- async def search_documents(self, query: str, limit: int = 10, threshold: float = 0.5) -> List[Document]:
293
+ async def search_documents(
294
+ self, query: str, limit: int = 10, threshold: float = 0.5, filter: str = ""
295
+ ) -> List[Document]:
296
"""
297
Search for documents similar to the query across the entire store.
298
@@ -410,34 +304,30 @@ class DocumentQueryStore:
304
Returns:
305
List of matching documents
306
"""
307
+
308
+ # DB not initialized, no documents inside
309
+ if not self.vector_db:
310
+ return []
311
+
312
# Handle empty query
313
if not query:
415
- PrintStyle.standard("Empty search query, returning empty results")
314
return []
315
418
- # Apply rate limiter
419
- await self.agent.rate_limiter(
420
- model_config=self.agent.config.embeddings_model,
421
- input=query
422
- )
423
-
316
# Perform search
317
try:
426
- results = self.vectorstore.similarity_search_with_score(
427
- query=query,
428
- k=limit,
429
- score_threshold=threshold
318
+ results = await self.vector_db.search_by_similarity_threshold(
319
+ query=query, limit=limit, threshold=threshold, filter=filter
320
)
321
432
- # Extract documents from results (which are (doc, score) pairs)
433
- docs = [doc for doc, score in results]
434
- PrintStyle.standard(f"Search '{query}' returned {len(docs)} results")
435
- return docs
322
+ PrintStyle.standard(f"Search '{query}' returned {len(results)} results")
323
+ return results
324
except Exception as e:
325
PrintStyle.error(f"Error searching documents: {str(e)}")
326
return []
327
440
- async def search_document(self, document_uri: str, query: str, limit: int = 10, threshold: float = 0.5) -> List[Document]:
328
+ async def search_document(
329
+ self, document_uri: str, query: str, limit: int = 10, threshold: float = 0.5
330
+ ) -> List[Document]:
331
"""
332
Search for content within a specific document.
333
@@ -450,59 +340,10 @@ class DocumentQueryStore:
340
Returns:
341
List of matching document chunks
342
"""
453
- # Normalize the URI
454
- document_uri = self._normalize_uri(document_uri)
455
-
456
- # Handle empty query
457
- if not query:
458
- PrintStyle.standard("Empty search query, returning empty results")
459
- return []
460
-
461
- # Check if document exists
462
- if not await self.document_exists(document_uri):
463
- PrintStyle.error(f"Document not found: {document_uri}")
464
- return []
465
-
466
- # Apply rate limiter
467
- await self.agent.rate_limiter(
468
- model_config=self.agent.config.embeddings_model,
469
- input=query
343
+ return await self.search_documents(
344
+ query, limit, threshold, f"document_uri == '{document_uri}'"
345
)
346
472
- # Perform search with document filter
473
- try:
474
- # Create metadata filter function
475
- def filter_fn(doc_metadata):
476
- return doc_metadata.get("document_uri") == document_uri
477
-
478
- results = self.vectorstore.similarity_search_with_score(
479
- query=query,
480
- k=limit,
481
- score_threshold=threshold,
482
- filter=filter_fn
483
- )
484
-
485
- # Extract documents from results
486
- docs = [doc for doc, score in results]
487
- PrintStyle.standard(f"Search '{query}' in document '{document_uri}' returned {len(docs)} results")
488
-
489
- # Try with lower threshold if no results
490
- if not docs and threshold > 0.3:
491
- PrintStyle.standard("No results found, trying with lower threshold (0.3)")
492
- results = self.vectorstore.similarity_search_with_score(
493
- query=query,
494
- k=limit,
495
- score_threshold=0.3,
496
- filter=filter_fn
497
- )
498
- docs = [doc for doc, score in results]
499
- PrintStyle.standard(f"Retry search returned {len(docs)} results")
500
-
501
- return docs
502
- except Exception as e:
503
- PrintStyle.error(f"Error searching within document: {str(e)}")
504
- return []
505
-
347
async def list_documents(self) -> List[str]:
348
"""
349
Get a list of all document URIs in the store.
@@ -510,9 +351,13 @@ class DocumentQueryStore:
351
Returns:
352
List of document URIs
353
"""
354
+ # DB not initialized, no documents inside
355
+ if not self.vector_db:
356
+ return []
357
+
358
# Extract unique URIs
359
uris = set()
515
- for doc in self.vectorstore.docstore._dict.values(): # type: ignore
360
+ for doc in self.vector_db.db.get_all_docs().values():
361
if isinstance(doc.metadata, dict):
362
uri = doc.metadata.get("document_uri")
363
if uri:
@@ -525,27 +370,37 @@ class DocumentQueryHelper:
370
371
def __init__(self, agent: Agent):
372
self.agent = agent
528
- self.store: DocumentQueryStore = asyncio.run(DocumentQueryStore.get(agent))
373
+ self.store = DocumentQueryStore.get(agent)
374
530
- async def document_qa(self, document_uri: str, questions: Sequence[str]) -> Tuple[bool, str]:
531
- _ = await self.document_get_content(document_uri)
375
+ async def document_qa(
376
+ self, document_uri: str, questions: Sequence[str]
377
+ ) -> Tuple[bool, str]:
378
+ # index document
379
+ _ = await self.document_get_content(document_uri, True)
380
content = ""
381
for question in questions:
382
human_content = f'Search Query: "{question}"'
535
- system_content = self.agent.parse_prompt("fw.document_query.optmimize_query.md")
536
-
537
- optimized_query = await self.agent.call_utility_model(
538
- system=system_content,
539
- message=human_content
383
+ system_content = self.agent.parse_prompt(
384
+ "fw.document_query.optmimize_query.md"
385
)
386
387
+ optimized_query = (
388
+ await self.agent.call_utility_model(
389
+ system=system_content, message=human_content
390
+ )
391
+ ).strip()
392
+
393
+ normalized_uri = self.store.normalize_uri(document_uri)
394
chunks = await self.store.search_document(
543
- document_uri=document_uri,
544
- query=str(optimized_query),
545
- limit=10000,
546
- threshold=0.66
395
+ document_uri=normalized_uri,
396
+ query=optimized_query,
397
+ limit=100,
398
+ threshold=DEFAULT_SEARCH_THRESHOLD,
399
+ )
400
+ content += (
401
+ "\n\n----\n\n".join([chunk.page_content for chunk in chunks])
402
+ + "\n\n----\n\n"
403
)
548
- content += "\n\n----\n\n".join([chunk.page_content for chunk in chunks]) + "\n\n----\n\n"
404
405
if not content:
406
content = f"!!! No content found for document: {document_uri} matching queries: {json.dumps(questions)}"
@@ -553,19 +408,23 @@ class DocumentQueryHelper:
408
409
questions_str = "\n".join([f" * {question}" for question in questions])
410
556
- qa_system_message = self.agent.parse_prompt("fw.document_query.system_prompt.md")
411
+ qa_system_message = self.agent.parse_prompt(
412
+ "fw.document_query.system_prompt.md"
413
+ )
414
qa_user_message = f"# Document:\n{content}\n\n# Queries:\n{questions_str}"
415
416
ai_response = await self.agent.call_chat_model(
560
- prompt=ChatPromptTemplate.from_messages([
561
- SystemMessage(content=qa_system_message),
562
- HumanMessage(content=qa_user_message),
563
- ])
417
+ prompt=ChatPromptTemplate.from_messages(
418
+ [
419
+ SystemMessage(content=qa_system_message),
420
+ HumanMessage(content=qa_user_message),
421
+ ]
422
+ )
423
)
424
425
return True, str(ai_response)
426
568
- async def document_get_content(self, document_uri: str) -> str:
427
+ async def document_get_content(self, document_uri: str, add_to_db: bool = False) -> str:
428
url = urlparse(document_uri)
429
scheme = url.scheme or "file"
430
mimetype, encoding = mimetypes.guess_type(document_uri)
@@ -579,7 +438,11 @@ class DocumentQueryHelper:
438
while not response and retries < 3:
439
try:
440
async with aiohttp.ClientSession() as session:
582
- response = await session.head(document_uri, timeout=aiohttp.ClientTimeout(total=2.0), allow_redirects=True)
441
+ response = await session.head(
442
+ document_uri,
443
+ timeout=aiohttp.ClientTimeout(total=2.0),
444
+ allow_redirects=True,
445
+ )
446
if response.status > 399:
447
raise Exception(response.status)
448
break
@@ -589,32 +452,41 @@ class DocumentQueryHelper:
452
retries += 1
453
454
if not response:
592
- raise ValueError(f"DocumentQueryHelper::document_get_content: Document fetch error: {document_uri} ({last_error})")
455
+ raise ValueError(
456
+ f"DocumentQueryHelper::document_get_content: Document fetch error: {document_uri} ({last_error})"
457
+ )
458
459
mimetype = response.headers["content-type"]
460
if "content-length" in response.headers:
596
- content_length = float(response.headers["content-length"]) / 1024 / 1024 # MB
461
+ content_length = (
462
+ float(response.headers["content-length"]) / 1024 / 1024
463
+ ) # MB
464
if content_length > 50.0:
598
- raise ValueError(f"Document content length exceeds max. 50MB: {content_length} MB ({document_uri})")
599
- if mimetype and '; charset=' in mimetype:
600
- mimetype = mimetype.split('; charset=')[0]
465
+ raise ValueError(
466
+ f"Document content length exceeds max. 50MB: {content_length} MB ({document_uri})"
467
+ )
468
+ if mimetype and "; charset=" in mimetype:
469
+ mimetype = mimetype.split("; charset=")[0]
470
471
if scheme == "file":
472
try:
604
- document_uri = os.path.abspath(url.path)
473
+ document_uri = files.fix_dev_path(url.path)
474
except Exception as e:
475
raise ValueError(f"Invalid document path '{url.path}'") from e
476
477
if encoding:
609
- raise ValueError(f"Compressed documents are unsupported '{encoding}' ({document_uri})")
478
+ raise ValueError(
479
+ f"Compressed documents are unsupported '{encoding}' ({document_uri})"
480
+ )
481
482
if mimetype == "application/octet-stream":
612
- raise ValueError(f"Unsupported document mimetype '{mimetype}' ({document_uri})")
483
+ raise ValueError(
484
+ f"Unsupported document mimetype '{mimetype}' ({document_uri})"
485
+ )
486
487
# Use the store's normalization method
615
- document_uri_norm = self.store._normalize_uri(document_uri)
488
+ document_uri_norm = self.store.normalize_uri(document_uri)
489
617
- await self.store.expire_documents(7)
490
exists = await self.store.document_exists(document_uri_norm)
491
document_content = ""
492
if not exists:
@@ -627,14 +499,19 @@ class DocumentQueryHelper:
499
elif mimetype == "application/pdf":
500
document_content = self.handle_pdf_document(document_uri, scheme)
501
else:
630
- document_content = self.handle_unstructured_document(document_uri, scheme)
631
- await self.store.add_document(document_content, document_uri_norm)
502
+ document_content = self.handle_unstructured_document(
503
+ document_uri, scheme
504
+ )
505
+ if add_to_db:
506
+ await self.store.add_document(document_content, document_uri_norm)
507
else:
508
doc = await self.store.get_document(document_uri_norm)
509
if doc:
510
document_content = doc.page_content
511
else:
637
- raise ValueError(f"DocumentQueryHelper::document_get_content: Document not found: {document_uri_norm}")
512
+ raise ValueError(
513
+ f"DocumentQueryHelper::document_get_content: Document not found: {document_uri_norm}"
514
+ )
515
return document_content
516
517
def handle_image_document(self, document: str, scheme: str) -> str:
@@ -646,14 +523,19 @@ class DocumentQueryHelper:
523
parts: list[Document] = loader.load()
524
elif scheme == "file":
525
# Use RFC file operations instead of TextLoader
649
- file_content_bytes = rfc_files.read_file_bin(document)
650
- file_content = file_content_bytes.decode('utf-8')
526
+ file_content_bytes = files.read_file_bin(document)
527
+ file_content = file_content_bytes.decode("utf-8")
528
# Create Document manually since we're not using TextLoader
529
parts = [Document(page_content=file_content, metadata={"source": document})]
530
else:
531
raise ValueError(f"Unsupported scheme: {scheme}")
532
656
- return "\n".join([element.page_content for element in MarkdownifyTransformer().transform_documents(parts)])
533
+ return "\n".join(
534
+ [
535
+ element.page_content
536
+ for element in MarkdownifyTransformer().transform_documents(parts)
537
+ ]
538
+ )
539
540
def handle_text_document(self, document: str, scheme: str) -> str:
541
if scheme in ["http", "https"]:
@@ -661,10 +543,12 @@ class DocumentQueryHelper:
543
elements: list[Document] = loader.load()
544
elif scheme == "file":
545
# Use RFC file operations instead of TextLoader
664
- file_content_bytes = rfc_files.read_file_bin(document)
665
- file_content = file_content_bytes.decode('utf-8')
546
+ file_content_bytes = files.read_file_bin(document)
547
+ file_content = file_content_bytes.decode("utf-8")
548
# Create Document manually since we're not using TextLoader
667
- elements = [Document(page_content=file_content, metadata={"source": document})]
549
+ elements = [
550
+ Document(page_content=file_content, metadata={"source": document})
551
+ ]
552
else:
553
raise ValueError(f"Unsupported scheme: {scheme}")
554
@@ -674,27 +558,33 @@ class DocumentQueryHelper:
558
temp_file_path = ""
559
if scheme == "file":
560
# Use RFC file operations to read the PDF file as binary
677
- file_content_bytes = rfc_files.read_file_bin(document)
561
+ file_content_bytes = files.read_file_bin(document)
562
# Create a temporary file for PyMuPDFLoader since it needs a file path
563
import tempfile
680
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
564
+
565
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
566
temp_file.write(file_content_bytes)
567
temp_file_path = temp_file.name
568
elif scheme in ["http", "https"]:
569
# download the file from the web url to a temporary file using python libraries for downloading
570
import requests
571
import tempfile
687
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
572
+
573
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
574
response = requests.get(document, timeout=10.0)
575
if response.status_code != 200:
690
- raise ValueError(f"DocumentQueryHelper::handle_pdf_document: Failed to download PDF from {document}: {response.status_code}")
576
+ raise ValueError(
577
+ f"DocumentQueryHelper::handle_pdf_document: Failed to download PDF from {document}: {response.status_code}"
578
+ )
579
temp_file.write(response.content)
580
temp_file_path = temp_file.name
581
else:
582
raise ValueError(f"Unsupported scheme: {scheme}")
583
584
if not os.path.exists(temp_file_path):
697
- raise ValueError(f"DocumentQueryHelper::handle_pdf_document: Temporary file not found: {temp_file_path}")
585
+ raise ValueError(
586
+ f"DocumentQueryHelper::handle_pdf_document: Temporary file not found: {temp_file_path}"
587
+ )
588
589
try:
590
try:
@@ -710,17 +600,21 @@ class DocumentQueryHelper:
600
elements: list[Document] = loader.load()
601
contents = "\n".join([element.page_content for element in elements])
602
except Exception as e:
713
- PrintStyle.error(f"DocumentQueryHelper::handle_pdf_document: Error loading with PyMuPDF: {e}")
603
+ PrintStyle.error(
604
+ f"DocumentQueryHelper::handle_pdf_document: Error loading with PyMuPDF: {e}"
605
+ )
606
contents = ""
607
608
if not contents:
609
import pdf2image
610
import pytesseract
611
720
- PrintStyle.debug(f"DocumentQueryHelper::handle_pdf_document: FALLBACK Converting PDF to images: {temp_file_path}")
612
+ PrintStyle.debug(
613
+ f"DocumentQueryHelper::handle_pdf_document: FALLBACK Converting PDF to images: {temp_file_path}"
614
+ )
615
616
# Convert PDF to images
723
- pages = pdf2image.convert_from_path(temp_file_path) # type: ignore
617
+ pages = pdf2image.convert_from_path(temp_file_path) # type: ignore
618
for page in pages:
619
contents += pytesseract.image_to_string(page) + "\n\n"
620
@@ -742,10 +636,11 @@ class DocumentQueryHelper:
636
elements = loader.load()
637
elif scheme == "file":
638
# Use RFC file operations to read the file as binary
745
- file_content_bytes = rfc_files.read_file_bin(document)
639
+ file_content_bytes = files.read_file_bin(document)
640
# Create a temporary file for UnstructuredLoader since it needs a file path
641
import tempfile
642
import os
643
+
644
# Get file extension to preserve it for proper processing
645
_, ext = os.path.splitext(document)
646
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as temp_file:
python/helpers/files.py
+8
@@ -253,8 +253,16 @@ def make_dirs(relative_path: str):
253
254
255
def get_abs_path(*relative_paths):
256
+ "Convert relative paths to absolute paths based on the base directory."
257
return os.path.join(get_base_dir(), *relative_paths)
258
259
+def fix_dev_path(path:str):
260
+ "On dev environment, convert /a0/... paths to local absolute paths"
261
+ from python.helpers.runtime import is_development
262
+ if is_development():
263
+ if path.startswith("/a0/"):
264
+ path = path.replace("/a0/", "")
265
+ return get_abs_path(path)
266
267
def exists(*relative_paths):
268
path = get_abs_path(*relative_paths)
python/helpers/vector_db.py
+53
-17
@@ -27,27 +27,40 @@ class MyFaiss(FAISS):
27
async def aget_by_ids(self, ids: Sequence[str], /) -> List[Document]:
28
return self.get_by_ids(ids)
29
30
+ def get_all_docs(self) -> dict[str, Document]:
31
+ return self.docstore._dict # type: ignore
32
+
33
34
class VectorDB:
32
- def __init__(self, agent: Agent):
33
- self.agent = agent
34
- self.store = InMemoryByteStore()
35
- self.model = agent.get_embedding_model()
36
-
37
- self.embedder = CacheBackedEmbeddings.from_bytes_store(
38
- self.model,
39
- self.store,
40
- namespace=getattr(
41
- self.model,
42
- "model",
43
- getattr(self.model, "model_name", "default"),
44
- ),
35
+
36
+ _cached_embeddings: dict[str, CacheBackedEmbeddings] = {}
37
+
38
+ @staticmethod
39
+ def _get_embeddings(agent: Agent):
40
+ model = agent.get_embedding_model()
41
+ namespace = getattr(
42
+ model,
43
+ "model",
44
+ getattr(model, "model_name", "default"),
45
)
46
+ if namespace not in VectorDB._cached_embeddings:
47
+ store = InMemoryByteStore()
48
+ VectorDB._cached_embeddings[namespace] = (
49
+ CacheBackedEmbeddings.from_bytes_store(
50
+ model,
51
+ store,
52
+ namespace=namespace,
53
+ )
54
+ )
55
+ return VectorDB._cached_embeddings[namespace]
56
47
- self.index = faiss.IndexFlatIP(len(self.embedder.embed_query("example")))
57
+ def __init__(self, agent: Agent):
58
+ self.agent = agent
59
+ self.embeddings = self._get_embeddings(agent)
60
+ self.index = faiss.IndexFlatIP(len(self.embeddings.embed_query("example")))
61
62
self.db = MyFaiss(
50
- embedding_function=self.embedder,
63
+ embedding_function=self.embeddings,
64
index=self.index,
65
docstore=InMemoryDocstore(),
66
index_to_docstore_id={},
@@ -56,7 +69,7 @@ class VectorDB:
69
relevance_score_fn=cosine_normalizer,
70
)
71
59
- async def search_similarity_threshold(
72
+ async def search_by_similarity_threshold(
73
self, query: str, limit: int, threshold: float, filter: str = ""
74
):
75
comparator = get_comparator(filter) if filter else None
@@ -74,6 +87,18 @@ class VectorDB:
87
filter=comparator,
88
)
89
90
+ async def search_by_metadata(self, filter: str, limit: int = 0) -> list[Document]:
91
+ comparator = get_comparator(filter)
92
+ all_docs = self.db.get_all_docs()
93
+ result = []
94
+ for doc in all_docs.values():
95
+ if comparator(doc.metadata):
96
+ result.append(doc)
97
+ # stop if limit reached and limit > 0
98
+ if limit > 0 and len(result) >= limit:
99
+ break
100
+ return result
101
+
102
async def insert_documents(self, docs: list[Document]):
103
ids = [str(uuid.uuid4()) for _ in range(len(docs))]
104
@@ -90,6 +115,16 @@ class VectorDB:
115
self.db.add_documents(documents=docs, ids=ids)
116
return ids
117
118
+ async def delete_documents_by_ids(self, ids: list[str]):
119
+ # aget_by_ids is not yet implemented in faiss, need to do a workaround
120
+ rem_docs = await self.db.aget_by_ids(
121
+ ids
122
+ ) # existing docs to remove (prevents error)
123
+ if rem_docs:
124
+ rem_ids = [doc.metadata["id"] for doc in rem_docs] # ids to remove
125
+ await self.db.adelete(ids=rem_ids)
126
+ return rem_docs
127
+
128
129
def format_docs_plain(docs: list[Document]) -> list[str]:
130
result = []
@@ -113,7 +148,8 @@ def cosine_normalizer(val: float) -> float:
148
def get_comparator(condition: str):
149
def comparator(data: dict[str, Any]):
150
try:
116
- return eval(condition, {}, data)
151
+ result = eval(condition, {}, data)
152
+ return result
153
except Exception as e:
154
# PrintStyle.error(f"Error evaluating condition: {e}")
155
return False