haiku.rag 0.9.2__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
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())
@@ -45,7 +45,8 @@ class BaseResearchAgent[T](ABC):
45
45
  model=model_obj,
46
46
  deps_type=ResearchDependencies,
47
47
  output_type=agent_output_type,
48
- system_prompt=self.get_system_prompt(),
48
+ instructions=self.get_system_prompt(),
49
+ retries=3,
49
50
  )
50
51
 
51
52
  # Register tools
@@ -75,7 +76,6 @@ class BaseResearchAgent[T](ABC):
75
76
  """Return the system prompt for this agent."""
76
77
  pass
77
78
 
78
- @abstractmethod
79
79
  def register_tools(self) -> None:
80
80
  """Register agent-specific tools."""
81
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
 
@@ -34,9 +39,47 @@ class AnalysisEvaluationAgent(BaseResearchAgent[EvaluationResult]):
34
39
  def __init__(self, provider: str, model: str) -> None:
35
40
  super().__init__(provider, model, output_type=EvaluationResult)
36
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
+
37
84
  def get_system_prompt(self) -> str:
38
85
  return EVALUATION_AGENT_PROMPT
39
-
40
- def register_tools(self) -> None:
41
- """No additional tools needed - uses LLM capabilities directly."""
42
- pass
@@ -1,13 +1,15 @@
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,
@@ -31,7 +33,9 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
31
33
  """Orchestrator agent that coordinates the research workflow."""
32
34
 
33
35
  def __init__(
34
- 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,
35
39
  ):
36
40
  # Use provided values or fall back to config defaults
37
41
  provider = provider or Config.RESEARCH_PROVIDER or Config.QA_PROVIDER
@@ -53,30 +57,15 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
53
57
  def get_system_prompt(self) -> str:
54
58
  return ORCHESTRATOR_PROMPT
55
59
 
56
- def register_tools(self) -> None:
57
- """Register orchestration tools."""
58
- # Tools are no longer needed - orchestrator directly calls agents
59
- pass
60
-
61
- def _format_context_for_prompt(self, context: ResearchContext) -> str:
62
- """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."""
63
66
 
64
- context_data = {
65
- "original_question": context.original_question,
66
- "unanswered_questions": context.sub_questions,
67
- "qa_responses": [
68
- {
69
- "question": qa.query,
70
- "answer": qa.answer,
71
- "context_snippets": qa.context,
72
- "sources": qa.sources,
73
- }
74
- for qa in context.qa_responses
75
- ],
76
- "insights": context.insights,
77
- "gaps": context.gaps,
78
- }
79
- 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
80
69
 
81
70
  async def conduct_research(
82
71
  self,
@@ -85,7 +74,6 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
85
74
  max_iterations: int = 3,
86
75
  confidence_threshold: float = 0.8,
87
76
  verbose: bool = False,
88
- console: Console | None = None,
89
77
  ) -> ResearchReport:
90
78
  """Conduct comprehensive research on a question.
91
79
 
@@ -95,7 +83,6 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
95
83
  max_iterations: Maximum number of search-analyze-clarify cycles
96
84
  confidence_threshold: Minimum confidence level to stop research (0-1)
97
85
  verbose: If True, print progress and intermediate results
98
- console: Optional Rich console for output
99
86
 
100
87
  Returns:
101
88
  ResearchReport with comprehensive findings
@@ -104,44 +91,27 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
104
91
  # Initialize context
105
92
  context = ResearchContext(original_question=question)
106
93
  deps = ResearchDependencies(client=client, context=context)
94
+ if verbose:
95
+ deps.console = Console()
107
96
 
108
- # Use provided console or create a new one
109
- console = console or Console() if verbose else None
110
-
111
- # Run a simple presearch survey to summarize KB context
112
- if console:
113
- console.print(
114
- "\n[bold cyan]🔎 Presearch: summarizing KB context...[/bold cyan]"
115
- )
116
-
117
- presearch_result = await self.presearch_agent.run(question, deps=deps)
118
-
97
+ console = deps.console
119
98
  # Create initial research plan
120
99
  if console:
121
100
  console.print("\n[bold cyan]📋 Creating research plan...[/bold cyan]")
122
101
 
123
- # Include the presearch summary to ground the planning step.
124
-
125
- planning_context_xml = format_as_xml(
126
- {
127
- "original_question": question,
128
- "presearch_summary": presearch_result.output or "",
129
- },
130
- root_tag="planning_context",
131
- )
132
-
102
+ # Run a simple presearch survey to summarize KB context
103
+ presearch_result = await self.presearch_agent.run(question, deps=deps)
133
104
  plan_prompt = (
134
105
  "Create a research plan for the main question below.\n\n"
135
106
  f"Main question: {question}\n\n"
136
107
  "Use this brief presearch summary to inform the plan. Focus the 3 sub-questions "
137
108
  "on the most important aspects not already obvious from the current KB context.\n\n"
138
- f"{planning_context_xml}"
109
+ f"{presearch_result.output}"
139
110
  )
140
111
 
141
112
  plan_result: AgentRunResult[ResearchPlan] = await self.run(
142
113
  plan_prompt, deps=deps
143
114
  )
144
-
145
115
  context.sub_questions = plan_result.output.sub_questions
146
116
 
147
117
  if console:
@@ -152,7 +122,6 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
152
122
  console.print(" [bold]Sub-questions:[/bold]")
153
123
  for i, sq in enumerate(plan_result.output.sub_questions, 1):
154
124
  console.print(f" {i}. {sq}")
155
- console.print()
156
125
 
157
126
  # Execute research iterations
158
127
  for iteration in range(max_iterations):
@@ -163,7 +132,6 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
163
132
 
164
133
  # Check if we have questions to search
165
134
  if not context.sub_questions:
166
- # No more questions to explore
167
135
  if console:
168
136
  console.print(
169
137
  "[yellow]No more questions to explore. Concluding research.[/yellow]"
@@ -171,90 +139,20 @@ class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
171
139
  break
172
140
 
173
141
  # Use current sub-questions for this iteration
174
- questions_to_search = context.sub_questions
142
+ questions_to_search = context.sub_questions[:]
175
143
 
176
144
  # Search phase - answer all questions in this iteration
177
145
  if console:
178
146
  console.print(
179
147
  f"\n[bold cyan]🔍 Searching & Answering {len(questions_to_search)} questions:[/bold cyan]"
180
148
  )
181
- for i, q in enumerate(questions_to_search, 1):
182
- console.print(f" {i}. {q}")
183
149
 
184
- # Run searches for all questions and remove answered ones
185
- answered_questions = []
186
150
  for search_question in questions_to_search:
187
- try:
188
- await self.search_agent.run(search_question, deps=deps)
189
- except Exception as e: # pragma: no cover - defensive
190
- if console:
191
- console.print(
192
- f"\n [red]×[/red] Omitting failed question: {search_question} ({e})"
193
- )
194
- finally:
195
- answered_questions.append(search_question)
196
-
197
- if console and context.qa_responses:
198
- # Show the last QA response (which should be for this question)
199
- latest_qa = context.qa_responses[-1]
200
- answer_preview = (
201
- latest_qa.answer[:150] + "..."
202
- if len(latest_qa.answer) > 150
203
- else latest_qa.answer
204
- )
205
- console.print(
206
- f"\n [green]✓[/green] {search_question[:50]}..."
207
- if len(search_question) > 50
208
- else f"\n [green]✓[/green] {search_question}"
209
- )
210
- console.print(f" {answer_preview}")
211
-
212
- # Remove answered questions from the list
213
- for question in answered_questions:
214
- if question in context.sub_questions:
215
- context.sub_questions.remove(question)
151
+ await self.search_agent.run(search_question, deps=deps)
216
152
 
217
153
  # Analysis and Evaluation phase
218
- if console:
219
- console.print(
220
- "\n[bold cyan]📊 Analyzing and evaluating research progress...[/bold cyan]"
221
- )
222
-
223
- # Format context for the evaluation agent
224
- context_xml = self._format_context_for_prompt(context)
225
- evaluation_prompt = f"""Analyze all gathered information and evaluate the completeness of research.
226
-
227
- {context_xml}
228
-
229
- Evaluate the research progress for the original question and identify any remaining gaps."""
230
-
231
- evaluation_result = await self.evaluation_agent.run(
232
- evaluation_prompt,
233
- deps=deps,
234
- )
235
-
236
- if console and evaluation_result.output:
237
- output = evaluation_result.output
238
- if output.key_insights:
239
- console.print(" [bold]Key insights:[/bold]")
240
- for insight in output.key_insights:
241
- console.print(f" • {insight}")
242
- console.print(
243
- f" Confidence: [yellow]{output.confidence_score:.1%}[/yellow]"
244
- )
245
- status = (
246
- "[green]Yes[/green]" if output.is_sufficient else "[red]No[/red]"
247
- )
248
- console.print(f" Sufficient: {status}")
249
154
 
250
- # Store insights
251
- for insight in evaluation_result.output.key_insights:
252
- context.add_insight(insight)
253
-
254
- # Add new questions to the sub-questions list
255
- for new_q in evaluation_result.output.new_questions:
256
- if new_q not in context.sub_questions:
257
- context.sub_questions.append(new_q)
155
+ evaluation_result = await self.evaluation_agent.run("", deps=deps)
258
156
 
259
157
  # Check if research is sufficient
260
158
  if self._should_stop_research(evaluation_result, confidence_threshold):
@@ -265,36 +163,8 @@ Evaluate the research progress for the original question and identify any remain
265
163
  break
266
164
 
267
165
  # Generate final report
268
- if console:
269
- console.print(
270
- "\n[bold cyan]📝 Generating final research report...[/bold cyan]"
271
- )
272
-
273
- # Format context for the synthesis agent
274
- final_context_xml = self._format_context_for_prompt(context)
275
- synthesis_prompt = f"""Generate a comprehensive research report based on all gathered information.
276
-
277
- {final_context_xml}
278
-
279
- Create a detailed report that synthesizes all findings into a coherent response."""
280
-
281
166
  report_result: AgentRunResult[ResearchReport] = await self.synthesis_agent.run(
282
- synthesis_prompt, deps=deps
167
+ "", deps=deps
283
168
  )
284
169
 
285
- if console:
286
- console.print("[bold green]✅ Research complete![/bold green]")
287
-
288
170
  return report_result.output
289
-
290
- def _should_stop_research(
291
- self,
292
- evaluation_result: AgentRunResult[EvaluationResult],
293
- confidence_threshold: float,
294
- ) -> bool:
295
- """Determine if research should stop based on evaluation."""
296
-
297
- result = evaluation_result.output
298
-
299
- # Stop if the agent indicates sufficient information AND confidence exceeds threshold
300
- return result.is_sufficient and result.confidence_score >= confidence_threshold
@@ -15,6 +15,12 @@ class PresearchSurveyAgent(BaseResearchAgent[str]):
15
15
  async def run(
16
16
  self, prompt: str, deps: ResearchDependencies, **kwargs
17
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
+
18
24
  return await super().run(prompt, deps, **kwargs)
19
25
 
20
26
  def get_system_prompt(self) -> str:
@@ -28,7 +34,6 @@ class PresearchSurveyAgent(BaseResearchAgent[str]):
28
34
  limit: int = 6,
29
35
  ) -> str:
30
36
  """Return verbatim concatenation of relevant chunk texts."""
31
- query = query.replace('"', "")
32
37
  results = await ctx.deps.client.search(query, limit=limit)
33
38
  expanded = await ctx.deps.client.expand_context(results)
34
39
  return "\n\n".join(chunk.content for chunk, _ in expanded)
@@ -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,9 +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
- # XXX: Investigate how to do that with lancedb
46
- query = query.replace('"', "")
47
51
  search_results = await ctx.deps.client.search(query, limit=limit)
48
52
  expanded = await ctx.deps.client.expand_context(search_results)
49
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
 
@@ -30,11 +35,26 @@ class SynthesisAgent(BaseResearchAgent[ResearchReport]):
30
35
  def __init__(self, provider: str, model: str) -> None:
31
36
  super().__init__(provider, model, output_type=ResearchReport)
32
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
+
33
59
  def get_system_prompt(self) -> str:
34
60
  return SYNTHESIS_AGENT_PROMPT
35
-
36
- def register_tools(self) -> None:
37
- """Register synthesis-specific tools."""
38
- # The agent will use its LLM capabilities directly for synthesis
39
- # The structured output will guide the report generation
40
- 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.2
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=o64L7aj5V8lYHxWhAKBNj1tGfXiN6xr0_Cc1dEYd3As,11483
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
@@ -25,26 +25,27 @@ haiku/rag/reranking/cohere.py,sha256=1iTdiaa8vvb6oHVB2qpWzUOVkyfUcimVSZp6Qr4aq4c
25
25
  haiku/rag/reranking/mxbai.py,sha256=46sVTsTIkzIX9THgM3u8HaEmgY7evvEyB-N54JTHvK8,867
26
26
  haiku/rag/reranking/vllm.py,sha256=xVGH9ss-ISWdJ5SKUUHUbTqBo7PIEmA_SQv0ScdJ6XA,1479
27
27
  haiku/rag/research/__init__.py,sha256=qLF41YayAxW_VeHhuTceVuz9hw1FNbuRV9VMhonUMW0,1078
28
- haiku/rag/research/base.py,sha256=ZUvqh-IxU8r4mOPRKjwZcHciKcIfrTnP6Q_9jVElelQ,4041
29
- haiku/rag/research/dependencies.py,sha256=vZctKC5donqhm8LFO6hQdXZZXzjdW1__4eUlhyZn058,1573
30
- haiku/rag/research/evaluation_agent.py,sha256=yyBobKr8MRwiox59I2Jqycp02ju9EGVaI9FceRGL188,1386
31
- haiku/rag/research/orchestrator.py,sha256=LrxRG74BWun0T4uOxhc9AuitxbPioS_eG_nE098ftyY,11765
32
- haiku/rag/research/presearch_agent.py,sha256=vf-WlY46g5tuuLKMBuPXXYYffynsBw7KVLr8LoTNHnU,1292
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
33
  haiku/rag/research/prompts.py,sha256=pVRB7_b_p3JaLF1bC3ANTbSFY78ypSjDhoq6peoU6jo,5685
34
- haiku/rag/research/search_agent.py,sha256=0iK7vCd9w7h8pWJgB6VUSPOdjlzB8peboNSXxuEGBK0,2464
35
- haiku/rag/research/synthesis_agent.py,sha256=jo5rg7aL4zGXLQP105cANqRPIiwJLqYe2unO5BQkNvE,1511
34
+ haiku/rag/research/search_agent.py,sha256=xn2MlEyL9te_dtZqTzW81lGw7fYmyUzn26mvzX52hNA,2599
35
+ haiku/rag/research/synthesis_agent.py,sha256=FQCt8wbaaKOwgIOQazTNAmohBMZRUDoVzHkByYhbGg8,2182
36
36
  haiku/rag/store/__init__.py,sha256=hq0W0DAC7ysqhWSP2M2uHX8cbG6kbr-sWHxhq6qQcY0,103
37
- haiku/rag/store/engine.py,sha256=fNrykqMX7PRSCt4LSRfuJ66OLrb8BVYq2bpbfI2iaWU,8455
37
+ haiku/rag/store/engine.py,sha256=-3MZJYft2XTWaLuyKha8DKhWQeU5E5CBeskXXF5fXso,9555
38
38
  haiku/rag/store/models/__init__.py,sha256=s0E72zneGlowvZrFWaNxHYjOAUjgWdLxzdYsnvNRVlY,88
39
- haiku/rag/store/models/chunk.py,sha256=ZNyTfO6lh3rXWLVYO3TZcitbL4LSUGr42fR6jQQ5iQc,364
39
+ haiku/rag/store/models/chunk.py,sha256=Ww_hj3DMwJLNM33l1GvIP84yzDFc6cxfiWcotUfWSYg,383
40
40
  haiku/rag/store/models/document.py,sha256=zSSpt6pyrMJAIXGQvIcqojcqUzwZnhp3WxVokaWxNRc,396
41
41
  haiku/rag/store/repositories/__init__.py,sha256=Olv5dLfBQINRV3HrsfUpjzkZ7Qm7goEYyMNykgo_DaY,291
42
- haiku/rag/store/repositories/chunk.py,sha256=1RmPyEYRYOFbrALbmLOo62t3f-xO2KgxUjcvPdrRZlc,14467
43
- haiku/rag/store/repositories/document.py,sha256=XoLCrMrZqs0iCZoHlDOfRDaVUux77Vdu5iZczduF1rY,7812
44
- haiku/rag/store/repositories/settings.py,sha256=wx3fuP_5CpPflZHRrIkeoer6ml-iD0qXERh5k6MQRzI,5291
45
- haiku/rag/store/upgrades/__init__.py,sha256=wUiEoSiHTahvuagx93E4FB07v123AhdbOjwUkPusiIg,14
46
- haiku_rag-0.9.2.dist-info/METADATA,sha256=IM9tGvye83CRTj2wOFtPP7oD9KtJvp3RXh4QdCFknD4,4681
47
- haiku_rag-0.9.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
48
- haiku_rag-0.9.2.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
49
- haiku_rag-0.9.2.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
50
- haiku_rag-0.9.2.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,,