haiku.rag 0.8.1__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of haiku.rag might be problematic. Click here for more details.

haiku/rag/app.py CHANGED
@@ -9,6 +9,7 @@ from haiku.rag.client import HaikuRAG
9
9
  from haiku.rag.config import Config
10
10
  from haiku.rag.mcp import create_mcp_server
11
11
  from haiku.rag.monitor import FileWatcher
12
+ from haiku.rag.research.orchestrator import ResearchOrchestrator
12
13
  from haiku.rag.store.models.chunk import Chunk
13
14
  from haiku.rag.store.models.document import Document
14
15
 
@@ -78,6 +79,85 @@ class HaikuRAGApp:
78
79
  except Exception as e:
79
80
  self.console.print(f"[red]Error: {e}[/red]")
80
81
 
82
+ async def research(
83
+ self, question: str, max_iterations: int = 3, verbose: bool = False
84
+ ):
85
+ """Run multi-agent research on a question."""
86
+ async with HaikuRAG(db_path=self.db_path) as client:
87
+ try:
88
+ # Create orchestrator with default config or fallback to QA
89
+ orchestrator = ResearchOrchestrator()
90
+
91
+ if verbose:
92
+ self.console.print(
93
+ f"[bold cyan]Starting research with {orchestrator.provider}:{orchestrator.model}[/bold cyan]"
94
+ )
95
+ self.console.print(f"[bold blue]Question:[/bold blue] {question}")
96
+ self.console.print()
97
+
98
+ # Conduct research
99
+ report = await orchestrator.conduct_research(
100
+ question=question,
101
+ client=client,
102
+ max_iterations=max_iterations,
103
+ verbose=verbose,
104
+ console=self.console if verbose else None,
105
+ )
106
+
107
+ # Display the report
108
+ self.console.print("[bold green]Research Report[/bold green]")
109
+ self.console.rule()
110
+
111
+ # Title and Executive Summary
112
+ self.console.print(f"[bold]{report.title}[/bold]")
113
+ self.console.print()
114
+ self.console.print("[bold cyan]Executive Summary:[/bold cyan]")
115
+ self.console.print(report.executive_summary)
116
+ self.console.print()
117
+
118
+ # Main Findings
119
+ if report.main_findings:
120
+ self.console.print("[bold cyan]Main Findings:[/bold cyan]")
121
+ for finding in report.main_findings:
122
+ self.console.print(f"• {finding}")
123
+ self.console.print()
124
+
125
+ # Themes
126
+ if report.themes:
127
+ self.console.print("[bold cyan]Key Themes:[/bold cyan]")
128
+ for theme, explanation in report.themes.items():
129
+ self.console.print(f"• [bold]{theme}[/bold]: {explanation}")
130
+ self.console.print()
131
+
132
+ # Conclusions
133
+ if report.conclusions:
134
+ self.console.print("[bold cyan]Conclusions:[/bold cyan]")
135
+ for conclusion in report.conclusions:
136
+ self.console.print(f"• {conclusion}")
137
+ self.console.print()
138
+
139
+ # Recommendations
140
+ if report.recommendations:
141
+ self.console.print("[bold cyan]Recommendations:[/bold cyan]")
142
+ for rec in report.recommendations:
143
+ self.console.print(f"• {rec}")
144
+ self.console.print()
145
+
146
+ # Limitations
147
+ if report.limitations:
148
+ self.console.print("[bold yellow]Limitations:[/bold yellow]")
149
+ for limitation in report.limitations:
150
+ self.console.print(f"• {limitation}")
151
+ self.console.print()
152
+
153
+ # Sources Summary
154
+ if report.sources_summary:
155
+ self.console.print("[bold cyan]Sources:[/bold cyan]")
156
+ self.console.print(report.sources_summary)
157
+
158
+ except Exception as e:
159
+ self.console.print(f"[red]Error during research: {e}[/red]")
160
+
81
161
  async def rebuild(self):
82
162
  async with HaikuRAG(db_path=self.db_path, skip_validation=True) as client:
83
163
  try:
haiku/rag/cli.py CHANGED
@@ -3,6 +3,7 @@ import warnings
3
3
  from importlib.metadata import version
4
4
  from pathlib import Path
5
5
 
6
+ import logfire
6
7
  import typer
7
8
  from rich.console import Console
8
9
 
@@ -12,6 +13,9 @@ from haiku.rag.logging import configure_cli_logging
12
13
  from haiku.rag.migration import migrate_sqlite_to_lancedb
13
14
  from haiku.rag.utils import is_up_to_date
14
15
 
16
+ logfire.configure(send_to_logfire="if-token-present")
17
+ logfire.instrument_pydantic_ai()
18
+
15
19
  if not Config.ENV == "development":
16
20
  warnings.filterwarnings("ignore")
17
21
 
@@ -235,6 +239,38 @@ def ask(
235
239
  asyncio.run(app.ask(question=question, cite=cite))
236
240
 
237
241
 
242
+ @cli.command("research", help="Run multi-agent research and output a concise report")
243
+ def research(
244
+ question: str = typer.Argument(
245
+ help="The research question to investigate",
246
+ ),
247
+ max_iterations: int = typer.Option(
248
+ 3,
249
+ "--max-iterations",
250
+ "-n",
251
+ help="Maximum search/analyze iterations",
252
+ ),
253
+ db: Path = typer.Option(
254
+ Config.DEFAULT_DATA_DIR / "haiku.rag.lancedb",
255
+ "--db",
256
+ help="Path to the LanceDB database file",
257
+ ),
258
+ verbose: bool = typer.Option(
259
+ False,
260
+ "--verbose",
261
+ help="Show verbose progress output",
262
+ ),
263
+ ):
264
+ app = HaikuRAGApp(db_path=db)
265
+ asyncio.run(
266
+ app.research(
267
+ question=question,
268
+ max_iterations=max_iterations,
269
+ verbose=verbose,
270
+ )
271
+ )
272
+
273
+
238
274
  @cli.command("settings", help="Display current configuration settings")
239
275
  def settings():
240
276
  app = HaikuRAGApp(db_path=Path()) # Don't need actual DB for settings
haiku/rag/config.py CHANGED
@@ -27,7 +27,11 @@ class AppConfig(BaseModel):
27
27
  RERANK_MODEL: str = ""
28
28
 
29
29
  QA_PROVIDER: str = "ollama"
30
- QA_MODEL: str = "qwen3"
30
+ QA_MODEL: str = "gpt-oss"
31
+
32
+ # Research defaults (fallback to QA if not provided via env)
33
+ RESEARCH_PROVIDER: str = "ollama"
34
+ RESEARCH_MODEL: str = "gpt-oss"
31
35
 
32
36
  CHUNK_SIZE: int = 256
33
37
  CONTEXT_CHUNK_RADIUS: int = 0
@@ -37,9 +41,11 @@ class AppConfig(BaseModel):
37
41
  MARKDOWN_PREPROCESSOR: str = ""
38
42
 
39
43
  OLLAMA_BASE_URL: str = "http://localhost:11434"
44
+
40
45
  VLLM_EMBEDDINGS_BASE_URL: str = ""
41
46
  VLLM_RERANK_BASE_URL: str = ""
42
47
  VLLM_QA_BASE_URL: str = ""
48
+ VLLM_RESEARCH_BASE_URL: str = ""
43
49
 
44
50
  # Provider keys
45
51
  VOYAGE_API_KEY: str = ""
haiku/rag/qa/agent.py CHANGED
@@ -6,7 +6,7 @@ from pydantic_ai.providers.openai import OpenAIProvider
6
6
 
7
7
  from haiku.rag.client import HaikuRAG
8
8
  from haiku.rag.config import Config
9
- from haiku.rag.qa.prompts import SYSTEM_PROMPT, SYSTEM_PROMPT_WITH_CITATIONS
9
+ from haiku.rag.qa.prompts import QA_SYSTEM_PROMPT, QA_SYSTEM_PROMPT_WITH_CITATIONS
10
10
 
11
11
 
12
12
  class SearchResult(BaseModel):
@@ -31,7 +31,9 @@ class QuestionAnswerAgent:
31
31
  ):
32
32
  self._client = client
33
33
 
34
- system_prompt = SYSTEM_PROMPT_WITH_CITATIONS if use_citations else SYSTEM_PROMPT
34
+ system_prompt = (
35
+ QA_SYSTEM_PROMPT_WITH_CITATIONS if use_citations else QA_SYSTEM_PROMPT
36
+ )
35
37
  model_obj = self._get_model(provider, model)
36
38
 
37
39
  self._agent = Agent(
haiku/rag/qa/prompts.py CHANGED
@@ -1,4 +1,4 @@
1
- SYSTEM_PROMPT = """
1
+ QA_SYSTEM_PROMPT = """
2
2
  You are a knowledgeable assistant that helps users find information from a document knowledge base.
3
3
 
4
4
  Your process:
@@ -21,7 +21,7 @@ Be concise, and always maintain accuracy over completeness. Prefer short, direct
21
21
  /no_think
22
22
  """
23
23
 
24
- SYSTEM_PROMPT_WITH_CITATIONS = """
24
+ QA_SYSTEM_PROMPT_WITH_CITATIONS = """
25
25
  You are a knowledgeable assistant that helps users find information from a document knowledge base.
26
26
 
27
27
  IMPORTANT: You MUST use the search_documents tool for every question. Do not answer any question without first searching the knowledge base.
@@ -0,0 +1,35 @@
1
+ """Multi-agent research workflow for advanced RAG queries."""
2
+
3
+ from haiku.rag.research.base import (
4
+ BaseResearchAgent,
5
+ ResearchOutput,
6
+ SearchAnswer,
7
+ SearchResult,
8
+ )
9
+ from haiku.rag.research.dependencies import ResearchContext, ResearchDependencies
10
+ from haiku.rag.research.evaluation_agent import (
11
+ AnalysisEvaluationAgent,
12
+ EvaluationResult,
13
+ )
14
+ from haiku.rag.research.orchestrator import ResearchOrchestrator, ResearchPlan
15
+ from haiku.rag.research.search_agent import SearchSpecialistAgent
16
+ from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
17
+
18
+ __all__ = [
19
+ # Base classes
20
+ "BaseResearchAgent",
21
+ "ResearchDependencies",
22
+ "ResearchContext",
23
+ "SearchResult",
24
+ "ResearchOutput",
25
+ # Specialized agents
26
+ "SearchAnswer",
27
+ "SearchSpecialistAgent",
28
+ "AnalysisEvaluationAgent",
29
+ "EvaluationResult",
30
+ "SynthesisAgent",
31
+ "ResearchReport",
32
+ # Orchestrator
33
+ "ResearchOrchestrator",
34
+ "ResearchPlan",
35
+ ]
@@ -0,0 +1,122 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ from pydantic import BaseModel, Field
5
+ from pydantic_ai import Agent
6
+ from pydantic_ai.models.openai import OpenAIChatModel
7
+ from pydantic_ai.output import ToolOutput
8
+ from pydantic_ai.providers.ollama import OllamaProvider
9
+ from pydantic_ai.providers.openai import OpenAIProvider
10
+ from pydantic_ai.run import AgentRunResult
11
+
12
+ from haiku.rag.config import Config
13
+
14
+ if TYPE_CHECKING:
15
+ from haiku.rag.research.dependencies import ResearchDependencies
16
+
17
+
18
+ class BaseResearchAgent[T](ABC):
19
+ """Base class for all research agents."""
20
+
21
+ def __init__(
22
+ self,
23
+ provider: str,
24
+ model: str,
25
+ output_type: type[T],
26
+ ):
27
+ self.provider = provider
28
+ self.model = model
29
+ self.output_type = output_type
30
+
31
+ model_obj = self._get_model(provider, model)
32
+
33
+ # Import deps type lazily to avoid circular import during module load
34
+ from haiku.rag.research.dependencies import ResearchDependencies
35
+
36
+ self._agent = Agent(
37
+ model=model_obj,
38
+ deps_type=ResearchDependencies,
39
+ output_type=ToolOutput(self.output_type, max_retries=3),
40
+ system_prompt=self.get_system_prompt(),
41
+ )
42
+
43
+ # Register tools
44
+ self.register_tools()
45
+
46
+ def _get_model(self, provider: str, model: str):
47
+ """Get the appropriate model object for the provider."""
48
+ if provider == "ollama":
49
+ return OpenAIChatModel(
50
+ model_name=model,
51
+ provider=OllamaProvider(base_url=f"{Config.OLLAMA_BASE_URL}/v1"),
52
+ )
53
+ elif provider == "vllm":
54
+ return OpenAIChatModel(
55
+ model_name=model,
56
+ provider=OpenAIProvider(
57
+ base_url=f"{Config.VLLM_RESEARCH_BASE_URL or Config.VLLM_QA_BASE_URL}/v1",
58
+ api_key="none",
59
+ ),
60
+ )
61
+ else:
62
+ # For all other providers, use the provider:model format
63
+ return f"{provider}:{model}"
64
+
65
+ @abstractmethod
66
+ def get_system_prompt(self) -> str:
67
+ """Return the system prompt for this agent."""
68
+ pass
69
+
70
+ @abstractmethod
71
+ def register_tools(self) -> None:
72
+ """Register agent-specific tools."""
73
+ pass
74
+
75
+ async def run(
76
+ self, prompt: str, deps: ResearchDependencies, **kwargs
77
+ ) -> AgentRunResult[T]:
78
+ """Execute the agent."""
79
+ return await self._agent.run(prompt, deps=deps, **kwargs)
80
+
81
+ @property
82
+ def agent(self) -> Agent[Any, T]:
83
+ """Access the underlying Pydantic AI agent."""
84
+ return self._agent
85
+
86
+
87
+ class SearchResult(BaseModel):
88
+ """Standard search result format."""
89
+
90
+ content: str
91
+ score: float
92
+ document_uri: str
93
+ metadata: dict[str, Any] = Field(default_factory=dict)
94
+
95
+
96
+ class ResearchOutput(BaseModel):
97
+ """Standard research output format."""
98
+
99
+ summary: str
100
+ detailed_findings: list[str]
101
+ sources: list[str]
102
+ confidence: float
103
+
104
+
105
+ class SearchAnswer(BaseModel):
106
+ """Structured output for the SearchSpecialist agent."""
107
+
108
+ query: str = Field(description="The search query that was performed")
109
+ answer: str = Field(description="The answer generated based on the context")
110
+ context: list[str] = Field(
111
+ description=(
112
+ "Only the minimal set of relevant snippets (verbatim) that directly "
113
+ "support the answer"
114
+ )
115
+ )
116
+ sources: list[str] = Field(
117
+ description=(
118
+ "Document URIs corresponding to the snippets actually used in the"
119
+ " answer (one URI per snippet; omit if none)"
120
+ ),
121
+ default_factory=list,
122
+ )
@@ -0,0 +1,45 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from haiku.rag.client import HaikuRAG
4
+ from haiku.rag.research.base import SearchAnswer
5
+
6
+
7
+ class ResearchContext(BaseModel):
8
+ """Context shared across research agents."""
9
+
10
+ original_question: str = Field(description="The original research question")
11
+ sub_questions: list[str] = Field(
12
+ default_factory=list, description="Decomposed sub-questions"
13
+ )
14
+ qa_responses: list["SearchAnswer"] = Field(
15
+ default_factory=list, description="Structured QA pairs used during research"
16
+ )
17
+ insights: list[str] = Field(
18
+ default_factory=list, description="Key insights discovered"
19
+ )
20
+ gaps: list[str] = Field(
21
+ default_factory=list, description="Identified information gaps"
22
+ )
23
+
24
+ def add_qa_response(self, qa: "SearchAnswer") -> None:
25
+ """Add a structured QA response (minimal context already included)."""
26
+ self.qa_responses.append(qa)
27
+
28
+ def add_insight(self, insight: str) -> None:
29
+ """Add a key insight."""
30
+ if insight not in self.insights:
31
+ self.insights.append(insight)
32
+
33
+ def add_gap(self, gap: str) -> None:
34
+ """Identify an information gap."""
35
+ if gap not in self.gaps:
36
+ self.gaps.append(gap)
37
+
38
+
39
+ class ResearchDependencies(BaseModel):
40
+ """Dependencies for research agents with multi-agent context."""
41
+
42
+ model_config = {"arbitrary_types_allowed": True}
43
+
44
+ client: HaikuRAG = Field(description="RAG client for document operations")
45
+ context: ResearchContext = Field(description="Shared research context")
@@ -0,0 +1,40 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from haiku.rag.research.base import BaseResearchAgent
4
+ from haiku.rag.research.prompts import EVALUATION_AGENT_PROMPT
5
+
6
+
7
+ class EvaluationResult(BaseModel):
8
+ """Result of analysis and evaluation."""
9
+
10
+ key_insights: list[str] = Field(
11
+ description="Main insights extracted from the research so far"
12
+ )
13
+ new_questions: list[str] = Field(
14
+ description="New sub-questions to add to the research (max 3)", max_length=3
15
+ )
16
+ confidence_score: float = Field(
17
+ description="Confidence level in the completeness of research (0-1)",
18
+ ge=0.0,
19
+ le=1.0,
20
+ )
21
+ is_sufficient: bool = Field(
22
+ description="Whether the research is sufficient to answer the original question"
23
+ )
24
+ reasoning: str = Field(
25
+ description="Explanation of why the research is or isn't complete"
26
+ )
27
+
28
+
29
+ class AnalysisEvaluationAgent(BaseResearchAgent[EvaluationResult]):
30
+ """Agent that analyzes findings and evaluates research completeness."""
31
+
32
+ def __init__(self, provider: str, model: str) -> None:
33
+ super().__init__(provider, model, output_type=EvaluationResult)
34
+
35
+ def get_system_prompt(self) -> str:
36
+ return EVALUATION_AGENT_PROMPT
37
+
38
+ def register_tools(self) -> None:
39
+ """No additional tools needed - uses LLM capabilities directly."""
40
+ pass
@@ -0,0 +1,265 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, Field
4
+ from pydantic_ai.format_prompt import format_as_xml
5
+ from pydantic_ai.run import AgentRunResult
6
+ from rich.console import Console
7
+
8
+ from haiku.rag.config import Config
9
+ from haiku.rag.research.base import BaseResearchAgent
10
+ from haiku.rag.research.dependencies import ResearchContext, ResearchDependencies
11
+ from haiku.rag.research.evaluation_agent import (
12
+ AnalysisEvaluationAgent,
13
+ EvaluationResult,
14
+ )
15
+ from haiku.rag.research.prompts import ORCHESTRATOR_PROMPT
16
+ from haiku.rag.research.search_agent import SearchSpecialistAgent
17
+ from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
18
+
19
+
20
+ class ResearchPlan(BaseModel):
21
+ """Research execution plan."""
22
+
23
+ main_question: str = Field(description="The main research question")
24
+ sub_questions: list[str] = Field(
25
+ description="Decomposed sub-questions to investigate (max 3)", max_length=3
26
+ )
27
+
28
+
29
+ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
30
+ """Orchestrator agent that coordinates the research workflow."""
31
+
32
+ def __init__(
33
+ self, provider: str | None = Config.RESEARCH_PROVIDER, model: str | None = None
34
+ ):
35
+ # Use provided values or fall back to config defaults
36
+ provider = provider or Config.RESEARCH_PROVIDER or Config.QA_PROVIDER
37
+ model = model or Config.RESEARCH_MODEL or Config.QA_MODEL
38
+
39
+ super().__init__(provider, model, output_type=ResearchPlan)
40
+
41
+ self.search_agent: SearchSpecialistAgent = SearchSpecialistAgent(
42
+ provider, model
43
+ )
44
+ self.evaluation_agent: AnalysisEvaluationAgent = AnalysisEvaluationAgent(
45
+ provider, model
46
+ )
47
+ self.synthesis_agent: SynthesisAgent = SynthesisAgent(provider, model)
48
+
49
+ def get_system_prompt(self) -> str:
50
+ return ORCHESTRATOR_PROMPT
51
+
52
+ def register_tools(self) -> None:
53
+ """Register orchestration tools."""
54
+ # Tools are no longer needed - orchestrator directly calls agents
55
+ pass
56
+
57
+ def _format_context_for_prompt(self, context: ResearchContext) -> str:
58
+ """Format the research context as XML for inclusion in prompts."""
59
+
60
+ context_data = {
61
+ "original_question": context.original_question,
62
+ "unanswered_questions": context.sub_questions,
63
+ "qa_responses": [
64
+ {"question": qa.query, "answer": qa.answer}
65
+ for qa in context.qa_responses
66
+ ],
67
+ "insights": context.insights,
68
+ "gaps": context.gaps,
69
+ }
70
+ return format_as_xml(context_data, root_tag="research_context")
71
+
72
+ async def conduct_research(
73
+ self,
74
+ question: str,
75
+ client: Any,
76
+ max_iterations: int = 3,
77
+ confidence_threshold: float = 0.8,
78
+ verbose: bool = False,
79
+ console: Console | None = None,
80
+ ) -> ResearchReport:
81
+ """Conduct comprehensive research on a question.
82
+
83
+ Args:
84
+ question: The research question to investigate
85
+ client: HaikuRAG client for document operations
86
+ max_iterations: Maximum number of search-analyze-clarify cycles
87
+ confidence_threshold: Minimum confidence level to stop research (0-1)
88
+ verbose: If True, print progress and intermediate results
89
+ console: Optional Rich console for output
90
+
91
+ Returns:
92
+ ResearchReport with comprehensive findings
93
+ """
94
+
95
+ # Initialize context
96
+ context = ResearchContext(original_question=question)
97
+ deps = ResearchDependencies(client=client, context=context)
98
+
99
+ # Use provided console or create a new one
100
+ console = console or Console() if verbose else None
101
+
102
+ # Create initial research plan
103
+ if console:
104
+ console.print("\n[bold cyan]📋 Creating research plan...[/bold cyan]")
105
+
106
+ plan_result: AgentRunResult[ResearchPlan] = await self.run(
107
+ f"Create a research plan for: {question}", deps=deps
108
+ )
109
+
110
+ context.sub_questions = plan_result.output.sub_questions
111
+
112
+ if console:
113
+ console.print("\n[bold green]✅ Research Plan Created:[/bold green]")
114
+ console.print(
115
+ f" [bold]Main Question:[/bold] {plan_result.output.main_question}"
116
+ )
117
+ console.print(" [bold]Sub-questions:[/bold]")
118
+ for i, sq in enumerate(plan_result.output.sub_questions, 1):
119
+ console.print(f" {i}. {sq}")
120
+ console.print()
121
+
122
+ # Execute research iterations
123
+ for iteration in range(max_iterations):
124
+ if console:
125
+ console.rule(
126
+ f"[bold yellow]🔄 Iteration {iteration + 1}/{max_iterations}[/bold yellow]"
127
+ )
128
+
129
+ # Check if we have questions to search
130
+ if not context.sub_questions:
131
+ # No more questions to explore
132
+ if console:
133
+ console.print(
134
+ "[yellow]No more questions to explore. Concluding research.[/yellow]"
135
+ )
136
+ break
137
+
138
+ # Use current sub-questions for this iteration
139
+ questions_to_search = context.sub_questions
140
+
141
+ # Search phase - answer all questions in this iteration
142
+ if console:
143
+ console.print(
144
+ f"\n[bold cyan]🔍 Searching & Answering {len(questions_to_search)} questions:[/bold cyan]"
145
+ )
146
+ for i, q in enumerate(questions_to_search, 1):
147
+ console.print(f" {i}. {q}")
148
+
149
+ # Run searches for all questions and remove answered ones
150
+ answered_questions = []
151
+ for search_question in questions_to_search:
152
+ try:
153
+ await self.search_agent.run(search_question, deps=deps)
154
+ except Exception as e: # pragma: no cover - defensive
155
+ if console:
156
+ console.print(
157
+ f"\n [red]×[/red] Omitting failed question: {search_question} ({e})"
158
+ )
159
+ finally:
160
+ answered_questions.append(search_question)
161
+
162
+ if console and context.qa_responses:
163
+ # Show the last QA response (which should be for this question)
164
+ latest_qa = context.qa_responses[-1]
165
+ answer_preview = (
166
+ latest_qa.answer[:150] + "..."
167
+ if len(latest_qa.answer) > 150
168
+ else latest_qa.answer
169
+ )
170
+ console.print(
171
+ f"\n [green]✓[/green] {search_question[:50]}..."
172
+ if len(search_question) > 50
173
+ else f"\n [green]✓[/green] {search_question}"
174
+ )
175
+ console.print(f" {answer_preview}")
176
+
177
+ # Remove answered questions from the list
178
+ for question in answered_questions:
179
+ if question in context.sub_questions:
180
+ context.sub_questions.remove(question)
181
+
182
+ # Analysis and Evaluation phase
183
+ if console:
184
+ console.print(
185
+ "\n[bold cyan]📊 Analyzing and evaluating research progress...[/bold cyan]"
186
+ )
187
+
188
+ # Format context for the evaluation agent
189
+ context_xml = self._format_context_for_prompt(context)
190
+ evaluation_prompt = f"""Analyze all gathered information and evaluate the completeness of research.
191
+
192
+ {context_xml}
193
+
194
+ Evaluate the research progress for the original question and identify any remaining gaps."""
195
+
196
+ evaluation_result = await self.evaluation_agent.run(
197
+ evaluation_prompt,
198
+ deps=deps,
199
+ )
200
+
201
+ if console and evaluation_result.output:
202
+ output = evaluation_result.output
203
+ if output.key_insights:
204
+ console.print(" [bold]Key insights:[/bold]")
205
+ for insight in output.key_insights:
206
+ console.print(f" • {insight}")
207
+ console.print(
208
+ f" Confidence: [yellow]{output.confidence_score:.1%}[/yellow]"
209
+ )
210
+ status = (
211
+ "[green]Yes[/green]" if output.is_sufficient else "[red]No[/red]"
212
+ )
213
+ console.print(f" Sufficient: {status}")
214
+
215
+ # Store insights
216
+ for insight in evaluation_result.output.key_insights:
217
+ context.add_insight(insight)
218
+
219
+ # Add new questions to the sub-questions list
220
+ for new_q in evaluation_result.output.new_questions:
221
+ if new_q not in context.sub_questions:
222
+ context.sub_questions.append(new_q)
223
+
224
+ # Check if research is sufficient
225
+ if self._should_stop_research(evaluation_result, confidence_threshold):
226
+ if console:
227
+ console.print(
228
+ f"\n[bold green]✅ Stopping research:[/bold green] {evaluation_result.output.reasoning}"
229
+ )
230
+ break
231
+
232
+ # Generate final report
233
+ if console:
234
+ console.print(
235
+ "\n[bold cyan]📝 Generating final research report...[/bold cyan]"
236
+ )
237
+
238
+ # Format context for the synthesis agent
239
+ final_context_xml = self._format_context_for_prompt(context)
240
+ synthesis_prompt = f"""Generate a comprehensive research report based on all gathered information.
241
+
242
+ {final_context_xml}
243
+
244
+ Create a detailed report that synthesizes all findings into a coherent response."""
245
+
246
+ report_result: AgentRunResult[ResearchReport] = await self.synthesis_agent.run(
247
+ synthesis_prompt, deps=deps
248
+ )
249
+
250
+ if console:
251
+ console.print("[bold green]✅ Research complete![/bold green]")
252
+
253
+ return report_result.output
254
+
255
+ def _should_stop_research(
256
+ self,
257
+ evaluation_result: AgentRunResult[EvaluationResult],
258
+ confidence_threshold: float,
259
+ ) -> bool:
260
+ """Determine if research should stop based on evaluation."""
261
+
262
+ result = evaluation_result.output
263
+
264
+ # Stop if the agent indicates sufficient information AND confidence exceeds threshold
265
+ return result.is_sufficient and result.confidence_score >= confidence_threshold
@@ -0,0 +1,116 @@
1
+ ORCHESTRATOR_PROMPT = """You are a research orchestrator responsible for coordinating a comprehensive research workflow.
2
+
3
+ Your role is to:
4
+ 1. Understand and decompose the research question
5
+ 2. Plan a systematic research approach
6
+ 3. Coordinate specialized agents to gather and analyze information
7
+ 4. Ensure comprehensive coverage of the topic
8
+ 5. Iterate based on findings and gaps
9
+
10
+ Create a research plan that:
11
+ - Breaks down the question into at most 3 focused sub-questions
12
+ - Each sub-question should target a specific aspect of the research
13
+ - Prioritize the most important aspects to investigate
14
+ - Ensure comprehensive coverage within the 3-question limit
15
+ - IMPORTANT: Make each sub-question a standalone, self-contained query that can
16
+ be executed without additional context. Include necessary entities, scope,
17
+ timeframe, and qualifiers. Avoid pronouns like "it/they/this"; write queries
18
+ that make sense in isolation."""
19
+
20
+ SEARCH_AGENT_PROMPT = """You are a search and question-answering specialist.
21
+
22
+ Your role is to:
23
+ 1. Search the knowledge base for relevant information
24
+ 2. Analyze the retrieved documents
25
+ 3. Provide an accurate answer strictly grounded in the retrieved context
26
+
27
+ Output format:
28
+ - You must return a SearchAnswer model with fields:
29
+ - query: the question being answered (echo the user query)
30
+ - answer: your final answer based only on the provided context
31
+ - context: list[str] of only the minimal set of verbatim snippet texts you
32
+ used to justify the answer (do not include unrelated text; do not invent)
33
+ - sources: list[str] of document_uri values corresponding to the snippets you
34
+ actually used in the answer (one URI per context snippet, order aligned)
35
+
36
+ Tool usage:
37
+ - Always call the search_and_answer tool before drafting any answer.
38
+ - The tool returns XML containing only a list of snippets, where each snippet
39
+ has the verbatim `text`, a `score` indicating relevance, and the
40
+ `document_uri` it came from.
41
+ - You may call the tool multiple times to refine or broaden context, but do not
42
+ exceed 3 total tool calls per question. Prefer precision over volume.
43
+ - Use scores to prioritize evidence, but include only the minimal subset of
44
+ snippet texts (verbatim) in SearchAnswer.context.
45
+ - Set SearchAnswer.sources to the matching document_uris for the snippets you
46
+ used (one URI per snippet, aligned by order). Context must be text-only.
47
+ - If no relevant information is found, say so and return an empty context list.
48
+
49
+ Important:
50
+ - Do not include any content in the answer that is not supported by the context.
51
+ - Keep context snippets short (just the necessary lines), verbatim, and focused."""
52
+
53
+ EVALUATION_AGENT_PROMPT = """You are an analysis and evaluation specialist for research workflows.
54
+
55
+ You have access to:
56
+ - The original research question
57
+ - Question-answer pairs from search operations
58
+ - Raw search results and source documents
59
+ - Previously identified insights
60
+
61
+ Your dual role is to:
62
+
63
+ ANALYSIS:
64
+ 1. Extract key insights from all gathered information
65
+ 2. Identify patterns and connections across sources
66
+ 3. Synthesize findings into coherent understanding
67
+ 4. Focus on the most important discoveries
68
+
69
+ EVALUATION:
70
+ 1. Assess if we have sufficient information to answer the original question
71
+ 2. Calculate a confidence score (0-1) based on:
72
+ - Coverage of the main question's aspects
73
+ - Quality and consistency of sources
74
+ - Depth of information gathered
75
+ 3. Identify specific gaps that still need investigation
76
+ 4. Generate up to 3 new sub-questions that haven't been answered yet
77
+
78
+ Be critical and thorough in your evaluation. Only mark research as sufficient when:
79
+ - All major aspects of the question are addressed
80
+ - Sources provide consistent, reliable information
81
+ - The depth of coverage meets the question's requirements
82
+ - No critical gaps remain
83
+
84
+ Generate new sub-questions that:
85
+ - Target specific unexplored aspects not covered by existing questions
86
+ - Seek clarification on ambiguities
87
+ - Explore important edge cases or exceptions
88
+ - Are focused and actionable (max 3)
89
+ - Do NOT repeat or rephrase questions that have already been answered (see qa_responses)
90
+ - Should be genuinely new areas to explore
91
+ - Must be standalone, self-contained queries: include entities, scope, and any
92
+ needed qualifiers (e.g., timeframe, region), and avoid ambiguous pronouns so
93
+ they can be executed independently."""
94
+
95
+ SYNTHESIS_AGENT_PROMPT = """You are a synthesis specialist agent focused on creating comprehensive research reports.
96
+
97
+ Your role is to:
98
+ 1. Synthesize all gathered information into a coherent narrative
99
+ 2. Present findings in a clear, structured format
100
+ 3. Draw evidence-based conclusions
101
+ 4. Acknowledge limitations and uncertainties
102
+ 5. Provide actionable recommendations
103
+ 6. Maintain academic rigor and objectivity
104
+
105
+ Your report should be:
106
+ - Comprehensive yet concise
107
+ - Well-structured and easy to follow
108
+ - Based solely on evidence from the research
109
+ - Transparent about limitations
110
+ - Professional and objective in tone
111
+
112
+ Focus on creating a report that provides clear value to the reader by:
113
+ - Answering the original research question thoroughly
114
+ - Highlighting the most important findings
115
+ - Explaining the implications of the research
116
+ - Suggesting concrete next steps"""
@@ -0,0 +1,64 @@
1
+ from pydantic_ai import RunContext
2
+ from pydantic_ai.format_prompt import format_as_xml
3
+ from pydantic_ai.run import AgentRunResult
4
+
5
+ from haiku.rag.research.base import BaseResearchAgent, SearchAnswer
6
+ from haiku.rag.research.dependencies import ResearchDependencies
7
+ from haiku.rag.research.prompts import SEARCH_AGENT_PROMPT
8
+
9
+
10
+ class SearchSpecialistAgent(BaseResearchAgent[SearchAnswer]):
11
+ """Agent specialized in answering questions using RAG search."""
12
+
13
+ def __init__(self, provider: str, model: str) -> None:
14
+ super().__init__(provider, model, output_type=SearchAnswer)
15
+
16
+ async def run(
17
+ self, prompt: str, deps: ResearchDependencies, **kwargs
18
+ ) -> AgentRunResult[SearchAnswer]:
19
+ """Execute the agent and persist the QA pair in shared context.
20
+
21
+ Pydantic AI enforces `SearchAnswer` as the output model; we just store
22
+ the QA response with the last search results as sources.
23
+ """
24
+ result = await super().run(prompt, deps, **kwargs)
25
+
26
+ if result.output:
27
+ deps.context.add_qa_response(result.output)
28
+
29
+ return result
30
+
31
+ def get_system_prompt(self) -> str:
32
+ return SEARCH_AGENT_PROMPT
33
+
34
+ def register_tools(self) -> None:
35
+ """Register search-specific tools."""
36
+
37
+ @self.agent.tool
38
+ async def search_and_answer(
39
+ ctx: RunContext[ResearchDependencies],
40
+ query: str,
41
+ limit: int = 5,
42
+ ) -> str:
43
+ """Search the KB and return a concise context pack."""
44
+ # Remove quotes from queries as this requires positional indexing in lancedb
45
+ query = query.replace('"', "")
46
+ search_results = await ctx.deps.client.search(query, limit=limit)
47
+ expanded = await ctx.deps.client.expand_context(search_results)
48
+
49
+ snippet_entries = [
50
+ {
51
+ "text": chunk.content,
52
+ "score": score,
53
+ "document_uri": (chunk.document_uri or ""),
54
+ }
55
+ for chunk, score in expanded
56
+ ]
57
+
58
+ # Return an XML-formatted payload with the question and snippets.
59
+ if snippet_entries:
60
+ return format_as_xml(snippet_entries, root_tag="snippets")
61
+ else:
62
+ return (
63
+ f"No relevant information found in the knowledge base for: {query}"
64
+ )
@@ -0,0 +1,39 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from haiku.rag.research.base import BaseResearchAgent
4
+ from haiku.rag.research.prompts import SYNTHESIS_AGENT_PROMPT
5
+
6
+
7
+ class ResearchReport(BaseModel):
8
+ """Final research report structure."""
9
+
10
+ title: str = Field(description="Concise title for the research")
11
+ executive_summary: str = Field(description="Brief overview of key findings")
12
+ main_findings: list[str] = Field(
13
+ description="Primary research findings with supporting evidence"
14
+ )
15
+ themes: dict[str, str] = Field(description="Major themes and their explanations")
16
+ conclusions: list[str] = Field(description="Evidence-based conclusions")
17
+ limitations: list[str] = Field(description="Limitations of the current research")
18
+ recommendations: list[str] = Field(
19
+ description="Actionable recommendations based on findings"
20
+ )
21
+ sources_summary: str = Field(
22
+ description="Summary of sources used and their reliability"
23
+ )
24
+
25
+
26
+ class SynthesisAgent(BaseResearchAgent[ResearchReport]):
27
+ """Agent specialized in synthesizing research into comprehensive reports."""
28
+
29
+ def __init__(self, provider: str, model: str) -> None:
30
+ super().__init__(provider, model, output_type=ResearchReport)
31
+
32
+ def get_system_prompt(self) -> str:
33
+ return SYNTHESIS_AGENT_PROMPT
34
+
35
+ def register_tools(self) -> None:
36
+ """Register synthesis-specific tools."""
37
+ # The agent will use its LLM capabilities directly for synthesis
38
+ # The structured output will guide the report generation
39
+ pass
@@ -171,9 +171,10 @@ class ChunkRepository:
171
171
  processed_markdown, name="content.md"
172
172
  )
173
173
  except Exception as e:
174
- logger.warning(
174
+ logger.error(
175
175
  f"Failed to apply MARKDOWN_PREPROCESSOR '{preprocessor_path}': {e}. Proceeding without preprocessing."
176
176
  )
177
+ raise e
177
178
 
178
179
  chunk_texts = await chunker.chunk(processed_document)
179
180
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiku.rag
3
- Version: 0.8.1
4
- Summary: Retrieval Augmented Generation (RAG) with LanceDB
3
+ Version: 0.9.0
4
+ Summary: Agentic Retrieval Augmented Generation (RAG) with LanceDB
5
5
  Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
6
6
  License: MIT
7
7
  License-File: LICENSE
@@ -18,14 +18,13 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Typing :: Typed
20
20
  Requires-Python: >=3.12
21
- Requires-Dist: docling>=2.49.0
22
- Requires-Dist: fastmcp>=2.8.1
21
+ Requires-Dist: docling>=2.52.0
22
+ Requires-Dist: fastmcp>=2.12.3
23
23
  Requires-Dist: httpx>=0.28.1
24
- Requires-Dist: lancedb>=0.24.3
25
- Requires-Dist: ollama>=0.5.3
26
- Requires-Dist: pydantic-ai>=0.8.1
27
- Requires-Dist: pydantic>=2.11.7
28
- Requires-Dist: python-dotenv>=1.1.0
24
+ Requires-Dist: lancedb>=0.25.0
25
+ Requires-Dist: pydantic-ai>=1.0.8
26
+ Requires-Dist: pydantic>=2.11.9
27
+ Requires-Dist: python-dotenv>=1.1.1
29
28
  Requires-Dist: rich>=14.1.0
30
29
  Requires-Dist: tiktoken>=0.11.0
31
30
  Requires-Dist: typer>=0.16.1
@@ -33,7 +32,7 @@ Requires-Dist: watchfiles>=1.1.0
33
32
  Provides-Extra: mxbai
34
33
  Requires-Dist: mxbai-rerank>=0.1.6; extra == 'mxbai'
35
34
  Provides-Extra: voyageai
36
- Requires-Dist: voyageai>=0.3.2; extra == 'voyageai'
35
+ Requires-Dist: voyageai>=0.3.5; extra == 'voyageai'
37
36
  Description-Content-Type: text/markdown
38
37
 
39
38
  # Haiku RAG
@@ -128,4 +127,5 @@ Full documentation at: https://ggozad.github.io/haiku.rag/
128
127
  - [Configuration](https://ggozad.github.io/haiku.rag/configuration/) - Environment variables
129
128
  - [CLI](https://ggozad.github.io/haiku.rag/cli/) - Command reference
130
129
  - [Python API](https://ggozad.github.io/haiku.rag/python/) - Complete API docs
130
+ - [Agents](https://ggozad.github.io/haiku.rag/agents/) - QA agent and multi-agent research
131
131
  - [Benchmarks](https://ggozad.github.io/haiku.rag/benchmarks/) - Performance Benchmarks
@@ -1,9 +1,9 @@
1
1
  haiku/rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- haiku/rag/app.py,sha256=XlL6PNPSqeBKF6bemvdSfXEnQghywudwZv-C116NuZU,8254
2
+ haiku/rag/app.py,sha256=Smof7ZIe-oRGkDTap81VaKZGIeborD2X-oXKgBoJs9I,11763
3
3
  haiku/rag/chunker.py,sha256=PVe6ysv8UlacUd4Zb3_8RFWIaWDXnzBAy2VDJ4TaUsE,1555
4
- haiku/rag/cli.py,sha256=houkHTeVc89BA3zPksCjUooEnScSg1Ez_BIHBH6cmJQ,8920
4
+ haiku/rag/cli.py,sha256=3nlzrT5FPCyfnu51KHchLG4Cj2eVv9YsuGHMShBnVb0,9845
5
5
  haiku/rag/client.py,sha256=NJVGXzVzpoVy1sttz_xEU7mXWtObKT8pGpvo5pZyzwc,21288
6
- haiku/rag/config.py,sha256=k5SSh7nYIFKX5LcWYu4bP-4GV5Y-Wq1UzrLUtRAM5Pw,1954
6
+ haiku/rag/config.py,sha256=SPEIv2IElZmZh4Wsp8gk7ViRW5ZzD-UGmIqRAXscDdI,2134
7
7
  haiku/rag/logging.py,sha256=dm65AwADpcQsH5OAPtRA-4hsw0w5DK-sGOvzYkj6jzw,1720
8
8
  haiku/rag/mcp.py,sha256=bR9Y-Nz-hvjiql20Y0KE0hwNGwyjmPGX8K9d-qmXptY,4683
9
9
  haiku/rag/migration.py,sha256=M--KnSF3lxgKjxmokb4vuzGH-pV8eg0C_8e7jvPqW8Y,11058
@@ -17,25 +17,33 @@ haiku/rag/embeddings/openai.py,sha256=fIFCk-jpUtaW0xsnrQnJ824O0UCjaGG2sgvBzREhil
17
17
  haiku/rag/embeddings/vllm.py,sha256=vhaUnCn6VMkfSluLhWKtSV-sekFaPsp4pKo2N7-SBCY,626
18
18
  haiku/rag/embeddings/voyageai.py,sha256=UW-MW4tJKnPB6Fs2P7A3yt-ZeRm46H9npckchSriPX8,661
19
19
  haiku/rag/qa/__init__.py,sha256=Sl7Kzrg9CuBOcMF01wc1NtQhUNWjJI0MhIHfCWrb8V4,434
20
- haiku/rag/qa/agent.py,sha256=15-jMuF08U0uxGdqgQysKMZLr8BUWssI76PtyQ2Ngd8,2912
21
- haiku/rag/qa/prompts.py,sha256=xdT4cyrOrAK9UDgVqyev1wHF49jD57Bh40gx2sH4NPI,3341
20
+ haiku/rag/qa/agent.py,sha256=f7hGWhjgzJKwa5BJkAO0KCxbgpwigPz5E9a26S9TUYI,2948
21
+ haiku/rag/qa/prompts.py,sha256=LhRfDtO8Pb06lpr4PpwEaKUYItZ5OiIkeqcCogcssHY,3347
22
22
  haiku/rag/reranking/__init__.py,sha256=IRXHs4qPu6VbGJQpzSwhgtVWWumURH_vEoVFE-extlo,894
23
23
  haiku/rag/reranking/base.py,sha256=LM9yUSSJ414UgBZhFTgxGprlRqzfTe4I1vgjricz2JY,405
24
24
  haiku/rag/reranking/cohere.py,sha256=1iTdiaa8vvb6oHVB2qpWzUOVkyfUcimVSZp6Qr4aq4c,1049
25
25
  haiku/rag/reranking/mxbai.py,sha256=46sVTsTIkzIX9THgM3u8HaEmgY7evvEyB-N54JTHvK8,867
26
26
  haiku/rag/reranking/vllm.py,sha256=xVGH9ss-ISWdJ5SKUUHUbTqBo7PIEmA_SQv0ScdJ6XA,1479
27
+ haiku/rag/research/__init__.py,sha256=hwCVV8fxnHTrLV2KCJ_Clqe_pPrCwTz-RW2b0BeGHeE,982
28
+ haiku/rag/research/base.py,sha256=IW_VbeRlXTUfqh--jBS0dtIgSVXsbifPxZl8bfTLkDA,3686
29
+ haiku/rag/research/dependencies.py,sha256=vZctKC5donqhm8LFO6hQdXZZXzjdW1__4eUlhyZn058,1573
30
+ haiku/rag/research/evaluation_agent.py,sha256=3YWAdfC6n27wAIdla7M72IE1aS4GqoL9DbnW4K1b35M,1357
31
+ haiku/rag/research/orchestrator.py,sha256=AnDXCoJBbt4nYqaDKk5hiMi8WW1e8NwpRvzHLLnY3WY,10478
32
+ haiku/rag/research/prompts.py,sha256=C_d9OGNgHfwSUY6n5L2c2J6OpCeBHwxtMjrLQOkdcxU,5221
33
+ haiku/rag/research/search_agent.py,sha256=mYn3GlxoIPEle2NLkBqHI-VRV5PanoHxhjttVozsVis,2405
34
+ haiku/rag/research/synthesis_agent.py,sha256=E7Iwfe0EAlmglIRMmRQ3kaNmEWIyEMpVFK3k4SPC5BM,1559
27
35
  haiku/rag/store/__init__.py,sha256=hq0W0DAC7ysqhWSP2M2uHX8cbG6kbr-sWHxhq6qQcY0,103
28
36
  haiku/rag/store/engine.py,sha256=fNrykqMX7PRSCt4LSRfuJ66OLrb8BVYq2bpbfI2iaWU,8455
29
37
  haiku/rag/store/models/__init__.py,sha256=s0E72zneGlowvZrFWaNxHYjOAUjgWdLxzdYsnvNRVlY,88
30
38
  haiku/rag/store/models/chunk.py,sha256=ZNyTfO6lh3rXWLVYO3TZcitbL4LSUGr42fR6jQQ5iQc,364
31
39
  haiku/rag/store/models/document.py,sha256=zSSpt6pyrMJAIXGQvIcqojcqUzwZnhp3WxVokaWxNRc,396
32
40
  haiku/rag/store/repositories/__init__.py,sha256=Olv5dLfBQINRV3HrsfUpjzkZ7Qm7goEYyMNykgo_DaY,291
33
- haiku/rag/store/repositories/chunk.py,sha256=n4VMVFPhKj7K2V7llehrpH0wGa-3XYvl6gPYc5H09Vw,14445
41
+ haiku/rag/store/repositories/chunk.py,sha256=1RmPyEYRYOFbrALbmLOo62t3f-xO2KgxUjcvPdrRZlc,14467
34
42
  haiku/rag/store/repositories/document.py,sha256=XoLCrMrZqs0iCZoHlDOfRDaVUux77Vdu5iZczduF1rY,7812
35
43
  haiku/rag/store/repositories/settings.py,sha256=wx3fuP_5CpPflZHRrIkeoer6ml-iD0qXERh5k6MQRzI,5291
36
44
  haiku/rag/store/upgrades/__init__.py,sha256=wUiEoSiHTahvuagx93E4FB07v123AhdbOjwUkPusiIg,14
37
- haiku_rag-0.8.1.dist-info/METADATA,sha256=jPmTXHAXvT99zs0mFw_UXt80j8APLNkJuP7KspJaVro,4610
38
- haiku_rag-0.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
- haiku_rag-0.8.1.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
40
- haiku_rag-0.8.1.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
41
- haiku_rag-0.8.1.dist-info/RECORD,,
45
+ haiku_rag-0.9.0.dist-info/METADATA,sha256=ab5orVjoWGdapwaoPnwPdtuyetnErIxAvwDjl--9hfo,4681
46
+ haiku_rag-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
+ haiku_rag-0.9.0.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
48
+ haiku_rag-0.9.0.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
49
+ haiku_rag-0.9.0.dist-info/RECORD,,