haiku.rag 0.8.0__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,15 +27,25 @@ 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
34
38
 
39
+ # Optional dotted path or file path to a callable that preprocesses
40
+ # markdown content before chunking. Examples:
41
+ MARKDOWN_PREPROCESSOR: str = ""
42
+
35
43
  OLLAMA_BASE_URL: str = "http://localhost:11434"
44
+
36
45
  VLLM_EMBEDDINGS_BASE_URL: str = ""
37
46
  VLLM_RERANK_BASE_URL: str = ""
38
47
  VLLM_QA_BASE_URL: str = ""
48
+ VLLM_RESEARCH_BASE_URL: str = ""
39
49
 
40
50
  # Provider keys
41
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
haiku/rag/store/engine.py CHANGED
@@ -209,6 +209,21 @@ class Store:
209
209
  # LanceDB connections are automatically managed
210
210
  pass
211
211
 
212
+ def current_table_versions(self) -> dict[str, int]:
213
+ """Capture current versions of key tables for rollback using LanceDB's API."""
214
+ return {
215
+ "documents": int(self.documents_table.version),
216
+ "chunks": int(self.chunks_table.version),
217
+ "settings": int(self.settings_table.version),
218
+ }
219
+
220
+ def restore_table_versions(self, versions: dict[str, int]) -> bool:
221
+ """Restore tables to the provided versions using LanceDB's API."""
222
+ self.documents_table.restore(int(versions["documents"]))
223
+ self.chunks_table.restore(int(versions["chunks"]))
224
+ self.settings_table.restore(int(versions["settings"]))
225
+ return True
226
+
212
227
  @property
213
228
  def _connection(self):
214
229
  """Compatibility property for repositories expecting _connection."""
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import inspect
2
3
  import json
3
4
  import logging
4
5
  from uuid import uuid4
@@ -11,6 +12,7 @@ from haiku.rag.config import Config
11
12
  from haiku.rag.embeddings import get_embedder
12
13
  from haiku.rag.store.engine import DocumentRecord, Store
13
14
  from haiku.rag.store.models.chunk import Chunk
15
+ from haiku.rag.utils import load_callable, text_to_docling_document
14
16
 
15
17
  logger = logging.getLogger(__name__)
16
18
 
@@ -152,7 +154,29 @@ class ChunkRepository:
152
154
  self, document_id: str, document: DoclingDocument
153
155
  ) -> list[Chunk]:
154
156
  """Create chunks and embeddings for a document from DoclingDocument."""
155
- chunk_texts = await chunker.chunk(document)
157
+ # Optionally preprocess markdown before chunking
158
+ processed_document = document
159
+ preprocessor_path = Config.MARKDOWN_PREPROCESSOR
160
+ if preprocessor_path:
161
+ try:
162
+ pre_fn = load_callable(preprocessor_path)
163
+ markdown = document.export_to_markdown()
164
+ result = pre_fn(markdown)
165
+ if inspect.isawaitable(result):
166
+ result = await result # type: ignore[assignment]
167
+ processed_markdown = result
168
+ if not isinstance(processed_markdown, str):
169
+ raise ValueError("Preprocessor must return a markdown string")
170
+ processed_document = text_to_docling_document(
171
+ processed_markdown, name="content.md"
172
+ )
173
+ except Exception as e:
174
+ logger.error(
175
+ f"Failed to apply MARKDOWN_PREPROCESSOR '{preprocessor_path}': {e}. Proceeding without preprocessing."
176
+ )
177
+ raise e
178
+
179
+ chunk_texts = await chunker.chunk(processed_document)
156
180
 
157
181
  embeddings = await self.embedder.embed(chunk_texts)
158
182
 
@@ -171,44 +171,64 @@ class DocumentRepository:
171
171
  chunks: list["Chunk"] | None = None,
172
172
  ) -> Document:
173
173
  """Create a document with its chunks and embeddings."""
174
+ # Snapshot table versions for versioned rollback (if supported)
175
+ versions = self.store.current_table_versions()
176
+
174
177
  # Create the document
175
178
  created_doc = await self.create(entity)
176
179
 
177
- # Create chunks if not provided
178
- if chunks is None:
179
- assert created_doc.id is not None, (
180
- "Document ID should not be None after creation"
181
- )
182
- await self.chunk_repository.create_chunks_for_document(
183
- created_doc.id, docling_document
184
- )
185
- else:
186
- # Use provided chunks, set order from list position
187
- assert created_doc.id is not None, (
188
- "Document ID should not be None after creation"
189
- )
190
- for order, chunk in enumerate(chunks):
191
- chunk.document_id = created_doc.id
192
- chunk.metadata["order"] = order
193
- await self.chunk_repository.create(chunk)
194
-
195
- return created_doc
180
+ # Attempt to create chunks; on failure, prefer version rollback
181
+ try:
182
+ # Create chunks if not provided
183
+ if chunks is None:
184
+ assert created_doc.id is not None, (
185
+ "Document ID should not be None after creation"
186
+ )
187
+ await self.chunk_repository.create_chunks_for_document(
188
+ created_doc.id, docling_document
189
+ )
190
+ else:
191
+ # Use provided chunks, set order from list position
192
+ assert created_doc.id is not None, (
193
+ "Document ID should not be None after creation"
194
+ )
195
+ for order, chunk in enumerate(chunks):
196
+ chunk.document_id = created_doc.id
197
+ chunk.metadata["order"] = order
198
+ await self.chunk_repository.create(chunk)
199
+
200
+ return created_doc
201
+ except Exception:
202
+ # Roll back to the captured versions and re-raise
203
+ self.store.restore_table_versions(versions)
204
+ raise
196
205
 
197
206
  async def _update_with_docling(
198
207
  self, entity: Document, docling_document: DoclingDocument
199
208
  ) -> Document:
200
209
  """Update a document and regenerate its chunks."""
201
- # Delete existing chunks
202
210
  assert entity.id is not None, "Document ID is required for update"
211
+
212
+ # Snapshot table versions for versioned rollback
213
+ versions = self.store.current_table_versions()
214
+
215
+ # Delete existing chunks before writing new ones
203
216
  await self.chunk_repository.delete_by_document_id(entity.id)
204
217
 
205
- # Update the document
206
- updated_doc = await self.update(entity)
218
+ try:
219
+ # Update the document
220
+ updated_doc = await self.update(entity)
207
221
 
208
- # Create new chunks
209
- assert updated_doc.id is not None, "Document ID should not be None after update"
210
- await self.chunk_repository.create_chunks_for_document(
211
- updated_doc.id, docling_document
212
- )
222
+ # Create new chunks
223
+ assert updated_doc.id is not None, (
224
+ "Document ID should not be None after update"
225
+ )
226
+ await self.chunk_repository.create_chunks_for_document(
227
+ updated_doc.id, docling_document
228
+ )
213
229
 
214
- return updated_doc
230
+ return updated_doc
231
+ except Exception:
232
+ # Roll back to the captured versions and re-raise
233
+ self.store.restore_table_versions(versions)
234
+ raise
haiku/rag/utils.py CHANGED
@@ -1,10 +1,13 @@
1
1
  import asyncio
2
+ import importlib
3
+ import importlib.util
2
4
  import sys
3
5
  from collections.abc import Callable
4
6
  from functools import wraps
5
7
  from importlib import metadata
6
8
  from io import BytesIO
7
9
  from pathlib import Path
10
+ from types import ModuleType
8
11
 
9
12
  import httpx
10
13
  from docling.document_converter import DocumentConverter
@@ -106,3 +109,54 @@ def text_to_docling_document(text: str, name: str = "content.md") -> DoclingDocu
106
109
  converter = DocumentConverter()
107
110
  result = converter.convert(doc_stream)
108
111
  return result.document
112
+
113
+
114
+ def load_callable(path: str):
115
+ """Load a callable from a dotted path or file path.
116
+
117
+ Supported formats:
118
+ - "package.module:func" or "package.module.func"
119
+ - "path/to/file.py:func"
120
+
121
+ Returns the loaded callable. Raises ValueError on failure.
122
+ """
123
+ if not path:
124
+ raise ValueError("Empty callable path provided")
125
+
126
+ module_part = None
127
+ func_name = None
128
+
129
+ if ":" in path:
130
+ module_part, func_name = path.split(":", 1)
131
+ else:
132
+ # split by last dot for module.attr
133
+ if "." in path:
134
+ module_part, func_name = path.rsplit(".", 1)
135
+ else:
136
+ raise ValueError(
137
+ "Invalid callable path format. Use 'module:func' or 'module.func' or 'file.py:func'."
138
+ )
139
+
140
+ # Try file path first
141
+ mod: ModuleType | None = None
142
+ module_path = Path(module_part)
143
+ if module_path.suffix == ".py" and module_path.exists():
144
+ spec = importlib.util.spec_from_file_location(module_path.stem, module_path)
145
+ if spec and spec.loader:
146
+ mod = importlib.util.module_from_spec(spec)
147
+ spec.loader.exec_module(mod)
148
+ else:
149
+ # Import as a module path
150
+ try:
151
+ mod = importlib.import_module(module_part)
152
+ except Exception as e:
153
+ raise ValueError(f"Failed to import module '{module_part}': {e}")
154
+
155
+ if not hasattr(mod, func_name):
156
+ raise ValueError(f"Callable '{func_name}' not found in module '{module_part}'")
157
+ func = getattr(mod, func_name)
158
+ if not callable(func):
159
+ raise ValueError(
160
+ f"Attribute '{func_name}' in module '{module_part}' is not callable"
161
+ )
162
+ return func
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiku.rag
3
- Version: 0.8.0
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,15 +1,15 @@
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=3H41da9BU1R1y2JJHD0cOSErX_VSM1UXA7M2JSOxFXE,1795
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
10
10
  haiku/rag/monitor.py,sha256=r386nkhdlsU8UECwIuVwnrSlgMk3vNIuUZGNIzkZuec,2770
11
11
  haiku/rag/reader.py,sha256=qkPTMJuQ_o4sK-8zpDl9WFYe_MJ7aL_gUw6rczIpW-g,3274
12
- haiku/rag/utils.py,sha256=c8F0ECsFSqvQxzxINAOAnvShoOnJPLsOaNE3JEY2JSc,3230
12
+ haiku/rag/utils.py,sha256=aiuPu_rrfpyIvJJq0o5boUIIvCdNzdpKwAIPYYn3iG8,4965
13
13
  haiku/rag/embeddings/__init__.py,sha256=44IfDITGIFTflGT6UEmiYOwpWFVbYv5smLY59D0YeCs,1419
14
14
  haiku/rag/embeddings/base.py,sha256=BnSviKrlzjv3L0sZJs_T-pxfawd-bcTak-rsX-D2f3A,497
15
15
  haiku/rag/embeddings/ollama.py,sha256=LuLlHH6RGoO9_gFCIlbmesuXOj017gTw6z-p8Ez0CfE,595
@@ -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
- haiku/rag/store/engine.py,sha256=uzw09IOebaKo8b_FyvVHMUQMDVKfBpN7WGfuY3fKiEE,7757
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=v4y4eh4yIf6zJaWfHxljvnmb12dmvwdinzmxQt8Lvhs,13343
34
- haiku/rag/store/repositories/document.py,sha256=lP8Lo82KTP-qwXFRpYZ46WjeAdAsHwZ5pJcrXdz4g0U,6988
41
+ haiku/rag/store/repositories/chunk.py,sha256=1RmPyEYRYOFbrALbmLOo62t3f-xO2KgxUjcvPdrRZlc,14467
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.0.dist-info/METADATA,sha256=OZfvP7S7MBndpjjTg59UaD9JgB_W39pXpYAjyULjn8A,4610
38
- haiku_rag-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
- haiku_rag-0.8.0.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
40
- haiku_rag-0.8.0.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
41
- haiku_rag-0.8.0.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,,