haiku.rag 0.9.1__py3-none-any.whl → 0.9.3__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
@@ -101,7 +101,6 @@ class HaikuRAGApp:
101
101
  client=client,
102
102
  max_iterations=max_iterations,
103
103
  verbose=verbose,
104
- console=self.console if verbose else None,
105
104
  )
106
105
 
107
106
  # Display the report
@@ -122,12 +121,7 @@ class HaikuRAGApp:
122
121
  self.console.print(f"• {finding}")
123
122
  self.console.print()
124
123
 
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()
124
+ # (Themes section removed)
131
125
 
132
126
  # Conclusions
133
127
  if report.conclusions:
@@ -261,7 +255,7 @@ class HaikuRAGApp:
261
255
  elif transport == "sse":
262
256
  await server.run_sse_async()
263
257
  else:
264
- await server.run_http_async("streamable-http")
258
+ await server.run_http_async(transport="streamable-http")
265
259
  except KeyboardInterrupt:
266
260
  pass
267
261
  finally:
haiku/rag/client.py CHANGED
@@ -388,7 +388,7 @@ class HaikuRAG:
388
388
  all_chunks = adjacent_chunks + [chunk]
389
389
 
390
390
  # Get the range of orders for this expanded chunk
391
- orders = [c.metadata.get("order", 0) for c in all_chunks]
391
+ orders = [c.order for c in all_chunks]
392
392
  min_order = min(orders)
393
393
  max_order = max(orders)
394
394
 
@@ -398,9 +398,7 @@ class HaikuRAG:
398
398
  "score": score,
399
399
  "min_order": min_order,
400
400
  "max_order": max_order,
401
- "all_chunks": sorted(
402
- all_chunks, key=lambda c: c.metadata.get("order", 0)
403
- ),
401
+ "all_chunks": sorted(all_chunks, key=lambda c: c.order),
404
402
  }
405
403
  )
406
404
 
@@ -459,7 +457,7 @@ class HaikuRAG:
459
457
  # Merge all_chunks and deduplicate by order
460
458
  all_chunks_dict = {}
461
459
  for chunk in current["all_chunks"] + range_info["all_chunks"]:
462
- order = chunk.metadata.get("order", 0)
460
+ order = chunk.order
463
461
  all_chunks_dict[order] = chunk
464
462
  current["all_chunks"] = [
465
463
  all_chunks_dict[order] for order in sorted(all_chunks_dict.keys())
haiku/rag/qa/agent.py CHANGED
@@ -49,6 +49,9 @@ class QuestionAnswerAgent:
49
49
  limit: int = 3,
50
50
  ) -> list[SearchResult]:
51
51
  """Search the knowledge base for relevant documents."""
52
+
53
+ # Remove quotes from queries as this requires positional indexing in lancedb
54
+ query = query.replace('"', "")
52
55
  search_results = await ctx.deps.client.search(query, limit=limit)
53
56
  expanded_results = await ctx.deps.client.expand_context(search_results)
54
57
 
@@ -12,6 +12,7 @@ from haiku.rag.research.evaluation_agent import (
12
12
  EvaluationResult,
13
13
  )
14
14
  from haiku.rag.research.orchestrator import ResearchOrchestrator, ResearchPlan
15
+ from haiku.rag.research.presearch_agent import PresearchSurveyAgent
15
16
  from haiku.rag.research.search_agent import SearchSpecialistAgent
16
17
  from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
17
18
 
@@ -25,6 +26,7 @@ __all__ = [
25
26
  # Specialized agents
26
27
  "SearchAnswer",
27
28
  "SearchSpecialistAgent",
29
+ "PresearchSurveyAgent",
28
30
  "AnalysisEvaluationAgent",
29
31
  "EvaluationResult",
30
32
  "SynthesisAgent",
@@ -33,11 +33,20 @@ class BaseResearchAgent[T](ABC):
33
33
  # Import deps type lazily to avoid circular import during module load
34
34
  from haiku.rag.research.dependencies import ResearchDependencies
35
35
 
36
+ # If the agent is expected to return plain text, pass `str` directly.
37
+ # Otherwise, wrap the model with ToolOutput for robust tool-handling retries.
38
+ agent_output_type: Any
39
+ if self.output_type is str: # plain text output
40
+ agent_output_type = str
41
+ else:
42
+ agent_output_type = ToolOutput(self.output_type, max_retries=3)
43
+
36
44
  self._agent = Agent(
37
45
  model=model_obj,
38
46
  deps_type=ResearchDependencies,
39
- output_type=ToolOutput(self.output_type, max_retries=3),
40
- system_prompt=self.get_system_prompt(),
47
+ output_type=agent_output_type,
48
+ instructions=self.get_system_prompt(),
49
+ retries=3,
41
50
  )
42
51
 
43
52
  # Register tools
@@ -67,7 +76,6 @@ class BaseResearchAgent[T](ABC):
67
76
  """Return the system prompt for this agent."""
68
77
  pass
69
78
 
70
- @abstractmethod
71
79
  def register_tools(self) -> None:
72
80
  """Register agent-specific tools."""
73
81
  pass
@@ -1,4 +1,6 @@
1
1
  from pydantic import BaseModel, Field
2
+ from pydantic_ai import format_as_xml
3
+ from rich.console import Console
2
4
 
3
5
  from haiku.rag.client import HaikuRAG
4
6
  from haiku.rag.research.base import SearchAnswer
@@ -43,3 +45,25 @@ class ResearchDependencies(BaseModel):
43
45
 
44
46
  client: HaikuRAG = Field(description="RAG client for document operations")
45
47
  context: ResearchContext = Field(description="Shared research context")
48
+ console: Console | None = None
49
+
50
+
51
+ def _format_context_for_prompt(context: ResearchContext) -> str:
52
+ """Format the research context as XML for inclusion in prompts."""
53
+
54
+ context_data = {
55
+ "original_question": context.original_question,
56
+ "unanswered_questions": context.sub_questions,
57
+ "qa_responses": [
58
+ {
59
+ "question": qa.query,
60
+ "answer": qa.answer,
61
+ "context_snippets": qa.context,
62
+ "sources": qa.sources,
63
+ }
64
+ for qa in context.qa_responses
65
+ ],
66
+ "insights": context.insights,
67
+ "gaps": context.gaps,
68
+ }
69
+ return format_as_xml(context_data, root_tag="research_context")
@@ -1,6 +1,11 @@
1
1
  from pydantic import BaseModel, Field
2
+ from pydantic_ai.run import AgentRunResult
2
3
 
3
4
  from haiku.rag.research.base import BaseResearchAgent
5
+ from haiku.rag.research.dependencies import (
6
+ ResearchDependencies,
7
+ _format_context_for_prompt,
8
+ )
4
9
  from haiku.rag.research.prompts import EVALUATION_AGENT_PROMPT
5
10
 
6
11
 
@@ -11,7 +16,9 @@ class EvaluationResult(BaseModel):
11
16
  description="Main insights extracted from the research so far"
12
17
  )
13
18
  new_questions: list[str] = Field(
14
- description="New sub-questions to add to the research (max 3)", max_length=3
19
+ description="New sub-questions to add to the research (max 3)",
20
+ max_length=3,
21
+ default=[],
15
22
  )
16
23
  confidence_score: float = Field(
17
24
  description="Confidence level in the completeness of research (0-1)",
@@ -32,9 +39,47 @@ class AnalysisEvaluationAgent(BaseResearchAgent[EvaluationResult]):
32
39
  def __init__(self, provider: str, model: str) -> None:
33
40
  super().__init__(provider, model, output_type=EvaluationResult)
34
41
 
42
+ async def run(
43
+ self, prompt: str, deps: ResearchDependencies, **kwargs
44
+ ) -> AgentRunResult[EvaluationResult]:
45
+ console = deps.console
46
+ if console:
47
+ console.print(
48
+ "\n[bold cyan]📊 Analyzing and evaluating research progress...[/bold cyan]"
49
+ )
50
+
51
+ # Format context for the evaluation agent
52
+ context_xml = _format_context_for_prompt(deps.context)
53
+ evaluation_prompt = f"""Analyze all gathered information and evaluate the completeness of research.
54
+
55
+ {context_xml}
56
+
57
+ Evaluate the research progress for the original question and identify any remaining gaps."""
58
+
59
+ result = await super().run(evaluation_prompt, deps, **kwargs)
60
+ output = result.output
61
+
62
+ # Store insights
63
+ for insight in output.key_insights:
64
+ deps.context.add_insight(insight)
65
+
66
+ # Add new questions to the sub-questions list
67
+ for new_q in output.new_questions:
68
+ if new_q not in deps.context.sub_questions:
69
+ deps.context.sub_questions.append(new_q)
70
+
71
+ if console:
72
+ if output.key_insights:
73
+ console.print(" [bold]Key insights:[/bold]")
74
+ for insight in output.key_insights:
75
+ console.print(f" • {insight}")
76
+ console.print(
77
+ f" Confidence: [yellow]{output.confidence_score:.1%}[/yellow]"
78
+ )
79
+ status = "[green]Yes[/green]" if output.is_sufficient else "[red]No[/red]"
80
+ console.print(f" Sufficient: {status}")
81
+
82
+ return result
83
+
35
84
  def get_system_prompt(self) -> str:
36
85
  return EVALUATION_AGENT_PROMPT
37
-
38
- def register_tools(self) -> None:
39
- """No additional tools needed - uses LLM capabilities directly."""
40
- pass
@@ -1,17 +1,20 @@
1
1
  from typing import Any
2
2
 
3
3
  from pydantic import BaseModel, Field
4
- from pydantic_ai.format_prompt import format_as_xml
5
4
  from pydantic_ai.run import AgentRunResult
6
5
  from rich.console import Console
7
6
 
8
7
  from haiku.rag.config import Config
9
8
  from haiku.rag.research.base import BaseResearchAgent
10
- from haiku.rag.research.dependencies import ResearchContext, ResearchDependencies
9
+ from haiku.rag.research.dependencies import (
10
+ ResearchContext,
11
+ ResearchDependencies,
12
+ )
11
13
  from haiku.rag.research.evaluation_agent import (
12
14
  AnalysisEvaluationAgent,
13
15
  EvaluationResult,
14
16
  )
17
+ from haiku.rag.research.presearch_agent import PresearchSurveyAgent
15
18
  from haiku.rag.research.prompts import ORCHESTRATOR_PROMPT
16
19
  from haiku.rag.research.search_agent import SearchSpecialistAgent
17
20
  from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
@@ -30,7 +33,9 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
30
33
  """Orchestrator agent that coordinates the research workflow."""
31
34
 
32
35
  def __init__(
33
- self, provider: str | None = Config.RESEARCH_PROVIDER, model: str | None = None
36
+ self,
37
+ provider: str | None = Config.RESEARCH_PROVIDER,
38
+ model: str | None = None,
34
39
  ):
35
40
  # Use provided values or fall back to config defaults
36
41
  provider = provider or Config.RESEARCH_PROVIDER or Config.QA_PROVIDER
@@ -41,6 +46,9 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
41
46
  self.search_agent: SearchSpecialistAgent = SearchSpecialistAgent(
42
47
  provider, model
43
48
  )
49
+ self.presearch_agent: PresearchSurveyAgent = PresearchSurveyAgent(
50
+ provider, model
51
+ )
44
52
  self.evaluation_agent: AnalysisEvaluationAgent = AnalysisEvaluationAgent(
45
53
  provider, model
46
54
  )
@@ -49,25 +57,15 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
49
57
  def get_system_prompt(self) -> str:
50
58
  return ORCHESTRATOR_PROMPT
51
59
 
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."""
60
+ def _should_stop_research(
61
+ self,
62
+ evaluation_result: AgentRunResult[EvaluationResult],
63
+ confidence_threshold: float,
64
+ ) -> bool:
65
+ """Determine if research should stop based on evaluation."""
59
66
 
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")
67
+ result = evaluation_result.output
68
+ return result.is_sufficient and result.confidence_score >= confidence_threshold
71
69
 
72
70
  async def conduct_research(
73
71
  self,
@@ -76,7 +74,6 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
76
74
  max_iterations: int = 3,
77
75
  confidence_threshold: float = 0.8,
78
76
  verbose: bool = False,
79
- console: Console | None = None,
80
77
  ) -> ResearchReport:
81
78
  """Conduct comprehensive research on a question.
82
79
 
@@ -86,7 +83,6 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
86
83
  max_iterations: Maximum number of search-analyze-clarify cycles
87
84
  confidence_threshold: Minimum confidence level to stop research (0-1)
88
85
  verbose: If True, print progress and intermediate results
89
- console: Optional Rich console for output
90
86
 
91
87
  Returns:
92
88
  ResearchReport with comprehensive findings
@@ -95,18 +91,27 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
95
91
  # Initialize context
96
92
  context = ResearchContext(original_question=question)
97
93
  deps = ResearchDependencies(client=client, context=context)
94
+ if verbose:
95
+ deps.console = Console()
98
96
 
99
- # Use provided console or create a new one
100
- console = console or Console() if verbose else None
101
-
97
+ console = deps.console
102
98
  # Create initial research plan
103
99
  if console:
104
100
  console.print("\n[bold cyan]📋 Creating research plan...[/bold cyan]")
105
101
 
106
- plan_result: AgentRunResult[ResearchPlan] = await self.run(
107
- f"Create a research plan for: {question}", deps=deps
102
+ # Run a simple presearch survey to summarize KB context
103
+ presearch_result = await self.presearch_agent.run(question, deps=deps)
104
+ plan_prompt = (
105
+ "Create a research plan for the main question below.\n\n"
106
+ f"Main question: {question}\n\n"
107
+ "Use this brief presearch summary to inform the plan. Focus the 3 sub-questions "
108
+ "on the most important aspects not already obvious from the current KB context.\n\n"
109
+ f"{presearch_result.output}"
108
110
  )
109
111
 
112
+ plan_result: AgentRunResult[ResearchPlan] = await self.run(
113
+ plan_prompt, deps=deps
114
+ )
110
115
  context.sub_questions = plan_result.output.sub_questions
111
116
 
112
117
  if console:
@@ -117,7 +122,6 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
117
122
  console.print(" [bold]Sub-questions:[/bold]")
118
123
  for i, sq in enumerate(plan_result.output.sub_questions, 1):
119
124
  console.print(f" {i}. {sq}")
120
- console.print()
121
125
 
122
126
  # Execute research iterations
123
127
  for iteration in range(max_iterations):
@@ -128,7 +132,6 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
128
132
 
129
133
  # Check if we have questions to search
130
134
  if not context.sub_questions:
131
- # No more questions to explore
132
135
  if console:
133
136
  console.print(
134
137
  "[yellow]No more questions to explore. Concluding research.[/yellow]"
@@ -136,90 +139,20 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
136
139
  break
137
140
 
138
141
  # Use current sub-questions for this iteration
139
- questions_to_search = context.sub_questions
142
+ questions_to_search = context.sub_questions[:]
140
143
 
141
144
  # Search phase - answer all questions in this iteration
142
145
  if console:
143
146
  console.print(
144
147
  f"\n[bold cyan]🔍 Searching & Answering {len(questions_to_search)} questions:[/bold cyan]"
145
148
  )
146
- for i, q in enumerate(questions_to_search, 1):
147
- console.print(f" {i}. {q}")
148
149
 
149
- # Run searches for all questions and remove answered ones
150
- answered_questions = []
151
150
  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)
151
+ await self.search_agent.run(search_question, deps=deps)
181
152
 
182
153
  # Analysis and Evaluation phase
183
- if console:
184
- console.print(
185
- "\n[bold cyan]📊 Analyzing and evaluating research progress...[/bold cyan]"
186
- )
187
154
 
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)
155
+ evaluation_result = await self.evaluation_agent.run("", deps=deps)
223
156
 
224
157
  # Check if research is sufficient
225
158
  if self._should_stop_research(evaluation_result, confidence_threshold):
@@ -230,36 +163,8 @@ Evaluate the research progress for the original question and identify any remain
230
163
  break
231
164
 
232
165
  # 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
166
  report_result: AgentRunResult[ResearchReport] = await self.synthesis_agent.run(
247
- synthesis_prompt, deps=deps
167
+ "", deps=deps
248
168
  )
249
169
 
250
- if console:
251
- console.print("[bold green]✅ Research complete![/bold green]")
252
-
253
170
  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,39 @@
1
+ from pydantic_ai import RunContext
2
+ from pydantic_ai.run import AgentRunResult
3
+
4
+ from haiku.rag.research.base import BaseResearchAgent
5
+ from haiku.rag.research.dependencies import ResearchDependencies
6
+ from haiku.rag.research.prompts import PRESEARCH_AGENT_PROMPT
7
+
8
+
9
+ class PresearchSurveyAgent(BaseResearchAgent[str]):
10
+ """Presearch agent that gathers verbatim context and summarizes it."""
11
+
12
+ def __init__(self, provider: str, model: str) -> None:
13
+ super().__init__(provider, model, str)
14
+
15
+ async def run(
16
+ self, prompt: str, deps: ResearchDependencies, **kwargs
17
+ ) -> AgentRunResult[str]:
18
+ console = deps.console
19
+ if console:
20
+ console.print(
21
+ "\n[bold cyan]🔎 Presearch: summarizing KB context...[/bold cyan]"
22
+ )
23
+
24
+ return await super().run(prompt, deps, **kwargs)
25
+
26
+ def get_system_prompt(self) -> str:
27
+ return PRESEARCH_AGENT_PROMPT
28
+
29
+ def register_tools(self) -> None:
30
+ @self.agent.tool
31
+ async def gather_context(
32
+ ctx: RunContext[ResearchDependencies],
33
+ query: str,
34
+ limit: int = 6,
35
+ ) -> str:
36
+ """Return verbatim concatenation of relevant chunk texts."""
37
+ results = await ctx.deps.client.search(query, limit=limit)
38
+ expanded = await ctx.deps.client.expand_context(results)
39
+ return "\n\n".join(chunk.content for chunk, _ in expanded)
@@ -114,3 +114,16 @@ Focus on creating a report that provides clear value to the reader by:
114
114
  - Highlighting the most important findings
115
115
  - Explaining the implications of the research
116
116
  - Suggesting concrete next steps"""
117
+
118
+ PRESEARCH_AGENT_PROMPT = """You are a rapid research surveyor.
119
+
120
+ Task:
121
+ - Call the gather_context tool once with the main question to obtain a
122
+ relevant texts from the Knowledge Base (KB).
123
+ - Read that context and produce a brief natural-language summary describing
124
+ what the KB appears to contain relative to the question.
125
+
126
+ Rules:
127
+ - Base the summary strictly on the provided text; do not invent.
128
+ - Output only the summary as plain text (one short paragraph).
129
+ """
@@ -21,10 +21,17 @@ class SearchSpecialistAgent(BaseResearchAgent[SearchAnswer]):
21
21
  Pydantic AI enforces `SearchAnswer` as the output model; we just store
22
22
  the QA response with the last search results as sources.
23
23
  """
24
- result = await super().run(prompt, deps, **kwargs)
24
+ console = deps.console
25
+ if console:
26
+ console.print(f"\t{prompt}")
25
27
 
26
- if result.output:
27
- deps.context.add_qa_response(result.output)
28
+ result = await super().run(prompt, deps, **kwargs)
29
+ deps.context.add_qa_response(result.output)
30
+ deps.context.sub_questions.remove(prompt)
31
+ if console:
32
+ answer = result.output.answer
33
+ answer_preview = answer[:150] + "…" if len(answer) > 150 else answer
34
+ console.log(f"\n [green]✓[/green] {answer_preview}")
28
35
 
29
36
  return result
30
37
 
@@ -41,8 +48,6 @@ class SearchSpecialistAgent(BaseResearchAgent[SearchAnswer]):
41
48
  limit: int = 5,
42
49
  ) -> str:
43
50
  """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
51
  search_results = await ctx.deps.client.search(query, limit=limit)
47
52
  expanded = await ctx.deps.client.expand_context(search_results)
48
53
 
@@ -1,6 +1,11 @@
1
1
  from pydantic import BaseModel, Field
2
+ from pydantic_ai.run import AgentRunResult
2
3
 
3
4
  from haiku.rag.research.base import BaseResearchAgent
5
+ from haiku.rag.research.dependencies import (
6
+ ResearchDependencies,
7
+ _format_context_for_prompt,
8
+ )
4
9
  from haiku.rag.research.prompts import SYNTHESIS_AGENT_PROMPT
5
10
 
6
11
 
@@ -12,11 +17,12 @@ class ResearchReport(BaseModel):
12
17
  main_findings: list[str] = Field(
13
18
  description="Primary research findings with supporting evidence"
14
19
  )
15
- themes: dict[str, str] = Field(description="Major themes and their explanations")
16
20
  conclusions: list[str] = Field(description="Evidence-based conclusions")
17
- limitations: list[str] = Field(description="Limitations of the current research")
21
+ limitations: list[str] = Field(
22
+ description="Limitations of the current research", default=[]
23
+ )
18
24
  recommendations: list[str] = Field(
19
- description="Actionable recommendations based on findings"
25
+ description="Actionable recommendations based on findings", default=[]
20
26
  )
21
27
  sources_summary: str = Field(
22
28
  description="Summary of sources used and their reliability"
@@ -29,11 +35,26 @@ class SynthesisAgent(BaseResearchAgent[ResearchReport]):
29
35
  def __init__(self, provider: str, model: str) -> None:
30
36
  super().__init__(provider, model, output_type=ResearchReport)
31
37
 
38
+ async def run(
39
+ self, prompt: str, deps: ResearchDependencies, **kwargs
40
+ ) -> AgentRunResult[ResearchReport]:
41
+ console = deps.console
42
+ if console:
43
+ console.print(
44
+ "\n[bold cyan]📝 Generating final research report...[/bold cyan]"
45
+ )
46
+
47
+ context_xml = _format_context_for_prompt(deps.context)
48
+ synthesis_prompt = f"""Generate a comprehensive research report based on all gathered information.
49
+
50
+ {context_xml}
51
+
52
+ Create a detailed report that synthesizes all findings into a coherent response."""
53
+ result = await super().run(synthesis_prompt, deps, **kwargs)
54
+ if console:
55
+ console.print("[bold green]✅ Research complete![/bold green]")
56
+
57
+ return result
58
+
32
59
  def get_system_prompt(self) -> str:
33
60
  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
@@ -35,6 +35,7 @@ def create_chunk_model(vector_dim: int):
35
35
  document_id: str
36
36
  content: str
37
37
  metadata: str = Field(default="{}")
38
+ order: int = Field(default=0)
38
39
  vector: Vector(vector_dim) = Field(default_factory=lambda: [0.0] * vector_dim) # type: ignore
39
40
 
40
41
  return ChunkRecord
@@ -117,8 +118,10 @@ class Store:
117
118
  self.chunks_table = self.db.open_table("chunks")
118
119
  else:
119
120
  self.chunks_table = self.db.create_table("chunks", schema=self.ChunkRecord)
120
- # Create FTS index on the new table
121
- self.chunks_table.create_fts_index("content", replace=True)
121
+ # Create FTS index on the new table with phrase query support
122
+ self.chunks_table.create_fts_index(
123
+ "content", replace=True, with_position=True, remove_stop_words=False
124
+ )
122
125
 
123
126
  # Create or get settings table
124
127
  if "settings" in existing_tables:
@@ -133,21 +136,41 @@ class Store:
133
136
  [SettingsRecord(id="settings", settings=json.dumps(settings_data))]
134
137
  )
135
138
 
136
- # Set current version in settings
137
- current_version = metadata.version("haiku.rag")
138
- self.set_haiku_version(current_version)
139
-
140
- # Check if we need to perform upgrades
139
+ # Run pending upgrades based on stored version and package version
141
140
  try:
142
- existing_settings = list(
143
- self.settings_table.search().limit(1).to_pydantic(SettingsRecord)
141
+ from haiku.rag.store.upgrades import run_pending_upgrades
142
+
143
+ current_version = metadata.version("haiku.rag")
144
+ db_version = self.get_haiku_version()
145
+
146
+ run_pending_upgrades(self, db_version, current_version)
147
+
148
+ # After upgrades complete (or if none), set stored version
149
+ # to the greater of the installed package version and the
150
+ # highest available upgrade step version in code.
151
+ try:
152
+ from packaging.version import parse as _v
153
+
154
+ from haiku.rag.store.upgrades import upgrades as _steps
155
+
156
+ highest_step = max((_v(u.version) for u in _steps), default=None)
157
+ effective_version = (
158
+ str(max(_v(current_version), highest_step))
159
+ if highest_step is not None
160
+ else current_version
161
+ )
162
+ except Exception:
163
+ effective_version = current_version
164
+
165
+ self.set_haiku_version(effective_version)
166
+ except Exception as e:
167
+ # Avoid hard failure on initial connection; log and continue so CLI remains usable.
168
+ logger.warning(
169
+ "Skipping upgrade due to error (db=%s -> pkg=%s): %s",
170
+ self.get_haiku_version(),
171
+ metadata.version("haiku.rag") if hasattr(metadata, "version") else "",
172
+ e,
144
173
  )
145
- if existing_settings:
146
- db_version = self.get_haiku_version() # noqa: F841
147
- # TODO: Add upgrade logic here similar to SQLite version when needed
148
- except Exception:
149
- # Settings table might not exist yet in fresh databases
150
- pass
151
174
 
152
175
  def get_haiku_version(self) -> str:
153
176
  """Returns the user version stored in settings."""
@@ -201,8 +224,10 @@ class Store:
201
224
  self.ChunkRecord = create_chunk_model(self.embedder._vector_dim)
202
225
  self.chunks_table = self.db.create_table("chunks", schema=self.ChunkRecord)
203
226
 
204
- # Create FTS index on the new table
205
- self.chunks_table.create_fts_index("content", replace=True)
227
+ # Create FTS index on the new table with phrase query support
228
+ self.chunks_table.create_fts_index(
229
+ "content", replace=True, with_position=True, remove_stop_words=False
230
+ )
206
231
 
207
232
  def close(self):
208
233
  """Close the database connection."""
@@ -10,6 +10,7 @@ class Chunk(BaseModel):
10
10
  document_id: str | None = None
11
11
  content: str
12
12
  metadata: dict = {}
13
+ order: int = 0
13
14
  document_uri: str | None = None
14
15
  document_meta: dict = {}
15
16
  embedding: list[float] | None = None
@@ -28,7 +28,9 @@ class ChunkRepository:
28
28
  def _ensure_fts_index(self) -> None:
29
29
  """Ensure FTS index exists on the content column."""
30
30
  try:
31
- self.store.chunks_table.create_fts_index("content", replace=True)
31
+ self.store.chunks_table.create_fts_index(
32
+ "content", replace=True, with_position=True, remove_stop_words=False
33
+ )
32
34
  except Exception as e:
33
35
  # Log the error but don't fail - FTS might already exist
34
36
  logger.debug(f"FTS index creation skipped: {e}")
@@ -59,11 +61,16 @@ class ChunkRepository:
59
61
  embedding = entity.embedding
60
62
  else:
61
63
  embedding = await self.embedder.embed(entity.content)
64
+ order_val = int(entity.order)
65
+
62
66
  chunk_record = self.store.ChunkRecord(
63
67
  id=chunk_id,
64
68
  document_id=entity.document_id,
65
69
  content=entity.content,
66
- metadata=json.dumps(entity.metadata),
70
+ metadata=json.dumps(
71
+ {k: v for k, v in entity.metadata.items() if k != "order"}
72
+ ),
73
+ order=order_val,
67
74
  vector=embedding,
68
75
  )
69
76
 
@@ -90,11 +97,13 @@ class ChunkRepository:
90
97
  return None
91
98
 
92
99
  chunk_record = results[0]
100
+ md = json.loads(chunk_record.metadata)
93
101
  return Chunk(
94
102
  id=chunk_record.id,
95
103
  document_id=chunk_record.document_id,
96
104
  content=chunk_record.content,
97
- metadata=json.loads(chunk_record.metadata) if chunk_record.metadata else {},
105
+ metadata=md,
106
+ order=chunk_record.order,
98
107
  )
99
108
 
100
109
  async def update(self, entity: Chunk) -> Chunk:
@@ -102,13 +111,17 @@ class ChunkRepository:
102
111
  assert entity.id, "Chunk ID is required for update"
103
112
 
104
113
  embedding = await self.embedder.embed(entity.content)
114
+ order_val = int(entity.order)
105
115
 
106
116
  self.store.chunks_table.update(
107
117
  where=f"id = '{entity.id}'",
108
118
  values={
109
119
  "document_id": entity.document_id,
110
120
  "content": entity.content,
111
- "metadata": json.dumps(entity.metadata),
121
+ "metadata": json.dumps(
122
+ {k: v for k, v in entity.metadata.items() if k != "order"}
123
+ ),
124
+ "order": order_val,
112
125
  "vector": embedding,
113
126
  },
114
127
  )
@@ -140,15 +153,19 @@ class ChunkRepository:
140
153
 
141
154
  results = list(query.to_pydantic(self.store.ChunkRecord))
142
155
 
143
- return [
144
- Chunk(
145
- id=chunk.id,
146
- document_id=chunk.document_id,
147
- content=chunk.content,
148
- metadata=json.loads(chunk.metadata) if chunk.metadata else {},
156
+ chunks: list[Chunk] = []
157
+ for rec in results:
158
+ md = json.loads(rec.metadata)
159
+ chunks.append(
160
+ Chunk(
161
+ id=rec.id,
162
+ document_id=rec.document_id,
163
+ content=rec.content,
164
+ metadata=md,
165
+ order=rec.order,
166
+ )
149
167
  )
150
- for chunk in results
151
- ]
168
+ return chunks
152
169
 
153
170
  async def create_chunks_for_document(
154
171
  self, document_id: str, document: DoclingDocument
@@ -191,7 +208,8 @@ class ChunkRepository:
191
208
  id=chunk_id,
192
209
  document_id=document_id,
193
210
  content=chunk_text,
194
- metadata=json.dumps({"order": order}),
211
+ metadata=json.dumps({}),
212
+ order=order,
195
213
  vector=embedding,
196
214
  )
197
215
  chunk_records.append(chunk_record)
@@ -200,7 +218,8 @@ class ChunkRepository:
200
218
  id=chunk_id,
201
219
  document_id=document_id,
202
220
  content=chunk_text,
203
- metadata={"order": order},
221
+ metadata={},
222
+ order=order,
204
223
  )
205
224
  created_chunks.append(chunk)
206
225
 
@@ -219,8 +238,10 @@ class ChunkRepository:
219
238
  self.store.chunks_table = self.store.db.create_table(
220
239
  "chunks", schema=self.store.ChunkRecord
221
240
  )
222
- # Create FTS index on the new table
223
- self.store.chunks_table.create_fts_index("content", replace=True)
241
+ # Create FTS index on the new table with phrase query support
242
+ self.store.chunks_table.create_fts_index(
243
+ "content", replace=True, with_position=True, remove_stop_words=False
244
+ )
224
245
 
225
246
  async def delete_by_document_id(self, document_id: str) -> bool:
226
247
  """Delete all chunks for a document."""
@@ -298,37 +319,36 @@ class ChunkRepository:
298
319
  doc_uri = doc_results[0].uri if doc_results else None
299
320
  doc_meta = doc_results[0].metadata if doc_results else "{}"
300
321
 
301
- # Sort by order in metadata
302
- chunks = [
303
- Chunk(
304
- id=chunk.id,
305
- document_id=chunk.document_id,
306
- content=chunk.content,
307
- metadata=json.loads(chunk.metadata) if chunk.metadata else {},
308
- document_uri=doc_uri,
309
- document_meta=json.loads(doc_meta) if doc_meta else {},
322
+ chunks: list[Chunk] = []
323
+ for rec in results:
324
+ md = json.loads(rec.metadata)
325
+ chunks.append(
326
+ Chunk(
327
+ id=rec.id,
328
+ document_id=rec.document_id,
329
+ content=rec.content,
330
+ metadata=md,
331
+ order=rec.order,
332
+ document_uri=doc_uri,
333
+ document_meta=json.loads(doc_meta),
334
+ )
310
335
  )
311
- for chunk in results
312
- ]
313
336
 
314
- chunks.sort(key=lambda c: c.metadata.get("order", 0))
337
+ chunks.sort(key=lambda c: c.order)
315
338
  return chunks
316
339
 
317
340
  async def get_adjacent_chunks(self, chunk: Chunk, num_adjacent: int) -> list[Chunk]:
318
341
  """Get adjacent chunks before and after the given chunk within the same document."""
319
342
  assert chunk.document_id, "Document id is required for adjacent chunk finding"
320
343
 
321
- chunk_order = chunk.metadata.get("order")
322
- if chunk_order is None:
323
- return []
344
+ chunk_order = chunk.order
324
345
 
325
- # Get all chunks for the document
346
+ # Fetch chunks for the same document and filter by order proximity
326
347
  all_chunks = await self.get_by_document_id(chunk.document_id)
327
348
 
328
- # Filter to adjacent chunks
329
- adjacent_chunks = []
349
+ adjacent_chunks: list[Chunk] = []
330
350
  for c in all_chunks:
331
- c_order = c.metadata.get("order", 0)
351
+ c_order = c.order
332
352
  if c.id != chunk.id and abs(c_order - chunk_order) <= num_adjacent:
333
353
  adjacent_chunks.append(c)
334
354
 
@@ -380,15 +400,16 @@ class ChunkRepository:
380
400
  doc_uri = doc.uri if doc else None
381
401
  doc_meta = doc.metadata if doc else "{}"
382
402
 
403
+ md = json.loads(chunk_record.metadata)
404
+
383
405
  chunk = Chunk(
384
406
  id=chunk_record.id,
385
407
  document_id=chunk_record.document_id,
386
408
  content=chunk_record.content,
387
- metadata=json.loads(chunk_record.metadata)
388
- if chunk_record.metadata
389
- else {},
409
+ metadata=md,
410
+ order=chunk_record.order,
390
411
  document_uri=doc_uri,
391
- document_meta=json.loads(doc_meta) if doc_meta else {},
412
+ document_meta=json.loads(doc_meta),
392
413
  )
393
414
 
394
415
  # Get score from arrow result
@@ -34,7 +34,7 @@ class DocumentRepository:
34
34
  id=record.id,
35
35
  content=record.content,
36
36
  uri=record.uri,
37
- metadata=json.loads(record.metadata) if record.metadata else {},
37
+ metadata=json.loads(record.metadata),
38
38
  created_at=datetime.fromisoformat(record.created_at)
39
39
  if record.created_at
40
40
  else datetime.now(),
@@ -194,7 +194,7 @@ class DocumentRepository:
194
194
  )
195
195
  for order, chunk in enumerate(chunks):
196
196
  chunk.document_id = created_doc.id
197
- chunk.metadata["order"] = order
197
+ chunk.order = order
198
198
  await self.chunk_repository.create(chunk)
199
199
 
200
200
  return created_doc
@@ -84,11 +84,18 @@ class SettingsRepository:
84
84
  )
85
85
 
86
86
  if existing:
87
- # Only update when configuration actually changed to avoid needless new versions
88
- existing_payload = (
89
- json.loads(existing[0].settings) if existing[0].settings else {}
90
- )
91
- if existing_payload != current_config:
87
+ # Preserve existing version if present to avoid interfering with upgrade flow
88
+ try:
89
+ existing_settings = (
90
+ json.loads(existing[0].settings) if existing[0].settings else {}
91
+ )
92
+ except Exception:
93
+ existing_settings = {}
94
+ if "version" in existing_settings:
95
+ current_config["version"] = existing_settings["version"]
96
+
97
+ # Update existing settings
98
+ if existing_settings != current_config:
92
99
  self.store.settings_table.update(
93
100
  where="id = 'settings'",
94
101
  values={"settings": json.dumps(current_config)},
@@ -1 +1,60 @@
1
- upgrades = []
1
+ import logging
2
+ from collections.abc import Callable
3
+ from dataclasses import dataclass
4
+
5
+ from packaging.version import Version, parse
6
+
7
+ from haiku.rag.store.engine import Store
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class Upgrade:
14
+ """Represents a database upgrade step."""
15
+
16
+ version: str
17
+ apply: Callable[[Store], None]
18
+ description: str = ""
19
+
20
+
21
+ # Registry of upgrade steps (ordered by version)
22
+ upgrades: list[Upgrade] = []
23
+
24
+
25
+ def run_pending_upgrades(store: Store, from_version: str, to_version: str) -> None:
26
+ """Run upgrades where from_version < step.version <= to_version."""
27
+ v_from: Version = parse(from_version)
28
+ v_to: Version = parse(to_version)
29
+
30
+ # Ensure that tests/development run available code upgrades even if the
31
+ # installed package version hasn't been bumped to include them yet.
32
+ if upgrades:
33
+ highest_step_version: Version = max(parse(u.version) for u in upgrades)
34
+ if highest_step_version > v_to:
35
+ v_to = highest_step_version
36
+
37
+ # Determine applicable steps
38
+ sorted_steps = sorted(upgrades, key=lambda u: parse(u.version))
39
+ applicable = [s for s in sorted_steps if v_from < parse(s.version) <= v_to]
40
+ if applicable:
41
+ logger.info("%d upgrade step(s) pending", len(applicable))
42
+
43
+ # Apply in ascending order
44
+ for idx, step in enumerate(applicable, start=1):
45
+ logger.info(
46
+ "Applying upgrade %s: %s (%d/%d)",
47
+ step.version,
48
+ step.description or "",
49
+ idx,
50
+ len(applicable),
51
+ )
52
+ step.apply(store)
53
+ logger.info("Completed upgrade %s", step.version)
54
+
55
+
56
+ from .v0_9_3 import upgrade_fts_phrase as upgrade_0_9_3_fts # noqa: E402
57
+ from .v0_9_3 import upgrade_order as upgrade_0_9_3_order # noqa: E402
58
+
59
+ upgrades.append(upgrade_0_9_3_order)
60
+ upgrades.append(upgrade_0_9_3_fts)
@@ -0,0 +1,112 @@
1
+ import json
2
+
3
+ from lancedb.pydantic import LanceModel, Vector
4
+ from pydantic import Field
5
+
6
+ from haiku.rag.store.engine import Store
7
+ from haiku.rag.store.upgrades import Upgrade
8
+
9
+
10
+ def _infer_vector_dim(store: Store) -> int:
11
+ """Infer vector dimension from existing data; fallback to embedder config."""
12
+ try:
13
+ arrow = store.chunks_table.search().limit(1).to_arrow()
14
+ rows = arrow.to_pylist()
15
+ if rows:
16
+ vec = rows[0].get("vector")
17
+ if isinstance(vec, list) and vec:
18
+ return len(vec)
19
+ except Exception:
20
+ pass
21
+ # Fallback to configured embedder vector dim
22
+ return getattr(store.embedder, "_vector_dim", 1024)
23
+
24
+
25
+ def _apply_chunk_order(store: Store) -> None:
26
+ """Add integer 'order' column to chunks and backfill from metadata."""
27
+
28
+ vector_dim = _infer_vector_dim(store)
29
+
30
+ class ChunkRecordV2(LanceModel):
31
+ id: str
32
+ document_id: str
33
+ content: str
34
+ metadata: str = Field(default="{}")
35
+ order: int = Field(default=0)
36
+ vector: Vector(vector_dim) = Field( # type: ignore
37
+ default_factory=lambda: [0.0] * vector_dim
38
+ )
39
+
40
+ # Read existing chunks
41
+ try:
42
+ chunks_arrow = store.chunks_table.search().to_arrow()
43
+ rows = chunks_arrow.to_pylist()
44
+ except Exception:
45
+ rows = []
46
+
47
+ new_chunk_records: list[ChunkRecordV2] = []
48
+ for row in rows:
49
+ md_raw = row.get("metadata") or "{}"
50
+ try:
51
+ md = json.loads(md_raw) if isinstance(md_raw, str) else md_raw
52
+ except Exception:
53
+ md = {}
54
+ # Extract and normalize order
55
+ order_val = 0
56
+ try:
57
+ if isinstance(md, dict) and "order" in md:
58
+ order_val = int(md["order"]) # type: ignore[arg-type]
59
+ except Exception:
60
+ order_val = 0
61
+
62
+ if isinstance(md, dict) and "order" in md:
63
+ md = {k: v for k, v in md.items() if k != "order"}
64
+
65
+ vec = row.get("vector") or [0.0] * vector_dim
66
+
67
+ new_chunk_records.append(
68
+ ChunkRecordV2(
69
+ id=row.get("id"),
70
+ document_id=row.get("document_id"),
71
+ content=row.get("content", ""),
72
+ metadata=json.dumps(md),
73
+ order=order_val,
74
+ vector=vec,
75
+ )
76
+ )
77
+
78
+ # Recreate chunks table with new schema
79
+ try:
80
+ store.db.drop_table("chunks")
81
+ except Exception:
82
+ pass
83
+
84
+ store.chunks_table = store.db.create_table("chunks", schema=ChunkRecordV2)
85
+ store.chunks_table.create_fts_index("content", replace=True)
86
+
87
+ if new_chunk_records:
88
+ store.chunks_table.add(new_chunk_records)
89
+
90
+
91
+ upgrade_order = Upgrade(
92
+ version="0.9.3",
93
+ apply=_apply_chunk_order,
94
+ description="Add 'order' column to chunks and backfill from metadata",
95
+ )
96
+
97
+
98
+ def _apply_fts_phrase_support(store: Store) -> None:
99
+ """Recreate FTS index with phrase query support and no stop-word removal."""
100
+ try:
101
+ store.chunks_table.create_fts_index(
102
+ "content", replace=True, with_position=True, remove_stop_words=False
103
+ )
104
+ except Exception:
105
+ pass
106
+
107
+
108
+ upgrade_fts_phrase = Upgrade(
109
+ version="0.9.3",
110
+ apply=_apply_fts_phrase_support,
111
+ description="Enable FTS phrase queries (with positions) and keep stop-words",
112
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiku.rag
3
- Version: 0.9.1
3
+ Version: 0.9.3
4
4
  Summary: Agentic Retrieval Augmented Generation (RAG) with LanceDB
5
5
  Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
6
6
  License: MIT
@@ -1,8 +1,8 @@
1
1
  haiku/rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- haiku/rag/app.py,sha256=Smof7ZIe-oRGkDTap81VaKZGIeborD2X-oXKgBoJs9I,11763
2
+ haiku/rag/app.py,sha256=nkud-OHic3HIgEEiNOKVvhmW98DPpDe6HokBSz-xV7w,11420
3
3
  haiku/rag/chunker.py,sha256=PVe6ysv8UlacUd4Zb3_8RFWIaWDXnzBAy2VDJ4TaUsE,1555
4
4
  haiku/rag/cli.py,sha256=3nlzrT5FPCyfnu51KHchLG4Cj2eVv9YsuGHMShBnVb0,9845
5
- haiku/rag/client.py,sha256=NJVGXzVzpoVy1sttz_xEU7mXWtObKT8pGpvo5pZyzwc,21288
5
+ haiku/rag/client.py,sha256=QgJQu7g7JjAzWN6R10NeDqpFf89Dml_LiWce4QRHLHc,21177
6
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
@@ -17,33 +17,35 @@ 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=f7hGWhjgzJKwa5BJkAO0KCxbgpwigPz5E9a26S9TUYI,2948
20
+ haiku/rag/qa/agent.py,sha256=f4Keh-ESgctNbTg96QL95HYjINVLOcxa8t8crx92MMk,3081
21
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=IphcKc8u5984DH9FctaPRdAaxYhm8UhI5wL34Y93e4w,3688
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
+ haiku/rag/research/__init__.py,sha256=qLF41YayAxW_VeHhuTceVuz9hw1FNbuRV9VMhonUMW0,1078
28
+ haiku/rag/research/base.py,sha256=X5n6myUG_Oz4i8WGfyKZ39YzK13rOkyvwGKwSBfL50k,4043
29
+ haiku/rag/research/dependencies.py,sha256=N7mnFwa_uyWYH0NtbEHp5JJvNGN64Q8HHfY41E8Irx0,2362
30
+ haiku/rag/research/evaluation_agent.py,sha256=VMegemd9Vln3jfZbeHzMfb7rUPFNzNxi5Y_l1zrddl8,2994
31
+ haiku/rag/research/orchestrator.py,sha256=nvSRdIs77kSb1CZaQUYZM_Zl5xLP8K6noVgnixpeLJI,6329
32
+ haiku/rag/research/presearch_agent.py,sha256=MpakZ9HSynv73EnWakwUuytfKpiN_8lEqZlVc3zZjGU,1427
33
+ haiku/rag/research/prompts.py,sha256=pVRB7_b_p3JaLF1bC3ANTbSFY78ypSjDhoq6peoU6jo,5685
34
+ haiku/rag/research/search_agent.py,sha256=xn2MlEyL9te_dtZqTzW81lGw7fYmyUzn26mvzX52hNA,2599
35
+ haiku/rag/research/synthesis_agent.py,sha256=FQCt8wbaaKOwgIOQazTNAmohBMZRUDoVzHkByYhbGg8,2182
35
36
  haiku/rag/store/__init__.py,sha256=hq0W0DAC7ysqhWSP2M2uHX8cbG6kbr-sWHxhq6qQcY0,103
36
- haiku/rag/store/engine.py,sha256=fNrykqMX7PRSCt4LSRfuJ66OLrb8BVYq2bpbfI2iaWU,8455
37
+ haiku/rag/store/engine.py,sha256=-3MZJYft2XTWaLuyKha8DKhWQeU5E5CBeskXXF5fXso,9555
37
38
  haiku/rag/store/models/__init__.py,sha256=s0E72zneGlowvZrFWaNxHYjOAUjgWdLxzdYsnvNRVlY,88
38
- haiku/rag/store/models/chunk.py,sha256=ZNyTfO6lh3rXWLVYO3TZcitbL4LSUGr42fR6jQQ5iQc,364
39
+ haiku/rag/store/models/chunk.py,sha256=Ww_hj3DMwJLNM33l1GvIP84yzDFc6cxfiWcotUfWSYg,383
39
40
  haiku/rag/store/models/document.py,sha256=zSSpt6pyrMJAIXGQvIcqojcqUzwZnhp3WxVokaWxNRc,396
40
41
  haiku/rag/store/repositories/__init__.py,sha256=Olv5dLfBQINRV3HrsfUpjzkZ7Qm7goEYyMNykgo_DaY,291
41
- haiku/rag/store/repositories/chunk.py,sha256=1RmPyEYRYOFbrALbmLOo62t3f-xO2KgxUjcvPdrRZlc,14467
42
- haiku/rag/store/repositories/document.py,sha256=XoLCrMrZqs0iCZoHlDOfRDaVUux77Vdu5iZczduF1rY,7812
43
- haiku/rag/store/repositories/settings.py,sha256=wx3fuP_5CpPflZHRrIkeoer6ml-iD0qXERh5k6MQRzI,5291
44
- haiku/rag/store/upgrades/__init__.py,sha256=wUiEoSiHTahvuagx93E4FB07v123AhdbOjwUkPusiIg,14
45
- haiku_rag-0.9.1.dist-info/METADATA,sha256=GuQnwDZrKNTMRhQyiEY38ZBfSqau4J4gjIXgeIoah8w,4681
46
- haiku_rag-0.9.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
- haiku_rag-0.9.1.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
48
- haiku_rag-0.9.1.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
49
- haiku_rag-0.9.1.dist-info/RECORD,,
42
+ haiku/rag/store/repositories/chunk.py,sha256=O2SEhQy3ZptWjwwpxS-L8KNq2tEqEBqheHfLw-M_FqA,15012
43
+ haiku/rag/store/repositories/document.py,sha256=m11SamQoGYs5ODfmarJGU1yIcqtgmnba-5bGOPQuYrI,7773
44
+ haiku/rag/store/repositories/settings.py,sha256=7XMBMavU8zRgdBoQzQg0Obfa7UKjuVnBugidTC6sEW0,5548
45
+ haiku/rag/store/upgrades/__init__.py,sha256=gDOxiq3wdZPr3JoenjNYxx0cpgZJhbaFKNX2fzXRq1Q,1852
46
+ haiku/rag/store/upgrades/v0_9_3.py,sha256=NrjNilQSgDtFWRbL3ZUtzQzJ8tf9u0dDRJtnDFwwbdw,3322
47
+ haiku_rag-0.9.3.dist-info/METADATA,sha256=iCae4YtZ0meIQTZLUNree_-74F3irXvArPvdSxVz8ZM,4681
48
+ haiku_rag-0.9.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
49
+ haiku_rag-0.9.3.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
50
+ haiku_rag-0.9.3.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
51
+ haiku_rag-0.9.3.dist-info/RECORD,,