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 +0 -1
- haiku/rag/client.py +3 -5
- haiku/rag/research/base.py +2 -2
- haiku/rag/research/dependencies.py +24 -0
- haiku/rag/research/evaluation_agent.py +47 -4
- haiku/rag/research/orchestrator.py +25 -155
- haiku/rag/research/presearch_agent.py +6 -1
- haiku/rag/research/search_agent.py +10 -6
- haiku/rag/research/synthesis_agent.py +26 -6
- haiku/rag/store/engine.py +42 -17
- haiku/rag/store/models/chunk.py +1 -0
- haiku/rag/store/repositories/chunk.py +60 -39
- haiku/rag/store/repositories/document.py +2 -2
- haiku/rag/store/repositories/settings.py +12 -5
- haiku/rag/store/upgrades/__init__.py +60 -1
- haiku/rag/store/upgrades/v0_9_3.py +112 -0
- {haiku_rag-0.9.2.dist-info → haiku_rag-0.9.3.dist-info}/METADATA +1 -1
- {haiku_rag-0.9.2.dist-info → haiku_rag-0.9.3.dist-info}/RECORD +21 -20
- {haiku_rag-0.9.2.dist-info → haiku_rag-0.9.3.dist-info}/WHEEL +0 -0
- {haiku_rag-0.9.2.dist-info → haiku_rag-0.9.3.dist-info}/entry_points.txt +0 -0
- {haiku_rag-0.9.2.dist-info → haiku_rag-0.9.3.dist-info}/licenses/LICENSE +0 -0
haiku/rag/app.py
CHANGED
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.
|
|
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.
|
|
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/research/base.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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"{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
console = deps.console
|
|
25
|
+
if console:
|
|
26
|
+
console.print(f"\t{prompt}")
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|
-
|
|
143
|
-
|
|
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(
|
|
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."""
|
haiku/rag/store/models/chunk.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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=
|
|
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(
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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({
|
|
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={
|
|
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(
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
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.
|
|
322
|
-
if chunk_order is None:
|
|
323
|
-
return []
|
|
344
|
+
chunk_order = chunk.order
|
|
324
345
|
|
|
325
|
-
#
|
|
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
|
-
|
|
329
|
-
adjacent_chunks = []
|
|
349
|
+
adjacent_chunks: list[Chunk] = []
|
|
330
350
|
for c in all_chunks:
|
|
331
|
-
c_order = c.
|
|
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=
|
|
388
|
-
|
|
389
|
-
else {},
|
|
409
|
+
metadata=md,
|
|
410
|
+
order=chunk_record.order,
|
|
390
411
|
document_uri=doc_uri,
|
|
391
|
-
document_meta=json.loads(doc_meta)
|
|
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)
|
|
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.
|
|
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
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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,8 +1,8 @@
|
|
|
1
1
|
haiku/rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
haiku/rag/app.py,sha256=
|
|
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=
|
|
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=
|
|
29
|
-
haiku/rag/research/dependencies.py,sha256=
|
|
30
|
-
haiku/rag/research/evaluation_agent.py,sha256=
|
|
31
|
-
haiku/rag/research/orchestrator.py,sha256=
|
|
32
|
-
haiku/rag/research/presearch_agent.py,sha256=
|
|
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=
|
|
35
|
-
haiku/rag/research/synthesis_agent.py,sha256=
|
|
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
|
|
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=
|
|
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=
|
|
43
|
-
haiku/rag/store/repositories/document.py,sha256=
|
|
44
|
-
haiku/rag/store/repositories/settings.py,sha256=
|
|
45
|
-
haiku/rag/store/upgrades/__init__.py,sha256=
|
|
46
|
-
|
|
47
|
-
haiku_rag-0.9.
|
|
48
|
-
haiku_rag-0.9.
|
|
49
|
-
haiku_rag-0.9.
|
|
50
|
-
haiku_rag-0.9.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|