haiku.rag 0.9.2__tar.gz → 0.9.3__tar.gz
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-0.9.2 → haiku_rag-0.9.3}/PKG-INFO +1 -1
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/agents.md +2 -2
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/pyproject.toml +2 -1
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/app.py +0 -1
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/client.py +3 -5
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/base.py +2 -2
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/dependencies.py +24 -0
- haiku_rag-0.9.3/src/haiku/rag/research/evaluation_agent.py +85 -0
- haiku_rag-0.9.3/src/haiku/rag/research/orchestrator.py +170 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/presearch_agent.py +6 -1
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/search_agent.py +10 -6
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/synthesis_agent.py +26 -6
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/engine.py +42 -17
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/models/chunk.py +1 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/repositories/chunk.py +60 -39
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/repositories/document.py +2 -2
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/repositories/settings.py +12 -5
- haiku_rag-0.9.3/src/haiku/rag/store/upgrades/__init__.py +60 -0
- haiku_rag-0.9.3/src/haiku/rag/store/upgrades/v0_9_3.py +112 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/generate_benchmark_db.py +1 -1
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/research/test_evaluation_agent.py +6 -3
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/research/test_orchestrator.py +22 -11
- haiku_rag-0.9.3/tests/research/test_search_agent.py +14 -0
- haiku_rag-0.9.3/tests/research/test_synthesis_agent.py +14 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_app.py +1 -1
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_chunk.py +4 -6
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_client.py +64 -57
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_document.py +2 -3
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/uv.lock +1 -1
- haiku_rag-0.9.2/src/haiku/rag/research/evaluation_agent.py +0 -42
- haiku_rag-0.9.2/src/haiku/rag/research/orchestrator.py +0 -300
- haiku_rag-0.9.2/src/haiku/rag/store/upgrades/__init__.py +0 -1
- haiku_rag-0.9.2/tests/research/test_search_agent.py +0 -11
- haiku_rag-0.9.2/tests/research/test_synthesis_agent.py +0 -11
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.github/FUNDING.yml +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.github/workflows/build-docs.yml +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.github/workflows/build-publish.yml +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.gitignore +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.pre-commit-config.yaml +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.python-version +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/LICENSE +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/README.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/benchmarks.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/cli.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/configuration.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/index.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/installation.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/mcp.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/python.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/server.md +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/mkdocs.yml +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/chunker.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/cli.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/config.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/base.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/ollama.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/openai.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/vllm.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/voyageai.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/logging.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/mcp.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/migration.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/monitor.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/qa/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/qa/agent.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/qa/prompts.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reader.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/base.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/cohere.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/mxbai.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/vllm.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/prompts.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/models/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/models/document.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/repositories/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/utils.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/__init__.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/conftest.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/llm_judge.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_chunker.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_cli.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_embedder.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_lancedb_connection.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_monitor.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_preprocessor.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_qa.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_reader.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_rebuild.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_reranker.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_search.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_settings.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_utils.py +0 -0
- {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_versioning.py +0 -0
|
@@ -70,14 +70,14 @@ from haiku.rag.client import HaikuRAG
|
|
|
70
70
|
from haiku.rag.research import ResearchOrchestrator
|
|
71
71
|
|
|
72
72
|
client = HaikuRAG(path_to_db)
|
|
73
|
-
orchestrator = ResearchOrchestrator(provider="
|
|
73
|
+
orchestrator = ResearchOrchestrator(provider="ollama", model="gpt-oss")
|
|
74
74
|
|
|
75
75
|
report = await orchestrator.conduct_research(
|
|
76
76
|
question="What are the main drivers and recent trends of global temperature anomalies since 1990?",
|
|
77
77
|
client=client,
|
|
78
78
|
max_iterations=2,
|
|
79
79
|
confidence_threshold=0.8,
|
|
80
|
-
verbose=
|
|
80
|
+
verbose=True,
|
|
81
81
|
)
|
|
82
82
|
|
|
83
83
|
print(report.title)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
|
+
|
|
2
3
|
name = "haiku.rag"
|
|
3
|
-
version = "0.9.2"
|
|
4
4
|
description = "Agentic Retrieval Augmented Generation (RAG) with LanceDB"
|
|
5
|
+
version = "0.9.3"
|
|
5
6
|
authors = [{ name = "Yiorgis Gozadinos", email = "ggozadinos@gmail.com" }]
|
|
6
7
|
license = { text = "MIT" }
|
|
7
8
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
@@ -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())
|
|
@@ -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")
|
|
@@ -0,0 +1,85 @@
|
|
|
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 EVALUATION_AGENT_PROMPT
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EvaluationResult(BaseModel):
|
|
13
|
+
"""Result of analysis and evaluation."""
|
|
14
|
+
|
|
15
|
+
key_insights: list[str] = Field(
|
|
16
|
+
description="Main insights extracted from the research so far"
|
|
17
|
+
)
|
|
18
|
+
new_questions: list[str] = Field(
|
|
19
|
+
description="New sub-questions to add to the research (max 3)",
|
|
20
|
+
max_length=3,
|
|
21
|
+
default=[],
|
|
22
|
+
)
|
|
23
|
+
confidence_score: float = Field(
|
|
24
|
+
description="Confidence level in the completeness of research (0-1)",
|
|
25
|
+
ge=0.0,
|
|
26
|
+
le=1.0,
|
|
27
|
+
)
|
|
28
|
+
is_sufficient: bool = Field(
|
|
29
|
+
description="Whether the research is sufficient to answer the original question"
|
|
30
|
+
)
|
|
31
|
+
reasoning: str = Field(
|
|
32
|
+
description="Explanation of why the research is or isn't complete"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AnalysisEvaluationAgent(BaseResearchAgent[EvaluationResult]):
|
|
37
|
+
"""Agent that analyzes findings and evaluates research completeness."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, provider: str, model: str) -> None:
|
|
40
|
+
super().__init__(provider, model, output_type=EvaluationResult)
|
|
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
|
+
|
|
84
|
+
def get_system_prompt(self) -> str:
|
|
85
|
+
return EVALUATION_AGENT_PROMPT
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
|
@@ -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
|
|
@@ -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."""
|