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.

Files changed (98) hide show
  1. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/PKG-INFO +1 -1
  2. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/agents.md +2 -2
  3. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/pyproject.toml +2 -1
  4. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/app.py +0 -1
  5. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/client.py +3 -5
  6. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/base.py +2 -2
  7. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/dependencies.py +24 -0
  8. haiku_rag-0.9.3/src/haiku/rag/research/evaluation_agent.py +85 -0
  9. haiku_rag-0.9.3/src/haiku/rag/research/orchestrator.py +170 -0
  10. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/presearch_agent.py +6 -1
  11. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/search_agent.py +10 -6
  12. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/synthesis_agent.py +26 -6
  13. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/engine.py +42 -17
  14. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/models/chunk.py +1 -0
  15. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/repositories/chunk.py +60 -39
  16. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/repositories/document.py +2 -2
  17. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/repositories/settings.py +12 -5
  18. haiku_rag-0.9.3/src/haiku/rag/store/upgrades/__init__.py +60 -0
  19. haiku_rag-0.9.3/src/haiku/rag/store/upgrades/v0_9_3.py +112 -0
  20. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/generate_benchmark_db.py +1 -1
  21. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/research/test_evaluation_agent.py +6 -3
  22. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/research/test_orchestrator.py +22 -11
  23. haiku_rag-0.9.3/tests/research/test_search_agent.py +14 -0
  24. haiku_rag-0.9.3/tests/research/test_synthesis_agent.py +14 -0
  25. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_app.py +1 -1
  26. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_chunk.py +4 -6
  27. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_client.py +64 -57
  28. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_document.py +2 -3
  29. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/uv.lock +1 -1
  30. haiku_rag-0.9.2/src/haiku/rag/research/evaluation_agent.py +0 -42
  31. haiku_rag-0.9.2/src/haiku/rag/research/orchestrator.py +0 -300
  32. haiku_rag-0.9.2/src/haiku/rag/store/upgrades/__init__.py +0 -1
  33. haiku_rag-0.9.2/tests/research/test_search_agent.py +0 -11
  34. haiku_rag-0.9.2/tests/research/test_synthesis_agent.py +0 -11
  35. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.github/FUNDING.yml +0 -0
  36. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.github/workflows/build-docs.yml +0 -0
  37. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.github/workflows/build-publish.yml +0 -0
  38. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.gitignore +0 -0
  39. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.pre-commit-config.yaml +0 -0
  40. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/.python-version +0 -0
  41. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/LICENSE +0 -0
  42. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/README.md +0 -0
  43. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/benchmarks.md +0 -0
  44. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/cli.md +0 -0
  45. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/configuration.md +0 -0
  46. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/index.md +0 -0
  47. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/installation.md +0 -0
  48. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/mcp.md +0 -0
  49. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/python.md +0 -0
  50. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/docs/server.md +0 -0
  51. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/mkdocs.yml +0 -0
  52. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/__init__.py +0 -0
  53. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/chunker.py +0 -0
  54. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/cli.py +0 -0
  55. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/config.py +0 -0
  56. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/__init__.py +0 -0
  57. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/base.py +0 -0
  58. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/ollama.py +0 -0
  59. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/openai.py +0 -0
  60. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/vllm.py +0 -0
  61. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/embeddings/voyageai.py +0 -0
  62. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/logging.py +0 -0
  63. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/mcp.py +0 -0
  64. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/migration.py +0 -0
  65. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/monitor.py +0 -0
  66. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/qa/__init__.py +0 -0
  67. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/qa/agent.py +0 -0
  68. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/qa/prompts.py +0 -0
  69. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reader.py +0 -0
  70. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/__init__.py +0 -0
  71. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/base.py +0 -0
  72. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/cohere.py +0 -0
  73. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/mxbai.py +0 -0
  74. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/reranking/vllm.py +0 -0
  75. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/__init__.py +0 -0
  76. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/research/prompts.py +0 -0
  77. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/__init__.py +0 -0
  78. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/models/__init__.py +0 -0
  79. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/models/document.py +0 -0
  80. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/store/repositories/__init__.py +0 -0
  81. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/src/haiku/rag/utils.py +0 -0
  82. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/__init__.py +0 -0
  83. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/conftest.py +0 -0
  84. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/llm_judge.py +0 -0
  85. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_chunker.py +0 -0
  86. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_cli.py +0 -0
  87. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_embedder.py +0 -0
  88. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_lancedb_connection.py +0 -0
  89. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_monitor.py +0 -0
  90. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_preprocessor.py +0 -0
  91. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_qa.py +0 -0
  92. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_reader.py +0 -0
  93. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_rebuild.py +0 -0
  94. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_reranker.py +0 -0
  95. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_search.py +0 -0
  96. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_settings.py +0 -0
  97. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_utils.py +0 -0
  98. {haiku_rag-0.9.2 → haiku_rag-0.9.3}/tests/test_versioning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiku.rag
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: Agentic Retrieval Augmented Generation (RAG) with LanceDB
5
5
  Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
6
6
  License: MIT
@@ -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="openai", model="gpt-4o-mini")
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=False,
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" }
@@ -101,7 +101,6 @@ class HaikuRAGApp:
101
101
  client=client,
102
102
  max_iterations=max_iterations,
103
103
  verbose=verbose,
104
- console=self.console if verbose else None,
105
104
  )
106
105
 
107
106
  # Display the report
@@ -388,7 +388,7 @@ class HaikuRAG:
388
388
  all_chunks = adjacent_chunks + [chunk]
389
389
 
390
390
  # Get the range of orders for this expanded chunk
391
- orders = [c.metadata.get("order", 0) for c in all_chunks]
391
+ orders = [c.order for c in all_chunks]
392
392
  min_order = min(orders)
393
393
  max_order = max(orders)
394
394
 
@@ -398,9 +398,7 @@ class HaikuRAG:
398
398
  "score": score,
399
399
  "min_order": min_order,
400
400
  "max_order": max_order,
401
- "all_chunks": sorted(
402
- all_chunks, key=lambda c: c.metadata.get("order", 0)
403
- ),
401
+ "all_chunks": sorted(all_chunks, key=lambda c: c.order),
404
402
  }
405
403
  )
406
404
 
@@ -459,7 +457,7 @@ class HaikuRAG:
459
457
  # Merge all_chunks and deduplicate by order
460
458
  all_chunks_dict = {}
461
459
  for chunk in current["all_chunks"] + range_info["all_chunks"]:
462
- order = chunk.metadata.get("order", 0)
460
+ order = chunk.order
463
461
  all_chunks_dict[order] = chunk
464
462
  current["all_chunks"] = [
465
463
  all_chunks_dict[order] for order in sorted(all_chunks_dict.keys())
@@ -45,7 +45,8 @@ class BaseResearchAgent[T](ABC):
45
45
  model=model_obj,
46
46
  deps_type=ResearchDependencies,
47
47
  output_type=agent_output_type,
48
- system_prompt=self.get_system_prompt(),
48
+ instructions=self.get_system_prompt(),
49
+ retries=3,
49
50
  )
50
51
 
51
52
  # Register tools
@@ -75,7 +76,6 @@ class BaseResearchAgent[T](ABC):
75
76
  """Return the system prompt for this agent."""
76
77
  pass
77
78
 
78
- @abstractmethod
79
79
  def register_tools(self) -> None:
80
80
  """Register agent-specific tools."""
81
81
  pass
@@ -1,4 +1,6 @@
1
1
  from pydantic import BaseModel, Field
2
+ from pydantic_ai import format_as_xml
3
+ from rich.console import Console
2
4
 
3
5
  from haiku.rag.client import HaikuRAG
4
6
  from haiku.rag.research.base import SearchAnswer
@@ -43,3 +45,25 @@ class ResearchDependencies(BaseModel):
43
45
 
44
46
  client: HaikuRAG = Field(description="RAG client for document operations")
45
47
  context: ResearchContext = Field(description="Shared research context")
48
+ console: Console | None = None
49
+
50
+
51
+ def _format_context_for_prompt(context: ResearchContext) -> str:
52
+ """Format the research context as XML for inclusion in prompts."""
53
+
54
+ context_data = {
55
+ "original_question": context.original_question,
56
+ "unanswered_questions": context.sub_questions,
57
+ "qa_responses": [
58
+ {
59
+ "question": qa.query,
60
+ "answer": qa.answer,
61
+ "context_snippets": qa.context,
62
+ "sources": qa.sources,
63
+ }
64
+ for qa in context.qa_responses
65
+ ],
66
+ "insights": context.insights,
67
+ "gaps": context.gaps,
68
+ }
69
+ return format_as_xml(context_data, root_tag="research_context")
@@ -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
- result = await super().run(prompt, deps, **kwargs)
24
+ console = deps.console
25
+ if console:
26
+ console.print(f"\t{prompt}")
25
27
 
26
- if result.output:
27
- deps.context.add_qa_response(result.output)
28
+ result = await super().run(prompt, deps, **kwargs)
29
+ deps.context.add_qa_response(result.output)
30
+ deps.context.sub_questions.remove(prompt)
31
+ if console:
32
+ answer = result.output.answer
33
+ answer_preview = answer[:150] + "…" if len(answer) > 150 else answer
34
+ console.log(f"\n [green]✓[/green] {answer_preview}")
28
35
 
29
36
  return result
30
37
 
@@ -41,9 +48,6 @@ class SearchSpecialistAgent(BaseResearchAgent[SearchAnswer]):
41
48
  limit: int = 5,
42
49
  ) -> str:
43
50
  """Search the KB and return a concise context pack."""
44
- # Remove quotes from queries as this requires positional indexing in lancedb
45
- # XXX: Investigate how to do that with lancedb
46
- query = query.replace('"', "")
47
51
  search_results = await ctx.deps.client.search(query, limit=limit)
48
52
  expanded = await ctx.deps.client.expand_context(search_results)
49
53
 
@@ -1,6 +1,11 @@
1
1
  from pydantic import BaseModel, Field
2
+ from pydantic_ai.run import AgentRunResult
2
3
 
3
4
  from haiku.rag.research.base import BaseResearchAgent
5
+ from haiku.rag.research.dependencies import (
6
+ ResearchDependencies,
7
+ _format_context_for_prompt,
8
+ )
4
9
  from haiku.rag.research.prompts import SYNTHESIS_AGENT_PROMPT
5
10
 
6
11
 
@@ -30,11 +35,26 @@ class SynthesisAgent(BaseResearchAgent[ResearchReport]):
30
35
  def __init__(self, provider: str, model: str) -> None:
31
36
  super().__init__(provider, model, output_type=ResearchReport)
32
37
 
38
+ async def run(
39
+ self, prompt: str, deps: ResearchDependencies, **kwargs
40
+ ) -> AgentRunResult[ResearchReport]:
41
+ console = deps.console
42
+ if console:
43
+ console.print(
44
+ "\n[bold cyan]📝 Generating final research report...[/bold cyan]"
45
+ )
46
+
47
+ context_xml = _format_context_for_prompt(deps.context)
48
+ synthesis_prompt = f"""Generate a comprehensive research report based on all gathered information.
49
+
50
+ {context_xml}
51
+
52
+ Create a detailed report that synthesizes all findings into a coherent response."""
53
+ result = await super().run(synthesis_prompt, deps, **kwargs)
54
+ if console:
55
+ console.print("[bold green]✅ Research complete![/bold green]")
56
+
57
+ return result
58
+
33
59
  def get_system_prompt(self) -> str:
34
60
  return SYNTHESIS_AGENT_PROMPT
35
-
36
- def register_tools(self) -> None:
37
- """Register synthesis-specific tools."""
38
- # The agent will use its LLM capabilities directly for synthesis
39
- # The structured output will guide the report generation
40
- pass
@@ -35,6 +35,7 @@ def create_chunk_model(vector_dim: int):
35
35
  document_id: str
36
36
  content: str
37
37
  metadata: str = Field(default="{}")
38
+ order: int = Field(default=0)
38
39
  vector: Vector(vector_dim) = Field(default_factory=lambda: [0.0] * vector_dim) # type: ignore
39
40
 
40
41
  return ChunkRecord
@@ -117,8 +118,10 @@ class Store:
117
118
  self.chunks_table = self.db.open_table("chunks")
118
119
  else:
119
120
  self.chunks_table = self.db.create_table("chunks", schema=self.ChunkRecord)
120
- # Create FTS index on the new table
121
- self.chunks_table.create_fts_index("content", replace=True)
121
+ # Create FTS index on the new table with phrase query support
122
+ self.chunks_table.create_fts_index(
123
+ "content", replace=True, with_position=True, remove_stop_words=False
124
+ )
122
125
 
123
126
  # Create or get settings table
124
127
  if "settings" in existing_tables:
@@ -133,21 +136,41 @@ class Store:
133
136
  [SettingsRecord(id="settings", settings=json.dumps(settings_data))]
134
137
  )
135
138
 
136
- # Set current version in settings
137
- current_version = metadata.version("haiku.rag")
138
- self.set_haiku_version(current_version)
139
-
140
- # Check if we need to perform upgrades
139
+ # Run pending upgrades based on stored version and package version
141
140
  try:
142
- existing_settings = list(
143
- self.settings_table.search().limit(1).to_pydantic(SettingsRecord)
141
+ from haiku.rag.store.upgrades import run_pending_upgrades
142
+
143
+ current_version = metadata.version("haiku.rag")
144
+ db_version = self.get_haiku_version()
145
+
146
+ run_pending_upgrades(self, db_version, current_version)
147
+
148
+ # After upgrades complete (or if none), set stored version
149
+ # to the greater of the installed package version and the
150
+ # highest available upgrade step version in code.
151
+ try:
152
+ from packaging.version import parse as _v
153
+
154
+ from haiku.rag.store.upgrades import upgrades as _steps
155
+
156
+ highest_step = max((_v(u.version) for u in _steps), default=None)
157
+ effective_version = (
158
+ str(max(_v(current_version), highest_step))
159
+ if highest_step is not None
160
+ else current_version
161
+ )
162
+ except Exception:
163
+ effective_version = current_version
164
+
165
+ self.set_haiku_version(effective_version)
166
+ except Exception as e:
167
+ # Avoid hard failure on initial connection; log and continue so CLI remains usable.
168
+ logger.warning(
169
+ "Skipping upgrade due to error (db=%s -> pkg=%s): %s",
170
+ self.get_haiku_version(),
171
+ metadata.version("haiku.rag") if hasattr(metadata, "version") else "",
172
+ e,
144
173
  )
145
- if existing_settings:
146
- db_version = self.get_haiku_version() # noqa: F841
147
- # TODO: Add upgrade logic here similar to SQLite version when needed
148
- except Exception:
149
- # Settings table might not exist yet in fresh databases
150
- pass
151
174
 
152
175
  def get_haiku_version(self) -> str:
153
176
  """Returns the user version stored in settings."""
@@ -201,8 +224,10 @@ class Store:
201
224
  self.ChunkRecord = create_chunk_model(self.embedder._vector_dim)
202
225
  self.chunks_table = self.db.create_table("chunks", schema=self.ChunkRecord)
203
226
 
204
- # Create FTS index on the new table
205
- self.chunks_table.create_fts_index("content", replace=True)
227
+ # Create FTS index on the new table with phrase query support
228
+ self.chunks_table.create_fts_index(
229
+ "content", replace=True, with_position=True, remove_stop_words=False
230
+ )
206
231
 
207
232
  def close(self):
208
233
  """Close the database connection."""
@@ -10,6 +10,7 @@ class Chunk(BaseModel):
10
10
  document_id: str | None = None
11
11
  content: str
12
12
  metadata: dict = {}
13
+ order: int = 0
13
14
  document_uri: str | None = None
14
15
  document_meta: dict = {}
15
16
  embedding: list[float] | None = None