haiku.rag 0.10.2__py3-none-any.whl → 0.19.3__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 (56) hide show
  1. README.md +172 -0
  2. {haiku_rag-0.10.2.dist-info → haiku_rag-0.19.3.dist-info}/METADATA +79 -51
  3. haiku_rag-0.19.3.dist-info/RECORD +6 -0
  4. {haiku_rag-0.10.2.dist-info → haiku_rag-0.19.3.dist-info}/WHEEL +1 -1
  5. haiku/rag/__init__.py +0 -0
  6. haiku/rag/app.py +0 -437
  7. haiku/rag/chunker.py +0 -51
  8. haiku/rag/cli.py +0 -466
  9. haiku/rag/client.py +0 -605
  10. haiku/rag/config.py +0 -81
  11. haiku/rag/embeddings/__init__.py +0 -35
  12. haiku/rag/embeddings/base.py +0 -15
  13. haiku/rag/embeddings/ollama.py +0 -17
  14. haiku/rag/embeddings/openai.py +0 -16
  15. haiku/rag/embeddings/vllm.py +0 -19
  16. haiku/rag/embeddings/voyageai.py +0 -17
  17. haiku/rag/logging.py +0 -56
  18. haiku/rag/mcp.py +0 -156
  19. haiku/rag/migration.py +0 -316
  20. haiku/rag/monitor.py +0 -73
  21. haiku/rag/qa/__init__.py +0 -15
  22. haiku/rag/qa/agent.py +0 -91
  23. haiku/rag/qa/prompts.py +0 -60
  24. haiku/rag/reader.py +0 -115
  25. haiku/rag/reranking/__init__.py +0 -34
  26. haiku/rag/reranking/base.py +0 -13
  27. haiku/rag/reranking/cohere.py +0 -34
  28. haiku/rag/reranking/mxbai.py +0 -28
  29. haiku/rag/reranking/vllm.py +0 -44
  30. haiku/rag/research/__init__.py +0 -20
  31. haiku/rag/research/common.py +0 -53
  32. haiku/rag/research/dependencies.py +0 -47
  33. haiku/rag/research/graph.py +0 -29
  34. haiku/rag/research/models.py +0 -70
  35. haiku/rag/research/nodes/evaluate.py +0 -80
  36. haiku/rag/research/nodes/plan.py +0 -63
  37. haiku/rag/research/nodes/search.py +0 -93
  38. haiku/rag/research/nodes/synthesize.py +0 -51
  39. haiku/rag/research/prompts.py +0 -114
  40. haiku/rag/research/state.py +0 -25
  41. haiku/rag/store/__init__.py +0 -4
  42. haiku/rag/store/engine.py +0 -269
  43. haiku/rag/store/models/__init__.py +0 -4
  44. haiku/rag/store/models/chunk.py +0 -17
  45. haiku/rag/store/models/document.py +0 -17
  46. haiku/rag/store/repositories/__init__.py +0 -9
  47. haiku/rag/store/repositories/chunk.py +0 -424
  48. haiku/rag/store/repositories/document.py +0 -237
  49. haiku/rag/store/repositories/settings.py +0 -155
  50. haiku/rag/store/upgrades/__init__.py +0 -62
  51. haiku/rag/store/upgrades/v0_10_1.py +0 -64
  52. haiku/rag/store/upgrades/v0_9_3.py +0 -112
  53. haiku/rag/utils.py +0 -199
  54. haiku_rag-0.10.2.dist-info/RECORD +0 -54
  55. {haiku_rag-0.10.2.dist-info → haiku_rag-0.19.3.dist-info}/entry_points.txt +0 -0
  56. {haiku_rag-0.10.2.dist-info → haiku_rag-0.19.3.dist-info}/licenses/LICENSE +0 -0
haiku/rag/app.py DELETED
@@ -1,437 +0,0 @@
1
- import asyncio
2
- import json
3
- from importlib.metadata import version as pkg_version
4
- from pathlib import Path
5
-
6
- from rich.console import Console
7
- from rich.markdown import Markdown
8
- from rich.progress import Progress
9
-
10
- from haiku.rag.client import HaikuRAG
11
- from haiku.rag.config import Config
12
- from haiku.rag.mcp import create_mcp_server
13
- from haiku.rag.monitor import FileWatcher
14
- from haiku.rag.research.dependencies import ResearchContext
15
- from haiku.rag.research.graph import (
16
- PlanNode,
17
- ResearchDeps,
18
- ResearchState,
19
- build_research_graph,
20
- )
21
- from haiku.rag.store.models.chunk import Chunk
22
- from haiku.rag.store.models.document import Document
23
-
24
-
25
- class HaikuRAGApp:
26
- def __init__(self, db_path: Path):
27
- self.db_path = db_path
28
- self.console = Console()
29
-
30
- async def info(self):
31
- """Display read-only information about the database without modifying it."""
32
-
33
- import lancedb
34
-
35
- # Basic: show path
36
- self.console.print("[bold]haiku.rag database info[/bold]")
37
- self.console.print(
38
- f" [repr.attrib_name]path[/repr.attrib_name]: {self.db_path}"
39
- )
40
-
41
- if not self.db_path.exists():
42
- self.console.print("[red]Database path does not exist.[/red]")
43
- return
44
-
45
- # Connect without going through Store to avoid upgrades/validation writes
46
- try:
47
- db = lancedb.connect(self.db_path)
48
- table_names = set(db.table_names())
49
- except Exception as e:
50
- self.console.print(f"[red]Failed to open database: {e}[/red]")
51
- return
52
-
53
- try:
54
- ldb_version = pkg_version("lancedb")
55
- except Exception:
56
- ldb_version = "unknown"
57
- try:
58
- hr_version = pkg_version("haiku.rag")
59
- except Exception:
60
- hr_version = "unknown"
61
- try:
62
- docling_version = pkg_version("docling")
63
- except Exception:
64
- docling_version = "unknown"
65
-
66
- # Read settings (if present) to find stored haiku.rag version and embedding config
67
- stored_version = "unknown"
68
- embed_provider: str | None = None
69
- embed_model: str | None = None
70
- vector_dim: int | None = None
71
-
72
- if "settings" in table_names:
73
- settings_tbl = db.open_table("settings")
74
- arrow = settings_tbl.search().where("id = 'settings'").limit(1).to_arrow()
75
- rows = arrow.to_pylist() if arrow is not None else []
76
- if rows:
77
- raw = rows[0].get("settings") or "{}"
78
- data = json.loads(raw) if isinstance(raw, str) else (raw or {})
79
- stored_version = str(data.get("version", stored_version))
80
- embed_provider = data.get("EMBEDDINGS_PROVIDER")
81
- embed_model = data.get("EMBEDDINGS_MODEL")
82
- vector_dim = (
83
- int(data.get("EMBEDDINGS_VECTOR_DIM")) # pyright: ignore[reportArgumentType]
84
- if data.get("EMBEDDINGS_VECTOR_DIM") is not None
85
- else None
86
- )
87
-
88
- num_docs = 0
89
- if "documents" in table_names:
90
- docs_tbl = db.open_table("documents")
91
- num_docs = int(docs_tbl.count_rows()) # type: ignore[attr-defined]
92
-
93
- # Table versions per table (direct API)
94
- doc_versions = (
95
- len(list(db.open_table("documents").list_versions()))
96
- if "documents" in table_names
97
- else 0
98
- )
99
- chunk_versions = (
100
- len(list(db.open_table("chunks").list_versions()))
101
- if "chunks" in table_names
102
- else 0
103
- )
104
-
105
- self.console.print(
106
- f" [repr.attrib_name]haiku.rag version (db)[/repr.attrib_name]: {stored_version}"
107
- )
108
- if embed_provider or embed_model or vector_dim:
109
- provider_part = embed_provider or "unknown"
110
- model_part = embed_model or "unknown"
111
- dim_part = f"{vector_dim}" if vector_dim is not None else "unknown"
112
- self.console.print(
113
- " [repr.attrib_name]embeddings[/repr.attrib_name]: "
114
- f"{provider_part}/{model_part} (dim: {dim_part})"
115
- )
116
- else:
117
- self.console.print(
118
- " [repr.attrib_name]embeddings[/repr.attrib_name]: unknown"
119
- )
120
- self.console.print(
121
- f" [repr.attrib_name]documents[/repr.attrib_name]: {num_docs}"
122
- )
123
- self.console.print(
124
- f" [repr.attrib_name]versions (documents)[/repr.attrib_name]: {doc_versions}"
125
- )
126
- self.console.print(
127
- f" [repr.attrib_name]versions (chunks)[/repr.attrib_name]: {chunk_versions}"
128
- )
129
- self.console.rule()
130
- self.console.print("[bold]Versions[/bold]")
131
- self.console.print(
132
- f" [repr.attrib_name]haiku.rag[/repr.attrib_name]: {hr_version}"
133
- )
134
- self.console.print(
135
- f" [repr.attrib_name]lancedb[/repr.attrib_name]: {ldb_version}"
136
- )
137
- self.console.print(
138
- f" [repr.attrib_name]docling[/repr.attrib_name]: {docling_version}"
139
- )
140
-
141
- async def list_documents(self):
142
- async with HaikuRAG(db_path=self.db_path) as self.client:
143
- documents = await self.client.list_documents()
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) 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) as self.client:
159
- doc = await self.client.create_document_from_source(
160
- source, title=title, metadata=metadata
161
- )
162
- self._rich_print_document(doc, truncate=True)
163
- self.console.print(
164
- f"[bold green]Document {doc.id} added successfully.[/bold green]"
165
- )
166
-
167
- async def get_document(self, doc_id: str):
168
- async with HaikuRAG(db_path=self.db_path) as self.client:
169
- doc = await self.client.get_document_by_id(doc_id)
170
- if doc is None:
171
- self.console.print(f"[red]Document with id {doc_id} not found.[/red]")
172
- return
173
- self._rich_print_document(doc, truncate=False)
174
-
175
- async def delete_document(self, doc_id: str):
176
- async with HaikuRAG(db_path=self.db_path) as self.client:
177
- deleted = await self.client.delete_document(doc_id)
178
- if deleted:
179
- self.console.print(
180
- f"[bold green]Document {doc_id} deleted successfully.[/bold green]"
181
- )
182
- else:
183
- self.console.print(
184
- f"[yellow]Document with id {doc_id} not found.[/yellow]"
185
- )
186
-
187
- async def search(self, query: str, limit: int = 5):
188
- async with HaikuRAG(db_path=self.db_path) as self.client:
189
- results = await self.client.search(query, limit=limit)
190
- if not results:
191
- self.console.print("[yellow]No results found.[/yellow]")
192
- return
193
- for chunk, score in results:
194
- self._rich_print_search_result(chunk, score)
195
-
196
- async def ask(self, question: str, cite: bool = False):
197
- async with HaikuRAG(db_path=self.db_path) as self.client:
198
- try:
199
- answer = await self.client.ask(question, cite=cite)
200
- self.console.print(f"[bold blue]Question:[/bold blue] {question}")
201
- self.console.print()
202
- self.console.print("[bold green]Answer:[/bold green]")
203
- self.console.print(Markdown(answer))
204
- except Exception as e:
205
- self.console.print(f"[red]Error: {e}[/red]")
206
-
207
- async def research(
208
- self,
209
- question: str,
210
- max_iterations: int = 3,
211
- confidence_threshold: float = 0.8,
212
- max_concurrency: int = 1,
213
- verbose: bool = False,
214
- ):
215
- """Run research via the pydantic-graph pipeline (default)."""
216
- async with HaikuRAG(db_path=self.db_path) as client:
217
- try:
218
- if verbose:
219
- self.console.print("[bold cyan]Starting research[/bold cyan]")
220
- self.console.print(f"[bold blue]Question:[/bold blue] {question}")
221
- self.console.print()
222
-
223
- graph = build_research_graph()
224
- state = ResearchState(
225
- question=question,
226
- context=ResearchContext(original_question=question),
227
- max_iterations=max_iterations,
228
- confidence_threshold=confidence_threshold,
229
- max_concurrency=max_concurrency,
230
- )
231
- deps = ResearchDeps(
232
- client=client, console=self.console if verbose else None
233
- )
234
-
235
- start = PlanNode(
236
- provider=Config.RESEARCH_PROVIDER or Config.QA_PROVIDER,
237
- model=Config.RESEARCH_MODEL or Config.QA_MODEL,
238
- )
239
- # Prefer graph.run; fall back to iter if unavailable
240
- report = None
241
- try:
242
- result = await graph.run(start, state=state, deps=deps)
243
- report = result.output
244
- except Exception:
245
- from pydantic_graph import End
246
-
247
- async with graph.iter(start, state=state, deps=deps) as run:
248
- node = run.next_node
249
- while not isinstance(node, End):
250
- node = await run.next(node)
251
- if run.result:
252
- report = run.result.output
253
- if report is None:
254
- raise RuntimeError("Graph did not produce a report")
255
-
256
- # Display the report
257
- self.console.print("[bold green]Research Report[/bold green]")
258
- self.console.rule()
259
-
260
- # Title and Executive Summary
261
- self.console.print(f"[bold]{report.title}[/bold]")
262
- self.console.print()
263
- self.console.print("[bold cyan]Executive Summary:[/bold cyan]")
264
- self.console.print(report.executive_summary)
265
- self.console.print()
266
-
267
- # Confidence (from last evaluation)
268
- if state.last_eval:
269
- conf = state.last_eval.confidence_score # type: ignore[attr-defined]
270
- self.console.print(f"[bold cyan]Confidence:[/bold cyan] {conf:.1%}")
271
- self.console.print()
272
-
273
- # Main Findings
274
- if report.main_findings:
275
- self.console.print("[bold cyan]Main Findings:[/bold cyan]")
276
- for finding in report.main_findings:
277
- self.console.print(f"• {finding}")
278
- self.console.print()
279
-
280
- # (Themes section removed)
281
-
282
- # Conclusions
283
- if report.conclusions:
284
- self.console.print("[bold cyan]Conclusions:[/bold cyan]")
285
- for conclusion in report.conclusions:
286
- self.console.print(f"• {conclusion}")
287
- self.console.print()
288
-
289
- # Recommendations
290
- if report.recommendations:
291
- self.console.print("[bold cyan]Recommendations:[/bold cyan]")
292
- for rec in report.recommendations:
293
- self.console.print(f"• {rec}")
294
- self.console.print()
295
-
296
- # Limitations
297
- if report.limitations:
298
- self.console.print("[bold yellow]Limitations:[/bold yellow]")
299
- for limitation in report.limitations:
300
- self.console.print(f"• {limitation}")
301
- self.console.print()
302
-
303
- # Sources Summary
304
- if report.sources_summary:
305
- self.console.print("[bold cyan]Sources:[/bold cyan]")
306
- self.console.print(report.sources_summary)
307
-
308
- except Exception as e:
309
- self.console.print(f"[red]Error during research: {e}[/red]")
310
-
311
- async def rebuild(self):
312
- async with HaikuRAG(db_path=self.db_path, skip_validation=True) as client:
313
- try:
314
- documents = await client.list_documents()
315
- total_docs = len(documents)
316
-
317
- if total_docs == 0:
318
- self.console.print(
319
- "[yellow]No documents found in database.[/yellow]"
320
- )
321
- return
322
-
323
- self.console.print(
324
- f"[bold cyan]Rebuilding database with {total_docs} documents...[/bold cyan]"
325
- )
326
- with Progress() as progress:
327
- task = progress.add_task("Rebuilding...", total=total_docs)
328
- async for _ in client.rebuild_database():
329
- progress.update(task, advance=1)
330
-
331
- self.console.print(
332
- "[bold green]Database rebuild completed successfully.[/bold green]"
333
- )
334
- except Exception as e:
335
- self.console.print(f"[red]Error rebuilding database: {e}[/red]")
336
-
337
- async def vacuum(self):
338
- """Run database maintenance: optimize and cleanup table history."""
339
- try:
340
- async with HaikuRAG(db_path=self.db_path, skip_validation=True) as client:
341
- await client.vacuum()
342
- self.console.print(
343
- "[bold green]Vacuum completed successfully.[/bold green]"
344
- )
345
- except Exception as e:
346
- self.console.print(f"[red]Error during vacuum: {e}[/red]")
347
-
348
- def show_settings(self):
349
- """Display current configuration settings."""
350
- self.console.print("[bold]haiku.rag configuration[/bold]")
351
- self.console.print()
352
-
353
- # Get all config fields dynamically
354
- for field_name, field_value in Config.model_dump().items():
355
- # Format the display value
356
- if isinstance(field_value, str) and (
357
- "key" in field_name.lower()
358
- or "password" in field_name.lower()
359
- or "token" in field_name.lower()
360
- ):
361
- # Hide sensitive values but show if they're set
362
- display_value = "✓ Set" if field_value else "✗ Not set"
363
- else:
364
- display_value = field_value
365
-
366
- self.console.print(
367
- f" [repr.attrib_name]{field_name}[/repr.attrib_name]: {display_value}"
368
- )
369
-
370
- def _rich_print_document(self, doc: Document, truncate: bool = False):
371
- """Format a document for display."""
372
- if truncate:
373
- content = doc.content.splitlines()
374
- if len(content) > 3:
375
- content = content[:3] + ["\n…"]
376
- content = "\n".join(content)
377
- content = Markdown(content)
378
- else:
379
- content = Markdown(doc.content)
380
- title_part = (
381
- f" [repr.attrib_name]title[/repr.attrib_name]: {doc.title}"
382
- if doc.title
383
- else ""
384
- )
385
- self.console.print(
386
- f"[repr.attrib_name]id[/repr.attrib_name]: {doc.id} "
387
- f"[repr.attrib_name]uri[/repr.attrib_name]: {doc.uri}"
388
- + title_part
389
- + f" [repr.attrib_name]meta[/repr.attrib_name]: {doc.metadata}"
390
- )
391
- self.console.print(
392
- f"[repr.attrib_name]created at[/repr.attrib_name]: {doc.created_at} [repr.attrib_name]updated at[/repr.attrib_name]: {doc.updated_at}"
393
- )
394
- self.console.print("[repr.attrib_name]content[/repr.attrib_name]:")
395
- self.console.print(content)
396
- self.console.rule()
397
-
398
- def _rich_print_search_result(self, chunk: Chunk, score: float):
399
- """Format a search result chunk for display."""
400
- content = Markdown(chunk.content)
401
- self.console.print(
402
- f"[repr.attrib_name]document_id[/repr.attrib_name]: {chunk.document_id} "
403
- f"[repr.attrib_name]score[/repr.attrib_name]: {score:.4f}"
404
- )
405
- if chunk.document_uri:
406
- self.console.print("[repr.attrib_name]document uri[/repr.attrib_name]:")
407
- self.console.print(chunk.document_uri)
408
- if chunk.document_title:
409
- self.console.print("[repr.attrib_name]document title[/repr.attrib_name]:")
410
- self.console.print(chunk.document_title)
411
- if chunk.document_meta:
412
- self.console.print("[repr.attrib_name]document meta[/repr.attrib_name]:")
413
- self.console.print(chunk.document_meta)
414
- self.console.print("[repr.attrib_name]content[/repr.attrib_name]:")
415
- self.console.print(content)
416
- self.console.rule()
417
-
418
- async def serve(self, transport: str | None = None):
419
- """Start the MCP server."""
420
- async with HaikuRAG(self.db_path) as client:
421
- monitor = FileWatcher(paths=Config.MONITOR_DIRECTORIES, client=client)
422
- monitor_task = asyncio.create_task(monitor.observe())
423
- server = create_mcp_server(self.db_path)
424
-
425
- try:
426
- if transport == "stdio":
427
- await server.run_stdio_async()
428
- else:
429
- await server.run_http_async(transport="streamable-http")
430
- except KeyboardInterrupt:
431
- pass
432
- finally:
433
- monitor_task.cancel()
434
- try:
435
- await monitor_task
436
- except asyncio.CancelledError:
437
- pass
haiku/rag/chunker.py DELETED
@@ -1,51 +0,0 @@
1
- from typing import ClassVar
2
-
3
- import tiktoken
4
- from docling.chunking import HybridChunker # type: ignore
5
- from docling_core.transforms.chunker.tokenizer.openai import OpenAITokenizer
6
- from docling_core.types.doc.document import DoclingDocument
7
-
8
- from haiku.rag.config import Config
9
-
10
-
11
- class Chunker:
12
- """A class that chunks text into smaller pieces for embedding and retrieval.
13
-
14
- Uses docling's structure-aware chunking to create semantically meaningful chunks
15
- that respect document boundaries.
16
-
17
- Args:
18
- chunk_size: The maximum size of a chunk in tokens.
19
- """
20
-
21
- encoder: ClassVar[tiktoken.Encoding] = tiktoken.encoding_for_model("gpt-4o")
22
-
23
- def __init__(
24
- self,
25
- chunk_size: int = Config.CHUNK_SIZE,
26
- ):
27
- self.chunk_size = chunk_size
28
- tokenizer = OpenAITokenizer(
29
- tokenizer=tiktoken.encoding_for_model("gpt-4o"), max_tokens=chunk_size
30
- )
31
-
32
- self.chunker = HybridChunker(tokenizer=tokenizer) # type: ignore
33
-
34
- async def chunk(self, document: DoclingDocument) -> list[str]:
35
- """Split the document into chunks using docling's structure-aware chunking.
36
-
37
- Args:
38
- document: The DoclingDocument to be split into chunks.
39
-
40
- Returns:
41
- A list of text chunks with semantic boundaries.
42
- """
43
- if document is None:
44
- return []
45
-
46
- # Chunk using docling's hybrid chunker
47
- chunks = list(self.chunker.chunk(document))
48
- return [self.chunker.contextualize(chunk) for chunk in chunks]
49
-
50
-
51
- chunker = Chunker()