haiku.rag-slim 0.16.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.
Potentially problematic release.
This version of haiku.rag-slim might be problematic. Click here for more details.
- haiku/rag/__init__.py +0 -0
- haiku/rag/app.py +542 -0
- haiku/rag/chunker.py +65 -0
- haiku/rag/cli.py +466 -0
- haiku/rag/client.py +731 -0
- haiku/rag/config/__init__.py +74 -0
- haiku/rag/config/loader.py +94 -0
- haiku/rag/config/models.py +99 -0
- haiku/rag/embeddings/__init__.py +49 -0
- haiku/rag/embeddings/base.py +25 -0
- haiku/rag/embeddings/ollama.py +28 -0
- haiku/rag/embeddings/openai.py +26 -0
- haiku/rag/embeddings/vllm.py +29 -0
- haiku/rag/embeddings/voyageai.py +27 -0
- haiku/rag/graph/__init__.py +26 -0
- haiku/rag/graph/agui/__init__.py +53 -0
- haiku/rag/graph/agui/cli_renderer.py +135 -0
- haiku/rag/graph/agui/emitter.py +197 -0
- haiku/rag/graph/agui/events.py +254 -0
- haiku/rag/graph/agui/server.py +310 -0
- haiku/rag/graph/agui/state.py +34 -0
- haiku/rag/graph/agui/stream.py +86 -0
- haiku/rag/graph/common/__init__.py +5 -0
- haiku/rag/graph/common/models.py +42 -0
- haiku/rag/graph/common/nodes.py +265 -0
- haiku/rag/graph/common/prompts.py +46 -0
- haiku/rag/graph/common/utils.py +44 -0
- haiku/rag/graph/deep_qa/__init__.py +1 -0
- haiku/rag/graph/deep_qa/dependencies.py +27 -0
- haiku/rag/graph/deep_qa/graph.py +243 -0
- haiku/rag/graph/deep_qa/models.py +20 -0
- haiku/rag/graph/deep_qa/prompts.py +59 -0
- haiku/rag/graph/deep_qa/state.py +56 -0
- haiku/rag/graph/research/__init__.py +3 -0
- haiku/rag/graph/research/common.py +87 -0
- haiku/rag/graph/research/dependencies.py +151 -0
- haiku/rag/graph/research/graph.py +295 -0
- haiku/rag/graph/research/models.py +166 -0
- haiku/rag/graph/research/prompts.py +107 -0
- haiku/rag/graph/research/state.py +85 -0
- haiku/rag/logging.py +56 -0
- haiku/rag/mcp.py +245 -0
- haiku/rag/monitor.py +194 -0
- haiku/rag/qa/__init__.py +33 -0
- haiku/rag/qa/agent.py +93 -0
- haiku/rag/qa/prompts.py +60 -0
- haiku/rag/reader.py +135 -0
- haiku/rag/reranking/__init__.py +63 -0
- haiku/rag/reranking/base.py +13 -0
- haiku/rag/reranking/cohere.py +34 -0
- haiku/rag/reranking/mxbai.py +28 -0
- haiku/rag/reranking/vllm.py +44 -0
- haiku/rag/reranking/zeroentropy.py +59 -0
- haiku/rag/store/__init__.py +4 -0
- haiku/rag/store/engine.py +309 -0
- haiku/rag/store/models/__init__.py +4 -0
- haiku/rag/store/models/chunk.py +17 -0
- haiku/rag/store/models/document.py +17 -0
- haiku/rag/store/repositories/__init__.py +9 -0
- haiku/rag/store/repositories/chunk.py +442 -0
- haiku/rag/store/repositories/document.py +261 -0
- haiku/rag/store/repositories/settings.py +165 -0
- haiku/rag/store/upgrades/__init__.py +62 -0
- haiku/rag/store/upgrades/v0_10_1.py +64 -0
- haiku/rag/store/upgrades/v0_9_3.py +112 -0
- haiku/rag/utils.py +211 -0
- haiku_rag_slim-0.16.0.dist-info/METADATA +128 -0
- haiku_rag_slim-0.16.0.dist-info/RECORD +71 -0
- haiku_rag_slim-0.16.0.dist-info/WHEEL +4 -0
- haiku_rag_slim-0.16.0.dist-info/entry_points.txt +2 -0
- haiku_rag_slim-0.16.0.dist-info/licenses/LICENSE +7 -0
haiku/rag/__init__.py
ADDED
|
File without changes
|
haiku/rag/app.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from importlib.metadata import version as pkg_version
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
from rich.progress import Progress
|
|
10
|
+
|
|
11
|
+
from haiku.rag.client import HaikuRAG
|
|
12
|
+
from haiku.rag.config import AppConfig, Config
|
|
13
|
+
from haiku.rag.graph.agui import AGUIConsoleRenderer, stream_graph
|
|
14
|
+
from haiku.rag.graph.research.dependencies import ResearchContext
|
|
15
|
+
from haiku.rag.graph.research.graph import build_research_graph
|
|
16
|
+
from haiku.rag.graph.research.state import ResearchDeps, ResearchState
|
|
17
|
+
from haiku.rag.mcp import create_mcp_server
|
|
18
|
+
from haiku.rag.monitor import FileWatcher
|
|
19
|
+
from haiku.rag.store.models.chunk import Chunk
|
|
20
|
+
from haiku.rag.store.models.document import Document
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HaikuRAGApp:
|
|
26
|
+
def __init__(self, db_path: Path, config: AppConfig = Config):
|
|
27
|
+
self.db_path = db_path
|
|
28
|
+
self.config = config
|
|
29
|
+
self.console = Console()
|
|
30
|
+
|
|
31
|
+
async def info(self):
|
|
32
|
+
"""Display read-only information about the database without modifying it."""
|
|
33
|
+
|
|
34
|
+
import lancedb
|
|
35
|
+
|
|
36
|
+
# Basic: show path
|
|
37
|
+
self.console.print("[bold]haiku.rag database info[/bold]")
|
|
38
|
+
self.console.print(
|
|
39
|
+
f" [repr.attrib_name]path[/repr.attrib_name]: {self.db_path}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not self.db_path.exists():
|
|
43
|
+
self.console.print("[red]Database path does not exist.[/red]")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Connect without going through Store to avoid upgrades/validation writes
|
|
47
|
+
try:
|
|
48
|
+
db = lancedb.connect(self.db_path)
|
|
49
|
+
table_names = set(db.table_names())
|
|
50
|
+
except Exception as e:
|
|
51
|
+
self.console.print(f"[red]Failed to open database: {e}[/red]")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
ldb_version = pkg_version("lancedb")
|
|
56
|
+
except Exception:
|
|
57
|
+
ldb_version = "unknown"
|
|
58
|
+
try:
|
|
59
|
+
hr_version = pkg_version("haiku.rag-slim")
|
|
60
|
+
except Exception:
|
|
61
|
+
hr_version = "unknown"
|
|
62
|
+
try:
|
|
63
|
+
docling_version = pkg_version("docling")
|
|
64
|
+
except Exception:
|
|
65
|
+
docling_version = "unknown"
|
|
66
|
+
|
|
67
|
+
# Read settings (if present) to find stored haiku.rag version and embedding config
|
|
68
|
+
stored_version = "unknown"
|
|
69
|
+
embed_provider: str | None = None
|
|
70
|
+
embed_model: str | None = None
|
|
71
|
+
vector_dim: int | None = None
|
|
72
|
+
|
|
73
|
+
if "settings" in table_names:
|
|
74
|
+
settings_tbl = db.open_table("settings")
|
|
75
|
+
arrow = settings_tbl.search().where("id = 'settings'").limit(1).to_arrow()
|
|
76
|
+
rows = arrow.to_pylist() if arrow is not None else []
|
|
77
|
+
if rows:
|
|
78
|
+
raw = rows[0].get("settings") or "{}"
|
|
79
|
+
data = json.loads(raw) if isinstance(raw, str) else (raw or {})
|
|
80
|
+
stored_version = str(data.get("version", stored_version))
|
|
81
|
+
embeddings = data.get("embeddings", {})
|
|
82
|
+
embed_provider = embeddings.get("provider")
|
|
83
|
+
embed_model = embeddings.get("model")
|
|
84
|
+
vector_dim = embeddings.get("vector_dim")
|
|
85
|
+
|
|
86
|
+
num_docs = 0
|
|
87
|
+
if "documents" in table_names:
|
|
88
|
+
docs_tbl = db.open_table("documents")
|
|
89
|
+
num_docs = int(docs_tbl.count_rows()) # type: ignore[attr-defined]
|
|
90
|
+
|
|
91
|
+
# Table versions per table (direct API)
|
|
92
|
+
doc_versions = (
|
|
93
|
+
len(list(db.open_table("documents").list_versions()))
|
|
94
|
+
if "documents" in table_names
|
|
95
|
+
else 0
|
|
96
|
+
)
|
|
97
|
+
chunk_versions = (
|
|
98
|
+
len(list(db.open_table("chunks").list_versions()))
|
|
99
|
+
if "chunks" in table_names
|
|
100
|
+
else 0
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self.console.print(
|
|
104
|
+
f" [repr.attrib_name]haiku.rag version (db)[/repr.attrib_name]: {stored_version}"
|
|
105
|
+
)
|
|
106
|
+
if embed_provider or embed_model or vector_dim:
|
|
107
|
+
provider_part = embed_provider or "unknown"
|
|
108
|
+
model_part = embed_model or "unknown"
|
|
109
|
+
dim_part = f"{vector_dim}" if vector_dim is not None else "unknown"
|
|
110
|
+
self.console.print(
|
|
111
|
+
" [repr.attrib_name]embeddings[/repr.attrib_name]: "
|
|
112
|
+
f"{provider_part}/{model_part} (dim: {dim_part})"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
self.console.print(
|
|
116
|
+
" [repr.attrib_name]embeddings[/repr.attrib_name]: unknown"
|
|
117
|
+
)
|
|
118
|
+
self.console.print(
|
|
119
|
+
f" [repr.attrib_name]documents[/repr.attrib_name]: {num_docs}"
|
|
120
|
+
)
|
|
121
|
+
self.console.print(
|
|
122
|
+
f" [repr.attrib_name]versions (documents)[/repr.attrib_name]: {doc_versions}"
|
|
123
|
+
)
|
|
124
|
+
self.console.print(
|
|
125
|
+
f" [repr.attrib_name]versions (chunks)[/repr.attrib_name]: {chunk_versions}"
|
|
126
|
+
)
|
|
127
|
+
self.console.rule()
|
|
128
|
+
self.console.print("[bold]Versions[/bold]")
|
|
129
|
+
self.console.print(
|
|
130
|
+
f" [repr.attrib_name]haiku.rag[/repr.attrib_name]: {hr_version}"
|
|
131
|
+
)
|
|
132
|
+
self.console.print(
|
|
133
|
+
f" [repr.attrib_name]lancedb[/repr.attrib_name]: {ldb_version}"
|
|
134
|
+
)
|
|
135
|
+
self.console.print(
|
|
136
|
+
f" [repr.attrib_name]docling[/repr.attrib_name]: {docling_version}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def list_documents(self, filter: str | None = None):
|
|
140
|
+
async with HaikuRAG(
|
|
141
|
+
db_path=self.db_path, config=self.config, allow_create=False
|
|
142
|
+
) as self.client:
|
|
143
|
+
documents = await self.client.list_documents(filter=filter)
|
|
144
|
+
for doc in documents:
|
|
145
|
+
self._rich_print_document(doc, truncate=True)
|
|
146
|
+
|
|
147
|
+
async def add_document_from_text(self, text: str, metadata: dict | None = None):
|
|
148
|
+
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
|
149
|
+
doc = await self.client.create_document(text, metadata=metadata)
|
|
150
|
+
self._rich_print_document(doc, truncate=True)
|
|
151
|
+
self.console.print(
|
|
152
|
+
f"[bold green]Document {doc.id} added successfully.[/bold green]"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def add_document_from_source(
|
|
156
|
+
self, source: str, title: str | None = None, metadata: dict | None = None
|
|
157
|
+
):
|
|
158
|
+
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
|
159
|
+
result = await self.client.create_document_from_source(
|
|
160
|
+
source, title=title, metadata=metadata
|
|
161
|
+
)
|
|
162
|
+
if isinstance(result, list):
|
|
163
|
+
for doc in result:
|
|
164
|
+
self._rich_print_document(doc, truncate=True)
|
|
165
|
+
self.console.print(
|
|
166
|
+
f"[bold green]{len(result)} documents added successfully.[/bold green]"
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
self._rich_print_document(result, truncate=True)
|
|
170
|
+
self.console.print(
|
|
171
|
+
f"[bold green]Document {result.id} added successfully.[/bold green]"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def get_document(self, doc_id: str):
|
|
175
|
+
async with HaikuRAG(
|
|
176
|
+
db_path=self.db_path, config=self.config, allow_create=False
|
|
177
|
+
) as self.client:
|
|
178
|
+
doc = await self.client.get_document_by_id(doc_id)
|
|
179
|
+
if doc is None:
|
|
180
|
+
self.console.print(f"[red]Document with id {doc_id} not found.[/red]")
|
|
181
|
+
return
|
|
182
|
+
self._rich_print_document(doc, truncate=False)
|
|
183
|
+
|
|
184
|
+
async def delete_document(self, doc_id: str):
|
|
185
|
+
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
|
186
|
+
deleted = await self.client.delete_document(doc_id)
|
|
187
|
+
if deleted:
|
|
188
|
+
self.console.print(
|
|
189
|
+
f"[bold green]Document {doc_id} deleted successfully.[/bold green]"
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
self.console.print(
|
|
193
|
+
f"[yellow]Document with id {doc_id} not found.[/yellow]"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def search(self, query: str, limit: int = 5, filter: str | None = None):
|
|
197
|
+
async with HaikuRAG(
|
|
198
|
+
db_path=self.db_path, config=self.config, allow_create=False
|
|
199
|
+
) as self.client:
|
|
200
|
+
results = await self.client.search(query, limit=limit, filter=filter)
|
|
201
|
+
if not results:
|
|
202
|
+
self.console.print("[yellow]No results found.[/yellow]")
|
|
203
|
+
return
|
|
204
|
+
for chunk, score in results:
|
|
205
|
+
self._rich_print_search_result(chunk, score)
|
|
206
|
+
|
|
207
|
+
async def ask(
|
|
208
|
+
self,
|
|
209
|
+
question: str,
|
|
210
|
+
cite: bool = False,
|
|
211
|
+
deep: bool = False,
|
|
212
|
+
verbose: bool = False,
|
|
213
|
+
):
|
|
214
|
+
"""Ask a question using the RAG system.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
question: The question to ask
|
|
218
|
+
cite: Include citations in the answer
|
|
219
|
+
deep: Use deep QA mode (multi-step reasoning)
|
|
220
|
+
verbose: Show verbose output
|
|
221
|
+
"""
|
|
222
|
+
async with HaikuRAG(
|
|
223
|
+
db_path=self.db_path, config=self.config, allow_create=False
|
|
224
|
+
) as self.client:
|
|
225
|
+
try:
|
|
226
|
+
if deep:
|
|
227
|
+
from haiku.rag.graph.deep_qa.dependencies import DeepQAContext
|
|
228
|
+
from haiku.rag.graph.deep_qa.graph import build_deep_qa_graph
|
|
229
|
+
from haiku.rag.graph.deep_qa.state import DeepQADeps, DeepQAState
|
|
230
|
+
|
|
231
|
+
graph = build_deep_qa_graph(config=self.config)
|
|
232
|
+
context = DeepQAContext(
|
|
233
|
+
original_question=question, use_citations=cite
|
|
234
|
+
)
|
|
235
|
+
state = DeepQAState.from_config(context=context, config=self.config)
|
|
236
|
+
deps = DeepQADeps(client=self.client)
|
|
237
|
+
|
|
238
|
+
if verbose:
|
|
239
|
+
# Use AG-UI renderer to process and display events
|
|
240
|
+
from haiku.rag.graph.agui import AGUIConsoleRenderer
|
|
241
|
+
|
|
242
|
+
renderer = AGUIConsoleRenderer(self.console)
|
|
243
|
+
result_dict = await renderer.render(
|
|
244
|
+
stream_graph(graph, state, deps)
|
|
245
|
+
)
|
|
246
|
+
# Result should be a dict with 'answer' key
|
|
247
|
+
answer = result_dict.get("answer", "") if result_dict else ""
|
|
248
|
+
else:
|
|
249
|
+
# Run without rendering events, just get the result
|
|
250
|
+
result = await graph.run(state=state, deps=deps)
|
|
251
|
+
answer = result.answer
|
|
252
|
+
else:
|
|
253
|
+
answer = await self.client.ask(question, cite=cite)
|
|
254
|
+
|
|
255
|
+
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
|
256
|
+
self.console.print()
|
|
257
|
+
self.console.print("[bold green]Answer:[/bold green]")
|
|
258
|
+
self.console.print(Markdown(answer))
|
|
259
|
+
except Exception as e:
|
|
260
|
+
self.console.print(f"[red]Error: {e}[/red]")
|
|
261
|
+
|
|
262
|
+
async def research(self, question: str, verbose: bool = False):
|
|
263
|
+
"""Run research via the pydantic-graph pipeline.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
question: The research question
|
|
267
|
+
verbose: Show AG-UI event stream during execution
|
|
268
|
+
"""
|
|
269
|
+
async with HaikuRAG(
|
|
270
|
+
db_path=self.db_path, config=self.config, allow_create=False
|
|
271
|
+
) as client:
|
|
272
|
+
try:
|
|
273
|
+
self.console.print("[bold cyan]Starting research[/bold cyan]")
|
|
274
|
+
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
|
275
|
+
self.console.print()
|
|
276
|
+
|
|
277
|
+
graph = build_research_graph(config=self.config)
|
|
278
|
+
context = ResearchContext(original_question=question)
|
|
279
|
+
state = ResearchState.from_config(context=context, config=self.config)
|
|
280
|
+
deps = ResearchDeps(client=client)
|
|
281
|
+
|
|
282
|
+
if verbose:
|
|
283
|
+
# Use AG-UI renderer to process and display events
|
|
284
|
+
renderer = AGUIConsoleRenderer(self.console)
|
|
285
|
+
report_dict = await renderer.render(
|
|
286
|
+
stream_graph(graph, state, deps)
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
# Run without rendering events, just get the result
|
|
290
|
+
report = await graph.run(state=state, deps=deps)
|
|
291
|
+
report_dict = (
|
|
292
|
+
report.model_dump() if hasattr(report, "model_dump") else report
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if report_dict is None:
|
|
296
|
+
self.console.print("[red]Research did not produce a report.[/red]")
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# Convert dict to ResearchReport model
|
|
300
|
+
from haiku.rag.graph.research.models import ResearchReport
|
|
301
|
+
|
|
302
|
+
report = ResearchReport.model_validate(report_dict)
|
|
303
|
+
|
|
304
|
+
# Display the report
|
|
305
|
+
self.console.print("[bold green]Research Report[/bold green]")
|
|
306
|
+
self.console.rule()
|
|
307
|
+
|
|
308
|
+
# Title and Executive Summary
|
|
309
|
+
self.console.print(f"[bold]{report.title}[/bold]")
|
|
310
|
+
self.console.print()
|
|
311
|
+
self.console.print("[bold cyan]Executive Summary:[/bold cyan]")
|
|
312
|
+
self.console.print(report.executive_summary)
|
|
313
|
+
self.console.print()
|
|
314
|
+
|
|
315
|
+
# Confidence (from last evaluation)
|
|
316
|
+
if state.last_eval:
|
|
317
|
+
conf = state.last_eval.confidence_score # type: ignore[attr-defined]
|
|
318
|
+
self.console.print(f"[bold cyan]Confidence:[/bold cyan] {conf:.1%}")
|
|
319
|
+
self.console.print()
|
|
320
|
+
|
|
321
|
+
# Main Findings
|
|
322
|
+
if report.main_findings:
|
|
323
|
+
self.console.print("[bold cyan]Main Findings:[/bold cyan]")
|
|
324
|
+
for finding in report.main_findings:
|
|
325
|
+
self.console.print(f"• {finding}")
|
|
326
|
+
self.console.print()
|
|
327
|
+
|
|
328
|
+
# (Themes section removed)
|
|
329
|
+
|
|
330
|
+
# Conclusions
|
|
331
|
+
if report.conclusions:
|
|
332
|
+
self.console.print("[bold cyan]Conclusions:[/bold cyan]")
|
|
333
|
+
for conclusion in report.conclusions:
|
|
334
|
+
self.console.print(f"• {conclusion}")
|
|
335
|
+
self.console.print()
|
|
336
|
+
|
|
337
|
+
# Recommendations
|
|
338
|
+
if report.recommendations:
|
|
339
|
+
self.console.print("[bold cyan]Recommendations:[/bold cyan]")
|
|
340
|
+
for rec in report.recommendations:
|
|
341
|
+
self.console.print(f"• {rec}")
|
|
342
|
+
self.console.print()
|
|
343
|
+
|
|
344
|
+
# Limitations
|
|
345
|
+
if report.limitations:
|
|
346
|
+
self.console.print("[bold yellow]Limitations:[/bold yellow]")
|
|
347
|
+
for limitation in report.limitations:
|
|
348
|
+
self.console.print(f"• {limitation}")
|
|
349
|
+
self.console.print()
|
|
350
|
+
|
|
351
|
+
# Sources Summary
|
|
352
|
+
if report.sources_summary:
|
|
353
|
+
self.console.print("[bold cyan]Sources:[/bold cyan]")
|
|
354
|
+
self.console.print(report.sources_summary)
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
self.console.print(f"[red]Error during research: {e}[/red]")
|
|
358
|
+
|
|
359
|
+
async def rebuild(self):
|
|
360
|
+
async with HaikuRAG(
|
|
361
|
+
db_path=self.db_path, config=self.config, skip_validation=True
|
|
362
|
+
) as client:
|
|
363
|
+
try:
|
|
364
|
+
documents = await client.list_documents()
|
|
365
|
+
total_docs = len(documents)
|
|
366
|
+
|
|
367
|
+
if total_docs == 0:
|
|
368
|
+
self.console.print(
|
|
369
|
+
"[yellow]No documents found in database.[/yellow]"
|
|
370
|
+
)
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
self.console.print(
|
|
374
|
+
f"[bold cyan]Rebuilding database with {total_docs} documents...[/bold cyan]"
|
|
375
|
+
)
|
|
376
|
+
with Progress() as progress:
|
|
377
|
+
task = progress.add_task("Rebuilding...", total=total_docs)
|
|
378
|
+
async for _ in client.rebuild_database():
|
|
379
|
+
progress.update(task, advance=1)
|
|
380
|
+
|
|
381
|
+
self.console.print(
|
|
382
|
+
"[bold green]Database rebuild completed successfully.[/bold green]"
|
|
383
|
+
)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
self.console.print(f"[red]Error rebuilding database: {e}[/red]")
|
|
386
|
+
|
|
387
|
+
async def vacuum(self):
|
|
388
|
+
"""Run database maintenance: optimize and cleanup table history."""
|
|
389
|
+
try:
|
|
390
|
+
async with HaikuRAG(
|
|
391
|
+
db_path=self.db_path, config=self.config, skip_validation=True
|
|
392
|
+
) as client:
|
|
393
|
+
await client.vacuum()
|
|
394
|
+
self.console.print(
|
|
395
|
+
"[bold green]Vacuum completed successfully.[/bold green]"
|
|
396
|
+
)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
self.console.print(f"[red]Error during vacuum: {e}[/red]")
|
|
399
|
+
|
|
400
|
+
def show_settings(self):
|
|
401
|
+
"""Display current configuration settings."""
|
|
402
|
+
self.console.print("[bold]haiku.rag configuration[/bold]")
|
|
403
|
+
self.console.print()
|
|
404
|
+
|
|
405
|
+
# Get all config fields dynamically
|
|
406
|
+
for field_name, field_value in self.config.model_dump().items():
|
|
407
|
+
# Format the display value
|
|
408
|
+
if isinstance(field_value, str) and (
|
|
409
|
+
"key" in field_name.lower()
|
|
410
|
+
or "password" in field_name.lower()
|
|
411
|
+
or "token" in field_name.lower()
|
|
412
|
+
):
|
|
413
|
+
# Hide sensitive values but show if they're set
|
|
414
|
+
display_value = "✓ Set" if field_value else "✗ Not set"
|
|
415
|
+
else:
|
|
416
|
+
display_value = field_value
|
|
417
|
+
|
|
418
|
+
self.console.print(
|
|
419
|
+
f" [repr.attrib_name]{field_name}[/repr.attrib_name]: {display_value}"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def _rich_print_document(self, doc: Document, truncate: bool = False):
|
|
423
|
+
"""Format a document for display."""
|
|
424
|
+
if truncate:
|
|
425
|
+
content = doc.content.splitlines()
|
|
426
|
+
if len(content) > 3:
|
|
427
|
+
content = content[:3] + ["\n…"]
|
|
428
|
+
content = "\n".join(content)
|
|
429
|
+
content = Markdown(content)
|
|
430
|
+
else:
|
|
431
|
+
content = Markdown(doc.content)
|
|
432
|
+
title_part = (
|
|
433
|
+
f" [repr.attrib_name]title[/repr.attrib_name]: {doc.title}"
|
|
434
|
+
if doc.title
|
|
435
|
+
else ""
|
|
436
|
+
)
|
|
437
|
+
self.console.print(
|
|
438
|
+
f"[repr.attrib_name]id[/repr.attrib_name]: {doc.id} "
|
|
439
|
+
f"[repr.attrib_name]uri[/repr.attrib_name]: {doc.uri}"
|
|
440
|
+
+ title_part
|
|
441
|
+
+ f" [repr.attrib_name]meta[/repr.attrib_name]: {doc.metadata}"
|
|
442
|
+
)
|
|
443
|
+
self.console.print(
|
|
444
|
+
f"[repr.attrib_name]created at[/repr.attrib_name]: {doc.created_at} [repr.attrib_name]updated at[/repr.attrib_name]: {doc.updated_at}"
|
|
445
|
+
)
|
|
446
|
+
self.console.print("[repr.attrib_name]content[/repr.attrib_name]:")
|
|
447
|
+
self.console.print(content)
|
|
448
|
+
self.console.rule()
|
|
449
|
+
|
|
450
|
+
def _rich_print_search_result(self, chunk: Chunk, score: float):
|
|
451
|
+
"""Format a search result chunk for display."""
|
|
452
|
+
content = Markdown(chunk.content)
|
|
453
|
+
self.console.print(
|
|
454
|
+
f"[repr.attrib_name]document_id[/repr.attrib_name]: {chunk.document_id} "
|
|
455
|
+
f"[repr.attrib_name]score[/repr.attrib_name]: {score:.4f}"
|
|
456
|
+
)
|
|
457
|
+
if chunk.document_uri:
|
|
458
|
+
self.console.print("[repr.attrib_name]document uri[/repr.attrib_name]:")
|
|
459
|
+
self.console.print(chunk.document_uri)
|
|
460
|
+
if chunk.document_title:
|
|
461
|
+
self.console.print("[repr.attrib_name]document title[/repr.attrib_name]:")
|
|
462
|
+
self.console.print(chunk.document_title)
|
|
463
|
+
if chunk.document_meta:
|
|
464
|
+
self.console.print("[repr.attrib_name]document meta[/repr.attrib_name]:")
|
|
465
|
+
self.console.print(chunk.document_meta)
|
|
466
|
+
self.console.print("[repr.attrib_name]content[/repr.attrib_name]:")
|
|
467
|
+
self.console.print(content)
|
|
468
|
+
self.console.rule()
|
|
469
|
+
|
|
470
|
+
async def serve(
|
|
471
|
+
self,
|
|
472
|
+
enable_monitor: bool = True,
|
|
473
|
+
enable_mcp: bool = True,
|
|
474
|
+
mcp_transport: str | None = None,
|
|
475
|
+
mcp_port: int = 8001,
|
|
476
|
+
enable_agui: bool = False,
|
|
477
|
+
):
|
|
478
|
+
"""Start the server with selected services."""
|
|
479
|
+
async with HaikuRAG(self.db_path, config=self.config) as client:
|
|
480
|
+
tasks = []
|
|
481
|
+
|
|
482
|
+
# Start file monitor if enabled
|
|
483
|
+
if enable_monitor:
|
|
484
|
+
monitor = FileWatcher(client=client, config=self.config)
|
|
485
|
+
monitor_task = asyncio.create_task(monitor.observe())
|
|
486
|
+
tasks.append(monitor_task)
|
|
487
|
+
|
|
488
|
+
# Start MCP server if enabled
|
|
489
|
+
if enable_mcp:
|
|
490
|
+
server = create_mcp_server(self.db_path, config=self.config)
|
|
491
|
+
|
|
492
|
+
async def run_mcp():
|
|
493
|
+
if mcp_transport == "stdio":
|
|
494
|
+
await server.run_stdio_async()
|
|
495
|
+
else:
|
|
496
|
+
logger.info(f"Starting MCP server on port {mcp_port}")
|
|
497
|
+
await server.run_http_async(
|
|
498
|
+
transport="streamable-http", port=mcp_port
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
mcp_task = asyncio.create_task(run_mcp())
|
|
502
|
+
tasks.append(mcp_task)
|
|
503
|
+
|
|
504
|
+
# Start AG-UI server if enabled
|
|
505
|
+
if enable_agui:
|
|
506
|
+
|
|
507
|
+
async def run_agui():
|
|
508
|
+
import uvicorn
|
|
509
|
+
|
|
510
|
+
from haiku.rag.graph.agui import create_agui_server
|
|
511
|
+
|
|
512
|
+
logger.info(
|
|
513
|
+
f"Starting AG-UI server on {self.config.agui.host}:{self.config.agui.port}"
|
|
514
|
+
)
|
|
515
|
+
app = create_agui_server(self.config, db_path=self.db_path)
|
|
516
|
+
config = uvicorn.Config(
|
|
517
|
+
app=app,
|
|
518
|
+
host=self.config.agui.host,
|
|
519
|
+
port=self.config.agui.port,
|
|
520
|
+
log_level="info",
|
|
521
|
+
)
|
|
522
|
+
server = uvicorn.Server(config)
|
|
523
|
+
await server.serve()
|
|
524
|
+
|
|
525
|
+
agui_task = asyncio.create_task(run_agui())
|
|
526
|
+
tasks.append(agui_task)
|
|
527
|
+
|
|
528
|
+
if not tasks:
|
|
529
|
+
logger.warning("No services enabled")
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
# Wait for any task to complete (or KeyboardInterrupt)
|
|
534
|
+
await asyncio.gather(*tasks)
|
|
535
|
+
except KeyboardInterrupt:
|
|
536
|
+
pass
|
|
537
|
+
finally:
|
|
538
|
+
# Cancel all tasks
|
|
539
|
+
for task in tasks:
|
|
540
|
+
task.cancel()
|
|
541
|
+
# Wait for cancellation
|
|
542
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
haiku/rag/chunker.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import ClassVar
|
|
2
|
+
|
|
3
|
+
import tiktoken
|
|
4
|
+
from docling_core.transforms.chunker.tokenizer.openai import OpenAITokenizer
|
|
5
|
+
from docling_core.types.doc.document import DoclingDocument
|
|
6
|
+
|
|
7
|
+
from haiku.rag.config import Config
|
|
8
|
+
|
|
9
|
+
# Check if docling is available
|
|
10
|
+
try:
|
|
11
|
+
import docling # noqa: F401
|
|
12
|
+
|
|
13
|
+
DOCLING_AVAILABLE = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
DOCLING_AVAILABLE = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Chunker:
|
|
19
|
+
"""A class that chunks text into smaller pieces for embedding and retrieval.
|
|
20
|
+
|
|
21
|
+
Uses docling's structure-aware chunking to create semantically meaningful chunks
|
|
22
|
+
that respect document boundaries.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
chunk_size: The maximum size of a chunk in tokens.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
encoder: ClassVar[tiktoken.Encoding] = tiktoken.encoding_for_model("gpt-4o")
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
chunk_size: int = Config.processing.chunk_size,
|
|
33
|
+
):
|
|
34
|
+
if not DOCLING_AVAILABLE:
|
|
35
|
+
raise ImportError(
|
|
36
|
+
"Docling is required for chunking. "
|
|
37
|
+
"Install with: pip install haiku.rag-slim[docling]"
|
|
38
|
+
)
|
|
39
|
+
from docling.chunking import HybridChunker # type: ignore
|
|
40
|
+
|
|
41
|
+
self.chunk_size = chunk_size
|
|
42
|
+
tokenizer = OpenAITokenizer(
|
|
43
|
+
tokenizer=tiktoken.encoding_for_model("gpt-4o"), max_tokens=chunk_size
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self.chunker = HybridChunker(tokenizer=tokenizer) # type: ignore
|
|
47
|
+
|
|
48
|
+
async def chunk(self, document: DoclingDocument) -> list[str]:
|
|
49
|
+
"""Split the document into chunks using docling's structure-aware chunking.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
document: The DoclingDocument to be split into chunks.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A list of text chunks with semantic boundaries.
|
|
56
|
+
"""
|
|
57
|
+
if document is None:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
# Chunk using docling's hybrid chunker
|
|
61
|
+
chunks = list(self.chunker.chunk(document))
|
|
62
|
+
return [self.chunker.contextualize(chunk) for chunk in chunks]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
chunker = Chunker()
|