haiku.rag 0.2.0__py3-none-any.whl → 0.3.1__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 might be problematic. Click here for more details.

haiku/rag/app.py CHANGED
@@ -3,6 +3,7 @@ from pathlib import Path
3
3
 
4
4
  from rich.console import Console
5
5
  from rich.markdown import Markdown
6
+ from rich.progress import Progress
6
7
 
7
8
  from haiku.rag.client import HaikuRAG
8
9
  from haiku.rag.config import Config
@@ -61,6 +62,61 @@ class HaikuRAGApp:
61
62
  for chunk, score in results:
62
63
  self._rich_print_search_result(chunk, score)
63
64
 
65
+ async def ask(self, question: str):
66
+ async with HaikuRAG(db_path=self.db_path) as self.client:
67
+ try:
68
+ answer = await self.client.ask(question)
69
+ self.console.print(f"[bold blue]Question:[/bold blue] {question}")
70
+ self.console.print()
71
+ self.console.print("[bold green]Answer:[/bold green]")
72
+ self.console.print(Markdown(answer))
73
+ except Exception as e:
74
+ self.console.print(f"[red]Error: {e}[/red]")
75
+
76
+ async def rebuild(self):
77
+ async with HaikuRAG(db_path=self.db_path) as client:
78
+ try:
79
+ documents = await client.list_documents()
80
+ total_docs = len(documents)
81
+
82
+ if total_docs == 0:
83
+ self.console.print(
84
+ "[yellow]No documents found in database.[/yellow]"
85
+ )
86
+ return
87
+
88
+ self.console.print(
89
+ f"[b]Rebuilding database with {total_docs} documents...[/b]"
90
+ )
91
+ with Progress() as progress:
92
+ task = progress.add_task("Rebuilding...", total=total_docs)
93
+ async for _ in client.rebuild_database():
94
+ progress.update(task, advance=1)
95
+
96
+ self.console.print("[b]Database rebuild completed successfully.[/b]")
97
+ except Exception as e:
98
+ self.console.print(f"[red]Error rebuilding database: {e}[/red]")
99
+
100
+ def show_settings(self):
101
+ """Display current configuration settings."""
102
+ self.console.print("[bold]haiku.rag configuration[/bold]")
103
+ self.console.print()
104
+
105
+ # Get all config fields dynamically
106
+ for field_name, field_value in Config.model_dump().items():
107
+ # Format the display value
108
+ if isinstance(field_value, str) and (
109
+ "key" in field_name.lower()
110
+ or "password" in field_name.lower()
111
+ or "token" in field_name.lower()
112
+ ):
113
+ # Hide sensitive values but show if they're set
114
+ display_value = "✓ Set" if field_value else "✗ Not set"
115
+ else:
116
+ display_value = field_value
117
+
118
+ self.console.print(f" [cyan]{field_name}[/cyan]: {display_value}")
119
+
64
120
  def _rich_print_document(self, doc: Document, truncate: bool = False):
65
121
  """Format a document for display."""
66
122
  if truncate:
@@ -88,6 +144,12 @@ class HaikuRAGApp:
88
144
  f"[repr.attrib_name]document_id[/repr.attrib_name]: {chunk.document_id} "
89
145
  f"[repr.attrib_name]score[/repr.attrib_name]: {score:.4f}"
90
146
  )
147
+ if chunk.document_uri:
148
+ self.console.print("[repr.attrib_name]document uri[/repr.attrib_name]:")
149
+ self.console.print(chunk.document_uri)
150
+ if chunk.document_meta:
151
+ self.console.print("[repr.attrib_name]document meta[/repr.attrib_name]:")
152
+ self.console.print(chunk.document_meta)
91
153
  self.console.print("[repr.attrib_name]content[/repr.attrib_name]:")
92
154
  self.console.print(content)
93
155
  self.console.rule()
haiku/rag/cli.py CHANGED
@@ -113,6 +113,42 @@ def search(
113
113
  event_loop.run_until_complete(app.search(query=query, limit=limit, k=k))
114
114
 
115
115
 
116
+ @cli.command("ask", help="Ask a question using the QA agent")
117
+ def ask(
118
+ question: str = typer.Argument(
119
+ help="The question to ask",
120
+ ),
121
+ db: Path = typer.Option(
122
+ get_default_data_dir() / "haiku.rag.sqlite",
123
+ "--db",
124
+ help="Path to the SQLite database file",
125
+ ),
126
+ ):
127
+ app = HaikuRAGApp(db_path=db)
128
+ event_loop.run_until_complete(app.ask(question=question))
129
+
130
+
131
+ @cli.command("settings", help="Display current configuration settings")
132
+ def settings():
133
+ app = HaikuRAGApp(db_path=Path()) # Don't need actual DB for settings
134
+ app.show_settings()
135
+
136
+
137
+ @cli.command(
138
+ "rebuild",
139
+ help="Rebuild the database by deleting all chunks and re-indexing all documents",
140
+ )
141
+ def rebuild(
142
+ db: Path = typer.Option(
143
+ get_default_data_dir() / "haiku.rag.sqlite",
144
+ "--db",
145
+ help="Path to the SQLite database file",
146
+ ),
147
+ ):
148
+ app = HaikuRAGApp(db_path=db)
149
+ event_loop.run_until_complete(app.rebuild())
150
+
151
+
116
152
  @cli.command(
117
153
  "serve", help="Start the haiku.rag MCP server (by default in streamable HTTP mode)"
118
154
  )
haiku/rag/client.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import hashlib
2
2
  import mimetypes
3
3
  import tempfile
4
+ from collections.abc import AsyncGenerator
4
5
  from pathlib import Path
5
6
  from typing import Literal
6
7
  from urllib.parse import urlparse
@@ -36,7 +37,7 @@ class HaikuRAG:
36
37
  """Async context manager entry."""
37
38
  return self
38
39
 
39
- async def __aexit__(self, exc_type, exc_val, exc_tb):
40
+ async def __aexit__(self, exc_type, exc_val, exc_tb): # noqa: ARG002
40
41
  """Async context manager exit."""
41
42
  self.close()
42
43
  return False
@@ -256,6 +257,43 @@ class HaikuRAG:
256
257
  """
257
258
  return await self.chunk_repository.search_chunks_hybrid(query, limit, k)
258
259
 
260
+ async def ask(self, question: str) -> str:
261
+ """Ask a question using the configured QA agent.
262
+
263
+ Args:
264
+ question: The question to ask
265
+
266
+ Returns:
267
+ The generated answer as a string
268
+ """
269
+ from haiku.rag.qa import get_qa_agent
270
+
271
+ qa_agent = get_qa_agent(self)
272
+ return await qa_agent.answer(question)
273
+
274
+ async def rebuild_database(self) -> AsyncGenerator[int, None]:
275
+ """Rebuild the database by deleting all chunks and re-indexing all documents.
276
+
277
+ Yields:
278
+ int: The ID of the document currently being processed
279
+ """
280
+ documents = await self.list_documents()
281
+
282
+ if not documents:
283
+ return
284
+
285
+ await self.chunk_repository.delete_all()
286
+
287
+ for doc in documents:
288
+ if doc.id is not None:
289
+ await self.chunk_repository.create_chunks_for_document(
290
+ doc.id, doc.content, commit=False
291
+ )
292
+ yield doc.id
293
+
294
+ if self.store._connection:
295
+ self.store._connection.commit()
296
+
259
297
  def close(self):
260
298
  """Close the underlying store connection."""
261
299
  self.store.close()
haiku/rag/config.py CHANGED
@@ -19,11 +19,19 @@ class AppConfig(BaseModel):
19
19
  EMBEDDINGS_MODEL: str = "mxbai-embed-large"
20
20
  EMBEDDINGS_VECTOR_DIM: int = 1024
21
21
 
22
+ QA_PROVIDER: str = "ollama"
23
+ QA_MODEL: str = "qwen3"
24
+
22
25
  CHUNK_SIZE: int = 256
23
26
  CHUNK_OVERLAP: int = 32
24
27
 
25
28
  OLLAMA_BASE_URL: str = "http://localhost:11434"
26
29
 
30
+ # Provider keys
31
+ VOYAGE_API_KEY: str = ""
32
+ OPENAI_API_KEY: str = ""
33
+ ANTHROPIC_API_KEY: str = ""
34
+
27
35
  @field_validator("MONITOR_DIRECTORIES", mode="before")
28
36
  @classmethod
29
37
  def parse_monitor_directories(cls, v):
@@ -38,3 +46,9 @@ class AppConfig(BaseModel):
38
46
 
39
47
  # Expose Config object for app to import
40
48
  Config = AppConfig.model_validate(os.environ)
49
+ if Config.OPENAI_API_KEY:
50
+ os.environ["OPENAI_API_KEY"] = Config.OPENAI_API_KEY
51
+ if Config.VOYAGE_API_KEY:
52
+ os.environ["VOYAGE_API_KEY"] = Config.VOYAGE_API_KEY
53
+ if Config.ANTHROPIC_API_KEY:
54
+ os.environ["ANTHROPIC_API_KEY"] = Config.ANTHROPIC_API_KEY
haiku/rag/monitor.py CHANGED
@@ -49,7 +49,6 @@ class FileWatcher:
49
49
  try:
50
50
  uri = file.as_uri()
51
51
  existing_doc = await self.client.get_document_by_uri(uri)
52
- print(uri)
53
52
  if existing_doc:
54
53
  doc = await self.client.create_document_from_source(str(file))
55
54
  logger.info(f"Updated document {existing_doc.id} from {file}")
@@ -0,0 +1,39 @@
1
+ from haiku.rag.client import HaikuRAG
2
+ from haiku.rag.config import Config
3
+ from haiku.rag.qa.base import QuestionAnswerAgentBase
4
+ from haiku.rag.qa.ollama import QuestionAnswerOllamaAgent
5
+
6
+
7
+ def get_qa_agent(client: HaikuRAG, model: str = "") -> QuestionAnswerAgentBase:
8
+ """
9
+ Factory function to get the appropriate QA agent based on the configuration.
10
+ """
11
+
12
+ if Config.QA_PROVIDER == "ollama":
13
+ return QuestionAnswerOllamaAgent(client, model or Config.QA_MODEL)
14
+
15
+ if Config.QA_PROVIDER == "openai":
16
+ try:
17
+ from haiku.rag.qa.openai import QuestionAnswerOpenAIAgent
18
+ except ImportError:
19
+ raise ImportError(
20
+ "OpenAI QA agent requires the 'openai' package. "
21
+ "Please install haiku.rag with the 'openai' extra:"
22
+ "uv pip install haiku.rag --extra openai"
23
+ )
24
+ return QuestionAnswerOpenAIAgent(client, model or "gpt-4o-mini")
25
+
26
+ if Config.QA_PROVIDER == "anthropic":
27
+ try:
28
+ from haiku.rag.qa.anthropic import QuestionAnswerAnthropicAgent
29
+ except ImportError:
30
+ raise ImportError(
31
+ "Anthropic QA agent requires the 'anthropic' package. "
32
+ "Please install haiku.rag with the 'anthropic' extra:"
33
+ "uv pip install haiku.rag --extra anthropic"
34
+ )
35
+ return QuestionAnswerAnthropicAgent(
36
+ client, model or "claude-3-5-haiku-20241022"
37
+ )
38
+
39
+ raise ValueError(f"Unsupported QA provider: {Config.QA_PROVIDER}")
@@ -0,0 +1,112 @@
1
+ from collections.abc import Sequence
2
+
3
+ try:
4
+ from anthropic import AsyncAnthropic
5
+ from anthropic.types import MessageParam, TextBlock, ToolParam, ToolUseBlock
6
+
7
+ from haiku.rag.client import HaikuRAG
8
+ from haiku.rag.qa.base import QuestionAnswerAgentBase
9
+
10
+ class QuestionAnswerAnthropicAgent(QuestionAnswerAgentBase):
11
+ def __init__(self, client: HaikuRAG, model: str = "claude-3-5-haiku-20241022"):
12
+ super().__init__(client, model or self._model)
13
+ self.tools: Sequence[ToolParam] = [
14
+ ToolParam(
15
+ name="search_documents",
16
+ description="Search the knowledge base for relevant documents",
17
+ input_schema={
18
+ "type": "object",
19
+ "properties": {
20
+ "query": {
21
+ "type": "string",
22
+ "description": "The search query to find relevant documents",
23
+ },
24
+ "limit": {
25
+ "type": "integer",
26
+ "description": "Maximum number of results to return",
27
+ "default": 3,
28
+ },
29
+ },
30
+ "required": ["query"],
31
+ },
32
+ )
33
+ ]
34
+
35
+ async def answer(self, question: str) -> str:
36
+ anthropic_client = AsyncAnthropic()
37
+
38
+ messages: list[MessageParam] = [{"role": "user", "content": question}]
39
+
40
+ response = await anthropic_client.messages.create(
41
+ model=self._model,
42
+ max_tokens=4096,
43
+ system=self._system_prompt,
44
+ messages=messages,
45
+ tools=self.tools,
46
+ temperature=0.0,
47
+ )
48
+
49
+ if response.stop_reason == "tool_use":
50
+ messages.append({"role": "assistant", "content": response.content})
51
+
52
+ # Process tool calls
53
+ tool_results = []
54
+ for content_block in response.content:
55
+ if isinstance(content_block, ToolUseBlock):
56
+ if content_block.name == "search_documents":
57
+ args = content_block.input
58
+ query = (
59
+ args.get("query", question)
60
+ if isinstance(args, dict)
61
+ else question
62
+ )
63
+ limit = (
64
+ int(args.get("limit", 3))
65
+ if isinstance(args, dict)
66
+ else 3
67
+ )
68
+
69
+ search_results = await self._client.search(
70
+ query, limit=limit
71
+ )
72
+
73
+ context_chunks = []
74
+ for chunk, score in search_results:
75
+ context_chunks.append(
76
+ f"Content: {chunk.content}\nScore: {score:.4f}"
77
+ )
78
+
79
+ context = "\n\n".join(context_chunks)
80
+
81
+ tool_results.append(
82
+ {
83
+ "type": "tool_result",
84
+ "tool_use_id": content_block.id,
85
+ "content": context,
86
+ }
87
+ )
88
+
89
+ if tool_results:
90
+ messages.append({"role": "user", "content": tool_results})
91
+
92
+ final_response = await anthropic_client.messages.create(
93
+ model=self._model,
94
+ max_tokens=4096,
95
+ system=self._system_prompt,
96
+ messages=messages,
97
+ temperature=0.0,
98
+ )
99
+ if final_response.content:
100
+ first_content = final_response.content[0]
101
+ if isinstance(first_content, TextBlock):
102
+ return first_content.text
103
+ return ""
104
+
105
+ if response.content:
106
+ first_content = response.content[0]
107
+ if isinstance(first_content, TextBlock):
108
+ return first_content.text
109
+ return ""
110
+
111
+ except ImportError:
112
+ pass
haiku/rag/qa/base.py ADDED
@@ -0,0 +1,41 @@
1
+ from haiku.rag.client import HaikuRAG
2
+ from haiku.rag.qa.prompts import SYSTEM_PROMPT
3
+
4
+
5
+ class QuestionAnswerAgentBase:
6
+ _model: str = ""
7
+ _system_prompt: str = SYSTEM_PROMPT
8
+
9
+ def __init__(self, client: HaikuRAG, model: str = ""):
10
+ self._model = model
11
+ self._client = client
12
+
13
+ async def answer(self, question: str) -> str:
14
+ raise NotImplementedError(
15
+ "QABase is an abstract class. Please implement the answer method in a subclass."
16
+ )
17
+
18
+ tools = [
19
+ {
20
+ "type": "function",
21
+ "function": {
22
+ "name": "search_documents",
23
+ "description": "Search the knowledge base for relevant documents",
24
+ "parameters": {
25
+ "type": "object",
26
+ "properties": {
27
+ "query": {
28
+ "type": "string",
29
+ "description": "The search query to find relevant documents",
30
+ },
31
+ "limit": {
32
+ "type": "integer",
33
+ "description": "Maximum number of results to return",
34
+ "default": 3,
35
+ },
36
+ },
37
+ "required": ["query"],
38
+ },
39
+ },
40
+ }
41
+ ]
haiku/rag/qa/ollama.py ADDED
@@ -0,0 +1,67 @@
1
+ from ollama import AsyncClient
2
+
3
+ from haiku.rag.client import HaikuRAG
4
+ from haiku.rag.config import Config
5
+ from haiku.rag.qa.base import QuestionAnswerAgentBase
6
+
7
+ OLLAMA_OPTIONS = {"temperature": 0.0, "seed": 42, "num_ctx": 64000}
8
+
9
+
10
+ class QuestionAnswerOllamaAgent(QuestionAnswerAgentBase):
11
+ def __init__(self, client: HaikuRAG, model: str = Config.QA_MODEL):
12
+ super().__init__(client, model or self._model)
13
+
14
+ async def answer(self, question: str) -> str:
15
+ ollama_client = AsyncClient(host=Config.OLLAMA_BASE_URL)
16
+
17
+ # Define the search tool
18
+
19
+ messages = [
20
+ {"role": "system", "content": self._system_prompt},
21
+ {"role": "user", "content": question},
22
+ ]
23
+
24
+ # Initial response with tool calling
25
+ response = await ollama_client.chat(
26
+ model=self._model,
27
+ messages=messages,
28
+ tools=self.tools,
29
+ options=OLLAMA_OPTIONS,
30
+ think=False,
31
+ )
32
+
33
+ if response.get("message", {}).get("tool_calls"):
34
+ for tool_call in response["message"]["tool_calls"]:
35
+ if tool_call["function"]["name"] == "search_documents":
36
+ args = tool_call["function"]["arguments"]
37
+ query = args.get("query", question)
38
+ limit = int(args.get("limit", 3))
39
+
40
+ search_results = await self._client.search(query, limit=limit)
41
+
42
+ context_chunks = []
43
+ for chunk, score in search_results:
44
+ context_chunks.append(
45
+ f"Content: {chunk.content}\nScore: {score:.4f}"
46
+ )
47
+
48
+ context = "\n\n".join(context_chunks)
49
+
50
+ messages.append(response["message"])
51
+ messages.append(
52
+ {
53
+ "role": "tool",
54
+ "content": context,
55
+ "tool_call_id": tool_call.get("id", "search_tool"),
56
+ }
57
+ )
58
+
59
+ final_response = await ollama_client.chat(
60
+ model=self._model,
61
+ messages=messages,
62
+ think=False,
63
+ options=OLLAMA_OPTIONS,
64
+ )
65
+ return final_response["message"]["content"]
66
+ else:
67
+ return response["message"]["content"]
haiku/rag/qa/openai.py ADDED
@@ -0,0 +1,101 @@
1
+ from collections.abc import Sequence
2
+
3
+ try:
4
+ from openai import AsyncOpenAI
5
+ from openai.types.chat import (
6
+ ChatCompletionAssistantMessageParam,
7
+ ChatCompletionMessageParam,
8
+ ChatCompletionSystemMessageParam,
9
+ ChatCompletionToolMessageParam,
10
+ ChatCompletionUserMessageParam,
11
+ )
12
+ from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam
13
+
14
+ from haiku.rag.client import HaikuRAG
15
+ from haiku.rag.qa.base import QuestionAnswerAgentBase
16
+
17
+ class QuestionAnswerOpenAIAgent(QuestionAnswerAgentBase):
18
+ def __init__(self, client: HaikuRAG, model: str = "gpt-4o-mini"):
19
+ super().__init__(client, model or self._model)
20
+ self.tools: Sequence[ChatCompletionToolParam] = [
21
+ ChatCompletionToolParam(tool) for tool in self.tools
22
+ ]
23
+
24
+ async def answer(self, question: str) -> str:
25
+ openai_client = AsyncOpenAI()
26
+
27
+ # Define the search tool
28
+
29
+ messages: list[ChatCompletionMessageParam] = [
30
+ ChatCompletionSystemMessageParam(
31
+ role="system", content=self._system_prompt
32
+ ),
33
+ ChatCompletionUserMessageParam(role="user", content=question),
34
+ ]
35
+
36
+ # Initial response with tool calling
37
+ response = await openai_client.chat.completions.create(
38
+ model=self._model,
39
+ messages=messages,
40
+ tools=self.tools,
41
+ temperature=0.0,
42
+ )
43
+
44
+ response_message = response.choices[0].message
45
+
46
+ if response_message.tool_calls:
47
+ messages.append(
48
+ ChatCompletionAssistantMessageParam(
49
+ role="assistant",
50
+ content=response_message.content,
51
+ tool_calls=[
52
+ {
53
+ "id": tc.id,
54
+ "type": "function",
55
+ "function": {
56
+ "name": tc.function.name,
57
+ "arguments": tc.function.arguments,
58
+ },
59
+ }
60
+ for tc in response_message.tool_calls
61
+ ],
62
+ )
63
+ )
64
+
65
+ for tool_call in response_message.tool_calls:
66
+ if tool_call.function.name == "search_documents":
67
+ import json
68
+
69
+ args = json.loads(tool_call.function.arguments)
70
+ query = args.get("query", question)
71
+ limit = int(args.get("limit", 3))
72
+
73
+ search_results = await self._client.search(query, limit=limit)
74
+
75
+ context_chunks = []
76
+ for chunk, score in search_results:
77
+ context_chunks.append(
78
+ f"Content: {chunk.content}\nScore: {score:.4f}"
79
+ )
80
+
81
+ context = "\n\n".join(context_chunks)
82
+
83
+ messages.append(
84
+ ChatCompletionToolMessageParam(
85
+ role="tool",
86
+ content=context,
87
+ tool_call_id=tool_call.id,
88
+ )
89
+ )
90
+
91
+ final_response = await openai_client.chat.completions.create(
92
+ model=self._model,
93
+ messages=messages,
94
+ temperature=0.0,
95
+ )
96
+ return final_response.choices[0].message.content or ""
97
+ else:
98
+ return response_message.content or ""
99
+
100
+ except ImportError:
101
+ pass
@@ -0,0 +1,7 @@
1
+ SYSTEM_PROMPT = """
2
+ You are a helpful assistant that uses a RAG library to answer the user's prompt.
3
+ Your task is to provide a concise and accurate answer based on the provided context.
4
+ You should ask the provided tools to find relevant documents and then use the content of those documents to answer the question.
5
+ Never make up information, always use the context to answer the question.
6
+ If the context does not contain enough information to answer the question, respond with "I cannot answer that based on the provided context."
7
+ """
@@ -3,10 +3,12 @@ from pydantic import BaseModel
3
3
 
4
4
  class Chunk(BaseModel):
5
5
  """
6
- Represents a document with an ID, content, and metadata.
6
+ Represents a chunk with content, metadata, and optional document information.
7
7
  """
8
8
 
9
9
  id: int | None = None
10
10
  document_id: int
11
11
  content: str
12
12
  metadata: dict = {}
13
+ document_uri: str | None = None
14
+ document_meta: dict = {}
@@ -208,6 +208,22 @@ class ChunkRepository(BaseRepository[Chunk]):
208
208
 
209
209
  return created_chunks
210
210
 
211
+ async def delete_all(self, commit: bool = True) -> bool:
212
+ """Delete all chunks from the database."""
213
+ if self.store._connection is None:
214
+ raise ValueError("Store connection is not available")
215
+
216
+ cursor = self.store._connection.cursor()
217
+
218
+ cursor.execute("DELETE FROM chunks_fts")
219
+ cursor.execute("DELETE FROM chunk_embeddings")
220
+ cursor.execute("DELETE FROM chunks")
221
+
222
+ deleted = cursor.rowcount > 0
223
+ if commit:
224
+ self.store._connection.commit()
225
+ return deleted
226
+
211
227
  async def delete_by_document_id(
212
228
  self, document_id: int, commit: bool = True
213
229
  ) -> bool:
@@ -240,9 +256,10 @@ class ChunkRepository(BaseRepository[Chunk]):
240
256
  # Search for similar chunks using sqlite-vec
241
257
  cursor.execute(
242
258
  """
243
- SELECT c.id, c.document_id, c.content, c.metadata, distance
259
+ SELECT c.id, c.document_id, c.content, c.metadata, distance, d.uri, d.metadata as document_metadata
244
260
  FROM chunk_embeddings
245
261
  JOIN chunks c ON c.id = chunk_embeddings.chunk_id
262
+ JOIN documents d ON c.document_id = d.id
246
263
  WHERE embedding MATCH :embedding AND k = :k
247
264
  ORDER BY distance
248
265
  """,
@@ -257,10 +274,14 @@ class ChunkRepository(BaseRepository[Chunk]):
257
274
  document_id=document_id,
258
275
  content=content,
259
276
  metadata=json.loads(metadata_json) if metadata_json else {},
277
+ document_uri=document_uri,
278
+ document_meta=json.loads(document_metadata_json)
279
+ if document_metadata_json
280
+ else {},
260
281
  ),
261
282
  1.0 / (1.0 + distance),
262
283
  )
263
- for chunk_id, document_id, content, metadata_json, distance in results
284
+ for chunk_id, document_id, content, metadata_json, distance, document_uri, document_metadata_json in results
264
285
  ]
265
286
 
266
287
  async def search_chunks_fts(
@@ -281,9 +302,10 @@ class ChunkRepository(BaseRepository[Chunk]):
281
302
  # Search using FTS5
282
303
  cursor.execute(
283
304
  """
284
- SELECT c.id, c.document_id, c.content, c.metadata, rank
305
+ SELECT c.id, c.document_id, c.content, c.metadata, rank, d.uri, d.metadata as document_metadata
285
306
  FROM chunks_fts
286
307
  JOIN chunks c ON c.id = chunks_fts.rowid
308
+ JOIN documents d ON c.document_id = d.id
287
309
  WHERE chunks_fts MATCH :query
288
310
  ORDER BY rank
289
311
  LIMIT :limit
@@ -300,10 +322,14 @@ class ChunkRepository(BaseRepository[Chunk]):
300
322
  document_id=document_id,
301
323
  content=content,
302
324
  metadata=json.loads(metadata_json) if metadata_json else {},
325
+ document_uri=document_uri,
326
+ document_meta=json.loads(document_metadata_json)
327
+ if document_metadata_json
328
+ else {},
303
329
  ),
304
330
  -rank,
305
331
  )
306
- for chunk_id, document_id, content, metadata_json, rank in results
332
+ for chunk_id, document_id, content, metadata_json, rank, document_uri, document_metadata_json in results
307
333
  # FTS5 rank is negative BM25 score
308
334
  ]
309
335
 
@@ -325,7 +351,6 @@ class ChunkRepository(BaseRepository[Chunk]):
325
351
  words = re.findall(r"\b\w+\b", query.lower())
326
352
  # Join with OR to find chunks containing any of the keywords
327
353
  fts_query = " OR ".join(words) if words else query
328
-
329
354
  # Perform hybrid search using RRF (Reciprocal Rank Fusion)
330
355
  cursor.execute(
331
356
  """
@@ -369,9 +394,10 @@ class ChunkRepository(BaseRepository[Chunk]):
369
394
  LEFT JOIN vector_search v ON a.id = v.id
370
395
  LEFT JOIN fts_search f ON a.id = f.id
371
396
  )
372
- SELECT id, document_id, content, metadata, rrf_score
373
- FROM rrf_scores
374
- ORDER BY rrf_score DESC
397
+ SELECT r.id, r.document_id, r.content, r.metadata, r.rrf_score, d.uri, d.metadata as document_metadata
398
+ FROM rrf_scores r
399
+ JOIN documents d ON r.document_id = d.id
400
+ ORDER BY r.rrf_score DESC
375
401
  LIMIT :limit
376
402
  """,
377
403
  {
@@ -391,10 +417,14 @@ class ChunkRepository(BaseRepository[Chunk]):
391
417
  document_id=document_id,
392
418
  content=content,
393
419
  metadata=json.loads(metadata_json) if metadata_json else {},
420
+ document_uri=document_uri,
421
+ document_meta=json.loads(document_metadata_json)
422
+ if document_metadata_json
423
+ else {},
394
424
  ),
395
425
  rrf_score,
396
426
  )
397
- for chunk_id, document_id, content, metadata_json, rrf_score in results
427
+ for chunk_id, document_id, content, metadata_json, rrf_score, document_uri, document_metadata_json in results
398
428
  ]
399
429
 
400
430
  async def get_by_document_id(self, document_id: int) -> list[Chunk]:
@@ -405,9 +435,11 @@ class ChunkRepository(BaseRepository[Chunk]):
405
435
  cursor = self.store._connection.cursor()
406
436
  cursor.execute(
407
437
  """
408
- SELECT id, document_id, content, metadata
409
- FROM chunks WHERE document_id = :document_id
410
- ORDER BY JSON_EXTRACT(metadata, '$.order')
438
+ SELECT c.id, c.document_id, c.content, c.metadata, d.uri, d.metadata as document_metadata
439
+ FROM chunks c
440
+ JOIN documents d ON c.document_id = d.id
441
+ WHERE c.document_id = :document_id
442
+ ORDER BY JSON_EXTRACT(c.metadata, '$.order')
411
443
  """,
412
444
  {"document_id": document_id},
413
445
  )
@@ -419,6 +451,10 @@ class ChunkRepository(BaseRepository[Chunk]):
419
451
  document_id=document_id,
420
452
  content=content,
421
453
  metadata=json.loads(metadata_json) if metadata_json else {},
454
+ document_uri=document_uri,
455
+ document_meta=json.loads(document_metadata_json)
456
+ if document_metadata_json
457
+ else {},
422
458
  )
423
- for chunk_id, document_id, content, metadata_json in rows
459
+ for chunk_id, document_id, content, metadata_json, document_uri, document_metadata_json in rows
424
460
  ]
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: haiku.rag
3
+ Version: 0.3.1
4
+ Summary: Retrieval Augmented Generation (RAG) with SQLite
5
+ Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: RAG,mcp,ml,sqlite,sqlite-vec
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 10
14
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: fastmcp>=2.8.1
22
+ Requires-Dist: httpx>=0.28.1
23
+ Requires-Dist: markitdown[audio-transcription,docx,pdf,pptx,xlsx]>=0.1.2
24
+ Requires-Dist: ollama>=0.5.1
25
+ Requires-Dist: pydantic>=2.11.7
26
+ Requires-Dist: python-dotenv>=1.1.0
27
+ Requires-Dist: rich>=14.0.0
28
+ Requires-Dist: sqlite-vec>=0.1.6
29
+ Requires-Dist: tiktoken>=0.9.0
30
+ Requires-Dist: typer>=0.16.0
31
+ Requires-Dist: watchfiles>=1.1.0
32
+ Provides-Extra: anthropic
33
+ Requires-Dist: anthropic>=0.56.0; extra == 'anthropic'
34
+ Provides-Extra: openai
35
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
36
+ Provides-Extra: voyageai
37
+ Requires-Dist: voyageai>=0.3.2; extra == 'voyageai'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # Haiku SQLite RAG
41
+
42
+ Retrieval-Augmented Generation (RAG) library on SQLite.
43
+
44
+ `haiku.rag` is a Retrieval-Augmented Generation (RAG) library built to work on SQLite alone without the need for external vector databases. It uses [sqlite-vec](https://github.com/asg017/sqlite-vec) for storing the embeddings and performs semantic (vector) search as well as full-text search combined through Reciprocal Rank Fusion. Both open-source (Ollama) as well as commercial (OpenAI, VoyageAI) embedding providers are supported.
45
+
46
+ ## Features
47
+
48
+ - **Local SQLite**: No external servers required
49
+ - **Multiple embedding providers**: Ollama, VoyageAI, OpenAI
50
+ - **Multiple QA providers**: Ollama, OpenAI, Anthropic
51
+ - **Hybrid search**: Vector + full-text search with Reciprocal Rank Fusion
52
+ - **Question answering**: Built-in QA agents on your documents
53
+ - **File monitoring**: Auto-index files when run as server
54
+ - **40+ file formats**: PDF, DOCX, HTML, Markdown, audio, URLs
55
+ - **MCP server**: Expose as tools for AI assistants
56
+ - **CLI & Python API**: Use from command line or Python
57
+
58
+ ## Quick Start
59
+
60
+ ```bash
61
+ # Install
62
+ uv pip install haiku.rag
63
+
64
+ # Add documents
65
+ haiku-rag add "Your content here"
66
+ haiku-rag add-src document.pdf
67
+
68
+ # Search
69
+ haiku-rag search "query"
70
+
71
+ # Ask questions
72
+ haiku-rag ask "Who is the author of haiku.rag?"
73
+
74
+ # Rebuild database (re-chunk and re-embed all documents)
75
+ haiku-rag rebuild
76
+
77
+ # Start server with file monitoring
78
+ export MONITOR_DIRECTORIES="/path/to/docs"
79
+ haiku-rag serve
80
+ ```
81
+
82
+ ## Python Usage
83
+
84
+ ```python
85
+ from haiku.rag.client import HaikuRAG
86
+
87
+ async with HaikuRAG("database.db") as client:
88
+ # Add document
89
+ doc = await client.create_document("Your content")
90
+
91
+ # Search
92
+ results = await client.search("query")
93
+ for chunk, score in results:
94
+ print(f"{score:.3f}: {chunk.content}")
95
+
96
+ # Ask questions
97
+ answer = await client.ask("Who is the author of haiku.rag?")
98
+ print(answer)
99
+ ```
100
+
101
+ ## MCP Server
102
+
103
+ Use with AI assistants like Claude Desktop:
104
+
105
+ ```bash
106
+ haiku-rag serve --stdio
107
+ ```
108
+
109
+ Provides tools for document management and search directly in your AI assistant.
110
+
111
+ ## Documentation
112
+
113
+ Full documentation at: https://ggozad.github.io/haiku.rag/
114
+
115
+ - [Installation](https://ggozad.github.io/haiku.rag/installation/) - Provider setup
116
+ - [Configuration](https://ggozad.github.io/haiku.rag/configuration/) - Environment variables
117
+ - [CLI](https://ggozad.github.io/haiku.rag/cli/) - Command reference
118
+ - [Python API](https://ggozad.github.io/haiku.rag/python/) - Complete API docs
@@ -1,12 +1,12 @@
1
1
  haiku/rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- haiku/rag/app.py,sha256=FedUvIxPXCi7SmxUi9zJcxmoZBTQZJO00XIkoD-k87s,4915
2
+ haiku/rag/app.py,sha256=Foi_K-sAqHWsIAAaxY2Tb0hyXnMCi06LqIFCPiBS5n0,7627
3
3
  haiku/rag/chunker.py,sha256=lSSPWgNAe7gNZL_yNLmDtqxJix4YclOiG7gbARcEpV8,1871
4
- haiku/rag/cli.py,sha256=SvDPYHHdjPu8bEF8PgE4agUo-5j3Kuq_rS9Cav6xch0,4051
5
- haiku/rag/client.py,sha256=uWqcowc8J2Yv-liGBGSJnuQkFw4CDlf_ivOxp6E5C1M,9707
6
- haiku/rag/config.py,sha256=b346EQ7HCFy-OU3K-SzSSoOLMuQseHFoiysYZMO1zCc,1003
4
+ haiku/rag/cli.py,sha256=9F64IIm2c1nBKn7p9D5yYkVZr8HcjDemrzjF9SRGIY8,5017
5
+ haiku/rag/client.py,sha256=qoVgdsP_MH8wVcDTvPIcMgW7323tTjOXH8JKugz5snY,10847
6
+ haiku/rag/config.py,sha256=ctD_pu7nDOieirJofhNMO-OJIONLC5myvcru9iTm_ps,1433
7
7
  haiku/rag/logging.py,sha256=zTTGpGq5tPdcd7RpCbd9EGw1IZlQDbYkrCg9t9pqRc4,580
8
8
  haiku/rag/mcp.py,sha256=tMN6fNX7ZtAER1R6DL1GkC9HZozTC4HzuQs199p7icI,4551
9
- haiku/rag/monitor.py,sha256=aFJb5cnesEBIGyVzt8IXYrlTujiC1QSPczPuAam2yXw,2793
9
+ haiku/rag/monitor.py,sha256=r386nkhdlsU8UECwIuVwnrSlgMk3vNIuUZGNIzkZuec,2770
10
10
  haiku/rag/reader.py,sha256=S7-Z72pDvSHedvgt4-RkTOwZadG88Oed9keJ69SVITk,962
11
11
  haiku/rag/utils.py,sha256=6xVM6z2OmhzB4FEDlPbMsr_ZBBmCbMQb83nP6E2UdxY,629
12
12
  haiku/rag/embeddings/__init__.py,sha256=4jUPe2FyIf8BGZ7AncWSlBdNXG3URejBbnkhQf3JiD0,1505
@@ -14,17 +14,23 @@ haiku/rag/embeddings/base.py,sha256=PTAWKTU-Q-hXIhbRK1o6pIdpaW7DFdzJXQ0Nzc6VI-w,
14
14
  haiku/rag/embeddings/ollama.py,sha256=hWdrTiuJwNSRYCqP0WP-z6XXA3RBGkAiknZMsPLH0qU,441
15
15
  haiku/rag/embeddings/openai.py,sha256=reh8AykG2f9f5hhRDmqSsjiuCPi9SsXfe2YEZFlxXk8,550
16
16
  haiku/rag/embeddings/voyageai.py,sha256=jc0JywdLJD3Ee1MUv1m8MhWCEo0enNnVcrIBtUvD-Ss,534
17
+ haiku/rag/qa/__init__.py,sha256=xN36Sw5xj3rHiI3D9YGSoX4ywK0sSkmHnG0uf_3bj08,1534
18
+ haiku/rag/qa/anthropic.py,sha256=lzHRQxpEv6Qd6iBIqexUgWnq-ITqytppwkfOuRGWdDs,4556
19
+ haiku/rag/qa/base.py,sha256=4ZTM_l5FAZ9cA0f8NeqRJiUAmjatwCTmSoclFw0gTFQ,1349
20
+ haiku/rag/qa/ollama.py,sha256=poShrse-RgLTwa5gbVzoERNTrn5QRpovJCZKYkIpOZI,2393
21
+ haiku/rag/qa/openai.py,sha256=yBbSjGlG4Lo5p2B2NOTa5C6JceX0OJ1jXar_ABFZYYI,3849
22
+ haiku/rag/qa/prompts.py,sha256=dAz2HjD4eJ8tcW534Tx7EuFOs6pSv2kPr7yrHnHtS0E,535
17
23
  haiku/rag/store/__init__.py,sha256=hq0W0DAC7ysqhWSP2M2uHX8cbG6kbr-sWHxhq6qQcY0,103
18
24
  haiku/rag/store/engine.py,sha256=BeYZRZ08zaYeeu375ysnAL3tGz4roA3GzP7WRNwznCo,2603
19
25
  haiku/rag/store/models/__init__.py,sha256=s0E72zneGlowvZrFWaNxHYjOAUjgWdLxzdYsnvNRVlY,88
20
- haiku/rag/store/models/chunk.py,sha256=D-fLHXtItXXyClj_KaE1OV-QQ-urDGS7lTE-qv2VHjw,223
26
+ haiku/rag/store/models/chunk.py,sha256=lmbPOOTz-N4PXhrA5XCUxyRcSTZBo135fqkV1mwnGcE,309
21
27
  haiku/rag/store/models/document.py,sha256=TVXVY-nQs-1vCORQEs9rA7zOtndeGC4dgCoujLAS054,396
22
28
  haiku/rag/store/repositories/__init__.py,sha256=uIBhxjQh-4o3O-ck8b7BQ58qXQTuJdPvrDIHVhY5T1A,263
23
29
  haiku/rag/store/repositories/base.py,sha256=cm3VyQXhtxvRfk1uJHpA0fDSxMpYN-mjQmRiDiLsQ68,1008
24
- haiku/rag/store/repositories/chunk.py,sha256=6zABVlb5zbMQ4s50z9qb53ieHYaiv4CjgxpbsXxs814,14639
30
+ haiku/rag/store/repositories/chunk.py,sha256=gik7ZPOK3gCoG6tU1pGueAZBPmJxIb7obYFUhwINrYg,16497
25
31
  haiku/rag/store/repositories/document.py,sha256=xpWOpjHFbhVwNJ1gpusEKNY6l_Qyibg9y_bdHCwcfpk,7133
26
- haiku_rag-0.2.0.dist-info/METADATA,sha256=o9PPoiXU7VIRAuQVwFvfQg4w-8ufz5aLo9PuG0ykWuI,7468
27
- haiku_rag-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
- haiku_rag-0.2.0.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
29
- haiku_rag-0.2.0.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
30
- haiku_rag-0.2.0.dist-info/RECORD,,
32
+ haiku_rag-0.3.1.dist-info/METADATA,sha256=_1rJ4s0aq82EkBRPfaPmRZ84QGYTfACyV5V_hk3F118,3931
33
+ haiku_rag-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
+ haiku_rag-0.3.1.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
35
+ haiku_rag-0.3.1.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
36
+ haiku_rag-0.3.1.dist-info/RECORD,,
@@ -1,230 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: haiku.rag
3
- Version: 0.2.0
4
- Summary: Retrieval Augmented Generation (RAG) with SQLite
5
- Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
6
- License: MIT
7
- License-File: LICENSE
8
- Keywords: RAG,mcp,ml,sqlite,sqlite-vec
9
- Classifier: Development Status :: 4 - Beta
10
- Classifier: Environment :: Console
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Operating System :: MacOS
13
- Classifier: Operating System :: Microsoft :: Windows :: Windows 10
14
- Classifier: Operating System :: Microsoft :: Windows :: Windows 11
15
- Classifier: Operating System :: POSIX :: Linux
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Typing :: Typed
20
- Requires-Python: >=3.10
21
- Requires-Dist: fastmcp>=2.8.1
22
- Requires-Dist: httpx>=0.28.1
23
- Requires-Dist: markitdown[audio-transcription,docx,pdf,pptx,xlsx]>=0.1.2
24
- Requires-Dist: ollama>=0.5.1
25
- Requires-Dist: pydantic>=2.11.7
26
- Requires-Dist: python-dotenv>=1.1.0
27
- Requires-Dist: rich>=14.0.0
28
- Requires-Dist: sqlite-vec>=0.1.6
29
- Requires-Dist: tiktoken>=0.9.0
30
- Requires-Dist: typer>=0.16.0
31
- Requires-Dist: watchfiles>=1.1.0
32
- Provides-Extra: openai
33
- Requires-Dist: openai>=1.0.0; extra == 'openai'
34
- Provides-Extra: voyageai
35
- Requires-Dist: voyageai>=0.3.2; extra == 'voyageai'
36
- Description-Content-Type: text/markdown
37
-
38
- # Haiku SQLite RAG
39
-
40
- A SQLite-based Retrieval-Augmented Generation (RAG) system built for efficient document storage, chunking, and hybrid search capabilities.
41
-
42
- ## Features
43
- - **Local SQLite**: No need to run additional servers
44
- - **Support for various embedding providers**: You can use Ollama, VoyageAI, OpenAI or add your own
45
- - **Hybrid Search**: Vector search using `sqlite-vec` combined with full-text search `FTS5`, using Reciprocal Rank Fusion
46
- - **Multi-format Support**: Parse 40+ file formats including PDF, DOCX, HTML, Markdown, audio and more. Or add a url!
47
- - **File monitoring** when run as a server automatically indexing your files
48
- - **MCP server** Exposes functionality as MCP tools.
49
- - **Python client** Call `haiku.rag` from your own python applications.
50
-
51
- ## Installation
52
-
53
- ```bash
54
- uv pip install haiku.rag
55
- ```
56
-
57
- By default Ollama (with the `mxbai-embed-large` model) is used for the embeddings.
58
- For other providers use:
59
-
60
- - **VoyageAI**: `uv pip install haiku.rag --extra voyageai`
61
- - **OpenAI**: `uv pip install haiku.rag --extra openai`
62
-
63
- ## Configuration
64
-
65
- You can set the directories to monitor using the `MONITOR_DIRECTORIES` environment variable (as comma separated values) :
66
-
67
- ```bash
68
- # Monitor single directory
69
- export MONITOR_DIRECTORIES="/path/to/documents,/another_path/to/documents"
70
- ```
71
-
72
- If you want to use an alternative embeddings provider (Ollama being the default) you will need to set the provider details through environment variables:
73
-
74
- By default:
75
-
76
- ```bash
77
- EMBEDDINGS_PROVIDER="ollama"
78
- EMBEDDINGS_MODEL="mxbai-embed-large" # or any other model
79
- EMBEDDINGS_VECTOR_DIM=1024
80
- ```
81
-
82
- For VoyageAI:
83
- ```bash
84
- EMBEDDINGS_PROVIDER="voyageai"
85
- EMBEDDINGS_MODEL="voyage-3.5" # or any other model
86
- EMBEDDINGS_VECTOR_DIM=1024
87
- VOYAGE_API_KEY="your-api-key"
88
- ```
89
-
90
- For OpenAI:
91
- ```bash
92
- EMBEDDINGS_PROVIDER="openai"
93
- EMBEDDINGS_MODEL="text-embedding-3-small" # or text-embedding-3-large
94
- EMBEDDINGS_VECTOR_DIM=1536
95
- OPENAI_API_KEY="your-api-key"
96
- ```
97
-
98
- ## Command Line Interface
99
-
100
- `haiku.rag` includes a CLI application for managing documents and performing searches from the command line:
101
-
102
- ### Available Commands
103
-
104
- ```bash
105
- # List all documents
106
- haiku-rag list
107
-
108
- # Add document from text
109
- haiku-rag add "Your document content here"
110
-
111
- # Add document from file or URL
112
- haiku-rag add-src /path/to/document.pdf
113
- haiku-rag add-src https://example.com/article.html
114
-
115
- # Get and display a specific document
116
- haiku-rag get 1
117
-
118
- # Delete a document by ID
119
- haiku-rag delete 1
120
-
121
- # Search documents
122
- haiku-rag search "machine learning"
123
-
124
- # Search with custom options
125
- haiku-rag search "python programming" --limit 10 --k 100
126
-
127
- # Start file monitoring & MCP server (default HTTP transport)
128
- haiku-rag serve # --stdio for stdio transport or --sse for SSE transport
129
- ```
130
-
131
- All commands support the `--db` option to specify a custom database path. Run
132
- ```bash
133
- haiku-rag command -h
134
- ```
135
- to see additional parameters for a command.
136
-
137
- ## File Monitoring & MCP server
138
-
139
- You can start the server (using Streamble HTTP, stdio or SSE transports) with:
140
-
141
- ```bash
142
- # Start with default HTTP transport
143
- haiku-rag serve # --stdio for stdio transport or --sse for SSE transport
144
- ```
145
-
146
- You need to have set the `MONITOR_DIRECTORIES` environment variable for monitoring to take place.
147
-
148
- ### File monitoring
149
-
150
- `haiku.rag` can watch directories for changes and automatically update the document store:
151
-
152
- - **Startup**: Scan all monitored directories and add any new files
153
- - **File Added/Modified**: Automatically parse and add/update the document in the database
154
- - **File Deleted**: Remove the corresponding document from the database
155
-
156
- ### MCP Server
157
-
158
- `haiku.rag` includes a Model Context Protocol (MCP) server that exposes RAG functionality as tools for AI assistants like Claude Desktop. The MCP server provides the following tools:
159
-
160
- - `add_document_from_file` - Add documents from local file paths
161
- - `add_document_from_url` - Add documents from URLs
162
- - `add_document_from_text` - Add documents from raw text content
163
- - `search_documents` - Search documents using hybrid search
164
- - `get_document` - Retrieve specific documents by ID
165
- - `list_documents` - List all documents with pagination
166
- - `delete_document` - Delete documents by ID
167
-
168
- ## Using `haiku.rag` from python
169
-
170
- ### Managing documents
171
-
172
- ```python
173
- from pathlib import Path
174
- from haiku.rag.client import HaikuRAG
175
-
176
- # Use as async context manager (recommended)
177
- async with HaikuRAG("path/to/database.db") as client:
178
- # Create document from text
179
- doc = await client.create_document(
180
- content="Your document content here",
181
- uri="doc://example",
182
- metadata={"source": "manual", "topic": "example"}
183
- )
184
-
185
- # Create document from file (auto-parses content)
186
- doc = await client.create_document_from_source("path/to/document.pdf")
187
-
188
- # Create document from URL
189
- doc = await client.create_document_from_source("https://example.com/article.html")
190
-
191
- # Retrieve documents
192
- doc = await client.get_document_by_id(1)
193
- doc = await client.get_document_by_uri("file:///path/to/document.pdf")
194
-
195
- # List all documents with pagination
196
- docs = await client.list_documents(limit=10, offset=0)
197
-
198
- # Update document content
199
- doc.content = "Updated content"
200
- await client.update_document(doc)
201
-
202
- # Delete document
203
- await client.delete_document(doc.id)
204
-
205
- # Search documents using hybrid search (vector + full-text)
206
- results = await client.search("machine learning algorithms", limit=5)
207
- for chunk, score in results:
208
- print(f"Score: {score:.3f}")
209
- print(f"Content: {chunk.content}")
210
- print(f"Document ID: {chunk.document_id}")
211
- print("---")
212
- ```
213
-
214
- ## Searching documents
215
-
216
- ```python
217
- async with HaikuRAG("database.db") as client:
218
-
219
- results = await client.search(
220
- query="machine learning",
221
- limit=5, # Maximum results to return, defaults to 5
222
- k=60 # RRF parameter for reciprocal rank fusion, defaults to 60
223
- )
224
-
225
- # Process results
226
- for chunk, relevance_score in results:
227
- print(f"Relevance: {relevance_score:.3f}")
228
- print(f"Content: {chunk.content}")
229
- print(f"From document: {chunk.document_id}")
230
- ```