haiku.rag 0.8.1__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of haiku.rag might be problematic. Click here for more details.
- haiku/rag/app.py +80 -0
- haiku/rag/cli.py +36 -0
- haiku/rag/config.py +7 -1
- haiku/rag/qa/agent.py +4 -2
- haiku/rag/qa/prompts.py +2 -2
- haiku/rag/research/__init__.py +35 -0
- haiku/rag/research/base.py +122 -0
- haiku/rag/research/dependencies.py +45 -0
- haiku/rag/research/evaluation_agent.py +40 -0
- haiku/rag/research/orchestrator.py +265 -0
- haiku/rag/research/prompts.py +116 -0
- haiku/rag/research/search_agent.py +64 -0
- haiku/rag/research/synthesis_agent.py +39 -0
- haiku/rag/store/repositories/chunk.py +2 -1
- {haiku_rag-0.8.1.dist-info → haiku_rag-0.9.0.dist-info}/METADATA +10 -10
- {haiku_rag-0.8.1.dist-info → haiku_rag-0.9.0.dist-info}/RECORD +19 -11
- {haiku_rag-0.8.1.dist-info → haiku_rag-0.9.0.dist-info}/WHEEL +0 -0
- {haiku_rag-0.8.1.dist-info → haiku_rag-0.9.0.dist-info}/entry_points.txt +0 -0
- {haiku_rag-0.8.1.dist-info → haiku_rag-0.9.0.dist-info}/licenses/LICENSE +0 -0
haiku/rag/app.py
CHANGED
|
@@ -9,6 +9,7 @@ from haiku.rag.client import HaikuRAG
|
|
|
9
9
|
from haiku.rag.config import Config
|
|
10
10
|
from haiku.rag.mcp import create_mcp_server
|
|
11
11
|
from haiku.rag.monitor import FileWatcher
|
|
12
|
+
from haiku.rag.research.orchestrator import ResearchOrchestrator
|
|
12
13
|
from haiku.rag.store.models.chunk import Chunk
|
|
13
14
|
from haiku.rag.store.models.document import Document
|
|
14
15
|
|
|
@@ -78,6 +79,85 @@ class HaikuRAGApp:
|
|
|
78
79
|
except Exception as e:
|
|
79
80
|
self.console.print(f"[red]Error: {e}[/red]")
|
|
80
81
|
|
|
82
|
+
async def research(
|
|
83
|
+
self, question: str, max_iterations: int = 3, verbose: bool = False
|
|
84
|
+
):
|
|
85
|
+
"""Run multi-agent research on a question."""
|
|
86
|
+
async with HaikuRAG(db_path=self.db_path) as client:
|
|
87
|
+
try:
|
|
88
|
+
# Create orchestrator with default config or fallback to QA
|
|
89
|
+
orchestrator = ResearchOrchestrator()
|
|
90
|
+
|
|
91
|
+
if verbose:
|
|
92
|
+
self.console.print(
|
|
93
|
+
f"[bold cyan]Starting research with {orchestrator.provider}:{orchestrator.model}[/bold cyan]"
|
|
94
|
+
)
|
|
95
|
+
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
|
96
|
+
self.console.print()
|
|
97
|
+
|
|
98
|
+
# Conduct research
|
|
99
|
+
report = await orchestrator.conduct_research(
|
|
100
|
+
question=question,
|
|
101
|
+
client=client,
|
|
102
|
+
max_iterations=max_iterations,
|
|
103
|
+
verbose=verbose,
|
|
104
|
+
console=self.console if verbose else None,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Display the report
|
|
108
|
+
self.console.print("[bold green]Research Report[/bold green]")
|
|
109
|
+
self.console.rule()
|
|
110
|
+
|
|
111
|
+
# Title and Executive Summary
|
|
112
|
+
self.console.print(f"[bold]{report.title}[/bold]")
|
|
113
|
+
self.console.print()
|
|
114
|
+
self.console.print("[bold cyan]Executive Summary:[/bold cyan]")
|
|
115
|
+
self.console.print(report.executive_summary)
|
|
116
|
+
self.console.print()
|
|
117
|
+
|
|
118
|
+
# Main Findings
|
|
119
|
+
if report.main_findings:
|
|
120
|
+
self.console.print("[bold cyan]Main Findings:[/bold cyan]")
|
|
121
|
+
for finding in report.main_findings:
|
|
122
|
+
self.console.print(f"• {finding}")
|
|
123
|
+
self.console.print()
|
|
124
|
+
|
|
125
|
+
# Themes
|
|
126
|
+
if report.themes:
|
|
127
|
+
self.console.print("[bold cyan]Key Themes:[/bold cyan]")
|
|
128
|
+
for theme, explanation in report.themes.items():
|
|
129
|
+
self.console.print(f"• [bold]{theme}[/bold]: {explanation}")
|
|
130
|
+
self.console.print()
|
|
131
|
+
|
|
132
|
+
# Conclusions
|
|
133
|
+
if report.conclusions:
|
|
134
|
+
self.console.print("[bold cyan]Conclusions:[/bold cyan]")
|
|
135
|
+
for conclusion in report.conclusions:
|
|
136
|
+
self.console.print(f"• {conclusion}")
|
|
137
|
+
self.console.print()
|
|
138
|
+
|
|
139
|
+
# Recommendations
|
|
140
|
+
if report.recommendations:
|
|
141
|
+
self.console.print("[bold cyan]Recommendations:[/bold cyan]")
|
|
142
|
+
for rec in report.recommendations:
|
|
143
|
+
self.console.print(f"• {rec}")
|
|
144
|
+
self.console.print()
|
|
145
|
+
|
|
146
|
+
# Limitations
|
|
147
|
+
if report.limitations:
|
|
148
|
+
self.console.print("[bold yellow]Limitations:[/bold yellow]")
|
|
149
|
+
for limitation in report.limitations:
|
|
150
|
+
self.console.print(f"• {limitation}")
|
|
151
|
+
self.console.print()
|
|
152
|
+
|
|
153
|
+
# Sources Summary
|
|
154
|
+
if report.sources_summary:
|
|
155
|
+
self.console.print("[bold cyan]Sources:[/bold cyan]")
|
|
156
|
+
self.console.print(report.sources_summary)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self.console.print(f"[red]Error during research: {e}[/red]")
|
|
160
|
+
|
|
81
161
|
async def rebuild(self):
|
|
82
162
|
async with HaikuRAG(db_path=self.db_path, skip_validation=True) as client:
|
|
83
163
|
try:
|
haiku/rag/cli.py
CHANGED
|
@@ -3,6 +3,7 @@ import warnings
|
|
|
3
3
|
from importlib.metadata import version
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
import logfire
|
|
6
7
|
import typer
|
|
7
8
|
from rich.console import Console
|
|
8
9
|
|
|
@@ -12,6 +13,9 @@ from haiku.rag.logging import configure_cli_logging
|
|
|
12
13
|
from haiku.rag.migration import migrate_sqlite_to_lancedb
|
|
13
14
|
from haiku.rag.utils import is_up_to_date
|
|
14
15
|
|
|
16
|
+
logfire.configure(send_to_logfire="if-token-present")
|
|
17
|
+
logfire.instrument_pydantic_ai()
|
|
18
|
+
|
|
15
19
|
if not Config.ENV == "development":
|
|
16
20
|
warnings.filterwarnings("ignore")
|
|
17
21
|
|
|
@@ -235,6 +239,38 @@ def ask(
|
|
|
235
239
|
asyncio.run(app.ask(question=question, cite=cite))
|
|
236
240
|
|
|
237
241
|
|
|
242
|
+
@cli.command("research", help="Run multi-agent research and output a concise report")
|
|
243
|
+
def research(
|
|
244
|
+
question: str = typer.Argument(
|
|
245
|
+
help="The research question to investigate",
|
|
246
|
+
),
|
|
247
|
+
max_iterations: int = typer.Option(
|
|
248
|
+
3,
|
|
249
|
+
"--max-iterations",
|
|
250
|
+
"-n",
|
|
251
|
+
help="Maximum search/analyze iterations",
|
|
252
|
+
),
|
|
253
|
+
db: Path = typer.Option(
|
|
254
|
+
Config.DEFAULT_DATA_DIR / "haiku.rag.lancedb",
|
|
255
|
+
"--db",
|
|
256
|
+
help="Path to the LanceDB database file",
|
|
257
|
+
),
|
|
258
|
+
verbose: bool = typer.Option(
|
|
259
|
+
False,
|
|
260
|
+
"--verbose",
|
|
261
|
+
help="Show verbose progress output",
|
|
262
|
+
),
|
|
263
|
+
):
|
|
264
|
+
app = HaikuRAGApp(db_path=db)
|
|
265
|
+
asyncio.run(
|
|
266
|
+
app.research(
|
|
267
|
+
question=question,
|
|
268
|
+
max_iterations=max_iterations,
|
|
269
|
+
verbose=verbose,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
238
274
|
@cli.command("settings", help="Display current configuration settings")
|
|
239
275
|
def settings():
|
|
240
276
|
app = HaikuRAGApp(db_path=Path()) # Don't need actual DB for settings
|
haiku/rag/config.py
CHANGED
|
@@ -27,7 +27,11 @@ class AppConfig(BaseModel):
|
|
|
27
27
|
RERANK_MODEL: str = ""
|
|
28
28
|
|
|
29
29
|
QA_PROVIDER: str = "ollama"
|
|
30
|
-
QA_MODEL: str = "
|
|
30
|
+
QA_MODEL: str = "gpt-oss"
|
|
31
|
+
|
|
32
|
+
# Research defaults (fallback to QA if not provided via env)
|
|
33
|
+
RESEARCH_PROVIDER: str = "ollama"
|
|
34
|
+
RESEARCH_MODEL: str = "gpt-oss"
|
|
31
35
|
|
|
32
36
|
CHUNK_SIZE: int = 256
|
|
33
37
|
CONTEXT_CHUNK_RADIUS: int = 0
|
|
@@ -37,9 +41,11 @@ class AppConfig(BaseModel):
|
|
|
37
41
|
MARKDOWN_PREPROCESSOR: str = ""
|
|
38
42
|
|
|
39
43
|
OLLAMA_BASE_URL: str = "http://localhost:11434"
|
|
44
|
+
|
|
40
45
|
VLLM_EMBEDDINGS_BASE_URL: str = ""
|
|
41
46
|
VLLM_RERANK_BASE_URL: str = ""
|
|
42
47
|
VLLM_QA_BASE_URL: str = ""
|
|
48
|
+
VLLM_RESEARCH_BASE_URL: str = ""
|
|
43
49
|
|
|
44
50
|
# Provider keys
|
|
45
51
|
VOYAGE_API_KEY: str = ""
|
haiku/rag/qa/agent.py
CHANGED
|
@@ -6,7 +6,7 @@ from pydantic_ai.providers.openai import OpenAIProvider
|
|
|
6
6
|
|
|
7
7
|
from haiku.rag.client import HaikuRAG
|
|
8
8
|
from haiku.rag.config import Config
|
|
9
|
-
from haiku.rag.qa.prompts import
|
|
9
|
+
from haiku.rag.qa.prompts import QA_SYSTEM_PROMPT, QA_SYSTEM_PROMPT_WITH_CITATIONS
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SearchResult(BaseModel):
|
|
@@ -31,7 +31,9 @@ class QuestionAnswerAgent:
|
|
|
31
31
|
):
|
|
32
32
|
self._client = client
|
|
33
33
|
|
|
34
|
-
system_prompt =
|
|
34
|
+
system_prompt = (
|
|
35
|
+
QA_SYSTEM_PROMPT_WITH_CITATIONS if use_citations else QA_SYSTEM_PROMPT
|
|
36
|
+
)
|
|
35
37
|
model_obj = self._get_model(provider, model)
|
|
36
38
|
|
|
37
39
|
self._agent = Agent(
|
haiku/rag/qa/prompts.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
QA_SYSTEM_PROMPT = """
|
|
2
2
|
You are a knowledgeable assistant that helps users find information from a document knowledge base.
|
|
3
3
|
|
|
4
4
|
Your process:
|
|
@@ -21,7 +21,7 @@ Be concise, and always maintain accuracy over completeness. Prefer short, direct
|
|
|
21
21
|
/no_think
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
QA_SYSTEM_PROMPT_WITH_CITATIONS = """
|
|
25
25
|
You are a knowledgeable assistant that helps users find information from a document knowledge base.
|
|
26
26
|
|
|
27
27
|
IMPORTANT: You MUST use the search_documents tool for every question. Do not answer any question without first searching the knowledge base.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Multi-agent research workflow for advanced RAG queries."""
|
|
2
|
+
|
|
3
|
+
from haiku.rag.research.base import (
|
|
4
|
+
BaseResearchAgent,
|
|
5
|
+
ResearchOutput,
|
|
6
|
+
SearchAnswer,
|
|
7
|
+
SearchResult,
|
|
8
|
+
)
|
|
9
|
+
from haiku.rag.research.dependencies import ResearchContext, ResearchDependencies
|
|
10
|
+
from haiku.rag.research.evaluation_agent import (
|
|
11
|
+
AnalysisEvaluationAgent,
|
|
12
|
+
EvaluationResult,
|
|
13
|
+
)
|
|
14
|
+
from haiku.rag.research.orchestrator import ResearchOrchestrator, ResearchPlan
|
|
15
|
+
from haiku.rag.research.search_agent import SearchSpecialistAgent
|
|
16
|
+
from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Base classes
|
|
20
|
+
"BaseResearchAgent",
|
|
21
|
+
"ResearchDependencies",
|
|
22
|
+
"ResearchContext",
|
|
23
|
+
"SearchResult",
|
|
24
|
+
"ResearchOutput",
|
|
25
|
+
# Specialized agents
|
|
26
|
+
"SearchAnswer",
|
|
27
|
+
"SearchSpecialistAgent",
|
|
28
|
+
"AnalysisEvaluationAgent",
|
|
29
|
+
"EvaluationResult",
|
|
30
|
+
"SynthesisAgent",
|
|
31
|
+
"ResearchReport",
|
|
32
|
+
# Orchestrator
|
|
33
|
+
"ResearchOrchestrator",
|
|
34
|
+
"ResearchPlan",
|
|
35
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic_ai import Agent
|
|
6
|
+
from pydantic_ai.models.openai import OpenAIChatModel
|
|
7
|
+
from pydantic_ai.output import ToolOutput
|
|
8
|
+
from pydantic_ai.providers.ollama import OllamaProvider
|
|
9
|
+
from pydantic_ai.providers.openai import OpenAIProvider
|
|
10
|
+
from pydantic_ai.run import AgentRunResult
|
|
11
|
+
|
|
12
|
+
from haiku.rag.config import Config
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from haiku.rag.research.dependencies import ResearchDependencies
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseResearchAgent[T](ABC):
|
|
19
|
+
"""Base class for all research agents."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
provider: str,
|
|
24
|
+
model: str,
|
|
25
|
+
output_type: type[T],
|
|
26
|
+
):
|
|
27
|
+
self.provider = provider
|
|
28
|
+
self.model = model
|
|
29
|
+
self.output_type = output_type
|
|
30
|
+
|
|
31
|
+
model_obj = self._get_model(provider, model)
|
|
32
|
+
|
|
33
|
+
# Import deps type lazily to avoid circular import during module load
|
|
34
|
+
from haiku.rag.research.dependencies import ResearchDependencies
|
|
35
|
+
|
|
36
|
+
self._agent = Agent(
|
|
37
|
+
model=model_obj,
|
|
38
|
+
deps_type=ResearchDependencies,
|
|
39
|
+
output_type=ToolOutput(self.output_type, max_retries=3),
|
|
40
|
+
system_prompt=self.get_system_prompt(),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Register tools
|
|
44
|
+
self.register_tools()
|
|
45
|
+
|
|
46
|
+
def _get_model(self, provider: str, model: str):
|
|
47
|
+
"""Get the appropriate model object for the provider."""
|
|
48
|
+
if provider == "ollama":
|
|
49
|
+
return OpenAIChatModel(
|
|
50
|
+
model_name=model,
|
|
51
|
+
provider=OllamaProvider(base_url=f"{Config.OLLAMA_BASE_URL}/v1"),
|
|
52
|
+
)
|
|
53
|
+
elif provider == "vllm":
|
|
54
|
+
return OpenAIChatModel(
|
|
55
|
+
model_name=model,
|
|
56
|
+
provider=OpenAIProvider(
|
|
57
|
+
base_url=f"{Config.VLLM_RESEARCH_BASE_URL or Config.VLLM_QA_BASE_URL}/v1",
|
|
58
|
+
api_key="none",
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
# For all other providers, use the provider:model format
|
|
63
|
+
return f"{provider}:{model}"
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def get_system_prompt(self) -> str:
|
|
67
|
+
"""Return the system prompt for this agent."""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def register_tools(self) -> None:
|
|
72
|
+
"""Register agent-specific tools."""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
async def run(
|
|
76
|
+
self, prompt: str, deps: ResearchDependencies, **kwargs
|
|
77
|
+
) -> AgentRunResult[T]:
|
|
78
|
+
"""Execute the agent."""
|
|
79
|
+
return await self._agent.run(prompt, deps=deps, **kwargs)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def agent(self) -> Agent[Any, T]:
|
|
83
|
+
"""Access the underlying Pydantic AI agent."""
|
|
84
|
+
return self._agent
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SearchResult(BaseModel):
|
|
88
|
+
"""Standard search result format."""
|
|
89
|
+
|
|
90
|
+
content: str
|
|
91
|
+
score: float
|
|
92
|
+
document_uri: str
|
|
93
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ResearchOutput(BaseModel):
|
|
97
|
+
"""Standard research output format."""
|
|
98
|
+
|
|
99
|
+
summary: str
|
|
100
|
+
detailed_findings: list[str]
|
|
101
|
+
sources: list[str]
|
|
102
|
+
confidence: float
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class SearchAnswer(BaseModel):
|
|
106
|
+
"""Structured output for the SearchSpecialist agent."""
|
|
107
|
+
|
|
108
|
+
query: str = Field(description="The search query that was performed")
|
|
109
|
+
answer: str = Field(description="The answer generated based on the context")
|
|
110
|
+
context: list[str] = Field(
|
|
111
|
+
description=(
|
|
112
|
+
"Only the minimal set of relevant snippets (verbatim) that directly "
|
|
113
|
+
"support the answer"
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
sources: list[str] = Field(
|
|
117
|
+
description=(
|
|
118
|
+
"Document URIs corresponding to the snippets actually used in the"
|
|
119
|
+
" answer (one URI per snippet; omit if none)"
|
|
120
|
+
),
|
|
121
|
+
default_factory=list,
|
|
122
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
from haiku.rag.client import HaikuRAG
|
|
4
|
+
from haiku.rag.research.base import SearchAnswer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResearchContext(BaseModel):
|
|
8
|
+
"""Context shared across research agents."""
|
|
9
|
+
|
|
10
|
+
original_question: str = Field(description="The original research question")
|
|
11
|
+
sub_questions: list[str] = Field(
|
|
12
|
+
default_factory=list, description="Decomposed sub-questions"
|
|
13
|
+
)
|
|
14
|
+
qa_responses: list["SearchAnswer"] = Field(
|
|
15
|
+
default_factory=list, description="Structured QA pairs used during research"
|
|
16
|
+
)
|
|
17
|
+
insights: list[str] = Field(
|
|
18
|
+
default_factory=list, description="Key insights discovered"
|
|
19
|
+
)
|
|
20
|
+
gaps: list[str] = Field(
|
|
21
|
+
default_factory=list, description="Identified information gaps"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def add_qa_response(self, qa: "SearchAnswer") -> None:
|
|
25
|
+
"""Add a structured QA response (minimal context already included)."""
|
|
26
|
+
self.qa_responses.append(qa)
|
|
27
|
+
|
|
28
|
+
def add_insight(self, insight: str) -> None:
|
|
29
|
+
"""Add a key insight."""
|
|
30
|
+
if insight not in self.insights:
|
|
31
|
+
self.insights.append(insight)
|
|
32
|
+
|
|
33
|
+
def add_gap(self, gap: str) -> None:
|
|
34
|
+
"""Identify an information gap."""
|
|
35
|
+
if gap not in self.gaps:
|
|
36
|
+
self.gaps.append(gap)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ResearchDependencies(BaseModel):
|
|
40
|
+
"""Dependencies for research agents with multi-agent context."""
|
|
41
|
+
|
|
42
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
43
|
+
|
|
44
|
+
client: HaikuRAG = Field(description="RAG client for document operations")
|
|
45
|
+
context: ResearchContext = Field(description="Shared research context")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
from haiku.rag.research.base import BaseResearchAgent
|
|
4
|
+
from haiku.rag.research.prompts import EVALUATION_AGENT_PROMPT
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EvaluationResult(BaseModel):
|
|
8
|
+
"""Result of analysis and evaluation."""
|
|
9
|
+
|
|
10
|
+
key_insights: list[str] = Field(
|
|
11
|
+
description="Main insights extracted from the research so far"
|
|
12
|
+
)
|
|
13
|
+
new_questions: list[str] = Field(
|
|
14
|
+
description="New sub-questions to add to the research (max 3)", max_length=3
|
|
15
|
+
)
|
|
16
|
+
confidence_score: float = Field(
|
|
17
|
+
description="Confidence level in the completeness of research (0-1)",
|
|
18
|
+
ge=0.0,
|
|
19
|
+
le=1.0,
|
|
20
|
+
)
|
|
21
|
+
is_sufficient: bool = Field(
|
|
22
|
+
description="Whether the research is sufficient to answer the original question"
|
|
23
|
+
)
|
|
24
|
+
reasoning: str = Field(
|
|
25
|
+
description="Explanation of why the research is or isn't complete"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AnalysisEvaluationAgent(BaseResearchAgent[EvaluationResult]):
|
|
30
|
+
"""Agent that analyzes findings and evaluates research completeness."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, provider: str, model: str) -> None:
|
|
33
|
+
super().__init__(provider, model, output_type=EvaluationResult)
|
|
34
|
+
|
|
35
|
+
def get_system_prompt(self) -> str:
|
|
36
|
+
return EVALUATION_AGENT_PROMPT
|
|
37
|
+
|
|
38
|
+
def register_tools(self) -> None:
|
|
39
|
+
"""No additional tools needed - uses LLM capabilities directly."""
|
|
40
|
+
pass
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic_ai.format_prompt import format_as_xml
|
|
5
|
+
from pydantic_ai.run import AgentRunResult
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from haiku.rag.config import Config
|
|
9
|
+
from haiku.rag.research.base import BaseResearchAgent
|
|
10
|
+
from haiku.rag.research.dependencies import ResearchContext, ResearchDependencies
|
|
11
|
+
from haiku.rag.research.evaluation_agent import (
|
|
12
|
+
AnalysisEvaluationAgent,
|
|
13
|
+
EvaluationResult,
|
|
14
|
+
)
|
|
15
|
+
from haiku.rag.research.prompts import ORCHESTRATOR_PROMPT
|
|
16
|
+
from haiku.rag.research.search_agent import SearchSpecialistAgent
|
|
17
|
+
from haiku.rag.research.synthesis_agent import ResearchReport, SynthesisAgent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ResearchPlan(BaseModel):
|
|
21
|
+
"""Research execution plan."""
|
|
22
|
+
|
|
23
|
+
main_question: str = Field(description="The main research question")
|
|
24
|
+
sub_questions: list[str] = Field(
|
|
25
|
+
description="Decomposed sub-questions to investigate (max 3)", max_length=3
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ResearchOrchestrator(BaseResearchAgent[ResearchPlan]):
|
|
30
|
+
"""Orchestrator agent that coordinates the research workflow."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self, provider: str | None = Config.RESEARCH_PROVIDER, model: str | None = None
|
|
34
|
+
):
|
|
35
|
+
# Use provided values or fall back to config defaults
|
|
36
|
+
provider = provider or Config.RESEARCH_PROVIDER or Config.QA_PROVIDER
|
|
37
|
+
model = model or Config.RESEARCH_MODEL or Config.QA_MODEL
|
|
38
|
+
|
|
39
|
+
super().__init__(provider, model, output_type=ResearchPlan)
|
|
40
|
+
|
|
41
|
+
self.search_agent: SearchSpecialistAgent = SearchSpecialistAgent(
|
|
42
|
+
provider, model
|
|
43
|
+
)
|
|
44
|
+
self.evaluation_agent: AnalysisEvaluationAgent = AnalysisEvaluationAgent(
|
|
45
|
+
provider, model
|
|
46
|
+
)
|
|
47
|
+
self.synthesis_agent: SynthesisAgent = SynthesisAgent(provider, model)
|
|
48
|
+
|
|
49
|
+
def get_system_prompt(self) -> str:
|
|
50
|
+
return ORCHESTRATOR_PROMPT
|
|
51
|
+
|
|
52
|
+
def register_tools(self) -> None:
|
|
53
|
+
"""Register orchestration tools."""
|
|
54
|
+
# Tools are no longer needed - orchestrator directly calls agents
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def _format_context_for_prompt(self, context: ResearchContext) -> str:
|
|
58
|
+
"""Format the research context as XML for inclusion in prompts."""
|
|
59
|
+
|
|
60
|
+
context_data = {
|
|
61
|
+
"original_question": context.original_question,
|
|
62
|
+
"unanswered_questions": context.sub_questions,
|
|
63
|
+
"qa_responses": [
|
|
64
|
+
{"question": qa.query, "answer": qa.answer}
|
|
65
|
+
for qa in context.qa_responses
|
|
66
|
+
],
|
|
67
|
+
"insights": context.insights,
|
|
68
|
+
"gaps": context.gaps,
|
|
69
|
+
}
|
|
70
|
+
return format_as_xml(context_data, root_tag="research_context")
|
|
71
|
+
|
|
72
|
+
async def conduct_research(
|
|
73
|
+
self,
|
|
74
|
+
question: str,
|
|
75
|
+
client: Any,
|
|
76
|
+
max_iterations: int = 3,
|
|
77
|
+
confidence_threshold: float = 0.8,
|
|
78
|
+
verbose: bool = False,
|
|
79
|
+
console: Console | None = None,
|
|
80
|
+
) -> ResearchReport:
|
|
81
|
+
"""Conduct comprehensive research on a question.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
question: The research question to investigate
|
|
85
|
+
client: HaikuRAG client for document operations
|
|
86
|
+
max_iterations: Maximum number of search-analyze-clarify cycles
|
|
87
|
+
confidence_threshold: Minimum confidence level to stop research (0-1)
|
|
88
|
+
verbose: If True, print progress and intermediate results
|
|
89
|
+
console: Optional Rich console for output
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
ResearchReport with comprehensive findings
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Initialize context
|
|
96
|
+
context = ResearchContext(original_question=question)
|
|
97
|
+
deps = ResearchDependencies(client=client, context=context)
|
|
98
|
+
|
|
99
|
+
# Use provided console or create a new one
|
|
100
|
+
console = console or Console() if verbose else None
|
|
101
|
+
|
|
102
|
+
# Create initial research plan
|
|
103
|
+
if console:
|
|
104
|
+
console.print("\n[bold cyan]📋 Creating research plan...[/bold cyan]")
|
|
105
|
+
|
|
106
|
+
plan_result: AgentRunResult[ResearchPlan] = await self.run(
|
|
107
|
+
f"Create a research plan for: {question}", deps=deps
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
context.sub_questions = plan_result.output.sub_questions
|
|
111
|
+
|
|
112
|
+
if console:
|
|
113
|
+
console.print("\n[bold green]✅ Research Plan Created:[/bold green]")
|
|
114
|
+
console.print(
|
|
115
|
+
f" [bold]Main Question:[/bold] {plan_result.output.main_question}"
|
|
116
|
+
)
|
|
117
|
+
console.print(" [bold]Sub-questions:[/bold]")
|
|
118
|
+
for i, sq in enumerate(plan_result.output.sub_questions, 1):
|
|
119
|
+
console.print(f" {i}. {sq}")
|
|
120
|
+
console.print()
|
|
121
|
+
|
|
122
|
+
# Execute research iterations
|
|
123
|
+
for iteration in range(max_iterations):
|
|
124
|
+
if console:
|
|
125
|
+
console.rule(
|
|
126
|
+
f"[bold yellow]🔄 Iteration {iteration + 1}/{max_iterations}[/bold yellow]"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Check if we have questions to search
|
|
130
|
+
if not context.sub_questions:
|
|
131
|
+
# No more questions to explore
|
|
132
|
+
if console:
|
|
133
|
+
console.print(
|
|
134
|
+
"[yellow]No more questions to explore. Concluding research.[/yellow]"
|
|
135
|
+
)
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
# Use current sub-questions for this iteration
|
|
139
|
+
questions_to_search = context.sub_questions
|
|
140
|
+
|
|
141
|
+
# Search phase - answer all questions in this iteration
|
|
142
|
+
if console:
|
|
143
|
+
console.print(
|
|
144
|
+
f"\n[bold cyan]🔍 Searching & Answering {len(questions_to_search)} questions:[/bold cyan]"
|
|
145
|
+
)
|
|
146
|
+
for i, q in enumerate(questions_to_search, 1):
|
|
147
|
+
console.print(f" {i}. {q}")
|
|
148
|
+
|
|
149
|
+
# Run searches for all questions and remove answered ones
|
|
150
|
+
answered_questions = []
|
|
151
|
+
for search_question in questions_to_search:
|
|
152
|
+
try:
|
|
153
|
+
await self.search_agent.run(search_question, deps=deps)
|
|
154
|
+
except Exception as e: # pragma: no cover - defensive
|
|
155
|
+
if console:
|
|
156
|
+
console.print(
|
|
157
|
+
f"\n [red]×[/red] Omitting failed question: {search_question} ({e})"
|
|
158
|
+
)
|
|
159
|
+
finally:
|
|
160
|
+
answered_questions.append(search_question)
|
|
161
|
+
|
|
162
|
+
if console and context.qa_responses:
|
|
163
|
+
# Show the last QA response (which should be for this question)
|
|
164
|
+
latest_qa = context.qa_responses[-1]
|
|
165
|
+
answer_preview = (
|
|
166
|
+
latest_qa.answer[:150] + "..."
|
|
167
|
+
if len(latest_qa.answer) > 150
|
|
168
|
+
else latest_qa.answer
|
|
169
|
+
)
|
|
170
|
+
console.print(
|
|
171
|
+
f"\n [green]✓[/green] {search_question[:50]}..."
|
|
172
|
+
if len(search_question) > 50
|
|
173
|
+
else f"\n [green]✓[/green] {search_question}"
|
|
174
|
+
)
|
|
175
|
+
console.print(f" {answer_preview}")
|
|
176
|
+
|
|
177
|
+
# Remove answered questions from the list
|
|
178
|
+
for question in answered_questions:
|
|
179
|
+
if question in context.sub_questions:
|
|
180
|
+
context.sub_questions.remove(question)
|
|
181
|
+
|
|
182
|
+
# Analysis and Evaluation phase
|
|
183
|
+
if console:
|
|
184
|
+
console.print(
|
|
185
|
+
"\n[bold cyan]📊 Analyzing and evaluating research progress...[/bold cyan]"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Format context for the evaluation agent
|
|
189
|
+
context_xml = self._format_context_for_prompt(context)
|
|
190
|
+
evaluation_prompt = f"""Analyze all gathered information and evaluate the completeness of research.
|
|
191
|
+
|
|
192
|
+
{context_xml}
|
|
193
|
+
|
|
194
|
+
Evaluate the research progress for the original question and identify any remaining gaps."""
|
|
195
|
+
|
|
196
|
+
evaluation_result = await self.evaluation_agent.run(
|
|
197
|
+
evaluation_prompt,
|
|
198
|
+
deps=deps,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if console and evaluation_result.output:
|
|
202
|
+
output = evaluation_result.output
|
|
203
|
+
if output.key_insights:
|
|
204
|
+
console.print(" [bold]Key insights:[/bold]")
|
|
205
|
+
for insight in output.key_insights:
|
|
206
|
+
console.print(f" • {insight}")
|
|
207
|
+
console.print(
|
|
208
|
+
f" Confidence: [yellow]{output.confidence_score:.1%}[/yellow]"
|
|
209
|
+
)
|
|
210
|
+
status = (
|
|
211
|
+
"[green]Yes[/green]" if output.is_sufficient else "[red]No[/red]"
|
|
212
|
+
)
|
|
213
|
+
console.print(f" Sufficient: {status}")
|
|
214
|
+
|
|
215
|
+
# Store insights
|
|
216
|
+
for insight in evaluation_result.output.key_insights:
|
|
217
|
+
context.add_insight(insight)
|
|
218
|
+
|
|
219
|
+
# Add new questions to the sub-questions list
|
|
220
|
+
for new_q in evaluation_result.output.new_questions:
|
|
221
|
+
if new_q not in context.sub_questions:
|
|
222
|
+
context.sub_questions.append(new_q)
|
|
223
|
+
|
|
224
|
+
# Check if research is sufficient
|
|
225
|
+
if self._should_stop_research(evaluation_result, confidence_threshold):
|
|
226
|
+
if console:
|
|
227
|
+
console.print(
|
|
228
|
+
f"\n[bold green]✅ Stopping research:[/bold green] {evaluation_result.output.reasoning}"
|
|
229
|
+
)
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
# Generate final report
|
|
233
|
+
if console:
|
|
234
|
+
console.print(
|
|
235
|
+
"\n[bold cyan]📝 Generating final research report...[/bold cyan]"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Format context for the synthesis agent
|
|
239
|
+
final_context_xml = self._format_context_for_prompt(context)
|
|
240
|
+
synthesis_prompt = f"""Generate a comprehensive research report based on all gathered information.
|
|
241
|
+
|
|
242
|
+
{final_context_xml}
|
|
243
|
+
|
|
244
|
+
Create a detailed report that synthesizes all findings into a coherent response."""
|
|
245
|
+
|
|
246
|
+
report_result: AgentRunResult[ResearchReport] = await self.synthesis_agent.run(
|
|
247
|
+
synthesis_prompt, deps=deps
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if console:
|
|
251
|
+
console.print("[bold green]✅ Research complete![/bold green]")
|
|
252
|
+
|
|
253
|
+
return report_result.output
|
|
254
|
+
|
|
255
|
+
def _should_stop_research(
|
|
256
|
+
self,
|
|
257
|
+
evaluation_result: AgentRunResult[EvaluationResult],
|
|
258
|
+
confidence_threshold: float,
|
|
259
|
+
) -> bool:
|
|
260
|
+
"""Determine if research should stop based on evaluation."""
|
|
261
|
+
|
|
262
|
+
result = evaluation_result.output
|
|
263
|
+
|
|
264
|
+
# Stop if the agent indicates sufficient information AND confidence exceeds threshold
|
|
265
|
+
return result.is_sufficient and result.confidence_score >= confidence_threshold
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
ORCHESTRATOR_PROMPT = """You are a research orchestrator responsible for coordinating a comprehensive research workflow.
|
|
2
|
+
|
|
3
|
+
Your role is to:
|
|
4
|
+
1. Understand and decompose the research question
|
|
5
|
+
2. Plan a systematic research approach
|
|
6
|
+
3. Coordinate specialized agents to gather and analyze information
|
|
7
|
+
4. Ensure comprehensive coverage of the topic
|
|
8
|
+
5. Iterate based on findings and gaps
|
|
9
|
+
|
|
10
|
+
Create a research plan that:
|
|
11
|
+
- Breaks down the question into at most 3 focused sub-questions
|
|
12
|
+
- Each sub-question should target a specific aspect of the research
|
|
13
|
+
- Prioritize the most important aspects to investigate
|
|
14
|
+
- Ensure comprehensive coverage within the 3-question limit
|
|
15
|
+
- IMPORTANT: Make each sub-question a standalone, self-contained query that can
|
|
16
|
+
be executed without additional context. Include necessary entities, scope,
|
|
17
|
+
timeframe, and qualifiers. Avoid pronouns like "it/they/this"; write queries
|
|
18
|
+
that make sense in isolation."""
|
|
19
|
+
|
|
20
|
+
SEARCH_AGENT_PROMPT = """You are a search and question-answering specialist.
|
|
21
|
+
|
|
22
|
+
Your role is to:
|
|
23
|
+
1. Search the knowledge base for relevant information
|
|
24
|
+
2. Analyze the retrieved documents
|
|
25
|
+
3. Provide an accurate answer strictly grounded in the retrieved context
|
|
26
|
+
|
|
27
|
+
Output format:
|
|
28
|
+
- You must return a SearchAnswer model with fields:
|
|
29
|
+
- query: the question being answered (echo the user query)
|
|
30
|
+
- answer: your final answer based only on the provided context
|
|
31
|
+
- context: list[str] of only the minimal set of verbatim snippet texts you
|
|
32
|
+
used to justify the answer (do not include unrelated text; do not invent)
|
|
33
|
+
- sources: list[str] of document_uri values corresponding to the snippets you
|
|
34
|
+
actually used in the answer (one URI per context snippet, order aligned)
|
|
35
|
+
|
|
36
|
+
Tool usage:
|
|
37
|
+
- Always call the search_and_answer tool before drafting any answer.
|
|
38
|
+
- The tool returns XML containing only a list of snippets, where each snippet
|
|
39
|
+
has the verbatim `text`, a `score` indicating relevance, and the
|
|
40
|
+
`document_uri` it came from.
|
|
41
|
+
- You may call the tool multiple times to refine or broaden context, but do not
|
|
42
|
+
exceed 3 total tool calls per question. Prefer precision over volume.
|
|
43
|
+
- Use scores to prioritize evidence, but include only the minimal subset of
|
|
44
|
+
snippet texts (verbatim) in SearchAnswer.context.
|
|
45
|
+
- Set SearchAnswer.sources to the matching document_uris for the snippets you
|
|
46
|
+
used (one URI per snippet, aligned by order). Context must be text-only.
|
|
47
|
+
- If no relevant information is found, say so and return an empty context list.
|
|
48
|
+
|
|
49
|
+
Important:
|
|
50
|
+
- Do not include any content in the answer that is not supported by the context.
|
|
51
|
+
- Keep context snippets short (just the necessary lines), verbatim, and focused."""
|
|
52
|
+
|
|
53
|
+
EVALUATION_AGENT_PROMPT = """You are an analysis and evaluation specialist for research workflows.
|
|
54
|
+
|
|
55
|
+
You have access to:
|
|
56
|
+
- The original research question
|
|
57
|
+
- Question-answer pairs from search operations
|
|
58
|
+
- Raw search results and source documents
|
|
59
|
+
- Previously identified insights
|
|
60
|
+
|
|
61
|
+
Your dual role is to:
|
|
62
|
+
|
|
63
|
+
ANALYSIS:
|
|
64
|
+
1. Extract key insights from all gathered information
|
|
65
|
+
2. Identify patterns and connections across sources
|
|
66
|
+
3. Synthesize findings into coherent understanding
|
|
67
|
+
4. Focus on the most important discoveries
|
|
68
|
+
|
|
69
|
+
EVALUATION:
|
|
70
|
+
1. Assess if we have sufficient information to answer the original question
|
|
71
|
+
2. Calculate a confidence score (0-1) based on:
|
|
72
|
+
- Coverage of the main question's aspects
|
|
73
|
+
- Quality and consistency of sources
|
|
74
|
+
- Depth of information gathered
|
|
75
|
+
3. Identify specific gaps that still need investigation
|
|
76
|
+
4. Generate up to 3 new sub-questions that haven't been answered yet
|
|
77
|
+
|
|
78
|
+
Be critical and thorough in your evaluation. Only mark research as sufficient when:
|
|
79
|
+
- All major aspects of the question are addressed
|
|
80
|
+
- Sources provide consistent, reliable information
|
|
81
|
+
- The depth of coverage meets the question's requirements
|
|
82
|
+
- No critical gaps remain
|
|
83
|
+
|
|
84
|
+
Generate new sub-questions that:
|
|
85
|
+
- Target specific unexplored aspects not covered by existing questions
|
|
86
|
+
- Seek clarification on ambiguities
|
|
87
|
+
- Explore important edge cases or exceptions
|
|
88
|
+
- Are focused and actionable (max 3)
|
|
89
|
+
- Do NOT repeat or rephrase questions that have already been answered (see qa_responses)
|
|
90
|
+
- Should be genuinely new areas to explore
|
|
91
|
+
- Must be standalone, self-contained queries: include entities, scope, and any
|
|
92
|
+
needed qualifiers (e.g., timeframe, region), and avoid ambiguous pronouns so
|
|
93
|
+
they can be executed independently."""
|
|
94
|
+
|
|
95
|
+
SYNTHESIS_AGENT_PROMPT = """You are a synthesis specialist agent focused on creating comprehensive research reports.
|
|
96
|
+
|
|
97
|
+
Your role is to:
|
|
98
|
+
1. Synthesize all gathered information into a coherent narrative
|
|
99
|
+
2. Present findings in a clear, structured format
|
|
100
|
+
3. Draw evidence-based conclusions
|
|
101
|
+
4. Acknowledge limitations and uncertainties
|
|
102
|
+
5. Provide actionable recommendations
|
|
103
|
+
6. Maintain academic rigor and objectivity
|
|
104
|
+
|
|
105
|
+
Your report should be:
|
|
106
|
+
- Comprehensive yet concise
|
|
107
|
+
- Well-structured and easy to follow
|
|
108
|
+
- Based solely on evidence from the research
|
|
109
|
+
- Transparent about limitations
|
|
110
|
+
- Professional and objective in tone
|
|
111
|
+
|
|
112
|
+
Focus on creating a report that provides clear value to the reader by:
|
|
113
|
+
- Answering the original research question thoroughly
|
|
114
|
+
- Highlighting the most important findings
|
|
115
|
+
- Explaining the implications of the research
|
|
116
|
+
- Suggesting concrete next steps"""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from pydantic_ai import RunContext
|
|
2
|
+
from pydantic_ai.format_prompt import format_as_xml
|
|
3
|
+
from pydantic_ai.run import AgentRunResult
|
|
4
|
+
|
|
5
|
+
from haiku.rag.research.base import BaseResearchAgent, SearchAnswer
|
|
6
|
+
from haiku.rag.research.dependencies import ResearchDependencies
|
|
7
|
+
from haiku.rag.research.prompts import SEARCH_AGENT_PROMPT
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchSpecialistAgent(BaseResearchAgent[SearchAnswer]):
|
|
11
|
+
"""Agent specialized in answering questions using RAG search."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, provider: str, model: str) -> None:
|
|
14
|
+
super().__init__(provider, model, output_type=SearchAnswer)
|
|
15
|
+
|
|
16
|
+
async def run(
|
|
17
|
+
self, prompt: str, deps: ResearchDependencies, **kwargs
|
|
18
|
+
) -> AgentRunResult[SearchAnswer]:
|
|
19
|
+
"""Execute the agent and persist the QA pair in shared context.
|
|
20
|
+
|
|
21
|
+
Pydantic AI enforces `SearchAnswer` as the output model; we just store
|
|
22
|
+
the QA response with the last search results as sources.
|
|
23
|
+
"""
|
|
24
|
+
result = await super().run(prompt, deps, **kwargs)
|
|
25
|
+
|
|
26
|
+
if result.output:
|
|
27
|
+
deps.context.add_qa_response(result.output)
|
|
28
|
+
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
def get_system_prompt(self) -> str:
|
|
32
|
+
return SEARCH_AGENT_PROMPT
|
|
33
|
+
|
|
34
|
+
def register_tools(self) -> None:
|
|
35
|
+
"""Register search-specific tools."""
|
|
36
|
+
|
|
37
|
+
@self.agent.tool
|
|
38
|
+
async def search_and_answer(
|
|
39
|
+
ctx: RunContext[ResearchDependencies],
|
|
40
|
+
query: str,
|
|
41
|
+
limit: int = 5,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Search the KB and return a concise context pack."""
|
|
44
|
+
# Remove quotes from queries as this requires positional indexing in lancedb
|
|
45
|
+
query = query.replace('"', "")
|
|
46
|
+
search_results = await ctx.deps.client.search(query, limit=limit)
|
|
47
|
+
expanded = await ctx.deps.client.expand_context(search_results)
|
|
48
|
+
|
|
49
|
+
snippet_entries = [
|
|
50
|
+
{
|
|
51
|
+
"text": chunk.content,
|
|
52
|
+
"score": score,
|
|
53
|
+
"document_uri": (chunk.document_uri or ""),
|
|
54
|
+
}
|
|
55
|
+
for chunk, score in expanded
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# Return an XML-formatted payload with the question and snippets.
|
|
59
|
+
if snippet_entries:
|
|
60
|
+
return format_as_xml(snippet_entries, root_tag="snippets")
|
|
61
|
+
else:
|
|
62
|
+
return (
|
|
63
|
+
f"No relevant information found in the knowledge base for: {query}"
|
|
64
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
from haiku.rag.research.base import BaseResearchAgent
|
|
4
|
+
from haiku.rag.research.prompts import SYNTHESIS_AGENT_PROMPT
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResearchReport(BaseModel):
|
|
8
|
+
"""Final research report structure."""
|
|
9
|
+
|
|
10
|
+
title: str = Field(description="Concise title for the research")
|
|
11
|
+
executive_summary: str = Field(description="Brief overview of key findings")
|
|
12
|
+
main_findings: list[str] = Field(
|
|
13
|
+
description="Primary research findings with supporting evidence"
|
|
14
|
+
)
|
|
15
|
+
themes: dict[str, str] = Field(description="Major themes and their explanations")
|
|
16
|
+
conclusions: list[str] = Field(description="Evidence-based conclusions")
|
|
17
|
+
limitations: list[str] = Field(description="Limitations of the current research")
|
|
18
|
+
recommendations: list[str] = Field(
|
|
19
|
+
description="Actionable recommendations based on findings"
|
|
20
|
+
)
|
|
21
|
+
sources_summary: str = Field(
|
|
22
|
+
description="Summary of sources used and their reliability"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SynthesisAgent(BaseResearchAgent[ResearchReport]):
|
|
27
|
+
"""Agent specialized in synthesizing research into comprehensive reports."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, provider: str, model: str) -> None:
|
|
30
|
+
super().__init__(provider, model, output_type=ResearchReport)
|
|
31
|
+
|
|
32
|
+
def get_system_prompt(self) -> str:
|
|
33
|
+
return SYNTHESIS_AGENT_PROMPT
|
|
34
|
+
|
|
35
|
+
def register_tools(self) -> None:
|
|
36
|
+
"""Register synthesis-specific tools."""
|
|
37
|
+
# The agent will use its LLM capabilities directly for synthesis
|
|
38
|
+
# The structured output will guide the report generation
|
|
39
|
+
pass
|
|
@@ -171,9 +171,10 @@ class ChunkRepository:
|
|
|
171
171
|
processed_markdown, name="content.md"
|
|
172
172
|
)
|
|
173
173
|
except Exception as e:
|
|
174
|
-
logger.
|
|
174
|
+
logger.error(
|
|
175
175
|
f"Failed to apply MARKDOWN_PREPROCESSOR '{preprocessor_path}': {e}. Proceeding without preprocessing."
|
|
176
176
|
)
|
|
177
|
+
raise e
|
|
177
178
|
|
|
178
179
|
chunk_texts = await chunker.chunk(processed_document)
|
|
179
180
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: haiku.rag
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Retrieval Augmented Generation (RAG) with LanceDB
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Agentic Retrieval Augmented Generation (RAG) with LanceDB
|
|
5
5
|
Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
License-File: LICENSE
|
|
@@ -18,14 +18,13 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
|
-
Requires-Dist: docling>=2.
|
|
22
|
-
Requires-Dist: fastmcp>=2.
|
|
21
|
+
Requires-Dist: docling>=2.52.0
|
|
22
|
+
Requires-Dist: fastmcp>=2.12.3
|
|
23
23
|
Requires-Dist: httpx>=0.28.1
|
|
24
|
-
Requires-Dist: lancedb>=0.
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist: pydantic
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist: python-dotenv>=1.1.0
|
|
24
|
+
Requires-Dist: lancedb>=0.25.0
|
|
25
|
+
Requires-Dist: pydantic-ai>=1.0.8
|
|
26
|
+
Requires-Dist: pydantic>=2.11.9
|
|
27
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
29
28
|
Requires-Dist: rich>=14.1.0
|
|
30
29
|
Requires-Dist: tiktoken>=0.11.0
|
|
31
30
|
Requires-Dist: typer>=0.16.1
|
|
@@ -33,7 +32,7 @@ Requires-Dist: watchfiles>=1.1.0
|
|
|
33
32
|
Provides-Extra: mxbai
|
|
34
33
|
Requires-Dist: mxbai-rerank>=0.1.6; extra == 'mxbai'
|
|
35
34
|
Provides-Extra: voyageai
|
|
36
|
-
Requires-Dist: voyageai>=0.3.
|
|
35
|
+
Requires-Dist: voyageai>=0.3.5; extra == 'voyageai'
|
|
37
36
|
Description-Content-Type: text/markdown
|
|
38
37
|
|
|
39
38
|
# Haiku RAG
|
|
@@ -128,4 +127,5 @@ Full documentation at: https://ggozad.github.io/haiku.rag/
|
|
|
128
127
|
- [Configuration](https://ggozad.github.io/haiku.rag/configuration/) - Environment variables
|
|
129
128
|
- [CLI](https://ggozad.github.io/haiku.rag/cli/) - Command reference
|
|
130
129
|
- [Python API](https://ggozad.github.io/haiku.rag/python/) - Complete API docs
|
|
130
|
+
- [Agents](https://ggozad.github.io/haiku.rag/agents/) - QA agent and multi-agent research
|
|
131
131
|
- [Benchmarks](https://ggozad.github.io/haiku.rag/benchmarks/) - Performance Benchmarks
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
haiku/rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
haiku/rag/app.py,sha256=
|
|
2
|
+
haiku/rag/app.py,sha256=Smof7ZIe-oRGkDTap81VaKZGIeborD2X-oXKgBoJs9I,11763
|
|
3
3
|
haiku/rag/chunker.py,sha256=PVe6ysv8UlacUd4Zb3_8RFWIaWDXnzBAy2VDJ4TaUsE,1555
|
|
4
|
-
haiku/rag/cli.py,sha256=
|
|
4
|
+
haiku/rag/cli.py,sha256=3nlzrT5FPCyfnu51KHchLG4Cj2eVv9YsuGHMShBnVb0,9845
|
|
5
5
|
haiku/rag/client.py,sha256=NJVGXzVzpoVy1sttz_xEU7mXWtObKT8pGpvo5pZyzwc,21288
|
|
6
|
-
haiku/rag/config.py,sha256=
|
|
6
|
+
haiku/rag/config.py,sha256=SPEIv2IElZmZh4Wsp8gk7ViRW5ZzD-UGmIqRAXscDdI,2134
|
|
7
7
|
haiku/rag/logging.py,sha256=dm65AwADpcQsH5OAPtRA-4hsw0w5DK-sGOvzYkj6jzw,1720
|
|
8
8
|
haiku/rag/mcp.py,sha256=bR9Y-Nz-hvjiql20Y0KE0hwNGwyjmPGX8K9d-qmXptY,4683
|
|
9
9
|
haiku/rag/migration.py,sha256=M--KnSF3lxgKjxmokb4vuzGH-pV8eg0C_8e7jvPqW8Y,11058
|
|
@@ -17,25 +17,33 @@ haiku/rag/embeddings/openai.py,sha256=fIFCk-jpUtaW0xsnrQnJ824O0UCjaGG2sgvBzREhil
|
|
|
17
17
|
haiku/rag/embeddings/vllm.py,sha256=vhaUnCn6VMkfSluLhWKtSV-sekFaPsp4pKo2N7-SBCY,626
|
|
18
18
|
haiku/rag/embeddings/voyageai.py,sha256=UW-MW4tJKnPB6Fs2P7A3yt-ZeRm46H9npckchSriPX8,661
|
|
19
19
|
haiku/rag/qa/__init__.py,sha256=Sl7Kzrg9CuBOcMF01wc1NtQhUNWjJI0MhIHfCWrb8V4,434
|
|
20
|
-
haiku/rag/qa/agent.py,sha256=
|
|
21
|
-
haiku/rag/qa/prompts.py,sha256=
|
|
20
|
+
haiku/rag/qa/agent.py,sha256=f7hGWhjgzJKwa5BJkAO0KCxbgpwigPz5E9a26S9TUYI,2948
|
|
21
|
+
haiku/rag/qa/prompts.py,sha256=LhRfDtO8Pb06lpr4PpwEaKUYItZ5OiIkeqcCogcssHY,3347
|
|
22
22
|
haiku/rag/reranking/__init__.py,sha256=IRXHs4qPu6VbGJQpzSwhgtVWWumURH_vEoVFE-extlo,894
|
|
23
23
|
haiku/rag/reranking/base.py,sha256=LM9yUSSJ414UgBZhFTgxGprlRqzfTe4I1vgjricz2JY,405
|
|
24
24
|
haiku/rag/reranking/cohere.py,sha256=1iTdiaa8vvb6oHVB2qpWzUOVkyfUcimVSZp6Qr4aq4c,1049
|
|
25
25
|
haiku/rag/reranking/mxbai.py,sha256=46sVTsTIkzIX9THgM3u8HaEmgY7evvEyB-N54JTHvK8,867
|
|
26
26
|
haiku/rag/reranking/vllm.py,sha256=xVGH9ss-ISWdJ5SKUUHUbTqBo7PIEmA_SQv0ScdJ6XA,1479
|
|
27
|
+
haiku/rag/research/__init__.py,sha256=hwCVV8fxnHTrLV2KCJ_Clqe_pPrCwTz-RW2b0BeGHeE,982
|
|
28
|
+
haiku/rag/research/base.py,sha256=IW_VbeRlXTUfqh--jBS0dtIgSVXsbifPxZl8bfTLkDA,3686
|
|
29
|
+
haiku/rag/research/dependencies.py,sha256=vZctKC5donqhm8LFO6hQdXZZXzjdW1__4eUlhyZn058,1573
|
|
30
|
+
haiku/rag/research/evaluation_agent.py,sha256=3YWAdfC6n27wAIdla7M72IE1aS4GqoL9DbnW4K1b35M,1357
|
|
31
|
+
haiku/rag/research/orchestrator.py,sha256=AnDXCoJBbt4nYqaDKk5hiMi8WW1e8NwpRvzHLLnY3WY,10478
|
|
32
|
+
haiku/rag/research/prompts.py,sha256=C_d9OGNgHfwSUY6n5L2c2J6OpCeBHwxtMjrLQOkdcxU,5221
|
|
33
|
+
haiku/rag/research/search_agent.py,sha256=mYn3GlxoIPEle2NLkBqHI-VRV5PanoHxhjttVozsVis,2405
|
|
34
|
+
haiku/rag/research/synthesis_agent.py,sha256=E7Iwfe0EAlmglIRMmRQ3kaNmEWIyEMpVFK3k4SPC5BM,1559
|
|
27
35
|
haiku/rag/store/__init__.py,sha256=hq0W0DAC7ysqhWSP2M2uHX8cbG6kbr-sWHxhq6qQcY0,103
|
|
28
36
|
haiku/rag/store/engine.py,sha256=fNrykqMX7PRSCt4LSRfuJ66OLrb8BVYq2bpbfI2iaWU,8455
|
|
29
37
|
haiku/rag/store/models/__init__.py,sha256=s0E72zneGlowvZrFWaNxHYjOAUjgWdLxzdYsnvNRVlY,88
|
|
30
38
|
haiku/rag/store/models/chunk.py,sha256=ZNyTfO6lh3rXWLVYO3TZcitbL4LSUGr42fR6jQQ5iQc,364
|
|
31
39
|
haiku/rag/store/models/document.py,sha256=zSSpt6pyrMJAIXGQvIcqojcqUzwZnhp3WxVokaWxNRc,396
|
|
32
40
|
haiku/rag/store/repositories/__init__.py,sha256=Olv5dLfBQINRV3HrsfUpjzkZ7Qm7goEYyMNykgo_DaY,291
|
|
33
|
-
haiku/rag/store/repositories/chunk.py,sha256=
|
|
41
|
+
haiku/rag/store/repositories/chunk.py,sha256=1RmPyEYRYOFbrALbmLOo62t3f-xO2KgxUjcvPdrRZlc,14467
|
|
34
42
|
haiku/rag/store/repositories/document.py,sha256=XoLCrMrZqs0iCZoHlDOfRDaVUux77Vdu5iZczduF1rY,7812
|
|
35
43
|
haiku/rag/store/repositories/settings.py,sha256=wx3fuP_5CpPflZHRrIkeoer6ml-iD0qXERh5k6MQRzI,5291
|
|
36
44
|
haiku/rag/store/upgrades/__init__.py,sha256=wUiEoSiHTahvuagx93E4FB07v123AhdbOjwUkPusiIg,14
|
|
37
|
-
haiku_rag-0.
|
|
38
|
-
haiku_rag-0.
|
|
39
|
-
haiku_rag-0.
|
|
40
|
-
haiku_rag-0.
|
|
41
|
-
haiku_rag-0.
|
|
45
|
+
haiku_rag-0.9.0.dist-info/METADATA,sha256=ab5orVjoWGdapwaoPnwPdtuyetnErIxAvwDjl--9hfo,4681
|
|
46
|
+
haiku_rag-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
47
|
+
haiku_rag-0.9.0.dist-info/entry_points.txt,sha256=G1U3nAkNd5YDYd4v0tuYFbriz0i-JheCsFuT9kIoGCI,48
|
|
48
|
+
haiku_rag-0.9.0.dist-info/licenses/LICENSE,sha256=eXZrWjSk9PwYFNK9yUczl3oPl95Z4V9UXH7bPN46iPo,1065
|
|
49
|
+
haiku_rag-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|