haiku.rag 0.9.2__py3-none-any.whl → 0.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. README.md +205 -0
  2. haiku_rag-0.14.0.dist-info/METADATA +227 -0
  3. haiku_rag-0.14.0.dist-info/RECORD +6 -0
  4. haiku/rag/__init__.py +0 -0
  5. haiku/rag/app.py +0 -267
  6. haiku/rag/chunker.py +0 -51
  7. haiku/rag/cli.py +0 -359
  8. haiku/rag/client.py +0 -565
  9. haiku/rag/config.py +0 -77
  10. haiku/rag/embeddings/__init__.py +0 -35
  11. haiku/rag/embeddings/base.py +0 -15
  12. haiku/rag/embeddings/ollama.py +0 -17
  13. haiku/rag/embeddings/openai.py +0 -16
  14. haiku/rag/embeddings/vllm.py +0 -19
  15. haiku/rag/embeddings/voyageai.py +0 -17
  16. haiku/rag/logging.py +0 -56
  17. haiku/rag/mcp.py +0 -144
  18. haiku/rag/migration.py +0 -316
  19. haiku/rag/monitor.py +0 -73
  20. haiku/rag/qa/__init__.py +0 -15
  21. haiku/rag/qa/agent.py +0 -89
  22. haiku/rag/qa/prompts.py +0 -60
  23. haiku/rag/reader.py +0 -115
  24. haiku/rag/reranking/__init__.py +0 -34
  25. haiku/rag/reranking/base.py +0 -13
  26. haiku/rag/reranking/cohere.py +0 -34
  27. haiku/rag/reranking/mxbai.py +0 -28
  28. haiku/rag/reranking/vllm.py +0 -44
  29. haiku/rag/research/__init__.py +0 -37
  30. haiku/rag/research/base.py +0 -130
  31. haiku/rag/research/dependencies.py +0 -45
  32. haiku/rag/research/evaluation_agent.py +0 -42
  33. haiku/rag/research/orchestrator.py +0 -300
  34. haiku/rag/research/presearch_agent.py +0 -34
  35. haiku/rag/research/prompts.py +0 -129
  36. haiku/rag/research/search_agent.py +0 -65
  37. haiku/rag/research/synthesis_agent.py +0 -40
  38. haiku/rag/store/__init__.py +0 -4
  39. haiku/rag/store/engine.py +0 -230
  40. haiku/rag/store/models/__init__.py +0 -4
  41. haiku/rag/store/models/chunk.py +0 -15
  42. haiku/rag/store/models/document.py +0 -16
  43. haiku/rag/store/repositories/__init__.py +0 -9
  44. haiku/rag/store/repositories/chunk.py +0 -399
  45. haiku/rag/store/repositories/document.py +0 -234
  46. haiku/rag/store/repositories/settings.py +0 -148
  47. haiku/rag/store/upgrades/__init__.py +0 -1
  48. haiku/rag/utils.py +0 -162
  49. haiku_rag-0.9.2.dist-info/METADATA +0 -131
  50. haiku_rag-0.9.2.dist-info/RECORD +0 -50
  51. {haiku_rag-0.9.2.dist-info → haiku_rag-0.14.0.dist-info}/WHEEL +0 -0
  52. {haiku_rag-0.9.2.dist-info → haiku_rag-0.14.0.dist-info}/entry_points.txt +0 -0
  53. {haiku_rag-0.9.2.dist-info → haiku_rag-0.14.0.dist-info}/licenses/LICENSE +0 -0
haiku/rag/client.py DELETED
@@ -1,565 +0,0 @@
1
- import hashlib
2
- import mimetypes
3
- import tempfile
4
- from collections.abc import AsyncGenerator
5
- from pathlib import Path
6
- from urllib.parse import urlparse
7
-
8
- import httpx
9
-
10
- from haiku.rag.config import Config
11
- from haiku.rag.reader import FileReader
12
- from haiku.rag.reranking import get_reranker
13
- from haiku.rag.store.engine import Store
14
- from haiku.rag.store.models.chunk import Chunk
15
- from haiku.rag.store.models.document import Document
16
- from haiku.rag.store.repositories.chunk import ChunkRepository
17
- from haiku.rag.store.repositories.document import DocumentRepository
18
- from haiku.rag.store.repositories.settings import SettingsRepository
19
- from haiku.rag.utils import text_to_docling_document
20
-
21
-
22
- class HaikuRAG:
23
- """High-level haiku-rag client."""
24
-
25
- def __init__(
26
- self,
27
- db_path: Path = Config.DEFAULT_DATA_DIR / "haiku.rag.lancedb",
28
- skip_validation: bool = False,
29
- ):
30
- """Initialize the RAG client with a database path.
31
-
32
- Args:
33
- db_path: Path to the database file.
34
- skip_validation: Whether to skip configuration validation on database load.
35
- """
36
- if not db_path.parent.exists():
37
- Path.mkdir(db_path.parent, parents=True)
38
- self.store = Store(db_path, skip_validation=skip_validation)
39
- self.document_repository = DocumentRepository(self.store)
40
- self.chunk_repository = ChunkRepository(self.store)
41
-
42
- async def __aenter__(self):
43
- """Async context manager entry."""
44
- return self
45
-
46
- async def __aexit__(self, exc_type, exc_val, exc_tb): # noqa: ARG002
47
- """Async context manager exit."""
48
- self.close()
49
- return False
50
-
51
- async def _create_document_with_docling(
52
- self,
53
- docling_document,
54
- uri: str | None = None,
55
- metadata: dict | None = None,
56
- chunks: list[Chunk] | None = None,
57
- ) -> Document:
58
- """Create a new document from DoclingDocument."""
59
- content = docling_document.export_to_markdown()
60
- document = Document(
61
- content=content,
62
- uri=uri,
63
- metadata=metadata or {},
64
- )
65
- return await self.document_repository._create_with_docling(
66
- document, docling_document, chunks
67
- )
68
-
69
- async def create_document(
70
- self,
71
- content: str,
72
- uri: str | None = None,
73
- metadata: dict | None = None,
74
- chunks: list[Chunk] | None = None,
75
- ) -> Document:
76
- """Create a new document with optional URI and metadata.
77
-
78
- Args:
79
- content: The text content of the document.
80
- uri: Optional URI identifier for the document.
81
- metadata: Optional metadata dictionary.
82
- chunks: Optional list of pre-created chunks to use instead of generating new ones.
83
-
84
- Returns:
85
- The created Document instance.
86
- """
87
- # Convert content to DoclingDocument for processing
88
- docling_document = text_to_docling_document(content)
89
-
90
- document = Document(
91
- content=content,
92
- uri=uri,
93
- metadata=metadata or {},
94
- )
95
- return await self.document_repository._create_with_docling(
96
- document, docling_document, chunks
97
- )
98
-
99
- async def create_document_from_source(
100
- self, source: str | Path, metadata: dict = {}
101
- ) -> Document:
102
- """Create or update a document from a file path or URL.
103
-
104
- Checks if a document with the same URI already exists:
105
- - If MD5 is unchanged, returns existing document
106
- - If MD5 changed, updates the document
107
- - If no document exists, creates a new one
108
-
109
- Args:
110
- source: File path (as string or Path) or URL to parse
111
- metadata: Optional metadata dictionary
112
-
113
- Returns:
114
- Document instance (created, updated, or existing)
115
-
116
- Raises:
117
- ValueError: If the file/URL cannot be parsed or doesn't exist
118
- httpx.RequestError: If URL request fails
119
- """
120
-
121
- # Check if it's a URL
122
- source_str = str(source)
123
- parsed_url = urlparse(source_str)
124
- if parsed_url.scheme in ("http", "https"):
125
- return await self._create_or_update_document_from_url(source_str, metadata)
126
- elif parsed_url.scheme == "file":
127
- # Handle file:// URI by converting to path
128
- source_path = Path(parsed_url.path)
129
- else:
130
- # Handle as regular file path
131
- source_path = Path(source) if isinstance(source, str) else source
132
- if source_path.suffix.lower() not in FileReader.extensions:
133
- raise ValueError(f"Unsupported file extension: {source_path.suffix}")
134
-
135
- if not source_path.exists():
136
- raise ValueError(f"File does not exist: {source_path}")
137
-
138
- uri = source_path.absolute().as_uri()
139
- md5_hash = hashlib.md5(source_path.read_bytes()).hexdigest()
140
-
141
- # Check if document already exists
142
- existing_doc = await self.get_document_by_uri(uri)
143
- if existing_doc and existing_doc.metadata.get("md5") == md5_hash:
144
- # MD5 unchanged, return existing document
145
- return existing_doc
146
-
147
- docling_document = FileReader.parse_file(source_path)
148
-
149
- # Get content type from file extension
150
- content_type, _ = mimetypes.guess_type(str(source_path))
151
- if not content_type:
152
- content_type = "application/octet-stream"
153
-
154
- # Merge metadata with contentType and md5
155
- metadata.update({"contentType": content_type, "md5": md5_hash})
156
-
157
- if existing_doc:
158
- # Update existing document
159
- existing_doc.content = docling_document.export_to_markdown()
160
- existing_doc.metadata = metadata
161
- return await self.document_repository._update_with_docling(
162
- existing_doc, docling_document
163
- )
164
- else:
165
- # Create new document using DoclingDocument
166
- return await self._create_document_with_docling(
167
- docling_document=docling_document, uri=uri, metadata=metadata
168
- )
169
-
170
- async def _create_or_update_document_from_url(
171
- self, url: str, metadata: dict = {}
172
- ) -> Document:
173
- """Create or update a document from a URL by downloading and parsing the content.
174
-
175
- Checks if a document with the same URI already exists:
176
- - If MD5 is unchanged, returns existing document
177
- - If MD5 changed, updates the document
178
- - If no document exists, creates a new one
179
-
180
- Args:
181
- url: URL to download and parse
182
- metadata: Optional metadata dictionary
183
-
184
- Returns:
185
- Document instance (created, updated, or existing)
186
-
187
- Raises:
188
- ValueError: If the content cannot be parsed
189
- httpx.RequestError: If URL request fails
190
- """
191
- async with httpx.AsyncClient() as client:
192
- response = await client.get(url)
193
- response.raise_for_status()
194
-
195
- md5_hash = hashlib.md5(response.content).hexdigest()
196
-
197
- # Check if document already exists
198
- existing_doc = await self.get_document_by_uri(url)
199
- if existing_doc and existing_doc.metadata.get("md5") == md5_hash:
200
- # MD5 unchanged, return existing document
201
- return existing_doc
202
-
203
- # Get content type to determine file extension
204
- content_type = response.headers.get("content-type", "").lower()
205
- file_extension = self._get_extension_from_content_type_or_url(
206
- url, content_type
207
- )
208
-
209
- if file_extension not in FileReader.extensions:
210
- raise ValueError(
211
- f"Unsupported content type/extension: {content_type}/{file_extension}"
212
- )
213
-
214
- # Create a temporary file with the appropriate extension
215
- with tempfile.NamedTemporaryFile(
216
- mode="wb", suffix=file_extension
217
- ) as temp_file:
218
- temp_file.write(response.content)
219
- temp_file.flush() # Ensure content is written to disk
220
- temp_path = Path(temp_file.name)
221
-
222
- # Parse the content using FileReader
223
- docling_document = FileReader.parse_file(temp_path)
224
-
225
- # Merge metadata with contentType and md5
226
- metadata.update({"contentType": content_type, "md5": md5_hash})
227
-
228
- if existing_doc:
229
- existing_doc.content = docling_document.export_to_markdown()
230
- existing_doc.metadata = metadata
231
- return await self.document_repository._update_with_docling(
232
- existing_doc, docling_document
233
- )
234
- else:
235
- return await self._create_document_with_docling(
236
- docling_document=docling_document, uri=url, metadata=metadata
237
- )
238
-
239
- def _get_extension_from_content_type_or_url(
240
- self, url: str, content_type: str
241
- ) -> str:
242
- """Determine file extension from content type or URL."""
243
- # Common content type mappings
244
- content_type_map = {
245
- "text/html": ".html",
246
- "text/plain": ".txt",
247
- "text/markdown": ".md",
248
- "application/pdf": ".pdf",
249
- "application/json": ".json",
250
- "text/csv": ".csv",
251
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
252
- "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
253
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
254
- }
255
-
256
- # Try content type first
257
- for ct, ext in content_type_map.items():
258
- if ct in content_type:
259
- return ext
260
-
261
- # Try URL extension
262
- parsed_url = urlparse(url)
263
- path = Path(parsed_url.path)
264
- if path.suffix:
265
- return path.suffix.lower()
266
-
267
- # Default to .html for web content
268
- return ".html"
269
-
270
- async def get_document_by_id(self, document_id: str) -> Document | None:
271
- """Get a document by its ID.
272
-
273
- Args:
274
- document_id: The unique identifier of the document.
275
-
276
- Returns:
277
- The Document instance if found, None otherwise.
278
- """
279
- return await self.document_repository.get_by_id(document_id)
280
-
281
- async def get_document_by_uri(self, uri: str) -> Document | None:
282
- """Get a document by its URI.
283
-
284
- Args:
285
- uri: The URI identifier of the document.
286
-
287
- Returns:
288
- The Document instance if found, None otherwise.
289
- """
290
- return await self.document_repository.get_by_uri(uri)
291
-
292
- async def update_document(self, document: Document) -> Document:
293
- """Update an existing document."""
294
- # Convert content to DoclingDocument
295
- docling_document = text_to_docling_document(document.content)
296
-
297
- return await self.document_repository._update_with_docling(
298
- document, docling_document
299
- )
300
-
301
- async def delete_document(self, document_id: str) -> bool:
302
- """Delete a document by its ID."""
303
- return await self.document_repository.delete(document_id)
304
-
305
- async def list_documents(
306
- self, limit: int | None = None, offset: int | None = None
307
- ) -> list[Document]:
308
- """List all documents with optional pagination.
309
-
310
- Args:
311
- limit: Maximum number of documents to return.
312
- offset: Number of documents to skip.
313
-
314
- Returns:
315
- List of Document instances.
316
- """
317
- return await self.document_repository.list_all(limit=limit, offset=offset)
318
-
319
- async def search(
320
- self, query: str, limit: int = 5, search_type: str = "hybrid"
321
- ) -> list[tuple[Chunk, float]]:
322
- """Search for relevant chunks using the specified search method with optional reranking.
323
-
324
- Args:
325
- query: The search query string.
326
- limit: Maximum number of results to return.
327
- search_type: Type of search - "vector", "fts", or "hybrid" (default).
328
-
329
- Returns:
330
- List of (chunk, score) tuples ordered by relevance.
331
- """
332
- # Get reranker if available
333
- reranker = get_reranker()
334
-
335
- if reranker is None:
336
- # No reranking - return direct search results
337
- return await self.chunk_repository.search(query, limit, search_type)
338
-
339
- # Get more initial results (3X) for reranking
340
- search_limit = limit * 3
341
- search_results = await self.chunk_repository.search(
342
- query, search_limit, search_type
343
- )
344
-
345
- # Apply reranking
346
- chunks = [chunk for chunk, _ in search_results]
347
- reranked_results = await reranker.rerank(query, chunks, top_n=limit)
348
-
349
- # Return reranked results with scores from reranker
350
- return reranked_results
351
-
352
- async def expand_context(
353
- self,
354
- search_results: list[tuple[Chunk, float]],
355
- radius: int = Config.CONTEXT_CHUNK_RADIUS,
356
- ) -> list[tuple[Chunk, float]]:
357
- """Expand search results with adjacent chunks, merging overlapping chunks.
358
-
359
- Args:
360
- search_results: List of (chunk, score) tuples from search.
361
- radius: Number of adjacent chunks to include before/after each chunk.
362
- Defaults to CONTEXT_CHUNK_RADIUS config setting.
363
-
364
- Returns:
365
- List of (chunk, score) tuples with expanded and merged context chunks.
366
- """
367
- if radius == 0:
368
- return search_results
369
-
370
- # Group chunks by document_id to handle merging within documents
371
- document_groups = {}
372
- for chunk, score in search_results:
373
- doc_id = chunk.document_id
374
- if doc_id not in document_groups:
375
- document_groups[doc_id] = []
376
- document_groups[doc_id].append((chunk, score))
377
-
378
- results = []
379
-
380
- for doc_id, doc_chunks in document_groups.items():
381
- # Get all expanded ranges for this document
382
- expanded_ranges = []
383
- for chunk, score in doc_chunks:
384
- adjacent_chunks = await self.chunk_repository.get_adjacent_chunks(
385
- chunk, radius
386
- )
387
-
388
- all_chunks = adjacent_chunks + [chunk]
389
-
390
- # Get the range of orders for this expanded chunk
391
- orders = [c.metadata.get("order", 0) for c in all_chunks]
392
- min_order = min(orders)
393
- max_order = max(orders)
394
-
395
- expanded_ranges.append(
396
- {
397
- "original_chunk": chunk,
398
- "score": score,
399
- "min_order": min_order,
400
- "max_order": max_order,
401
- "all_chunks": sorted(
402
- all_chunks, key=lambda c: c.metadata.get("order", 0)
403
- ),
404
- }
405
- )
406
-
407
- # Merge overlapping/adjacent ranges
408
- merged_ranges = self._merge_overlapping_ranges(expanded_ranges)
409
-
410
- # Create merged chunks
411
- for merged_range in merged_ranges:
412
- combined_content_parts = [c.content for c in merged_range["all_chunks"]]
413
-
414
- # Use the first original chunk for metadata
415
- original_chunk = merged_range["original_chunks"][0]
416
-
417
- merged_chunk = Chunk(
418
- id=original_chunk.id,
419
- document_id=original_chunk.document_id,
420
- content="".join(combined_content_parts),
421
- metadata=original_chunk.metadata,
422
- document_uri=original_chunk.document_uri,
423
- document_meta=original_chunk.document_meta,
424
- )
425
-
426
- # Use the highest score from merged chunks
427
- best_score = max(merged_range["scores"])
428
- results.append((merged_chunk, best_score))
429
-
430
- return results
431
-
432
- def _merge_overlapping_ranges(self, expanded_ranges):
433
- """Merge overlapping or adjacent expanded ranges."""
434
- if not expanded_ranges:
435
- return []
436
-
437
- # Sort by min_order
438
- sorted_ranges = sorted(expanded_ranges, key=lambda x: x["min_order"])
439
- merged = []
440
-
441
- current = {
442
- "min_order": sorted_ranges[0]["min_order"],
443
- "max_order": sorted_ranges[0]["max_order"],
444
- "original_chunks": [sorted_ranges[0]["original_chunk"]],
445
- "scores": [sorted_ranges[0]["score"]],
446
- "all_chunks": sorted_ranges[0]["all_chunks"],
447
- }
448
-
449
- for range_info in sorted_ranges[1:]:
450
- # Check if ranges overlap or are adjacent (max_order + 1 >= min_order)
451
- if current["max_order"] >= range_info["min_order"] - 1:
452
- # Merge ranges
453
- current["max_order"] = max(
454
- current["max_order"], range_info["max_order"]
455
- )
456
- current["original_chunks"].append(range_info["original_chunk"])
457
- current["scores"].append(range_info["score"])
458
-
459
- # Merge all_chunks and deduplicate by order
460
- all_chunks_dict = {}
461
- for chunk in current["all_chunks"] + range_info["all_chunks"]:
462
- order = chunk.metadata.get("order", 0)
463
- all_chunks_dict[order] = chunk
464
- current["all_chunks"] = [
465
- all_chunks_dict[order] for order in sorted(all_chunks_dict.keys())
466
- ]
467
- else:
468
- # No overlap, add current to merged and start new
469
- merged.append(current)
470
- current = {
471
- "min_order": range_info["min_order"],
472
- "max_order": range_info["max_order"],
473
- "original_chunks": [range_info["original_chunk"]],
474
- "scores": [range_info["score"]],
475
- "all_chunks": range_info["all_chunks"],
476
- }
477
-
478
- # Add the last range
479
- merged.append(current)
480
- return merged
481
-
482
- async def ask(self, question: str, cite: bool = False) -> str:
483
- """Ask a question using the configured QA agent.
484
-
485
- Args:
486
- question: The question to ask.
487
- cite: Whether to include citations in the response.
488
-
489
- Returns:
490
- The generated answer as a string.
491
- """
492
- from haiku.rag.qa import get_qa_agent
493
-
494
- qa_agent = get_qa_agent(self, use_citations=cite)
495
- return await qa_agent.answer(question)
496
-
497
- async def rebuild_database(self) -> AsyncGenerator[str, None]:
498
- """Rebuild the database by deleting all chunks and re-indexing all documents.
499
-
500
- For documents with URIs:
501
- - Deletes the document and re-adds it from source if source exists
502
- - Skips documents where source no longer exists
503
-
504
- For documents without URIs:
505
- - Re-creates chunks from existing content
506
-
507
- Yields:
508
- int: The ID of the document currently being processed
509
- """
510
- await self.chunk_repository.delete_all()
511
- self.store.recreate_embeddings_table()
512
-
513
- # Update settings to current config
514
- settings_repo = SettingsRepository(self.store)
515
- settings_repo.save_current_settings()
516
-
517
- documents = await self.list_documents()
518
-
519
- for doc in documents:
520
- assert doc.id is not None, "Document ID should not be None"
521
- if doc.uri:
522
- # Document has a URI - delete and try to re-add from source
523
- try:
524
- # Delete the old document first
525
- await self.delete_document(doc.id)
526
-
527
- # Try to re-create from source (this creates the document with chunks)
528
- new_doc = await self.create_document_from_source(
529
- doc.uri, doc.metadata or {}
530
- )
531
-
532
- assert new_doc.id is not None, "New document ID should not be None"
533
- yield new_doc.id
534
-
535
- except (FileNotFoundError, ValueError, OSError) as e:
536
- # Source doesn't exist or can't be accessed - document already deleted, skip
537
- print(f"Skipping document with URI {doc.uri}: {e}")
538
- continue
539
- except Exception as e:
540
- # Unexpected error - log it and skip
541
- print(
542
- f"Unexpected error processing document with URI {doc.uri}: {e}"
543
- )
544
- continue
545
- else:
546
- # Document without URI - re-create chunks from existing content
547
- docling_document = text_to_docling_document(doc.content)
548
- await self.chunk_repository.create_chunks_for_document(
549
- doc.id, docling_document
550
- )
551
- yield doc.id
552
-
553
- # Final maintenance: centralized vacuum to curb disk usage
554
- try:
555
- self.store.vacuum()
556
- except Exception:
557
- pass
558
-
559
- async def vacuum(self) -> None:
560
- """Optimize and clean up old versions across all tables."""
561
- self.store.vacuum()
562
-
563
- def close(self):
564
- """Close the underlying store connection."""
565
- self.store.close()
haiku/rag/config.py DELETED
@@ -1,77 +0,0 @@
1
- import os
2
- from pathlib import Path
3
-
4
- from dotenv import load_dotenv
5
- from pydantic import BaseModel, field_validator
6
-
7
- from haiku.rag.utils import get_default_data_dir
8
-
9
- load_dotenv()
10
-
11
-
12
- class AppConfig(BaseModel):
13
- ENV: str = "production"
14
-
15
- LANCEDB_API_KEY: str = ""
16
- LANCEDB_URI: str = ""
17
- LANCEDB_REGION: str = ""
18
-
19
- DEFAULT_DATA_DIR: Path = get_default_data_dir()
20
- MONITOR_DIRECTORIES: list[Path] = []
21
-
22
- EMBEDDINGS_PROVIDER: str = "ollama"
23
- EMBEDDINGS_MODEL: str = "mxbai-embed-large"
24
- EMBEDDINGS_VECTOR_DIM: int = 1024
25
-
26
- RERANK_PROVIDER: str = ""
27
- RERANK_MODEL: str = ""
28
-
29
- QA_PROVIDER: str = "ollama"
30
- QA_MODEL: str = "gpt-oss"
31
-
32
- # Research defaults (fallback to QA if not provided via env)
33
- RESEARCH_PROVIDER: str = "ollama"
34
- RESEARCH_MODEL: str = "gpt-oss"
35
-
36
- CHUNK_SIZE: int = 256
37
- CONTEXT_CHUNK_RADIUS: int = 0
38
-
39
- # Optional dotted path or file path to a callable that preprocesses
40
- # markdown content before chunking. Examples:
41
- MARKDOWN_PREPROCESSOR: str = ""
42
-
43
- OLLAMA_BASE_URL: str = "http://localhost:11434"
44
-
45
- VLLM_EMBEDDINGS_BASE_URL: str = ""
46
- VLLM_RERANK_BASE_URL: str = ""
47
- VLLM_QA_BASE_URL: str = ""
48
- VLLM_RESEARCH_BASE_URL: str = ""
49
-
50
- # Provider keys
51
- VOYAGE_API_KEY: str = ""
52
- OPENAI_API_KEY: str = ""
53
- ANTHROPIC_API_KEY: str = ""
54
- COHERE_API_KEY: str = ""
55
-
56
- @field_validator("MONITOR_DIRECTORIES", mode="before")
57
- @classmethod
58
- def parse_monitor_directories(cls, v):
59
- if isinstance(v, str):
60
- if not v.strip():
61
- return []
62
- return [
63
- Path(path.strip()).absolute() for path in v.split(",") if path.strip()
64
- ]
65
- return v
66
-
67
-
68
- # Expose Config object for app to import
69
- Config = AppConfig.model_validate(os.environ)
70
- if Config.OPENAI_API_KEY:
71
- os.environ["OPENAI_API_KEY"] = Config.OPENAI_API_KEY
72
- if Config.VOYAGE_API_KEY:
73
- os.environ["VOYAGE_API_KEY"] = Config.VOYAGE_API_KEY
74
- if Config.ANTHROPIC_API_KEY:
75
- os.environ["ANTHROPIC_API_KEY"] = Config.ANTHROPIC_API_KEY
76
- if Config.COHERE_API_KEY:
77
- os.environ["CO_API_KEY"] = Config.COHERE_API_KEY
@@ -1,35 +0,0 @@
1
- from haiku.rag.config import Config
2
- from haiku.rag.embeddings.base import EmbedderBase
3
- from haiku.rag.embeddings.ollama import Embedder as OllamaEmbedder
4
-
5
-
6
- def get_embedder() -> EmbedderBase:
7
- """
8
- Factory function to get the appropriate embedder based on the configuration.
9
- """
10
-
11
- if Config.EMBEDDINGS_PROVIDER == "ollama":
12
- return OllamaEmbedder(Config.EMBEDDINGS_MODEL, Config.EMBEDDINGS_VECTOR_DIM)
13
-
14
- if Config.EMBEDDINGS_PROVIDER == "voyageai":
15
- try:
16
- from haiku.rag.embeddings.voyageai import Embedder as VoyageAIEmbedder
17
- except ImportError:
18
- raise ImportError(
19
- "VoyageAI embedder requires the 'voyageai' package. "
20
- "Please install haiku.rag with the 'voyageai' extra: "
21
- "uv pip install haiku.rag[voyageai]"
22
- )
23
- return VoyageAIEmbedder(Config.EMBEDDINGS_MODEL, Config.EMBEDDINGS_VECTOR_DIM)
24
-
25
- if Config.EMBEDDINGS_PROVIDER == "openai":
26
- from haiku.rag.embeddings.openai import Embedder as OpenAIEmbedder
27
-
28
- return OpenAIEmbedder(Config.EMBEDDINGS_MODEL, Config.EMBEDDINGS_VECTOR_DIM)
29
-
30
- if Config.EMBEDDINGS_PROVIDER == "vllm":
31
- from haiku.rag.embeddings.vllm import Embedder as VllmEmbedder
32
-
33
- return VllmEmbedder(Config.EMBEDDINGS_MODEL, Config.EMBEDDINGS_VECTOR_DIM)
34
-
35
- raise ValueError(f"Unsupported embedding provider: {Config.EMBEDDINGS_PROVIDER}")
@@ -1,15 +0,0 @@
1
- from haiku.rag.config import Config
2
-
3
-
4
- class EmbedderBase:
5
- _model: str = Config.EMBEDDINGS_MODEL
6
- _vector_dim: int = Config.EMBEDDINGS_VECTOR_DIM
7
-
8
- def __init__(self, model: str, vector_dim: int):
9
- self._model = model
10
- self._vector_dim = vector_dim
11
-
12
- async def embed(self, text: str | list[str]) -> list[float] | list[list[float]]:
13
- raise NotImplementedError(
14
- "Embedder is an abstract class. Please implement the embed method in a subclass."
15
- )