haiku.rag 0.10.0__tar.gz → 0.10.1__tar.gz
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 might be problematic. Click here for more details.
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/PKG-INFO +1 -1
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/agents.md +2 -1
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/cli.md +4 -3
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/configuration.md +10 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/index.md +2 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/mcp.md +1 -4
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/python.md +9 -3
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/server.md +0 -1
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/pyproject.toml +1 -1
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/app.py +14 -5
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/cli.py +55 -30
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/client.py +63 -21
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/config.py +4 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/mcp.py +18 -6
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/qa/agent.py +4 -2
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/qa/prompts.py +2 -2
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/models.py +2 -2
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/nodes/search.py +3 -1
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/prompts.py +4 -3
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/engine.py +14 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/models/chunk.py +1 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/models/document.py +1 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/repositories/chunk.py +4 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/repositories/document.py +3 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/upgrades/__init__.py +2 -0
- haiku_rag-0.10.1/src/haiku/rag/store/upgrades/v0_10_1.py +64 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/utils.py +8 -5
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_app.py +4 -4
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_cli.py +26 -34
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_client.py +49 -8
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_search.py +32 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/uv.lock +1 -1
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.github/FUNDING.yml +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.github/workflows/build-docs.yml +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.github/workflows/build-publish.yml +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.gitignore +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.pre-commit-config.yaml +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.python-version +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/LICENSE +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/README.md +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/benchmarks.md +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/installation.md +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/mkdocs.yml +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/chunker.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/base.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/ollama.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/openai.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/vllm.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/voyageai.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/logging.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/migration.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/monitor.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/qa/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reader.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/base.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/cohere.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/mxbai.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/vllm.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/common.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/dependencies.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/graph.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/nodes/evaluate.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/nodes/plan.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/nodes/synthesize.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/state.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/models/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/repositories/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/repositories/settings.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/upgrades/v0_9_3.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/__init__.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/conftest.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/generate_benchmark_db.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/llm_judge.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_chunk.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_chunker.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_document.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_embedder.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_lancedb_connection.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_monitor.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_preprocessor.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_qa.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_reader.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_rebuild.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_reranker.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_research_graph.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_research_graph_integration.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_settings.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_utils.py +0 -0
- {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_versioning.py +0 -0
|
@@ -13,7 +13,8 @@ The simple QA agent answers a single question using the knowledge base. It retri
|
|
|
13
13
|
Key points:
|
|
14
14
|
|
|
15
15
|
- Uses a single `search_documents` tool to fetch relevant chunks
|
|
16
|
-
- Can be run with or without inline citations in the prompt
|
|
16
|
+
- Can be run with or without inline citations in the prompt (citations prefer
|
|
17
|
+
document titles when present, otherwise URIs)
|
|
17
18
|
- Returns a plain string answer
|
|
18
19
|
|
|
19
20
|
Python usage:
|
|
@@ -33,6 +33,9 @@ From file or URL:
|
|
|
33
33
|
```bash
|
|
34
34
|
haiku-rag add-src /path/to/document.pdf
|
|
35
35
|
haiku-rag add-src https://example.com/article.html
|
|
36
|
+
|
|
37
|
+
# Optionally set a human‑readable title stored in the DB schema
|
|
38
|
+
haiku-rag add-src /mnt/data/doc1.pdf --title "Q3 Financial Report"
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
!!! note
|
|
@@ -83,6 +86,7 @@ haiku-rag ask "Who is the author of haiku.rag?" --cite
|
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
The QA agent will search your documents for relevant information and provide a comprehensive answer. With `--cite`, responses include citations showing which documents were used.
|
|
89
|
+
When available, citations use the document title; otherwise they fall back to the URI.
|
|
86
90
|
|
|
87
91
|
## Research
|
|
88
92
|
|
|
@@ -111,9 +115,6 @@ haiku-rag serve
|
|
|
111
115
|
|
|
112
116
|
# stdio transport
|
|
113
117
|
haiku-rag serve --stdio
|
|
114
|
-
|
|
115
|
-
# SSE transport
|
|
116
|
-
haiku-rag serve --sse
|
|
117
118
|
```
|
|
118
119
|
|
|
119
120
|
## Settings
|
|
@@ -211,6 +211,16 @@ Authentication is handled through standard cloud provider credentials (AWS CLI,
|
|
|
211
211
|
|
|
212
212
|
**Note:** Table optimization is automatically handled by LanceDB Cloud (`db://` URIs) and is disabled for better performance. For object storage backends (S3, Azure, GCS), optimization is still performed locally.
|
|
213
213
|
|
|
214
|
+
#### Disable database auto-creation
|
|
215
|
+
|
|
216
|
+
By default, haiku.rag creates the local LanceDB directory and required tables on first use. To prevent accidental database creation and fail fast if a database hasn’t been set up yet, set:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
DISABLE_DB_AUTOCREATE=true
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
When enabled, for local paths, haiku.rag errors if the LanceDB directory does not exist, and it will not create parent directories.
|
|
223
|
+
|
|
214
224
|
### Document Processing
|
|
215
225
|
|
|
216
226
|
```bash
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
- **Extended file format support**: Parse 40+ file formats including PDF, DOCX, HTML, Markdown, code files and more. Or add a URL!
|
|
16
16
|
- **MCP server**: Exposes functionality as MCP tools
|
|
17
17
|
- **CLI commands**: Access all functionality from your terminal
|
|
18
|
+
- Add sources from text, files, or URLs, optionally with a human‑readable title
|
|
18
19
|
- **Python client**: Call `haiku.rag` from your own python applications
|
|
19
20
|
|
|
20
21
|
## Quick Start
|
|
@@ -42,6 +43,7 @@ async with HaikuRAG("database.lancedb") as client:
|
|
|
42
43
|
Or use the CLI:
|
|
43
44
|
```bash
|
|
44
45
|
haiku-rag add "Your document content"
|
|
46
|
+
haiku-rag add-src /path/to/document.pdf --title "Q3 Financial Report"
|
|
45
47
|
haiku-rag search "query"
|
|
46
48
|
haiku-rag ask "Who is the author of haiku.rag?"
|
|
47
49
|
haiku-rag migrate old_database.sqlite # Migrate from SQLite
|
|
@@ -19,7 +19,7 @@ The MCP server exposes `haiku.rag` as MCP tools for compatible MCP clients.
|
|
|
19
19
|
|
|
20
20
|
## Starting MCP Server
|
|
21
21
|
|
|
22
|
-
The MCP server starts automatically with the serve command and supports Streamable HTTP
|
|
22
|
+
The MCP server starts automatically with the serve command and supports Streamable HTTP and stdio transports:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# Default streamable HTTP transport
|
|
@@ -27,7 +27,4 @@ haiku-rag serve
|
|
|
27
27
|
|
|
28
28
|
# stdio transport (for Claude Desktop)
|
|
29
29
|
haiku-rag serve --stdio
|
|
30
|
-
|
|
31
|
-
# SSE transport
|
|
32
|
-
haiku-rag serve --sse
|
|
33
30
|
```
|
|
@@ -23,6 +23,7 @@ From text:
|
|
|
23
23
|
doc = await client.create_document(
|
|
24
24
|
content="Your document content here",
|
|
25
25
|
uri="doc://example",
|
|
26
|
+
title="My Example Document", # optional human‑readable title
|
|
26
27
|
metadata={"source": "manual", "topic": "example"}
|
|
27
28
|
)
|
|
28
29
|
```
|
|
@@ -54,12 +55,16 @@ doc = await client.create_document(
|
|
|
54
55
|
|
|
55
56
|
From file:
|
|
56
57
|
```python
|
|
57
|
-
doc = await client.create_document_from_source(
|
|
58
|
+
doc = await client.create_document_from_source(
|
|
59
|
+
"path/to/document.pdf", title="Project Brief"
|
|
60
|
+
)
|
|
58
61
|
```
|
|
59
62
|
|
|
60
63
|
From URL:
|
|
61
64
|
```python
|
|
62
|
-
doc = await client.create_document_from_source(
|
|
65
|
+
doc = await client.create_document_from_source(
|
|
66
|
+
"https://example.com/article.html", title="Example Article"
|
|
67
|
+
)
|
|
63
68
|
```
|
|
64
69
|
|
|
65
70
|
### Retrieving Documents
|
|
@@ -159,6 +164,7 @@ for chunk, relevance_score in results:
|
|
|
159
164
|
print(f"Content: {chunk.content}")
|
|
160
165
|
print(f"From document: {chunk.document_id}")
|
|
161
166
|
print(f"Document URI: {chunk.document_uri}")
|
|
167
|
+
print(f"Document Title: {chunk.document_title}") # when available
|
|
162
168
|
print(f"Document metadata: {chunk.document_meta}")
|
|
163
169
|
```
|
|
164
170
|
|
|
@@ -201,7 +207,7 @@ answer = await client.ask("Who is the author of haiku.rag?", cite=True)
|
|
|
201
207
|
print(answer)
|
|
202
208
|
```
|
|
203
209
|
|
|
204
|
-
The QA agent will search your documents for relevant information and use the configured LLM to generate a comprehensive answer. With `cite=True`, responses include citations showing which documents were used as sources.
|
|
210
|
+
The QA agent will search your documents for relevant information and use the configured LLM to generate a comprehensive answer. With `cite=True`, responses include citations showing which documents were used as sources. Citations prefer the document title when present, otherwise they use the URI.
|
|
205
211
|
|
|
206
212
|
The QA provider and model can be configured via environment variables (see [Configuration](configuration.md)).
|
|
207
213
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
name = "haiku.rag"
|
|
4
4
|
description = "Agentic Retrieval Augmented Generation (RAG) with LanceDB"
|
|
5
|
-
version = "0.10.
|
|
5
|
+
version = "0.10.1"
|
|
6
6
|
authors = [{ name = "Yiorgis Gozadinos", email = "ggozadinos@gmail.com" }]
|
|
7
7
|
license = { text = "MIT" }
|
|
8
8
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
@@ -39,9 +39,9 @@ class HaikuRAGApp:
|
|
|
39
39
|
f"[b]Document with id [cyan]{doc.id}[/cyan] added successfully.[/b]"
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
async def add_document_from_source(self, source: str):
|
|
42
|
+
async def add_document_from_source(self, source: str, title: str | None = None):
|
|
43
43
|
async with HaikuRAG(db_path=self.db_path) as self.client:
|
|
44
|
-
doc = await self.client.create_document_from_source(source)
|
|
44
|
+
doc = await self.client.create_document_from_source(source, title=title)
|
|
45
45
|
self._rich_print_document(doc, truncate=True)
|
|
46
46
|
self.console.print(
|
|
47
47
|
f"[b]Document with id [cyan]{doc.id}[/cyan] added successfully.[/b]"
|
|
@@ -252,8 +252,16 @@ class HaikuRAGApp:
|
|
|
252
252
|
content = Markdown(content)
|
|
253
253
|
else:
|
|
254
254
|
content = Markdown(doc.content)
|
|
255
|
+
title_part = (
|
|
256
|
+
f" [repr.attrib_name]title[/repr.attrib_name]: {doc.title}"
|
|
257
|
+
if doc.title
|
|
258
|
+
else ""
|
|
259
|
+
)
|
|
255
260
|
self.console.print(
|
|
256
|
-
f"[repr.attrib_name]id[/repr.attrib_name]: {doc.id}
|
|
261
|
+
f"[repr.attrib_name]id[/repr.attrib_name]: {doc.id} "
|
|
262
|
+
f"[repr.attrib_name]uri[/repr.attrib_name]: {doc.uri}"
|
|
263
|
+
+ title_part
|
|
264
|
+
+ f" [repr.attrib_name]meta[/repr.attrib_name]: {doc.metadata}"
|
|
257
265
|
)
|
|
258
266
|
self.console.print(
|
|
259
267
|
f"[repr.attrib_name]created at[/repr.attrib_name]: {doc.created_at} [repr.attrib_name]updated at[/repr.attrib_name]: {doc.updated_at}"
|
|
@@ -272,6 +280,9 @@ class HaikuRAGApp:
|
|
|
272
280
|
if chunk.document_uri:
|
|
273
281
|
self.console.print("[repr.attrib_name]document uri[/repr.attrib_name]:")
|
|
274
282
|
self.console.print(chunk.document_uri)
|
|
283
|
+
if chunk.document_title:
|
|
284
|
+
self.console.print("[repr.attrib_name]document title[/repr.attrib_name]:")
|
|
285
|
+
self.console.print(chunk.document_title)
|
|
275
286
|
if chunk.document_meta:
|
|
276
287
|
self.console.print("[repr.attrib_name]document meta[/repr.attrib_name]:")
|
|
277
288
|
self.console.print(chunk.document_meta)
|
|
@@ -289,8 +300,6 @@ class HaikuRAGApp:
|
|
|
289
300
|
try:
|
|
290
301
|
if transport == "stdio":
|
|
291
302
|
await server.run_stdio_async()
|
|
292
|
-
elif transport == "sse":
|
|
293
|
-
await server.run_sse_async()
|
|
294
303
|
else:
|
|
295
304
|
await server.run_http_async(transport="streamable-http")
|
|
296
305
|
except KeyboardInterrupt:
|
|
@@ -3,28 +3,16 @@ import warnings
|
|
|
3
3
|
from importlib.metadata import version
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
import logfire
|
|
7
6
|
import typer
|
|
8
|
-
from rich.console import Console
|
|
9
7
|
|
|
10
|
-
from haiku.rag.app import HaikuRAGApp
|
|
11
8
|
from haiku.rag.config import Config
|
|
12
9
|
from haiku.rag.logging import configure_cli_logging
|
|
13
|
-
from haiku.rag.migration import migrate_sqlite_to_lancedb
|
|
14
10
|
from haiku.rag.utils import is_up_to_date
|
|
15
11
|
|
|
16
|
-
if Config.ENV == "development":
|
|
17
|
-
logfire.configure(send_to_logfire="if-token-present")
|
|
18
|
-
logfire.instrument_pydantic_ai()
|
|
19
|
-
else:
|
|
20
|
-
warnings.filterwarnings("ignore")
|
|
21
|
-
|
|
22
12
|
cli = typer.Typer(
|
|
23
13
|
context_settings={"help_option_names": ["-h", "--help"]}, no_args_is_help=True
|
|
24
14
|
)
|
|
25
15
|
|
|
26
|
-
console = Console()
|
|
27
|
-
|
|
28
16
|
|
|
29
17
|
def complete_document_ids(ctx: typer.Context, incomplete: str):
|
|
30
18
|
"""Autocomplete document IDs from the selected DB."""
|
|
@@ -89,16 +77,16 @@ async def check_version():
|
|
|
89
77
|
"""Check if haiku.rag is up to date and show warning if not."""
|
|
90
78
|
up_to_date, current_version, latest_version = await is_up_to_date()
|
|
91
79
|
if not up_to_date:
|
|
92
|
-
|
|
93
|
-
f"
|
|
80
|
+
typer.echo(
|
|
81
|
+
f"Warning: haiku.rag is outdated. Current: {current_version}, Latest: {latest_version}",
|
|
94
82
|
)
|
|
95
|
-
|
|
83
|
+
typer.echo("Please update.")
|
|
96
84
|
|
|
97
85
|
|
|
98
86
|
def version_callback(value: bool):
|
|
99
87
|
if value:
|
|
100
88
|
v = version("haiku.rag")
|
|
101
|
-
|
|
89
|
+
typer.echo(f"haiku.rag version {v}")
|
|
102
90
|
raise typer.Exit()
|
|
103
91
|
|
|
104
92
|
|
|
@@ -113,10 +101,26 @@ def main(
|
|
|
113
101
|
),
|
|
114
102
|
):
|
|
115
103
|
"""haiku.rag CLI - Vector database RAG system"""
|
|
116
|
-
#
|
|
117
|
-
|
|
104
|
+
# Configure logging minimally for CLI context
|
|
105
|
+
if Config.ENV == "development":
|
|
106
|
+
# Lazy import logfire only in development
|
|
107
|
+
try:
|
|
108
|
+
import logfire # type: ignore
|
|
109
|
+
|
|
110
|
+
logfire.configure(send_to_logfire="if-token-present")
|
|
111
|
+
logfire.instrument_pydantic_ai()
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
else:
|
|
115
|
+
configure_cli_logging()
|
|
116
|
+
warnings.filterwarnings("ignore")
|
|
117
|
+
|
|
118
118
|
# Run version check before any command
|
|
119
|
-
|
|
119
|
+
try:
|
|
120
|
+
asyncio.run(check_version())
|
|
121
|
+
except Exception:
|
|
122
|
+
# Do not block CLI on version check issues
|
|
123
|
+
pass
|
|
120
124
|
|
|
121
125
|
|
|
122
126
|
@cli.command("list", help="List all stored documents")
|
|
@@ -127,6 +131,8 @@ def list_documents(
|
|
|
127
131
|
help="Path to the LanceDB database file",
|
|
128
132
|
),
|
|
129
133
|
):
|
|
134
|
+
from haiku.rag.app import HaikuRAGApp
|
|
135
|
+
|
|
130
136
|
app = HaikuRAGApp(db_path=db)
|
|
131
137
|
asyncio.run(app.list_documents())
|
|
132
138
|
|
|
@@ -142,6 +148,8 @@ def add_document_text(
|
|
|
142
148
|
help="Path to the LanceDB database file",
|
|
143
149
|
),
|
|
144
150
|
):
|
|
151
|
+
from haiku.rag.app import HaikuRAGApp
|
|
152
|
+
|
|
145
153
|
app = HaikuRAGApp(db_path=db)
|
|
146
154
|
asyncio.run(app.add_document_from_text(text=text))
|
|
147
155
|
|
|
@@ -152,14 +160,21 @@ def add_document_src(
|
|
|
152
160
|
help="The file path or URL of the document to add",
|
|
153
161
|
autocompletion=complete_local_paths,
|
|
154
162
|
),
|
|
163
|
+
title: str | None = typer.Option(
|
|
164
|
+
None,
|
|
165
|
+
"--title",
|
|
166
|
+
help="Optional human-readable title to store with the document",
|
|
167
|
+
),
|
|
155
168
|
db: Path = typer.Option(
|
|
156
169
|
Config.DEFAULT_DATA_DIR / "haiku.rag.lancedb",
|
|
157
170
|
"--db",
|
|
158
171
|
help="Path to the LanceDB database file",
|
|
159
172
|
),
|
|
160
173
|
):
|
|
174
|
+
from haiku.rag.app import HaikuRAGApp
|
|
175
|
+
|
|
161
176
|
app = HaikuRAGApp(db_path=db)
|
|
162
|
-
asyncio.run(app.add_document_from_source(source=source))
|
|
177
|
+
asyncio.run(app.add_document_from_source(source=source, title=title))
|
|
163
178
|
|
|
164
179
|
|
|
165
180
|
@cli.command("get", help="Get and display a document by its ID")
|
|
@@ -174,6 +189,8 @@ def get_document(
|
|
|
174
189
|
help="Path to the LanceDB database file",
|
|
175
190
|
),
|
|
176
191
|
):
|
|
192
|
+
from haiku.rag.app import HaikuRAGApp
|
|
193
|
+
|
|
177
194
|
app = HaikuRAGApp(db_path=db)
|
|
178
195
|
asyncio.run(app.get_document(doc_id=doc_id))
|
|
179
196
|
|
|
@@ -190,6 +207,8 @@ def delete_document(
|
|
|
190
207
|
help="Path to the LanceDB database file",
|
|
191
208
|
),
|
|
192
209
|
):
|
|
210
|
+
from haiku.rag.app import HaikuRAGApp
|
|
211
|
+
|
|
193
212
|
app = HaikuRAGApp(db_path=db)
|
|
194
213
|
asyncio.run(app.delete_document(doc_id=doc_id))
|
|
195
214
|
|
|
@@ -215,6 +234,8 @@ def search(
|
|
|
215
234
|
help="Path to the LanceDB database file",
|
|
216
235
|
),
|
|
217
236
|
):
|
|
237
|
+
from haiku.rag.app import HaikuRAGApp
|
|
238
|
+
|
|
218
239
|
app = HaikuRAGApp(db_path=db)
|
|
219
240
|
asyncio.run(app.search(query=query, limit=limit))
|
|
220
241
|
|
|
@@ -235,6 +256,8 @@ def ask(
|
|
|
235
256
|
help="Include citations in the response",
|
|
236
257
|
),
|
|
237
258
|
):
|
|
259
|
+
from haiku.rag.app import HaikuRAGApp
|
|
260
|
+
|
|
238
261
|
app = HaikuRAGApp(db_path=db)
|
|
239
262
|
asyncio.run(app.ask(question=question, cite=cite))
|
|
240
263
|
|
|
@@ -271,6 +294,8 @@ def research(
|
|
|
271
294
|
help="Show verbose progress output",
|
|
272
295
|
),
|
|
273
296
|
):
|
|
297
|
+
from haiku.rag.app import HaikuRAGApp
|
|
298
|
+
|
|
274
299
|
app = HaikuRAGApp(db_path=db)
|
|
275
300
|
asyncio.run(
|
|
276
301
|
app.research(
|
|
@@ -285,6 +310,8 @@ def research(
|
|
|
285
310
|
|
|
286
311
|
@cli.command("settings", help="Display current configuration settings")
|
|
287
312
|
def settings():
|
|
313
|
+
from haiku.rag.app import HaikuRAGApp
|
|
314
|
+
|
|
288
315
|
app = HaikuRAGApp(db_path=Path()) # Don't need actual DB for settings
|
|
289
316
|
app.show_settings()
|
|
290
317
|
|
|
@@ -300,6 +327,8 @@ def rebuild(
|
|
|
300
327
|
help="Path to the LanceDB database file",
|
|
301
328
|
),
|
|
302
329
|
):
|
|
330
|
+
from haiku.rag.app import HaikuRAGApp
|
|
331
|
+
|
|
303
332
|
app = HaikuRAGApp(db_path=db)
|
|
304
333
|
asyncio.run(app.rebuild())
|
|
305
334
|
|
|
@@ -312,6 +341,8 @@ def vacuum(
|
|
|
312
341
|
help="Path to the LanceDB database file",
|
|
313
342
|
),
|
|
314
343
|
):
|
|
344
|
+
from haiku.rag.app import HaikuRAGApp
|
|
345
|
+
|
|
315
346
|
app = HaikuRAGApp(db_path=db)
|
|
316
347
|
asyncio.run(app.vacuum())
|
|
317
348
|
|
|
@@ -330,24 +361,15 @@ def serve(
|
|
|
330
361
|
"--stdio",
|
|
331
362
|
help="Run MCP server on stdio Transport",
|
|
332
363
|
),
|
|
333
|
-
sse: bool = typer.Option(
|
|
334
|
-
False,
|
|
335
|
-
"--sse",
|
|
336
|
-
help="Run MCP server on SSE transport",
|
|
337
|
-
),
|
|
338
364
|
) -> None:
|
|
339
365
|
"""Start the MCP server."""
|
|
340
|
-
|
|
341
|
-
console.print("[red]Error: Cannot use both --stdio and --http options[/red]")
|
|
342
|
-
raise typer.Exit(1)
|
|
366
|
+
from haiku.rag.app import HaikuRAGApp
|
|
343
367
|
|
|
344
368
|
app = HaikuRAGApp(db_path=db)
|
|
345
369
|
|
|
346
370
|
transport = None
|
|
347
371
|
if stdio:
|
|
348
372
|
transport = "stdio"
|
|
349
|
-
elif sse:
|
|
350
|
-
transport = "sse"
|
|
351
373
|
|
|
352
374
|
asyncio.run(app.serve(transport=transport))
|
|
353
375
|
|
|
@@ -361,6 +383,9 @@ def migrate(
|
|
|
361
383
|
# Generate LanceDB path in same parent directory
|
|
362
384
|
lancedb_path = sqlite_path.parent / (sqlite_path.stem + ".lancedb")
|
|
363
385
|
|
|
386
|
+
# Lazy import to avoid heavy deps on simple invocations
|
|
387
|
+
from haiku.rag.migration import migrate_sqlite_to_lancedb
|
|
388
|
+
|
|
364
389
|
success = asyncio.run(migrate_sqlite_to_lancedb(sqlite_path, lancedb_path))
|
|
365
390
|
|
|
366
391
|
if not success:
|
|
@@ -33,8 +33,6 @@ class HaikuRAG:
|
|
|
33
33
|
db_path: Path to the database file.
|
|
34
34
|
skip_validation: Whether to skip configuration validation on database load.
|
|
35
35
|
"""
|
|
36
|
-
if not db_path.parent.exists():
|
|
37
|
-
Path.mkdir(db_path.parent, parents=True)
|
|
38
36
|
self.store = Store(db_path, skip_validation=skip_validation)
|
|
39
37
|
self.document_repository = DocumentRepository(self.store)
|
|
40
38
|
self.chunk_repository = ChunkRepository(self.store)
|
|
@@ -52,6 +50,7 @@ class HaikuRAG:
|
|
|
52
50
|
self,
|
|
53
51
|
docling_document,
|
|
54
52
|
uri: str | None = None,
|
|
53
|
+
title: str | None = None,
|
|
55
54
|
metadata: dict | None = None,
|
|
56
55
|
chunks: list[Chunk] | None = None,
|
|
57
56
|
) -> Document:
|
|
@@ -60,6 +59,7 @@ class HaikuRAG:
|
|
|
60
59
|
document = Document(
|
|
61
60
|
content=content,
|
|
62
61
|
uri=uri,
|
|
62
|
+
title=title,
|
|
63
63
|
metadata=metadata or {},
|
|
64
64
|
)
|
|
65
65
|
return await self.document_repository._create_with_docling(
|
|
@@ -70,6 +70,7 @@ class HaikuRAG:
|
|
|
70
70
|
self,
|
|
71
71
|
content: str,
|
|
72
72
|
uri: str | None = None,
|
|
73
|
+
title: str | None = None,
|
|
73
74
|
metadata: dict | None = None,
|
|
74
75
|
chunks: list[Chunk] | None = None,
|
|
75
76
|
) -> Document:
|
|
@@ -90,6 +91,7 @@ class HaikuRAG:
|
|
|
90
91
|
document = Document(
|
|
91
92
|
content=content,
|
|
92
93
|
uri=uri,
|
|
94
|
+
title=title,
|
|
93
95
|
metadata=metadata or {},
|
|
94
96
|
)
|
|
95
97
|
return await self.document_repository._create_with_docling(
|
|
@@ -97,7 +99,7 @@ class HaikuRAG:
|
|
|
97
99
|
)
|
|
98
100
|
|
|
99
101
|
async def create_document_from_source(
|
|
100
|
-
self, source: str | Path, metadata: dict =
|
|
102
|
+
self, source: str | Path, title: str | None = None, metadata: dict | None = None
|
|
101
103
|
) -> Document:
|
|
102
104
|
"""Create or update a document from a file path or URL.
|
|
103
105
|
|
|
@@ -118,11 +120,16 @@ class HaikuRAG:
|
|
|
118
120
|
httpx.RequestError: If URL request fails
|
|
119
121
|
"""
|
|
120
122
|
|
|
123
|
+
# Normalize metadata
|
|
124
|
+
metadata = metadata or {}
|
|
125
|
+
|
|
121
126
|
# Check if it's a URL
|
|
122
127
|
source_str = str(source)
|
|
123
128
|
parsed_url = urlparse(source_str)
|
|
124
129
|
if parsed_url.scheme in ("http", "https"):
|
|
125
|
-
return await self._create_or_update_document_from_url(
|
|
130
|
+
return await self._create_or_update_document_from_url(
|
|
131
|
+
source_str, title=title, metadata=metadata
|
|
132
|
+
)
|
|
126
133
|
elif parsed_url.scheme == "file":
|
|
127
134
|
# Handle file:// URI by converting to path
|
|
128
135
|
source_path = Path(parsed_url.path)
|
|
@@ -138,37 +145,51 @@ class HaikuRAG:
|
|
|
138
145
|
uri = source_path.absolute().as_uri()
|
|
139
146
|
md5_hash = hashlib.md5(source_path.read_bytes()).hexdigest()
|
|
140
147
|
|
|
148
|
+
# Get content type from file extension (do before early return)
|
|
149
|
+
content_type, _ = mimetypes.guess_type(str(source_path))
|
|
150
|
+
if not content_type:
|
|
151
|
+
content_type = "application/octet-stream"
|
|
152
|
+
# Merge metadata with contentType and md5
|
|
153
|
+
metadata.update({"contentType": content_type, "md5": md5_hash})
|
|
154
|
+
|
|
141
155
|
# Check if document already exists
|
|
142
156
|
existing_doc = await self.get_document_by_uri(uri)
|
|
143
157
|
if existing_doc and existing_doc.metadata.get("md5") == md5_hash:
|
|
144
|
-
# MD5 unchanged
|
|
158
|
+
# MD5 unchanged; update title/metadata if provided
|
|
159
|
+
updated = False
|
|
160
|
+
if title is not None and title != existing_doc.title:
|
|
161
|
+
existing_doc.title = title
|
|
162
|
+
updated = True
|
|
163
|
+
if metadata:
|
|
164
|
+
existing_doc.metadata = {**(existing_doc.metadata or {}), **metadata}
|
|
165
|
+
updated = True
|
|
166
|
+
if updated:
|
|
167
|
+
return await self.document_repository.update(existing_doc)
|
|
145
168
|
return existing_doc
|
|
146
169
|
|
|
170
|
+
# Parse file only when content changed or new document
|
|
147
171
|
docling_document = FileReader.parse_file(source_path)
|
|
148
172
|
|
|
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
173
|
if existing_doc:
|
|
158
174
|
# Update existing document
|
|
159
175
|
existing_doc.content = docling_document.export_to_markdown()
|
|
160
176
|
existing_doc.metadata = metadata
|
|
177
|
+
if title is not None:
|
|
178
|
+
existing_doc.title = title
|
|
161
179
|
return await self.document_repository._update_with_docling(
|
|
162
180
|
existing_doc, docling_document
|
|
163
181
|
)
|
|
164
182
|
else:
|
|
165
183
|
# Create new document using DoclingDocument
|
|
166
184
|
return await self._create_document_with_docling(
|
|
167
|
-
docling_document=docling_document,
|
|
185
|
+
docling_document=docling_document,
|
|
186
|
+
uri=uri,
|
|
187
|
+
title=title,
|
|
188
|
+
metadata=metadata,
|
|
168
189
|
)
|
|
169
190
|
|
|
170
191
|
async def _create_or_update_document_from_url(
|
|
171
|
-
self, url: str, metadata: dict =
|
|
192
|
+
self, url: str, title: str | None = None, metadata: dict | None = None
|
|
172
193
|
) -> Document:
|
|
173
194
|
"""Create or update a document from a URL by downloading and parsing the content.
|
|
174
195
|
|
|
@@ -188,20 +209,35 @@ class HaikuRAG:
|
|
|
188
209
|
ValueError: If the content cannot be parsed
|
|
189
210
|
httpx.RequestError: If URL request fails
|
|
190
211
|
"""
|
|
212
|
+
metadata = metadata or {}
|
|
213
|
+
|
|
191
214
|
async with httpx.AsyncClient() as client:
|
|
192
215
|
response = await client.get(url)
|
|
193
216
|
response.raise_for_status()
|
|
194
217
|
|
|
195
218
|
md5_hash = hashlib.md5(response.content).hexdigest()
|
|
196
219
|
|
|
220
|
+
# Get content type early (used for potential no-op update)
|
|
221
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
222
|
+
|
|
197
223
|
# Check if document already exists
|
|
198
224
|
existing_doc = await self.get_document_by_uri(url)
|
|
199
225
|
if existing_doc and existing_doc.metadata.get("md5") == md5_hash:
|
|
200
|
-
# MD5 unchanged
|
|
226
|
+
# MD5 unchanged; update title/metadata if provided
|
|
227
|
+
updated = False
|
|
228
|
+
if title is not None and title != existing_doc.title:
|
|
229
|
+
existing_doc.title = title
|
|
230
|
+
updated = True
|
|
231
|
+
metadata.update({"contentType": content_type, "md5": md5_hash})
|
|
232
|
+
if metadata:
|
|
233
|
+
existing_doc.metadata = {
|
|
234
|
+
**(existing_doc.metadata or {}),
|
|
235
|
+
**metadata,
|
|
236
|
+
}
|
|
237
|
+
updated = True
|
|
238
|
+
if updated:
|
|
239
|
+
return await self.document_repository.update(existing_doc)
|
|
201
240
|
return existing_doc
|
|
202
|
-
|
|
203
|
-
# Get content type to determine file extension
|
|
204
|
-
content_type = response.headers.get("content-type", "").lower()
|
|
205
241
|
file_extension = self._get_extension_from_content_type_or_url(
|
|
206
242
|
url, content_type
|
|
207
243
|
)
|
|
@@ -228,12 +264,17 @@ class HaikuRAG:
|
|
|
228
264
|
if existing_doc:
|
|
229
265
|
existing_doc.content = docling_document.export_to_markdown()
|
|
230
266
|
existing_doc.metadata = metadata
|
|
267
|
+
if title is not None:
|
|
268
|
+
existing_doc.title = title
|
|
231
269
|
return await self.document_repository._update_with_docling(
|
|
232
270
|
existing_doc, docling_document
|
|
233
271
|
)
|
|
234
272
|
else:
|
|
235
273
|
return await self._create_document_with_docling(
|
|
236
|
-
docling_document=docling_document,
|
|
274
|
+
docling_document=docling_document,
|
|
275
|
+
uri=url,
|
|
276
|
+
title=title,
|
|
277
|
+
metadata=metadata,
|
|
237
278
|
)
|
|
238
279
|
|
|
239
280
|
def _get_extension_from_content_type_or_url(
|
|
@@ -418,6 +459,7 @@ class HaikuRAG:
|
|
|
418
459
|
content="".join(combined_content_parts),
|
|
419
460
|
metadata=original_chunk.metadata,
|
|
420
461
|
document_uri=original_chunk.document_uri,
|
|
462
|
+
document_title=original_chunk.document_title,
|
|
421
463
|
document_meta=original_chunk.document_meta,
|
|
422
464
|
)
|
|
423
465
|
|
|
@@ -524,7 +566,7 @@ class HaikuRAG:
|
|
|
524
566
|
|
|
525
567
|
# Try to re-create from source (this creates the document with chunks)
|
|
526
568
|
new_doc = await self.create_document_from_source(
|
|
527
|
-
doc.uri, doc.metadata or {}
|
|
569
|
+
source=doc.uri, metadata=doc.metadata or {}
|
|
528
570
|
)
|
|
529
571
|
|
|
530
572
|
assert new_doc.id is not None, "New document ID should not be None"
|
|
@@ -53,6 +53,10 @@ class AppConfig(BaseModel):
|
|
|
53
53
|
ANTHROPIC_API_KEY: str = ""
|
|
54
54
|
COHERE_API_KEY: str = ""
|
|
55
55
|
|
|
56
|
+
# If true, refuse to auto-create a new LanceDB database or tables
|
|
57
|
+
# and error out when the database does not already exist.
|
|
58
|
+
DISABLE_DB_AUTOCREATE: bool = False
|
|
59
|
+
|
|
56
60
|
@field_validator("MONITOR_DIRECTORIES", mode="before")
|
|
57
61
|
@classmethod
|
|
58
62
|
def parse_monitor_directories(cls, v):
|