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.

Files changed (94) hide show
  1. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/PKG-INFO +1 -1
  2. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/agents.md +2 -1
  3. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/cli.md +4 -3
  4. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/configuration.md +10 -0
  5. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/index.md +2 -0
  6. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/mcp.md +1 -4
  7. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/python.md +9 -3
  8. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/server.md +0 -1
  9. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/pyproject.toml +1 -1
  10. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/app.py +14 -5
  11. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/cli.py +55 -30
  12. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/client.py +63 -21
  13. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/config.py +4 -0
  14. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/mcp.py +18 -6
  15. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/qa/agent.py +4 -2
  16. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/qa/prompts.py +2 -2
  17. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/models.py +2 -2
  18. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/nodes/search.py +3 -1
  19. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/prompts.py +4 -3
  20. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/engine.py +14 -0
  21. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/models/chunk.py +1 -0
  22. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/models/document.py +1 -0
  23. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/repositories/chunk.py +4 -0
  24. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/repositories/document.py +3 -0
  25. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/upgrades/__init__.py +2 -0
  26. haiku_rag-0.10.1/src/haiku/rag/store/upgrades/v0_10_1.py +64 -0
  27. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/utils.py +8 -5
  28. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_app.py +4 -4
  29. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_cli.py +26 -34
  30. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_client.py +49 -8
  31. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_search.py +32 -0
  32. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/uv.lock +1 -1
  33. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.github/FUNDING.yml +0 -0
  34. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.github/workflows/build-docs.yml +0 -0
  35. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.github/workflows/build-publish.yml +0 -0
  36. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.gitignore +0 -0
  37. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.pre-commit-config.yaml +0 -0
  38. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/.python-version +0 -0
  39. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/LICENSE +0 -0
  40. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/README.md +0 -0
  41. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/benchmarks.md +0 -0
  42. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/docs/installation.md +0 -0
  43. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/mkdocs.yml +0 -0
  44. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/__init__.py +0 -0
  45. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/chunker.py +0 -0
  46. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/__init__.py +0 -0
  47. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/base.py +0 -0
  48. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/ollama.py +0 -0
  49. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/openai.py +0 -0
  50. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/vllm.py +0 -0
  51. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/embeddings/voyageai.py +0 -0
  52. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/logging.py +0 -0
  53. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/migration.py +0 -0
  54. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/monitor.py +0 -0
  55. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/qa/__init__.py +0 -0
  56. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reader.py +0 -0
  57. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/__init__.py +0 -0
  58. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/base.py +0 -0
  59. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/cohere.py +0 -0
  60. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/mxbai.py +0 -0
  61. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/reranking/vllm.py +0 -0
  62. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/__init__.py +0 -0
  63. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/common.py +0 -0
  64. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/dependencies.py +0 -0
  65. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/graph.py +0 -0
  66. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/nodes/evaluate.py +0 -0
  67. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/nodes/plan.py +0 -0
  68. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/nodes/synthesize.py +0 -0
  69. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/research/state.py +0 -0
  70. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/__init__.py +0 -0
  71. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/models/__init__.py +0 -0
  72. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/repositories/__init__.py +0 -0
  73. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/repositories/settings.py +0 -0
  74. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/src/haiku/rag/store/upgrades/v0_9_3.py +0 -0
  75. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/__init__.py +0 -0
  76. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/conftest.py +0 -0
  77. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/generate_benchmark_db.py +0 -0
  78. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/llm_judge.py +0 -0
  79. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_chunk.py +0 -0
  80. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_chunker.py +0 -0
  81. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_document.py +0 -0
  82. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_embedder.py +0 -0
  83. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_lancedb_connection.py +0 -0
  84. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_monitor.py +0 -0
  85. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_preprocessor.py +0 -0
  86. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_qa.py +0 -0
  87. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_reader.py +0 -0
  88. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_rebuild.py +0 -0
  89. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_reranker.py +0 -0
  90. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_research_graph.py +0 -0
  91. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_research_graph_integration.py +0 -0
  92. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_settings.py +0 -0
  93. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_utils.py +0 -0
  94. {haiku_rag-0.10.0 → haiku_rag-0.10.1}/tests/test_versioning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiku.rag
3
- Version: 0.10.0
3
+ Version: 0.10.1
4
4
  Summary: Agentic Retrieval Augmented Generation (RAG) with LanceDB
5
5
  Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
6
6
  License: MIT
@@ -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, stdio and SSE transports:
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("path/to/document.pdf")
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("https://example.com/article.html")
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
 
@@ -11,7 +11,6 @@ haiku-rag serve
11
11
  Transport options:
12
12
  - Default - Streamable HTTP transport
13
13
  - `--stdio` - Standard input/output transport
14
- - `--sse` - Server-sent events transport
15
14
 
16
15
  ## File Monitoring
17
16
 
@@ -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.0"
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} [repr.attrib_name]uri[/repr.attrib_name]: {doc.uri} [repr.attrib_name]meta[/repr.attrib_name]: {doc.metadata}"
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
- console.print(
93
- f"[yellow]Warning: haiku.rag is outdated. Current: {current_version}, Latest: {latest_version}[/yellow]"
80
+ typer.echo(
81
+ f"Warning: haiku.rag is outdated. Current: {current_version}, Latest: {latest_version}",
94
82
  )
95
- console.print("[yellow]Please update.[/yellow]")
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
- console.print(f"haiku.rag version {v}")
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
- # Ensure only haiku.rag logs are emitted in CLI context
117
- configure_cli_logging()
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
- asyncio.run(check_version())
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
- if stdio and sse:
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(source_str, metadata)
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, return existing document
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, uri=uri, metadata=metadata
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, return existing document
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, uri=url, metadata=metadata
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):