haiku.rag 0.8.1__tar.gz → 0.9.0__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 (92) hide show
  1. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/.gitignore +2 -0
  2. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/PKG-INFO +10 -10
  3. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/README.md +1 -0
  4. haiku_rag-0.9.0/docs/agents.md +83 -0
  5. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/docs/index.md +1 -0
  6. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/docs/python.md +2 -0
  7. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/mkdocs.yml +2 -1
  8. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/pyproject.toml +16 -17
  9. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/app.py +80 -0
  10. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/cli.py +36 -0
  11. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/config.py +7 -1
  12. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/qa/agent.py +4 -2
  13. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/qa/prompts.py +2 -2
  14. haiku_rag-0.9.0/src/haiku/rag/research/__init__.py +35 -0
  15. haiku_rag-0.9.0/src/haiku/rag/research/base.py +122 -0
  16. haiku_rag-0.9.0/src/haiku/rag/research/dependencies.py +45 -0
  17. haiku_rag-0.9.0/src/haiku/rag/research/evaluation_agent.py +40 -0
  18. haiku_rag-0.9.0/src/haiku/rag/research/orchestrator.py +265 -0
  19. haiku_rag-0.9.0/src/haiku/rag/research/prompts.py +116 -0
  20. haiku_rag-0.9.0/src/haiku/rag/research/search_agent.py +64 -0
  21. haiku_rag-0.9.0/src/haiku/rag/research/synthesis_agent.py +39 -0
  22. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/repositories/chunk.py +2 -1
  23. haiku_rag-0.9.0/tests/research/test_evaluation_agent.py +14 -0
  24. haiku_rag-0.9.0/tests/research/test_orchestrator.py +179 -0
  25. haiku_rag-0.9.0/tests/research/test_search_agent.py +11 -0
  26. haiku_rag-0.9.0/tests/research/test_synthesis_agent.py +11 -0
  27. haiku_rag-0.9.0/uv.lock +4647 -0
  28. haiku_rag-0.8.1/uv.lock +0 -4004
  29. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/.github/FUNDING.yml +0 -0
  30. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/.github/workflows/build-docs.yml +0 -0
  31. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/.github/workflows/build-publish.yml +0 -0
  32. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/.pre-commit-config.yaml +0 -0
  33. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/.python-version +0 -0
  34. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/LICENSE +0 -0
  35. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/docs/benchmarks.md +0 -0
  36. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/docs/cli.md +0 -0
  37. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/docs/configuration.md +0 -0
  38. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/docs/installation.md +0 -0
  39. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/docs/mcp.md +0 -0
  40. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/docs/server.md +0 -0
  41. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/__init__.py +0 -0
  42. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/chunker.py +0 -0
  43. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/client.py +0 -0
  44. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/embeddings/__init__.py +0 -0
  45. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/embeddings/base.py +0 -0
  46. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/embeddings/ollama.py +0 -0
  47. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/embeddings/openai.py +0 -0
  48. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/embeddings/vllm.py +0 -0
  49. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/embeddings/voyageai.py +0 -0
  50. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/logging.py +0 -0
  51. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/mcp.py +0 -0
  52. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/migration.py +0 -0
  53. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/monitor.py +0 -0
  54. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/qa/__init__.py +0 -0
  55. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/reader.py +0 -0
  56. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/reranking/__init__.py +0 -0
  57. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/reranking/base.py +0 -0
  58. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/reranking/cohere.py +0 -0
  59. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/reranking/mxbai.py +0 -0
  60. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/reranking/vllm.py +0 -0
  61. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/__init__.py +0 -0
  62. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/engine.py +0 -0
  63. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/models/__init__.py +0 -0
  64. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/models/chunk.py +0 -0
  65. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/models/document.py +0 -0
  66. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/repositories/__init__.py +0 -0
  67. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/repositories/document.py +0 -0
  68. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/repositories/settings.py +0 -0
  69. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/store/upgrades/__init__.py +0 -0
  70. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/src/haiku/rag/utils.py +0 -0
  71. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/__init__.py +0 -0
  72. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/conftest.py +0 -0
  73. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/generate_benchmark_db.py +0 -0
  74. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/llm_judge.py +0 -0
  75. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_app.py +0 -0
  76. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_chunk.py +0 -0
  77. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_chunker.py +0 -0
  78. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_cli.py +0 -0
  79. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_client.py +0 -0
  80. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_document.py +0 -0
  81. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_embedder.py +0 -0
  82. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_lancedb_connection.py +0 -0
  83. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_monitor.py +0 -0
  84. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_preprocessor.py +0 -0
  85. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_qa.py +0 -0
  86. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_reader.py +0 -0
  87. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_rebuild.py +0 -0
  88. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_reranker.py +0 -0
  89. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_search.py +0 -0
  90. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_settings.py +0 -0
  91. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_utils.py +0 -0
  92. {haiku_rag-0.8.1 → haiku_rag-0.9.0}/tests/test_versioning.py +0 -0
@@ -18,3 +18,5 @@ tests/data/
18
18
  # environment variables
19
19
  .env
20
20
  TODO.md
21
+ PLAN.md
22
+ DEVNOTES.md
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiku.rag
3
- Version: 0.8.1
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.49.0
22
- Requires-Dist: fastmcp>=2.8.1
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.24.3
25
- Requires-Dist: ollama>=0.5.3
26
- Requires-Dist: pydantic-ai>=0.8.1
27
- Requires-Dist: pydantic>=2.11.7
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.2; extra == 'voyageai'
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
@@ -90,4 +90,5 @@ Full documentation at: https://ggozad.github.io/haiku.rag/
90
90
  - [Configuration](https://ggozad.github.io/haiku.rag/configuration/) - Environment variables
91
91
  - [CLI](https://ggozad.github.io/haiku.rag/cli/) - Command reference
92
92
  - [Python API](https://ggozad.github.io/haiku.rag/python/) - Complete API docs
93
+ - [Agents](https://ggozad.github.io/haiku.rag/agents/) - QA agent and multi-agent research
93
94
  - [Benchmarks](https://ggozad.github.io/haiku.rag/benchmarks/) - Performance Benchmarks
@@ -0,0 +1,83 @@
1
+ ## Agents
2
+
3
+ Two agentic flows are provided by haiku.rag:
4
+
5
+ - Simple QA Agent — a focused question answering agent
6
+ - Research Multi‑Agent — a multi‑step, analyzable research workflow
7
+
8
+
9
+ ### Simple QA Agent
10
+
11
+ The simple QA agent answers a single question using the knowledge base. It retrieves relevant chunks, optionally expands context around them, and asks the model to answer strictly based on that context.
12
+
13
+ Key points:
14
+
15
+ - Uses a single `search_documents` tool to fetch relevant chunks
16
+ - Can be run with or without inline citations in the prompt
17
+ - Returns a plain string answer
18
+
19
+ Python usage:
20
+
21
+ ```python
22
+ from haiku.rag.client import HaikuRAG
23
+ from haiku.rag.qa.agent import QuestionAnswerAgent
24
+
25
+ client = HaikuRAG(path_to_db)
26
+
27
+ # Choose a provider and model (see Configuration for env defaults)
28
+ agent = QuestionAnswerAgent(
29
+ client=client,
30
+ provider="openai", # or "ollama", "vllm", etc.
31
+ model="gpt-4o-mini",
32
+ use_citations=False, # set True to bias prompt towards citing sources
33
+ )
34
+
35
+ answer = await agent.answer("What is climate change?")
36
+ print(answer)
37
+ ```
38
+
39
+ ### Research Multi‑Agent
40
+
41
+ The research workflow coordinates specialized agents to plan, search, analyze, and synthesize a comprehensive answer. It is designed for deeper questions that benefit from iterative investigation and structured reporting.
42
+
43
+ Components:
44
+
45
+ - Orchestrator: Plans, coordinates, and loops until confidence is sufficient
46
+ - Search Specialist: Performs targeted RAG searches and answers sub‑questions
47
+ - Analysis & Evaluation: Extracts insights, identifies gaps, proposes new questions
48
+ - Synthesis: Produces a final structured research report
49
+
50
+ Primary models:
51
+
52
+ - `ResearchPlan` — produced by the orchestrator when planning
53
+ - `main_question: str`
54
+ - `sub_questions: list[str]` (standalone, self‑contained queries)
55
+ - `SearchAnswer` — produced by the search specialist for each sub‑question
56
+ - `query: str` — the executed sub‑question
57
+ - `answer: str` — the agent’s answer grounded in retrieved context
58
+ - `context: list[str]` — minimal verbatim snippets used for the answer
59
+ - `sources: list[str]` — document URIs aligned with `context`
60
+ - `EvaluationResult` — insights, new standalone questions, sufficiency & confidence
61
+ - `ResearchReport` — the final synthesized report
62
+
63
+
64
+ Python usage:
65
+
66
+ ```python
67
+ from haiku.rag.client import HaikuRAG
68
+ from haiku.rag.research import ResearchOrchestrator
69
+
70
+ client = HaikuRAG(path_to_db)
71
+ orchestrator = ResearchOrchestrator(provider="openai", model="gpt-4o-mini")
72
+
73
+ report = await orchestrator.conduct_research(
74
+ question="What are the main drivers and recent trends of global temperature anomalies since 1990?",
75
+ client=client,
76
+ max_iterations=2,
77
+ confidence_threshold=0.8,
78
+ verbose=False,
79
+ )
80
+
81
+ print(report.title)
82
+ print(report.executive_summary)
83
+ ```
@@ -55,6 +55,7 @@ haiku-rag migrate old_database.sqlite # Migrate from SQLite
55
55
  - [Server](server.md) - File monitoring and server mode
56
56
  - [MCP](mcp.md) - Model Context Protocol integration
57
57
  - [Python](python.md) - Python API reference
58
+ - [Agents](agents.md) - QA agent and multi-agent research
58
59
 
59
60
  ## License
60
61
 
@@ -204,3 +204,5 @@ print(answer)
204
204
  The QA agent will search your documents for relevant information and use the configured LLM to generate a comprehensive answer. With `cite=True`, responses include citations showing which documents were used as sources.
205
205
 
206
206
  The QA provider and model can be configured via environment variables (see [Configuration](configuration.md)).
207
+
208
+ See also: [Agents](agents.md) for details on the QA agent and the multi‑agent research workflow.
@@ -61,8 +61,9 @@ nav:
61
61
  - Configuration: configuration.md
62
62
  - CLI: cli.md
63
63
  - Server: server.md
64
- - MCP: mcp.md
64
+ - Agents: agents.md
65
65
  - Python: python.md
66
+ - MCP: mcp.md
66
67
  - Benchmarks: benchmarks.md
67
68
  markdown_extensions:
68
69
  - admonition
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "haiku.rag"
3
- version = "0.8.1"
4
- description = "Retrieval Augmented Generation (RAG) with LanceDB"
3
+ version = "0.9.0"
4
+ description = "Agentic Retrieval Augmented Generation (RAG) with LanceDB"
5
5
  authors = [{ name = "Yiorgis Gozadinos", email = "ggozadinos@gmail.com" }]
6
6
  license = { text = "MIT" }
7
7
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -22,14 +22,13 @@ classifiers = [
22
22
  ]
23
23
 
24
24
  dependencies = [
25
- "docling>=2.49.0",
26
- "fastmcp>=2.8.1",
25
+ "docling>=2.52.0",
26
+ "fastmcp>=2.12.3",
27
27
  "httpx>=0.28.1",
28
- "lancedb>=0.24.3",
29
- "ollama>=0.5.3",
30
- "pydantic>=2.11.7",
31
- "pydantic-ai>=0.8.1",
32
- "python-dotenv>=1.1.0",
28
+ "lancedb>=0.25.0",
29
+ "pydantic>=2.11.9",
30
+ "pydantic-ai>=1.0.8",
31
+ "python-dotenv>=1.1.1",
33
32
  "rich>=14.1.0",
34
33
  "tiktoken>=0.11.0",
35
34
  "typer>=0.16.1",
@@ -37,7 +36,7 @@ dependencies = [
37
36
  ]
38
37
 
39
38
  [project.optional-dependencies]
40
- voyageai = ["voyageai>=0.3.2"]
39
+ voyageai = ["voyageai>=0.3.5"]
41
40
  mxbai = ["mxbai-rerank>=0.1.6"]
42
41
 
43
42
  [project.scripts]
@@ -52,16 +51,16 @@ packages = ["src/haiku"]
52
51
 
53
52
  [dependency-groups]
54
53
  dev = [
55
- "datasets>=3.6.0",
56
- "logfire>=4.6.0",
54
+ "datasets>=4.1.0",
55
+ "logfire>=4.7.0",
57
56
  "mkdocs>=1.6.1",
58
57
  "mkdocs-material>=9.6.14",
59
58
  "pre-commit>=4.2.0",
60
- "pyright>=1.1.404",
61
- "pytest>=8.4.0",
62
- "pytest-asyncio>=1.0.0",
63
- "pytest-cov>=6.2.1",
64
- "ruff>=0.11.13",
59
+ "pyright>=1.1.405",
60
+ "pytest>=8.4.2",
61
+ "pytest-asyncio>=1.2.0",
62
+ "pytest-cov>=7.0.0",
63
+ "ruff>=0.13.0",
65
64
  ]
66
65
 
67
66
  [tool.ruff]
@@ -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:
@@ -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
@@ -27,7 +27,11 @@ class AppConfig(BaseModel):
27
27
  RERANK_MODEL: str = ""
28
28
 
29
29
  QA_PROVIDER: str = "ollama"
30
- QA_MODEL: str = "qwen3"
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 = ""
@@ -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 SYSTEM_PROMPT, SYSTEM_PROMPT_WITH_CITATIONS
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 = SYSTEM_PROMPT_WITH_CITATIONS if use_citations else 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(
@@ -1,4 +1,4 @@
1
- SYSTEM_PROMPT = """
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
- SYSTEM_PROMPT_WITH_CITATIONS = """
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")