haiku.rag 0.9.3__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of haiku.rag might be problematic. Click here for more details.
- haiku/rag/app.py +50 -13
- haiku/rag/cli.py +16 -4
- haiku/rag/reranking/mxbai.py +1 -1
- haiku/rag/research/__init__.py +10 -27
- haiku/rag/research/common.py +53 -0
- haiku/rag/research/dependencies.py +3 -25
- haiku/rag/research/graph.py +29 -0
- haiku/rag/research/models.py +70 -0
- haiku/rag/research/nodes/evaluate.py +80 -0
- haiku/rag/research/nodes/plan.py +63 -0
- haiku/rag/research/nodes/search.py +91 -0
- haiku/rag/research/nodes/synthesize.py +51 -0
- haiku/rag/research/prompts.py +97 -113
- haiku/rag/research/state.py +25 -0
- {haiku_rag-0.9.3.dist-info → haiku_rag-0.10.0.dist-info}/METADATA +37 -1
- {haiku_rag-0.9.3.dist-info → haiku_rag-0.10.0.dist-info}/RECORD +19 -17
- haiku/rag/research/base.py +0 -130
- haiku/rag/research/evaluation_agent.py +0 -85
- haiku/rag/research/orchestrator.py +0 -170
- haiku/rag/research/presearch_agent.py +0 -39
- haiku/rag/research/search_agent.py +0 -69
- haiku/rag/research/synthesis_agent.py +0 -60
- {haiku_rag-0.9.3.dist-info → haiku_rag-0.10.0.dist-info}/WHEEL +0 -0
- {haiku_rag-0.9.3.dist-info → haiku_rag-0.10.0.dist-info}/entry_points.txt +0 -0
- {haiku_rag-0.9.3.dist-info → haiku_rag-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel, Field
|
|
4
|
-
from pydantic_ai.run import AgentRunResult
|
|
5
|
-
from rich.console import Console
|
|
6
|
-
|
|
7
|
-
from haiku.rag.config import Config
|
|
8
|
-
from haiku.rag.research.base import BaseResearchAgent
|
|
9
|
-
from haiku.rag.research.dependencies import (
|
|
10
|
-
ResearchContext,
|
|
11
|
-
ResearchDependencies,
|
|
12
|
-
)
|
|
13
|
-
from haiku.rag.research.evaluation_agent import (
|
|
14
|
-
AnalysisEvaluationAgent,
|
|
15
|
-
EvaluationResult,
|
|
16
|
-
)
|
|
17
|
-
from haiku.rag.research.presearch_agent import PresearchSurveyAgent
|
|
18
|
-
from haiku.rag.research.prompts import ORCHESTRATOR_PROMPT
|
|
19
|
-
from haiku.rag.research.search_agent import SearchSpecialistAgent
|
|
20
|
-
from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class ResearchPlan(BaseModel):
|
|
24
|
-
"""Research execution plan."""
|
|
25
|
-
|
|
26
|
-
main_question: str = Field(description="The main research question")
|
|
27
|
-
sub_questions: list[str] = Field(
|
|
28
|
-
description="Decomposed sub-questions to investigate (max 3)", max_length=3
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
|
|
33
|
-
"""Orchestrator agent that coordinates the research workflow."""
|
|
34
|
-
|
|
35
|
-
def __init__(
|
|
36
|
-
self,
|
|
37
|
-
provider: str | None = Config.RESEARCH_PROVIDER,
|
|
38
|
-
model: str | None = None,
|
|
39
|
-
):
|
|
40
|
-
# Use provided values or fall back to config defaults
|
|
41
|
-
provider = provider or Config.RESEARCH_PROVIDER or Config.QA_PROVIDER
|
|
42
|
-
model = model or Config.RESEARCH_MODEL or Config.QA_MODEL
|
|
43
|
-
|
|
44
|
-
super().__init__(provider, model, output_type=ResearchPlan)
|
|
45
|
-
|
|
46
|
-
self.search_agent: SearchSpecialistAgent = SearchSpecialistAgent(
|
|
47
|
-
provider, model
|
|
48
|
-
)
|
|
49
|
-
self.presearch_agent: PresearchSurveyAgent = PresearchSurveyAgent(
|
|
50
|
-
provider, model
|
|
51
|
-
)
|
|
52
|
-
self.evaluation_agent: AnalysisEvaluationAgent = AnalysisEvaluationAgent(
|
|
53
|
-
provider, model
|
|
54
|
-
)
|
|
55
|
-
self.synthesis_agent: SynthesisAgent = SynthesisAgent(provider, model)
|
|
56
|
-
|
|
57
|
-
def get_system_prompt(self) -> str:
|
|
58
|
-
return ORCHESTRATOR_PROMPT
|
|
59
|
-
|
|
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."""
|
|
66
|
-
|
|
67
|
-
result = evaluation_result.output
|
|
68
|
-
return result.is_sufficient and result.confidence_score >= confidence_threshold
|
|
69
|
-
|
|
70
|
-
async def conduct_research(
|
|
71
|
-
self,
|
|
72
|
-
question: str,
|
|
73
|
-
client: Any,
|
|
74
|
-
max_iterations: int = 3,
|
|
75
|
-
confidence_threshold: float = 0.8,
|
|
76
|
-
verbose: bool = False,
|
|
77
|
-
) -> ResearchReport:
|
|
78
|
-
"""Conduct comprehensive research on a question.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
question: The research question to investigate
|
|
82
|
-
client: HaikuRAG client for document operations
|
|
83
|
-
max_iterations: Maximum number of search-analyze-clarify cycles
|
|
84
|
-
confidence_threshold: Minimum confidence level to stop research (0-1)
|
|
85
|
-
verbose: If True, print progress and intermediate results
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
ResearchReport with comprehensive findings
|
|
89
|
-
"""
|
|
90
|
-
|
|
91
|
-
# Initialize context
|
|
92
|
-
context = ResearchContext(original_question=question)
|
|
93
|
-
deps = ResearchDependencies(client=client, context=context)
|
|
94
|
-
if verbose:
|
|
95
|
-
deps.console = Console()
|
|
96
|
-
|
|
97
|
-
console = deps.console
|
|
98
|
-
# Create initial research plan
|
|
99
|
-
if console:
|
|
100
|
-
console.print("\n[bold cyan]📋 Creating research plan...[/bold cyan]")
|
|
101
|
-
|
|
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}"
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
plan_result: AgentRunResult[ResearchPlan] = await self.run(
|
|
113
|
-
plan_prompt, deps=deps
|
|
114
|
-
)
|
|
115
|
-
context.sub_questions = plan_result.output.sub_questions
|
|
116
|
-
|
|
117
|
-
if console:
|
|
118
|
-
console.print("\n[bold green]✅ Research Plan Created:[/bold green]")
|
|
119
|
-
console.print(
|
|
120
|
-
f" [bold]Main Question:[/bold] {plan_result.output.main_question}"
|
|
121
|
-
)
|
|
122
|
-
console.print(" [bold]Sub-questions:[/bold]")
|
|
123
|
-
for i, sq in enumerate(plan_result.output.sub_questions, 1):
|
|
124
|
-
console.print(f" {i}. {sq}")
|
|
125
|
-
|
|
126
|
-
# Execute research iterations
|
|
127
|
-
for iteration in range(max_iterations):
|
|
128
|
-
if console:
|
|
129
|
-
console.rule(
|
|
130
|
-
f"[bold yellow]🔄 Iteration {iteration + 1}/{max_iterations}[/bold yellow]"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# Check if we have questions to search
|
|
134
|
-
if not context.sub_questions:
|
|
135
|
-
if console:
|
|
136
|
-
console.print(
|
|
137
|
-
"[yellow]No more questions to explore. Concluding research.[/yellow]"
|
|
138
|
-
)
|
|
139
|
-
break
|
|
140
|
-
|
|
141
|
-
# Use current sub-questions for this iteration
|
|
142
|
-
questions_to_search = context.sub_questions[:]
|
|
143
|
-
|
|
144
|
-
# Search phase - answer all questions in this iteration
|
|
145
|
-
if console:
|
|
146
|
-
console.print(
|
|
147
|
-
f"\n[bold cyan]🔍 Searching & Answering {len(questions_to_search)} questions:[/bold cyan]"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
for search_question in questions_to_search:
|
|
151
|
-
await self.search_agent.run(search_question, deps=deps)
|
|
152
|
-
|
|
153
|
-
# Analysis and Evaluation phase
|
|
154
|
-
|
|
155
|
-
evaluation_result = await self.evaluation_agent.run("", deps=deps)
|
|
156
|
-
|
|
157
|
-
# Check if research is sufficient
|
|
158
|
-
if self._should_stop_research(evaluation_result, confidence_threshold):
|
|
159
|
-
if console:
|
|
160
|
-
console.print(
|
|
161
|
-
f"\n[bold green]✅ Stopping research:[/bold green] {evaluation_result.output.reasoning}"
|
|
162
|
-
)
|
|
163
|
-
break
|
|
164
|
-
|
|
165
|
-
# Generate final report
|
|
166
|
-
report_result: AgentRunResult[ResearchReport] = await self.synthesis_agent.run(
|
|
167
|
-
"", deps=deps
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
return report_result.output
|
|
@@ -1,39 +0,0 @@
|
|
|
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)
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
from pydantic_ai import RunContext
|
|
2
|
-
from pydantic_ai.format_prompt import format_as_xml
|
|
3
|
-
from pydantic_ai.run import AgentRunResult
|
|
4
|
-
|
|
5
|
-
from haiku.rag.research.base import BaseResearchAgent, SearchAnswer
|
|
6
|
-
from haiku.rag.research.dependencies import ResearchDependencies
|
|
7
|
-
from haiku.rag.research.prompts import SEARCH_AGENT_PROMPT
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class SearchSpecialistAgent(BaseResearchAgent[SearchAnswer]):
|
|
11
|
-
"""Agent specialized in answering questions using RAG search."""
|
|
12
|
-
|
|
13
|
-
def __init__(self, provider: str, model: str) -> None:
|
|
14
|
-
super().__init__(provider, model, output_type=SearchAnswer)
|
|
15
|
-
|
|
16
|
-
async def run(
|
|
17
|
-
self, prompt: str, deps: ResearchDependencies, **kwargs
|
|
18
|
-
) -> AgentRunResult[SearchAnswer]:
|
|
19
|
-
"""Execute the agent and persist the QA pair in shared context.
|
|
20
|
-
|
|
21
|
-
Pydantic AI enforces `SearchAnswer` as the output model; we just store
|
|
22
|
-
the QA response with the last search results as sources.
|
|
23
|
-
"""
|
|
24
|
-
console = deps.console
|
|
25
|
-
if console:
|
|
26
|
-
console.print(f"\t{prompt}")
|
|
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}")
|
|
35
|
-
|
|
36
|
-
return result
|
|
37
|
-
|
|
38
|
-
def get_system_prompt(self) -> str:
|
|
39
|
-
return SEARCH_AGENT_PROMPT
|
|
40
|
-
|
|
41
|
-
def register_tools(self) -> None:
|
|
42
|
-
"""Register search-specific tools."""
|
|
43
|
-
|
|
44
|
-
@self.agent.tool
|
|
45
|
-
async def search_and_answer(
|
|
46
|
-
ctx: RunContext[ResearchDependencies],
|
|
47
|
-
query: str,
|
|
48
|
-
limit: int = 5,
|
|
49
|
-
) -> str:
|
|
50
|
-
"""Search the KB and return a concise context pack."""
|
|
51
|
-
search_results = await ctx.deps.client.search(query, limit=limit)
|
|
52
|
-
expanded = await ctx.deps.client.expand_context(search_results)
|
|
53
|
-
|
|
54
|
-
snippet_entries = [
|
|
55
|
-
{
|
|
56
|
-
"text": chunk.content,
|
|
57
|
-
"score": score,
|
|
58
|
-
"document_uri": (chunk.document_uri or ""),
|
|
59
|
-
}
|
|
60
|
-
for chunk, score in expanded
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
# Return an XML-formatted payload with the question and snippets.
|
|
64
|
-
if snippet_entries:
|
|
65
|
-
return format_as_xml(snippet_entries, root_tag="snippets")
|
|
66
|
-
else:
|
|
67
|
-
return (
|
|
68
|
-
f"No relevant information found in the knowledge base for: {query}"
|
|
69
|
-
)
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel, Field
|
|
2
|
-
from pydantic_ai.run import AgentRunResult
|
|
3
|
-
|
|
4
|
-
from haiku.rag.research.base import BaseResearchAgent
|
|
5
|
-
from haiku.rag.research.dependencies import (
|
|
6
|
-
ResearchDependencies,
|
|
7
|
-
_format_context_for_prompt,
|
|
8
|
-
)
|
|
9
|
-
from haiku.rag.research.prompts import SYNTHESIS_AGENT_PROMPT
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ResearchReport(BaseModel):
|
|
13
|
-
"""Final research report structure."""
|
|
14
|
-
|
|
15
|
-
title: str = Field(description="Concise title for the research")
|
|
16
|
-
executive_summary: str = Field(description="Brief overview of key findings")
|
|
17
|
-
main_findings: list[str] = Field(
|
|
18
|
-
description="Primary research findings with supporting evidence"
|
|
19
|
-
)
|
|
20
|
-
conclusions: list[str] = Field(description="Evidence-based conclusions")
|
|
21
|
-
limitations: list[str] = Field(
|
|
22
|
-
description="Limitations of the current research", default=[]
|
|
23
|
-
)
|
|
24
|
-
recommendations: list[str] = Field(
|
|
25
|
-
description="Actionable recommendations based on findings", default=[]
|
|
26
|
-
)
|
|
27
|
-
sources_summary: str = Field(
|
|
28
|
-
description="Summary of sources used and their reliability"
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class SynthesisAgent(BaseResearchAgent[ResearchReport]):
|
|
33
|
-
"""Agent specialized in synthesizing research into comprehensive reports."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, provider: str, model: str) -> None:
|
|
36
|
-
super().__init__(provider, model, output_type=ResearchReport)
|
|
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
|
-
|
|
59
|
-
def get_system_prompt(self) -> str:
|
|
60
|
-
return SYNTHESIS_AGENT_PROMPT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|