haiku.rag-slim 0.16.0__py3-none-any.whl → 0.24.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-slim might be problematic. Click here for more details.

Files changed (94) hide show
  1. haiku/rag/app.py +430 -72
  2. haiku/rag/chunkers/__init__.py +31 -0
  3. haiku/rag/chunkers/base.py +31 -0
  4. haiku/rag/chunkers/docling_local.py +164 -0
  5. haiku/rag/chunkers/docling_serve.py +179 -0
  6. haiku/rag/cli.py +207 -24
  7. haiku/rag/cli_chat.py +489 -0
  8. haiku/rag/client.py +1251 -266
  9. haiku/rag/config/__init__.py +16 -10
  10. haiku/rag/config/loader.py +5 -44
  11. haiku/rag/config/models.py +126 -17
  12. haiku/rag/converters/__init__.py +31 -0
  13. haiku/rag/converters/base.py +63 -0
  14. haiku/rag/converters/docling_local.py +193 -0
  15. haiku/rag/converters/docling_serve.py +229 -0
  16. haiku/rag/converters/text_utils.py +237 -0
  17. haiku/rag/embeddings/__init__.py +123 -24
  18. haiku/rag/embeddings/voyageai.py +175 -20
  19. haiku/rag/graph/__init__.py +0 -11
  20. haiku/rag/graph/agui/__init__.py +8 -2
  21. haiku/rag/graph/agui/cli_renderer.py +1 -1
  22. haiku/rag/graph/agui/emitter.py +219 -31
  23. haiku/rag/graph/agui/server.py +20 -62
  24. haiku/rag/graph/agui/stream.py +1 -2
  25. haiku/rag/graph/research/__init__.py +5 -2
  26. haiku/rag/graph/research/dependencies.py +12 -126
  27. haiku/rag/graph/research/graph.py +390 -135
  28. haiku/rag/graph/research/models.py +91 -112
  29. haiku/rag/graph/research/prompts.py +99 -91
  30. haiku/rag/graph/research/state.py +35 -27
  31. haiku/rag/inspector/__init__.py +8 -0
  32. haiku/rag/inspector/app.py +259 -0
  33. haiku/rag/inspector/widgets/__init__.py +6 -0
  34. haiku/rag/inspector/widgets/chunk_list.py +100 -0
  35. haiku/rag/inspector/widgets/context_modal.py +89 -0
  36. haiku/rag/inspector/widgets/detail_view.py +130 -0
  37. haiku/rag/inspector/widgets/document_list.py +75 -0
  38. haiku/rag/inspector/widgets/info_modal.py +209 -0
  39. haiku/rag/inspector/widgets/search_modal.py +183 -0
  40. haiku/rag/inspector/widgets/visual_modal.py +126 -0
  41. haiku/rag/mcp.py +106 -102
  42. haiku/rag/monitor.py +33 -9
  43. haiku/rag/providers/__init__.py +5 -0
  44. haiku/rag/providers/docling_serve.py +108 -0
  45. haiku/rag/qa/__init__.py +12 -10
  46. haiku/rag/qa/agent.py +43 -61
  47. haiku/rag/qa/prompts.py +35 -57
  48. haiku/rag/reranking/__init__.py +9 -6
  49. haiku/rag/reranking/base.py +1 -1
  50. haiku/rag/reranking/cohere.py +5 -4
  51. haiku/rag/reranking/mxbai.py +5 -2
  52. haiku/rag/reranking/vllm.py +3 -4
  53. haiku/rag/reranking/zeroentropy.py +6 -5
  54. haiku/rag/store/__init__.py +2 -1
  55. haiku/rag/store/engine.py +242 -42
  56. haiku/rag/store/exceptions.py +4 -0
  57. haiku/rag/store/models/__init__.py +8 -2
  58. haiku/rag/store/models/chunk.py +190 -0
  59. haiku/rag/store/models/document.py +46 -0
  60. haiku/rag/store/repositories/chunk.py +141 -121
  61. haiku/rag/store/repositories/document.py +25 -84
  62. haiku/rag/store/repositories/settings.py +11 -14
  63. haiku/rag/store/upgrades/__init__.py +19 -3
  64. haiku/rag/store/upgrades/v0_10_1.py +1 -1
  65. haiku/rag/store/upgrades/v0_19_6.py +65 -0
  66. haiku/rag/store/upgrades/v0_20_0.py +68 -0
  67. haiku/rag/store/upgrades/v0_23_1.py +100 -0
  68. haiku/rag/store/upgrades/v0_9_3.py +3 -3
  69. haiku/rag/utils.py +371 -146
  70. {haiku_rag_slim-0.16.0.dist-info → haiku_rag_slim-0.24.0.dist-info}/METADATA +15 -12
  71. haiku_rag_slim-0.24.0.dist-info/RECORD +78 -0
  72. {haiku_rag_slim-0.16.0.dist-info → haiku_rag_slim-0.24.0.dist-info}/WHEEL +1 -1
  73. haiku/rag/chunker.py +0 -65
  74. haiku/rag/embeddings/base.py +0 -25
  75. haiku/rag/embeddings/ollama.py +0 -28
  76. haiku/rag/embeddings/openai.py +0 -26
  77. haiku/rag/embeddings/vllm.py +0 -29
  78. haiku/rag/graph/agui/events.py +0 -254
  79. haiku/rag/graph/common/__init__.py +0 -5
  80. haiku/rag/graph/common/models.py +0 -42
  81. haiku/rag/graph/common/nodes.py +0 -265
  82. haiku/rag/graph/common/prompts.py +0 -46
  83. haiku/rag/graph/common/utils.py +0 -44
  84. haiku/rag/graph/deep_qa/__init__.py +0 -1
  85. haiku/rag/graph/deep_qa/dependencies.py +0 -27
  86. haiku/rag/graph/deep_qa/graph.py +0 -243
  87. haiku/rag/graph/deep_qa/models.py +0 -20
  88. haiku/rag/graph/deep_qa/prompts.py +0 -59
  89. haiku/rag/graph/deep_qa/state.py +0 -56
  90. haiku/rag/graph/research/common.py +0 -87
  91. haiku/rag/reader.py +0 -135
  92. haiku_rag_slim-0.16.0.dist-info/RECORD +0 -71
  93. {haiku_rag_slim-0.16.0.dist-info → haiku_rag_slim-0.24.0.dist-info}/entry_points.txt +0 -0
  94. {haiku_rag_slim-0.16.0.dist-info → haiku_rag_slim-0.24.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,8 +18,7 @@ from starlette.responses import JSONResponse, StreamingResponse
18
18
  from starlette.routing import Route
19
19
 
20
20
  from haiku.rag.config.models import AGUIConfig
21
- from haiku.rag.graph.agui.emitter import AGUIEmitter
22
- from haiku.rag.graph.agui.events import AGUIEvent
21
+ from haiku.rag.graph.agui.emitter import AGUIEmitter, AGUIEvent
23
22
  from haiku.rag.graph.agui.stream import stream_graph
24
23
 
25
24
 
@@ -151,23 +150,25 @@ def format_sse_event(event: AGUIEvent) -> str:
151
150
  return f"data: {event_json}\n\n"
152
151
 
153
152
 
154
- def create_agui_server(config: "AppConfig", db_path: Path | None = None) -> Starlette:
155
- """Create AG-UI server with both research and deep ask endpoints.
153
+ def create_agui_server( # pragma: no cover
154
+ config: "AppConfig", db_path: Path | None = None
155
+ ) -> Starlette:
156
+ """Create AG-UI server with research endpoint.
156
157
 
157
158
  Args:
158
- config: Application config with research and qa settings
159
+ config: Application config with research settings
159
160
  db_path: Optional database path override
160
161
 
161
162
  Returns:
162
- Starlette app with research and deep ask endpoints
163
+ Starlette app with research endpoint
163
164
  """
164
165
  from haiku.rag.client import HaikuRAG
165
- from haiku.rag.graph.deep_qa.dependencies import DeepQAContext
166
- from haiku.rag.graph.deep_qa.graph import build_deep_qa_graph
167
- from haiku.rag.graph.deep_qa.state import DeepQADeps, DeepQAState
168
166
  from haiku.rag.graph.research.dependencies import ResearchContext
169
167
  from haiku.rag.graph.research.graph import build_research_graph
170
- from haiku.rag.graph.research.state import ResearchDeps, ResearchState
168
+ from haiku.rag.graph.research.state import (
169
+ ResearchDeps,
170
+ ResearchState,
171
+ )
171
172
 
172
173
  # Store client reference for proper lifecycle management
173
174
  _client_cache: dict[str, HaikuRAG] = {}
@@ -190,7 +191,14 @@ def create_agui_server(config: "AppConfig", db_path: Path | None = None) -> Star
190
191
  if messages:
191
192
  question = messages[0].get("content", "")
192
193
  context = ResearchContext(original_question=question)
193
- return ResearchState.from_config(context=context, config=config)
194
+ max_iterations = input_state.get("max_iterations")
195
+ confidence_threshold = input_state.get("confidence_threshold")
196
+ return ResearchState.from_config(
197
+ context=context,
198
+ config=config,
199
+ max_iterations=max_iterations,
200
+ confidence_threshold=confidence_threshold,
201
+ )
194
202
 
195
203
  def research_deps_factory(input_config: dict[str, Any]) -> ResearchDeps:
196
204
  effective_db_path = (
@@ -200,29 +208,7 @@ def create_agui_server(config: "AppConfig", db_path: Path | None = None) -> Star
200
208
  )
201
209
  return ResearchDeps(client=get_client(effective_db_path))
202
210
 
203
- # Deep ask graph factories
204
- def deep_ask_graph_factory() -> Graph:
205
- return build_deep_qa_graph(config)
206
-
207
- def deep_ask_state_factory(input_state: dict[str, Any]) -> DeepQAState:
208
- question = input_state.get("question", "")
209
- if not question:
210
- messages = input_state.get("messages", [])
211
- if messages:
212
- question = messages[0].get("content", "")
213
- use_citations = input_state.get("use_citations", False)
214
- context = DeepQAContext(original_question=question, use_citations=use_citations)
215
- return DeepQAState.from_config(context=context, config=config)
216
-
217
- def deep_ask_deps_factory(input_config: dict[str, Any]) -> DeepQADeps:
218
- effective_db_path = (
219
- db_path
220
- or input_config.get("db_path")
221
- or config.storage.data_dir / "haiku.rag.lancedb"
222
- )
223
- return DeepQADeps(client=get_client(effective_db_path))
224
-
225
- # Create event stream functions for each graph type
211
+ # Create event stream function
226
212
  async def research_event_stream(
227
213
  input_data: RunAgentInput,
228
214
  ) -> AsyncIterator[str]:
@@ -235,18 +221,6 @@ def create_agui_server(config: "AppConfig", db_path: Path | None = None) -> Star
235
221
  event_data = format_sse_event(event)
236
222
  yield event_data
237
223
 
238
- async def deep_ask_event_stream(
239
- input_data: RunAgentInput,
240
- ) -> AsyncIterator[str]:
241
- """Generate SSE event stream from deep ask graph execution."""
242
- graph = deep_ask_graph_factory()
243
- initial_state = deep_ask_state_factory(input_data.state)
244
- deps = deep_ask_deps_factory(input_data.config)
245
-
246
- async for event in stream_graph(graph, initial_state, deps):
247
- event_data = format_sse_event(event)
248
- yield event_data
249
-
250
224
  # Endpoint handlers
251
225
  async def stream_research(request: Request) -> StreamingResponse:
252
226
  """Research graph streaming endpoint."""
@@ -263,21 +237,6 @@ def create_agui_server(config: "AppConfig", db_path: Path | None = None) -> Star
263
237
  },
264
238
  )
265
239
 
266
- async def stream_deep_ask(request: Request) -> StreamingResponse:
267
- """Deep ask graph streaming endpoint."""
268
- body = await request.json()
269
- input_data = RunAgentInput(**body)
270
-
271
- return StreamingResponse(
272
- deep_ask_event_stream(input_data),
273
- media_type="text/event-stream",
274
- headers={
275
- "Cache-Control": "no-cache",
276
- "Connection": "keep-alive",
277
- "X-Accel-Buffering": "no",
278
- },
279
- )
280
-
281
240
  async def health_check(_: Request) -> JSONResponse:
282
241
  """Health check endpoint."""
283
242
  return JSONResponse({"status": "healthy"})
@@ -285,7 +244,6 @@ def create_agui_server(config: "AppConfig", db_path: Path | None = None) -> Star
285
244
  # Define routes
286
245
  routes = [
287
246
  Route("/v1/research/stream", stream_research, methods=["POST"]),
288
- Route("/v1/deep-ask/stream", stream_deep_ask, methods=["POST"]),
289
247
  Route("/health", health_check, methods=["GET"]),
290
248
  ]
291
249
 
@@ -8,8 +8,7 @@ from typing import Protocol, TypeVar
8
8
  from pydantic import BaseModel
9
9
  from pydantic_graph.beta import Graph
10
10
 
11
- from haiku.rag.graph.agui.emitter import AGUIEmitter
12
- from haiku.rag.graph.agui.events import AGUIEvent
11
+ from haiku.rag.graph.agui.emitter import AGUIEmitter, AGUIEvent
13
12
 
14
13
  StateT = TypeVar("StateT", bound=BaseModel)
15
14
  ResultT = TypeVar("ResultT")
@@ -1,3 +1,6 @@
1
- from haiku.rag.graph.common.models import SearchAnswer
2
1
  from haiku.rag.graph.research.dependencies import ResearchContext, ResearchDependencies
3
- from haiku.rag.graph.research.models import EvaluationResult, ResearchReport
2
+ from haiku.rag.graph.research.models import (
3
+ EvaluationResult,
4
+ ResearchReport,
5
+ SearchAnswer,
6
+ )
@@ -1,14 +1,12 @@
1
- from collections.abc import Iterable
1
+ from typing import TYPE_CHECKING, Any
2
2
 
3
- from pydantic import BaseModel, Field, PrivateAttr
3
+ from pydantic import BaseModel, Field
4
4
 
5
5
  from haiku.rag.client import HaikuRAG
6
- from haiku.rag.graph.common.models import SearchAnswer
7
- from haiku.rag.graph.research.models import (
8
- GapRecord,
9
- InsightAnalysis,
10
- InsightRecord,
11
- )
6
+ from haiku.rag.store.models import SearchResult
7
+
8
+ if TYPE_CHECKING:
9
+ from haiku.rag.graph.research.models import SearchAnswer
12
10
 
13
11
 
14
12
  class ResearchContext(BaseModel):
@@ -18,124 +16,14 @@ class ResearchContext(BaseModel):
18
16
  sub_questions: list[str] = Field(
19
17
  default_factory=list, description="Decomposed sub-questions"
20
18
  )
21
- qa_responses: list[SearchAnswer] = Field(
19
+ qa_responses: list[Any] = Field(
22
20
  default_factory=list, description="Structured QA pairs used during research"
23
21
  )
24
- insights: list[InsightRecord] = Field(
25
- default_factory=list, description="Key insights discovered"
26
- )
27
- gaps: list[GapRecord] = Field(
28
- default_factory=list, description="Identified information gaps"
29
- )
30
-
31
- # Private dict indexes for O(1) lookups
32
- _insights_by_id: dict[str, InsightRecord] = PrivateAttr(default_factory=dict)
33
- _gaps_by_id: dict[str, GapRecord] = PrivateAttr(default_factory=dict)
34
-
35
- def model_post_init(self, __context: object) -> None:
36
- """Build indexes after initialization."""
37
- self._insights_by_id = {ins.id: ins for ins in self.insights}
38
- self._gaps_by_id = {gap.id: gap for gap in self.gaps}
39
22
 
40
- def add_qa_response(self, qa: SearchAnswer) -> None:
41
- """Add a structured QA response (minimal context already included)."""
23
+ def add_qa_response(self, qa: "SearchAnswer") -> None:
24
+ """Add a structured QA response."""
42
25
  self.qa_responses.append(qa)
43
26
 
44
- def upsert_insights(self, records: Iterable[InsightRecord]) -> list[InsightRecord]:
45
- """Merge one or more insights into the shared context with deduplication."""
46
- merged: list[InsightRecord] = []
47
-
48
- for record in records:
49
- candidate = InsightRecord.model_validate(record)
50
- existing = self._insights_by_id.get(candidate.id)
51
-
52
- if existing:
53
- # Update existing insight
54
- existing.summary = candidate.summary
55
- existing.status = candidate.status
56
- if candidate.notes:
57
- existing.notes = candidate.notes
58
- existing.supporting_sources = _merge_unique(
59
- existing.supporting_sources, candidate.supporting_sources
60
- )
61
- existing.originating_questions = _merge_unique(
62
- existing.originating_questions, candidate.originating_questions
63
- )
64
- merged.append(existing)
65
- else:
66
- # Add new insight
67
- new_insight = candidate.model_copy(deep=True)
68
- self.insights.append(new_insight)
69
- self._insights_by_id[new_insight.id] = new_insight
70
- merged.append(new_insight)
71
-
72
- return merged
73
-
74
- def upsert_gaps(self, records: Iterable[GapRecord]) -> list[GapRecord]:
75
- """Merge one or more gap records into the shared context with deduplication."""
76
- merged: list[GapRecord] = []
77
-
78
- for record in records:
79
- candidate = GapRecord.model_validate(record)
80
- existing = self._gaps_by_id.get(candidate.id)
81
-
82
- if existing:
83
- # Update existing gap
84
- existing.description = candidate.description
85
- existing.severity = candidate.severity
86
- existing.blocking = candidate.blocking
87
- existing.resolved = candidate.resolved
88
- if candidate.notes:
89
- existing.notes = candidate.notes
90
- existing.supporting_sources = _merge_unique(
91
- existing.supporting_sources, candidate.supporting_sources
92
- )
93
- existing.resolved_by = _merge_unique(
94
- existing.resolved_by, candidate.resolved_by
95
- )
96
- merged.append(existing)
97
- else:
98
- # Add new gap
99
- new_gap = candidate.model_copy(deep=True)
100
- self.gaps.append(new_gap)
101
- self._gaps_by_id[new_gap.id] = new_gap
102
- merged.append(new_gap)
103
-
104
- return merged
105
-
106
- def mark_gap_resolved(
107
- self, identifier: str, resolved_by: Iterable[str] | None = None
108
- ) -> GapRecord | None:
109
- """Mark a gap as resolved by identifier."""
110
- gap = self._gaps_by_id.get(identifier)
111
- if gap is None:
112
- return None
113
-
114
- gap.resolved = True
115
- gap.blocking = False
116
- if resolved_by:
117
- gap.resolved_by = _merge_unique(gap.resolved_by, list(resolved_by))
118
- return gap
119
-
120
- def integrate_analysis(self, analysis: InsightAnalysis) -> None:
121
- """Apply an analysis result to the shared context."""
122
- merged_insights: list[InsightRecord] = []
123
- if analysis.highlights:
124
- merged_insights = self.upsert_insights(analysis.highlights)
125
- analysis.highlights = merged_insights
126
- if analysis.gap_assessments:
127
- merged_gaps = self.upsert_gaps(analysis.gap_assessments)
128
- analysis.gap_assessments = merged_gaps
129
- if analysis.resolved_gaps:
130
- resolved_by_list = (
131
- [ins.id for ins in merged_insights] if merged_insights else None
132
- )
133
- for resolved in analysis.resolved_gaps:
134
- self.mark_gap_resolved(resolved, resolved_by=resolved_by_list)
135
- for question in analysis.new_questions:
136
- if question not in self.sub_questions:
137
- self.sub_questions.append(question)
138
-
139
27
 
140
28
  class ResearchDependencies(BaseModel):
141
29
  """Dependencies for research agents with multi-agent context."""
@@ -144,8 +32,6 @@ class ResearchDependencies(BaseModel):
144
32
 
145
33
  client: HaikuRAG = Field(description="RAG client for document operations")
146
34
  context: ResearchContext = Field(description="Shared research context")
147
-
148
-
149
- def _merge_unique(existing: list[str], incoming: Iterable[str]) -> list[str]:
150
- """Merge two iterables preserving order while removing duplicates."""
151
- return [k for k in dict.fromkeys([*existing, *incoming]) if k]
35
+ search_results: list[SearchResult] = Field(
36
+ default_factory=list, description="Search results for citation resolution"
37
+ )