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.
- README.md +205 -0
- haiku_rag-0.14.0.dist-info/METADATA +227 -0
- haiku_rag-0.14.0.dist-info/RECORD +6 -0
- haiku/rag/__init__.py +0 -0
- haiku/rag/app.py +0 -267
- haiku/rag/chunker.py +0 -51
- haiku/rag/cli.py +0 -359
- haiku/rag/client.py +0 -565
- haiku/rag/config.py +0 -77
- haiku/rag/embeddings/__init__.py +0 -35
- haiku/rag/embeddings/base.py +0 -15
- haiku/rag/embeddings/ollama.py +0 -17
- haiku/rag/embeddings/openai.py +0 -16
- haiku/rag/embeddings/vllm.py +0 -19
- haiku/rag/embeddings/voyageai.py +0 -17
- haiku/rag/logging.py +0 -56
- haiku/rag/mcp.py +0 -144
- haiku/rag/migration.py +0 -316
- haiku/rag/monitor.py +0 -73
- haiku/rag/qa/__init__.py +0 -15
- haiku/rag/qa/agent.py +0 -89
- haiku/rag/qa/prompts.py +0 -60
- haiku/rag/reader.py +0 -115
- haiku/rag/reranking/__init__.py +0 -34
- haiku/rag/reranking/base.py +0 -13
- haiku/rag/reranking/cohere.py +0 -34
- haiku/rag/reranking/mxbai.py +0 -28
- haiku/rag/reranking/vllm.py +0 -44
- haiku/rag/research/__init__.py +0 -37
- haiku/rag/research/base.py +0 -130
- haiku/rag/research/dependencies.py +0 -45
- haiku/rag/research/evaluation_agent.py +0 -42
- haiku/rag/research/orchestrator.py +0 -300
- haiku/rag/research/presearch_agent.py +0 -34
- haiku/rag/research/prompts.py +0 -129
- haiku/rag/research/search_agent.py +0 -65
- haiku/rag/research/synthesis_agent.py +0 -40
- haiku/rag/store/__init__.py +0 -4
- haiku/rag/store/engine.py +0 -230
- haiku/rag/store/models/__init__.py +0 -4
- haiku/rag/store/models/chunk.py +0 -15
- haiku/rag/store/models/document.py +0 -16
- haiku/rag/store/repositories/__init__.py +0 -9
- haiku/rag/store/repositories/chunk.py +0 -399
- haiku/rag/store/repositories/document.py +0 -234
- haiku/rag/store/repositories/settings.py +0 -148
- haiku/rag/store/upgrades/__init__.py +0 -1
- haiku/rag/utils.py +0 -162
- haiku_rag-0.9.2.dist-info/METADATA +0 -131
- haiku_rag-0.9.2.dist-info/RECORD +0 -50
- {haiku_rag-0.9.2.dist-info → haiku_rag-0.14.0.dist-info}/WHEEL +0 -0
- {haiku_rag-0.9.2.dist-info → haiku_rag-0.14.0.dist-info}/entry_points.txt +0 -0
- {haiku_rag-0.9.2.dist-info → haiku_rag-0.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
from uuid import uuid4
|
|
5
|
-
|
|
6
|
-
from docling_core.types.doc.document import DoclingDocument
|
|
7
|
-
|
|
8
|
-
from haiku.rag.store.engine import DocumentRecord, Store
|
|
9
|
-
from haiku.rag.store.models.document import Document
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from haiku.rag.store.models.chunk import Chunk
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class DocumentRepository:
|
|
16
|
-
"""Repository for Document operations."""
|
|
17
|
-
|
|
18
|
-
def __init__(self, store: Store) -> None:
|
|
19
|
-
self.store = store
|
|
20
|
-
self._chunk_repository = None
|
|
21
|
-
|
|
22
|
-
@property
|
|
23
|
-
def chunk_repository(self):
|
|
24
|
-
"""Lazy-load ChunkRepository when needed."""
|
|
25
|
-
if self._chunk_repository is None:
|
|
26
|
-
from haiku.rag.store.repositories.chunk import ChunkRepository
|
|
27
|
-
|
|
28
|
-
self._chunk_repository = ChunkRepository(self.store)
|
|
29
|
-
return self._chunk_repository
|
|
30
|
-
|
|
31
|
-
def _record_to_document(self, record: DocumentRecord) -> Document:
|
|
32
|
-
"""Convert a DocumentRecord to a Document model."""
|
|
33
|
-
return Document(
|
|
34
|
-
id=record.id,
|
|
35
|
-
content=record.content,
|
|
36
|
-
uri=record.uri,
|
|
37
|
-
metadata=json.loads(record.metadata) if record.metadata else {},
|
|
38
|
-
created_at=datetime.fromisoformat(record.created_at)
|
|
39
|
-
if record.created_at
|
|
40
|
-
else datetime.now(),
|
|
41
|
-
updated_at=datetime.fromisoformat(record.updated_at)
|
|
42
|
-
if record.updated_at
|
|
43
|
-
else datetime.now(),
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
async def create(self, entity: Document) -> Document:
|
|
47
|
-
"""Create a document in the database."""
|
|
48
|
-
# Generate new UUID
|
|
49
|
-
doc_id = str(uuid4())
|
|
50
|
-
|
|
51
|
-
# Create timestamp
|
|
52
|
-
now = datetime.now().isoformat()
|
|
53
|
-
|
|
54
|
-
# Create document record
|
|
55
|
-
doc_record = DocumentRecord(
|
|
56
|
-
id=doc_id,
|
|
57
|
-
content=entity.content,
|
|
58
|
-
uri=entity.uri,
|
|
59
|
-
metadata=json.dumps(entity.metadata),
|
|
60
|
-
created_at=now,
|
|
61
|
-
updated_at=now,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
# Add to table
|
|
65
|
-
self.store.documents_table.add([doc_record])
|
|
66
|
-
|
|
67
|
-
entity.id = doc_id
|
|
68
|
-
entity.created_at = datetime.fromisoformat(now)
|
|
69
|
-
entity.updated_at = datetime.fromisoformat(now)
|
|
70
|
-
return entity
|
|
71
|
-
|
|
72
|
-
async def get_by_id(self, entity_id: str) -> Document | None:
|
|
73
|
-
"""Get a document by its ID."""
|
|
74
|
-
results = list(
|
|
75
|
-
self.store.documents_table.search()
|
|
76
|
-
.where(f"id = '{entity_id}'")
|
|
77
|
-
.limit(1)
|
|
78
|
-
.to_pydantic(DocumentRecord)
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
if not results:
|
|
82
|
-
return None
|
|
83
|
-
|
|
84
|
-
return self._record_to_document(results[0])
|
|
85
|
-
|
|
86
|
-
async def update(self, entity: Document) -> Document:
|
|
87
|
-
"""Update an existing document."""
|
|
88
|
-
assert entity.id, "Document ID is required for update"
|
|
89
|
-
|
|
90
|
-
# Update timestamp
|
|
91
|
-
now = datetime.now().isoformat()
|
|
92
|
-
entity.updated_at = datetime.fromisoformat(now)
|
|
93
|
-
|
|
94
|
-
# Update the record
|
|
95
|
-
self.store.documents_table.update(
|
|
96
|
-
where=f"id = '{entity.id}'",
|
|
97
|
-
values={
|
|
98
|
-
"content": entity.content,
|
|
99
|
-
"uri": entity.uri,
|
|
100
|
-
"metadata": json.dumps(entity.metadata),
|
|
101
|
-
"updated_at": now,
|
|
102
|
-
},
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
return entity
|
|
106
|
-
|
|
107
|
-
async def delete(self, entity_id: str) -> bool:
|
|
108
|
-
"""Delete a document by its ID."""
|
|
109
|
-
# Check if document exists
|
|
110
|
-
doc = await self.get_by_id(entity_id)
|
|
111
|
-
if doc is None:
|
|
112
|
-
return False
|
|
113
|
-
|
|
114
|
-
# Delete associated chunks first
|
|
115
|
-
await self.chunk_repository.delete_by_document_id(entity_id)
|
|
116
|
-
|
|
117
|
-
# Delete the document
|
|
118
|
-
self.store.documents_table.delete(f"id = '{entity_id}'")
|
|
119
|
-
return True
|
|
120
|
-
|
|
121
|
-
async def list_all(
|
|
122
|
-
self, limit: int | None = None, offset: int | None = None
|
|
123
|
-
) -> list[Document]:
|
|
124
|
-
"""List all documents with optional pagination."""
|
|
125
|
-
query = self.store.documents_table.search()
|
|
126
|
-
|
|
127
|
-
if offset is not None:
|
|
128
|
-
query = query.offset(offset)
|
|
129
|
-
if limit is not None:
|
|
130
|
-
query = query.limit(limit)
|
|
131
|
-
|
|
132
|
-
results = list(query.to_pydantic(DocumentRecord))
|
|
133
|
-
return [self._record_to_document(doc) for doc in results]
|
|
134
|
-
|
|
135
|
-
async def get_by_uri(self, uri: str) -> Document | None:
|
|
136
|
-
"""Get a document by its URI."""
|
|
137
|
-
results = list(
|
|
138
|
-
self.store.documents_table.search()
|
|
139
|
-
.where(f"uri = '{uri}'")
|
|
140
|
-
.limit(1)
|
|
141
|
-
.to_pydantic(DocumentRecord)
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
if not results:
|
|
145
|
-
return None
|
|
146
|
-
|
|
147
|
-
return self._record_to_document(results[0])
|
|
148
|
-
|
|
149
|
-
async def delete_all(self) -> None:
|
|
150
|
-
"""Delete all documents from the database."""
|
|
151
|
-
# Delete all chunks first
|
|
152
|
-
await self.chunk_repository.delete_all()
|
|
153
|
-
|
|
154
|
-
# Get count before deletion
|
|
155
|
-
count = len(
|
|
156
|
-
list(
|
|
157
|
-
self.store.documents_table.search().limit(1).to_pydantic(DocumentRecord)
|
|
158
|
-
)
|
|
159
|
-
)
|
|
160
|
-
if count > 0:
|
|
161
|
-
# Drop and recreate table to clear all data
|
|
162
|
-
self.store.db.drop_table("documents")
|
|
163
|
-
self.store.documents_table = self.store.db.create_table(
|
|
164
|
-
"documents", schema=DocumentRecord
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
async def _create_with_docling(
|
|
168
|
-
self,
|
|
169
|
-
entity: Document,
|
|
170
|
-
docling_document: DoclingDocument,
|
|
171
|
-
chunks: list["Chunk"] | None = None,
|
|
172
|
-
) -> Document:
|
|
173
|
-
"""Create a document with its chunks and embeddings."""
|
|
174
|
-
# Snapshot table versions for versioned rollback (if supported)
|
|
175
|
-
versions = self.store.current_table_versions()
|
|
176
|
-
|
|
177
|
-
# Create the document
|
|
178
|
-
created_doc = await self.create(entity)
|
|
179
|
-
|
|
180
|
-
# Attempt to create chunks; on failure, prefer version rollback
|
|
181
|
-
try:
|
|
182
|
-
# Create chunks if not provided
|
|
183
|
-
if chunks is None:
|
|
184
|
-
assert created_doc.id is not None, (
|
|
185
|
-
"Document ID should not be None after creation"
|
|
186
|
-
)
|
|
187
|
-
await self.chunk_repository.create_chunks_for_document(
|
|
188
|
-
created_doc.id, docling_document
|
|
189
|
-
)
|
|
190
|
-
else:
|
|
191
|
-
# Use provided chunks, set order from list position
|
|
192
|
-
assert created_doc.id is not None, (
|
|
193
|
-
"Document ID should not be None after creation"
|
|
194
|
-
)
|
|
195
|
-
for order, chunk in enumerate(chunks):
|
|
196
|
-
chunk.document_id = created_doc.id
|
|
197
|
-
chunk.metadata["order"] = order
|
|
198
|
-
await self.chunk_repository.create(chunk)
|
|
199
|
-
|
|
200
|
-
return created_doc
|
|
201
|
-
except Exception:
|
|
202
|
-
# Roll back to the captured versions and re-raise
|
|
203
|
-
self.store.restore_table_versions(versions)
|
|
204
|
-
raise
|
|
205
|
-
|
|
206
|
-
async def _update_with_docling(
|
|
207
|
-
self, entity: Document, docling_document: DoclingDocument
|
|
208
|
-
) -> Document:
|
|
209
|
-
"""Update a document and regenerate its chunks."""
|
|
210
|
-
assert entity.id is not None, "Document ID is required for update"
|
|
211
|
-
|
|
212
|
-
# Snapshot table versions for versioned rollback
|
|
213
|
-
versions = self.store.current_table_versions()
|
|
214
|
-
|
|
215
|
-
# Delete existing chunks before writing new ones
|
|
216
|
-
await self.chunk_repository.delete_by_document_id(entity.id)
|
|
217
|
-
|
|
218
|
-
try:
|
|
219
|
-
# Update the document
|
|
220
|
-
updated_doc = await self.update(entity)
|
|
221
|
-
|
|
222
|
-
# Create new chunks
|
|
223
|
-
assert updated_doc.id is not None, (
|
|
224
|
-
"Document ID should not be None after update"
|
|
225
|
-
)
|
|
226
|
-
await self.chunk_repository.create_chunks_for_document(
|
|
227
|
-
updated_doc.id, docling_document
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
return updated_doc
|
|
231
|
-
except Exception:
|
|
232
|
-
# Roll back to the captured versions and re-raise
|
|
233
|
-
self.store.restore_table_versions(versions)
|
|
234
|
-
raise
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
from haiku.rag.config import Config
|
|
4
|
-
from haiku.rag.store.engine import SettingsRecord, Store
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class ConfigMismatchError(Exception):
|
|
8
|
-
"""Raised when stored config doesn't match current config."""
|
|
9
|
-
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class SettingsRepository:
|
|
14
|
-
"""Repository for Settings operations."""
|
|
15
|
-
|
|
16
|
-
def __init__(self, store: Store) -> None:
|
|
17
|
-
self.store = store
|
|
18
|
-
|
|
19
|
-
async def create(self, entity: dict) -> dict:
|
|
20
|
-
"""Create settings in the database."""
|
|
21
|
-
settings_record = SettingsRecord(id="settings", settings=json.dumps(entity))
|
|
22
|
-
self.store.settings_table.add([settings_record])
|
|
23
|
-
return entity
|
|
24
|
-
|
|
25
|
-
async def get_by_id(self, entity_id: str) -> dict | None:
|
|
26
|
-
"""Get settings by ID."""
|
|
27
|
-
results = list(
|
|
28
|
-
self.store.settings_table.search()
|
|
29
|
-
.where(f"id = '{entity_id}'")
|
|
30
|
-
.limit(1)
|
|
31
|
-
.to_pydantic(SettingsRecord)
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
if not results:
|
|
35
|
-
return None
|
|
36
|
-
|
|
37
|
-
return json.loads(results[0].settings) if results[0].settings else {}
|
|
38
|
-
|
|
39
|
-
async def update(self, entity: dict) -> dict:
|
|
40
|
-
"""Update existing settings."""
|
|
41
|
-
self.store.settings_table.update(
|
|
42
|
-
where="id = 'settings'", values={"settings": json.dumps(entity)}
|
|
43
|
-
)
|
|
44
|
-
return entity
|
|
45
|
-
|
|
46
|
-
async def delete(self, entity_id: str) -> bool:
|
|
47
|
-
"""Delete settings by ID."""
|
|
48
|
-
self.store.settings_table.delete(f"id = '{entity_id}'")
|
|
49
|
-
return True
|
|
50
|
-
|
|
51
|
-
async def list_all(
|
|
52
|
-
self, limit: int | None = None, offset: int | None = None
|
|
53
|
-
) -> list[dict]:
|
|
54
|
-
"""List all settings."""
|
|
55
|
-
results = list(self.store.settings_table.search().to_pydantic(SettingsRecord))
|
|
56
|
-
return [
|
|
57
|
-
json.loads(record.settings) if record.settings else {} for record in results
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
def get_current_settings(self) -> dict:
|
|
61
|
-
"""Get the current settings."""
|
|
62
|
-
results = list(
|
|
63
|
-
self.store.settings_table.search()
|
|
64
|
-
.where("id = 'settings'")
|
|
65
|
-
.limit(1)
|
|
66
|
-
.to_pydantic(SettingsRecord)
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
if not results:
|
|
70
|
-
return {}
|
|
71
|
-
|
|
72
|
-
return json.loads(results[0].settings) if results[0].settings else {}
|
|
73
|
-
|
|
74
|
-
def save_current_settings(self) -> None:
|
|
75
|
-
"""Save the current configuration to the database."""
|
|
76
|
-
current_config = Config.model_dump(mode="json")
|
|
77
|
-
|
|
78
|
-
# Check if settings exist
|
|
79
|
-
existing = list(
|
|
80
|
-
self.store.settings_table.search()
|
|
81
|
-
.where("id = 'settings'")
|
|
82
|
-
.limit(1)
|
|
83
|
-
.to_pydantic(SettingsRecord)
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
if existing:
|
|
87
|
-
# Only update when configuration actually changed to avoid needless new versions
|
|
88
|
-
existing_payload = (
|
|
89
|
-
json.loads(existing[0].settings) if existing[0].settings else {}
|
|
90
|
-
)
|
|
91
|
-
if existing_payload != current_config:
|
|
92
|
-
self.store.settings_table.update(
|
|
93
|
-
where="id = 'settings'",
|
|
94
|
-
values={"settings": json.dumps(current_config)},
|
|
95
|
-
)
|
|
96
|
-
else:
|
|
97
|
-
# Create new settings
|
|
98
|
-
settings_record = SettingsRecord(
|
|
99
|
-
id="settings", settings=json.dumps(current_config)
|
|
100
|
-
)
|
|
101
|
-
self.store.settings_table.add([settings_record])
|
|
102
|
-
|
|
103
|
-
def validate_config_compatibility(self) -> None:
|
|
104
|
-
"""Validate that the current configuration is compatible with stored settings."""
|
|
105
|
-
stored_settings = self.get_current_settings()
|
|
106
|
-
|
|
107
|
-
# If no stored settings, this is a new database - save current config and return
|
|
108
|
-
if not stored_settings:
|
|
109
|
-
self.save_current_settings()
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
current_config = Config.model_dump(mode="json")
|
|
113
|
-
|
|
114
|
-
# Check if embedding provider or model has changed
|
|
115
|
-
stored_provider = stored_settings.get("EMBEDDINGS_PROVIDER")
|
|
116
|
-
current_provider = current_config.get("EMBEDDINGS_PROVIDER")
|
|
117
|
-
|
|
118
|
-
stored_model = stored_settings.get("EMBEDDINGS_MODEL")
|
|
119
|
-
current_model = current_config.get("EMBEDDINGS_MODEL")
|
|
120
|
-
|
|
121
|
-
stored_vector_dim = stored_settings.get("EMBEDDINGS_VECTOR_DIM")
|
|
122
|
-
current_vector_dim = current_config.get("EMBEDDINGS_VECTOR_DIM")
|
|
123
|
-
|
|
124
|
-
# Check for incompatible changes
|
|
125
|
-
incompatible_changes = []
|
|
126
|
-
|
|
127
|
-
if stored_provider and stored_provider != current_provider:
|
|
128
|
-
incompatible_changes.append(
|
|
129
|
-
f"Embedding provider changed from '{stored_provider}' to '{current_provider}'"
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
if stored_model and stored_model != current_model:
|
|
133
|
-
incompatible_changes.append(
|
|
134
|
-
f"Embedding model changed from '{stored_model}' to '{current_model}'"
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
if stored_vector_dim and stored_vector_dim != current_vector_dim:
|
|
138
|
-
incompatible_changes.append(
|
|
139
|
-
f"Vector dimension changed from {stored_vector_dim} to {current_vector_dim}"
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
if incompatible_changes:
|
|
143
|
-
error_msg = (
|
|
144
|
-
"Database configuration is incompatible with current settings:\n"
|
|
145
|
-
+ "\n".join(f" - {change}" for change in incompatible_changes)
|
|
146
|
-
)
|
|
147
|
-
error_msg += "\n\nPlease rebuild the database using: haiku-rag rebuild"
|
|
148
|
-
raise ConfigMismatchError(error_msg)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
upgrades = []
|
haiku/rag/utils.py
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import importlib
|
|
3
|
-
import importlib.util
|
|
4
|
-
import sys
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from functools import wraps
|
|
7
|
-
from importlib import metadata
|
|
8
|
-
from io import BytesIO
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from types import ModuleType
|
|
11
|
-
|
|
12
|
-
import httpx
|
|
13
|
-
from docling.document_converter import DocumentConverter
|
|
14
|
-
from docling_core.types.doc.document import DoclingDocument
|
|
15
|
-
from docling_core.types.io import DocumentStream
|
|
16
|
-
from packaging.version import Version, parse
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def debounce(wait: float) -> Callable:
|
|
20
|
-
"""
|
|
21
|
-
A decorator to debounce a function, ensuring it is called only after a specified delay
|
|
22
|
-
and always executes after the last call.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
wait (float): The debounce delay in seconds.
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
Callable: The decorated function.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
def decorator(func: Callable) -> Callable:
|
|
32
|
-
last_call = None
|
|
33
|
-
task = None
|
|
34
|
-
|
|
35
|
-
@wraps(func)
|
|
36
|
-
async def debounced(*args, **kwargs):
|
|
37
|
-
nonlocal last_call, task
|
|
38
|
-
last_call = asyncio.get_event_loop().time()
|
|
39
|
-
|
|
40
|
-
if task:
|
|
41
|
-
task.cancel()
|
|
42
|
-
|
|
43
|
-
async def call_func():
|
|
44
|
-
await asyncio.sleep(wait)
|
|
45
|
-
if asyncio.get_event_loop().time() - last_call >= wait: # type: ignore
|
|
46
|
-
await func(*args, **kwargs)
|
|
47
|
-
|
|
48
|
-
task = asyncio.create_task(call_func())
|
|
49
|
-
|
|
50
|
-
return debounced
|
|
51
|
-
|
|
52
|
-
return decorator
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def get_default_data_dir() -> Path:
|
|
56
|
-
"""Get the user data directory for the current system platform.
|
|
57
|
-
|
|
58
|
-
Linux: ~/.local/share/haiku.rag
|
|
59
|
-
macOS: ~/Library/Application Support/haiku.rag
|
|
60
|
-
Windows: C:/Users/<USER>/AppData/Roaming/haiku.rag
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
User Data Path.
|
|
64
|
-
"""
|
|
65
|
-
home = Path.home()
|
|
66
|
-
|
|
67
|
-
system_paths = {
|
|
68
|
-
"win32": home / "AppData/Roaming/haiku.rag",
|
|
69
|
-
"linux": home / ".local/share/haiku.rag",
|
|
70
|
-
"darwin": home / "Library/Application Support/haiku.rag",
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
data_path = system_paths[sys.platform]
|
|
74
|
-
return data_path
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
async def is_up_to_date() -> tuple[bool, Version, Version]:
|
|
78
|
-
"""Check whether haiku.rag is current.
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
A tuple containing a boolean indicating whether haiku.rag is current,
|
|
82
|
-
the running version and the latest version.
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
async with httpx.AsyncClient() as client:
|
|
86
|
-
running_version = parse(metadata.version("haiku.rag"))
|
|
87
|
-
try:
|
|
88
|
-
response = await client.get("https://pypi.org/pypi/haiku.rag/json")
|
|
89
|
-
data = response.json()
|
|
90
|
-
pypi_version = parse(data["info"]["version"])
|
|
91
|
-
except Exception:
|
|
92
|
-
# If no network connection, do not raise alarms.
|
|
93
|
-
pypi_version = running_version
|
|
94
|
-
return running_version >= pypi_version, running_version, pypi_version
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def text_to_docling_document(text: str, name: str = "content.md") -> DoclingDocument:
|
|
98
|
-
"""Convert text content to a DoclingDocument.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
text: The text content to convert.
|
|
102
|
-
name: The name to use for the document stream (defaults to "content.md").
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
A DoclingDocument created from the text content.
|
|
106
|
-
"""
|
|
107
|
-
bytes_io = BytesIO(text.encode("utf-8"))
|
|
108
|
-
doc_stream = DocumentStream(name=name, stream=bytes_io)
|
|
109
|
-
converter = DocumentConverter()
|
|
110
|
-
result = converter.convert(doc_stream)
|
|
111
|
-
return result.document
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def load_callable(path: str):
|
|
115
|
-
"""Load a callable from a dotted path or file path.
|
|
116
|
-
|
|
117
|
-
Supported formats:
|
|
118
|
-
- "package.module:func" or "package.module.func"
|
|
119
|
-
- "path/to/file.py:func"
|
|
120
|
-
|
|
121
|
-
Returns the loaded callable. Raises ValueError on failure.
|
|
122
|
-
"""
|
|
123
|
-
if not path:
|
|
124
|
-
raise ValueError("Empty callable path provided")
|
|
125
|
-
|
|
126
|
-
module_part = None
|
|
127
|
-
func_name = None
|
|
128
|
-
|
|
129
|
-
if ":" in path:
|
|
130
|
-
module_part, func_name = path.split(":", 1)
|
|
131
|
-
else:
|
|
132
|
-
# split by last dot for module.attr
|
|
133
|
-
if "." in path:
|
|
134
|
-
module_part, func_name = path.rsplit(".", 1)
|
|
135
|
-
else:
|
|
136
|
-
raise ValueError(
|
|
137
|
-
"Invalid callable path format. Use 'module:func' or 'module.func' or 'file.py:func'."
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
# Try file path first
|
|
141
|
-
mod: ModuleType | None = None
|
|
142
|
-
module_path = Path(module_part)
|
|
143
|
-
if module_path.suffix == ".py" and module_path.exists():
|
|
144
|
-
spec = importlib.util.spec_from_file_location(module_path.stem, module_path)
|
|
145
|
-
if spec and spec.loader:
|
|
146
|
-
mod = importlib.util.module_from_spec(spec)
|
|
147
|
-
spec.loader.exec_module(mod)
|
|
148
|
-
else:
|
|
149
|
-
# Import as a module path
|
|
150
|
-
try:
|
|
151
|
-
mod = importlib.import_module(module_part)
|
|
152
|
-
except Exception as e:
|
|
153
|
-
raise ValueError(f"Failed to import module '{module_part}': {e}")
|
|
154
|
-
|
|
155
|
-
if not hasattr(mod, func_name):
|
|
156
|
-
raise ValueError(f"Callable '{func_name}' not found in module '{module_part}'")
|
|
157
|
-
func = getattr(mod, func_name)
|
|
158
|
-
if not callable(func):
|
|
159
|
-
raise ValueError(
|
|
160
|
-
f"Attribute '{func_name}' in module '{module_part}' is not callable"
|
|
161
|
-
)
|
|
162
|
-
return func
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: haiku.rag
|
|
3
|
-
Version: 0.9.2
|
|
4
|
-
Summary: Agentic Retrieval Augmented Generation (RAG) with LanceDB
|
|
5
|
-
Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Keywords: RAG,lancedb,mcp,ml,vector-database
|
|
9
|
-
Classifier: Development Status :: 4 - Beta
|
|
10
|
-
Classifier: Environment :: Console
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Operating System :: MacOS
|
|
13
|
-
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
14
|
-
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
15
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Typing :: Typed
|
|
20
|
-
Requires-Python: >=3.12
|
|
21
|
-
Requires-Dist: docling>=2.52.0
|
|
22
|
-
Requires-Dist: fastmcp>=2.12.3
|
|
23
|
-
Requires-Dist: httpx>=0.28.1
|
|
24
|
-
Requires-Dist: lancedb>=0.25.0
|
|
25
|
-
Requires-Dist: pydantic-ai>=1.0.8
|
|
26
|
-
Requires-Dist: pydantic>=2.11.9
|
|
27
|
-
Requires-Dist: python-dotenv>=1.1.1
|
|
28
|
-
Requires-Dist: rich>=14.1.0
|
|
29
|
-
Requires-Dist: tiktoken>=0.11.0
|
|
30
|
-
Requires-Dist: typer>=0.16.1
|
|
31
|
-
Requires-Dist: watchfiles>=1.1.0
|
|
32
|
-
Provides-Extra: mxbai
|
|
33
|
-
Requires-Dist: mxbai-rerank>=0.1.6; extra == 'mxbai'
|
|
34
|
-
Provides-Extra: voyageai
|
|
35
|
-
Requires-Dist: voyageai>=0.3.5; extra == 'voyageai'
|
|
36
|
-
Description-Content-Type: text/markdown
|
|
37
|
-
|
|
38
|
-
# Haiku RAG
|
|
39
|
-
|
|
40
|
-
Retrieval-Augmented Generation (RAG) library built on LanceDB.
|
|
41
|
-
|
|
42
|
-
`haiku.rag` is a Retrieval-Augmented Generation (RAG) library built to work with LanceDB as a local vector database. It uses LanceDB for storing embeddings and performs semantic (vector) search as well as full-text search combined through native hybrid search with Reciprocal Rank Fusion. Both open-source (Ollama) as well as commercial (OpenAI, VoyageAI) embedding providers are supported.
|
|
43
|
-
|
|
44
|
-
> **Note**: Starting with version 0.7.0, haiku.rag uses LanceDB instead of SQLite. If you have an existing SQLite database, use `haiku-rag migrate old_database.sqlite` to migrate your data safely.
|
|
45
|
-
|
|
46
|
-
## Features
|
|
47
|
-
|
|
48
|
-
- **Local LanceDB**: No external servers required, supports also LanceDB cloud storage, S3, Google Cloud & Azure
|
|
49
|
-
- **Multiple embedding providers**: Ollama, VoyageAI, OpenAI, vLLM
|
|
50
|
-
- **Multiple QA providers**: Any provider/model supported by Pydantic AI
|
|
51
|
-
- **Native hybrid search**: Vector + full-text search with native LanceDB RRF reranking
|
|
52
|
-
- **Reranking**: Default search result reranking with MixedBread AI, Cohere, or vLLM
|
|
53
|
-
- **Question answering**: Built-in QA agents on your documents
|
|
54
|
-
- **File monitoring**: Auto-index files when run as server
|
|
55
|
-
- **40+ file formats**: PDF, DOCX, HTML, Markdown, code files, URLs
|
|
56
|
-
- **MCP server**: Expose as tools for AI assistants
|
|
57
|
-
- **CLI & Python API**: Use from command line or Python
|
|
58
|
-
|
|
59
|
-
## Quick Start
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
# Install
|
|
63
|
-
uv pip install haiku.rag
|
|
64
|
-
|
|
65
|
-
# Add documents
|
|
66
|
-
haiku-rag add "Your content here"
|
|
67
|
-
haiku-rag add-src document.pdf
|
|
68
|
-
|
|
69
|
-
# Search
|
|
70
|
-
haiku-rag search "query"
|
|
71
|
-
|
|
72
|
-
# Ask questions
|
|
73
|
-
haiku-rag ask "Who is the author of haiku.rag?"
|
|
74
|
-
|
|
75
|
-
# Ask questions with citations
|
|
76
|
-
haiku-rag ask "Who is the author of haiku.rag?" --cite
|
|
77
|
-
|
|
78
|
-
# Rebuild database (re-chunk and re-embed all documents)
|
|
79
|
-
haiku-rag rebuild
|
|
80
|
-
|
|
81
|
-
# Migrate from SQLite to LanceDB
|
|
82
|
-
haiku-rag migrate old_database.sqlite
|
|
83
|
-
|
|
84
|
-
# Start server with file monitoring
|
|
85
|
-
export MONITOR_DIRECTORIES="/path/to/docs"
|
|
86
|
-
haiku-rag serve
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Python Usage
|
|
90
|
-
|
|
91
|
-
```python
|
|
92
|
-
from haiku.rag.client import HaikuRAG
|
|
93
|
-
|
|
94
|
-
async with HaikuRAG("database.lancedb") as client:
|
|
95
|
-
# Add document
|
|
96
|
-
doc = await client.create_document("Your content")
|
|
97
|
-
|
|
98
|
-
# Search (reranking enabled by default)
|
|
99
|
-
results = await client.search("query")
|
|
100
|
-
for chunk, score in results:
|
|
101
|
-
print(f"{score:.3f}: {chunk.content}")
|
|
102
|
-
|
|
103
|
-
# Ask questions
|
|
104
|
-
answer = await client.ask("Who is the author of haiku.rag?")
|
|
105
|
-
print(answer)
|
|
106
|
-
|
|
107
|
-
# Ask questions with citations
|
|
108
|
-
answer = await client.ask("Who is the author of haiku.rag?", cite=True)
|
|
109
|
-
print(answer)
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## MCP Server
|
|
113
|
-
|
|
114
|
-
Use with AI assistants like Claude Desktop:
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
haiku-rag serve --stdio
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Provides tools for document management and search directly in your AI assistant.
|
|
121
|
-
|
|
122
|
-
## Documentation
|
|
123
|
-
|
|
124
|
-
Full documentation at: https://ggozad.github.io/haiku.rag/
|
|
125
|
-
|
|
126
|
-
- [Installation](https://ggozad.github.io/haiku.rag/installation/) - Provider setup
|
|
127
|
-
- [Configuration](https://ggozad.github.io/haiku.rag/configuration/) - Environment variables
|
|
128
|
-
- [CLI](https://ggozad.github.io/haiku.rag/cli/) - Command reference
|
|
129
|
-
- [Python API](https://ggozad.github.io/haiku.rag/python/) - Complete API docs
|
|
130
|
-
- [Agents](https://ggozad.github.io/haiku.rag/agents/) - QA agent and multi-agent research
|
|
131
|
-
- [Benchmarks](https://ggozad.github.io/haiku.rag/benchmarks/) - Performance Benchmarks
|