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.
- haiku/rag/app.py +430 -72
- haiku/rag/chunkers/__init__.py +31 -0
- haiku/rag/chunkers/base.py +31 -0
- haiku/rag/chunkers/docling_local.py +164 -0
- haiku/rag/chunkers/docling_serve.py +179 -0
- haiku/rag/cli.py +207 -24
- haiku/rag/cli_chat.py +489 -0
- haiku/rag/client.py +1251 -266
- haiku/rag/config/__init__.py +16 -10
- haiku/rag/config/loader.py +5 -44
- haiku/rag/config/models.py +126 -17
- haiku/rag/converters/__init__.py +31 -0
- haiku/rag/converters/base.py +63 -0
- haiku/rag/converters/docling_local.py +193 -0
- haiku/rag/converters/docling_serve.py +229 -0
- haiku/rag/converters/text_utils.py +237 -0
- haiku/rag/embeddings/__init__.py +123 -24
- haiku/rag/embeddings/voyageai.py +175 -20
- haiku/rag/graph/__init__.py +0 -11
- haiku/rag/graph/agui/__init__.py +8 -2
- haiku/rag/graph/agui/cli_renderer.py +1 -1
- haiku/rag/graph/agui/emitter.py +219 -31
- haiku/rag/graph/agui/server.py +20 -62
- haiku/rag/graph/agui/stream.py +1 -2
- haiku/rag/graph/research/__init__.py +5 -2
- haiku/rag/graph/research/dependencies.py +12 -126
- haiku/rag/graph/research/graph.py +390 -135
- haiku/rag/graph/research/models.py +91 -112
- haiku/rag/graph/research/prompts.py +99 -91
- haiku/rag/graph/research/state.py +35 -27
- haiku/rag/inspector/__init__.py +8 -0
- haiku/rag/inspector/app.py +259 -0
- haiku/rag/inspector/widgets/__init__.py +6 -0
- haiku/rag/inspector/widgets/chunk_list.py +100 -0
- haiku/rag/inspector/widgets/context_modal.py +89 -0
- haiku/rag/inspector/widgets/detail_view.py +130 -0
- haiku/rag/inspector/widgets/document_list.py +75 -0
- haiku/rag/inspector/widgets/info_modal.py +209 -0
- haiku/rag/inspector/widgets/search_modal.py +183 -0
- haiku/rag/inspector/widgets/visual_modal.py +126 -0
- haiku/rag/mcp.py +106 -102
- haiku/rag/monitor.py +33 -9
- haiku/rag/providers/__init__.py +5 -0
- haiku/rag/providers/docling_serve.py +108 -0
- haiku/rag/qa/__init__.py +12 -10
- haiku/rag/qa/agent.py +43 -61
- haiku/rag/qa/prompts.py +35 -57
- haiku/rag/reranking/__init__.py +9 -6
- haiku/rag/reranking/base.py +1 -1
- haiku/rag/reranking/cohere.py +5 -4
- haiku/rag/reranking/mxbai.py +5 -2
- haiku/rag/reranking/vllm.py +3 -4
- haiku/rag/reranking/zeroentropy.py +6 -5
- haiku/rag/store/__init__.py +2 -1
- haiku/rag/store/engine.py +242 -42
- haiku/rag/store/exceptions.py +4 -0
- haiku/rag/store/models/__init__.py +8 -2
- haiku/rag/store/models/chunk.py +190 -0
- haiku/rag/store/models/document.py +46 -0
- haiku/rag/store/repositories/chunk.py +141 -121
- haiku/rag/store/repositories/document.py +25 -84
- haiku/rag/store/repositories/settings.py +11 -14
- haiku/rag/store/upgrades/__init__.py +19 -3
- haiku/rag/store/upgrades/v0_10_1.py +1 -1
- haiku/rag/store/upgrades/v0_19_6.py +65 -0
- haiku/rag/store/upgrades/v0_20_0.py +68 -0
- haiku/rag/store/upgrades/v0_23_1.py +100 -0
- haiku/rag/store/upgrades/v0_9_3.py +3 -3
- haiku/rag/utils.py +371 -146
- {haiku_rag_slim-0.16.0.dist-info → haiku_rag_slim-0.24.0.dist-info}/METADATA +15 -12
- haiku_rag_slim-0.24.0.dist-info/RECORD +78 -0
- {haiku_rag_slim-0.16.0.dist-info → haiku_rag_slim-0.24.0.dist-info}/WHEEL +1 -1
- haiku/rag/chunker.py +0 -65
- haiku/rag/embeddings/base.py +0 -25
- haiku/rag/embeddings/ollama.py +0 -28
- haiku/rag/embeddings/openai.py +0 -26
- haiku/rag/embeddings/vllm.py +0 -29
- haiku/rag/graph/agui/events.py +0 -254
- haiku/rag/graph/common/__init__.py +0 -5
- haiku/rag/graph/common/models.py +0 -42
- haiku/rag/graph/common/nodes.py +0 -265
- haiku/rag/graph/common/prompts.py +0 -46
- haiku/rag/graph/common/utils.py +0 -44
- haiku/rag/graph/deep_qa/__init__.py +0 -1
- haiku/rag/graph/deep_qa/dependencies.py +0 -27
- haiku/rag/graph/deep_qa/graph.py +0 -243
- haiku/rag/graph/deep_qa/models.py +0 -20
- haiku/rag/graph/deep_qa/prompts.py +0 -59
- haiku/rag/graph/deep_qa/state.py +0 -56
- haiku/rag/graph/research/common.py +0 -87
- haiku/rag/reader.py +0 -135
- haiku_rag_slim-0.16.0.dist-info/RECORD +0 -71
- {haiku_rag_slim-0.16.0.dist-info → haiku_rag_slim-0.24.0.dist-info}/entry_points.txt +0 -0
- {haiku_rag_slim-0.16.0.dist-info → haiku_rag_slim-0.24.0.dist-info}/licenses/LICENSE +0 -0
haiku/rag/utils.py
CHANGED
|
@@ -1,51 +1,379 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import importlib
|
|
3
|
-
import importlib.util
|
|
4
1
|
import sys
|
|
5
|
-
from
|
|
6
|
-
from functools import wraps
|
|
2
|
+
from datetime import UTC, datetime
|
|
7
3
|
from importlib import metadata
|
|
8
|
-
from io import BytesIO
|
|
9
4
|
from pathlib import Path
|
|
10
|
-
from
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
11
6
|
|
|
7
|
+
from dateutil import parser as dateutil_parser
|
|
12
8
|
from packaging.version import Version, parse
|
|
13
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from rich.console import RenderableType
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
from haiku.rag.config.models import AppConfig, ModelConfig
|
|
14
|
+
from haiku.rag.graph.research.models import Citation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_datetime(s: str) -> datetime:
|
|
18
|
+
"""Parse a datetime string into a datetime object.
|
|
19
|
+
|
|
20
|
+
Supports:
|
|
21
|
+
- ISO 8601 format: "2025-01-15T14:30:00", "2025-01-15T14:30:00Z", "2025-01-15T14:30:00+00:00"
|
|
22
|
+
- Date only: "2025-01-15" (interpreted as 00:00:00)
|
|
23
|
+
- Various other formats via dateutil
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
s: String to parse
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Parsed datetime object
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: If the string cannot be parsed
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
return dateutil_parser.parse(s)
|
|
36
|
+
except (ValueError, TypeError) as e:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Could not parse datetime: {s}. "
|
|
39
|
+
"Use ISO 8601 format (e.g., 2025-01-15T14:30:00) or date (e.g., 2025-01-15)"
|
|
40
|
+
) from e
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_utc(dt: datetime) -> datetime:
|
|
44
|
+
"""Convert a datetime to UTC.
|
|
45
|
+
|
|
46
|
+
- Naive datetimes are assumed to be local time and converted to UTC
|
|
47
|
+
- Datetimes with timezone info are converted to UTC
|
|
48
|
+
- UTC datetimes are returned as-is
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
dt: Datetime to convert
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Datetime in UTC timezone
|
|
55
|
+
"""
|
|
56
|
+
if dt.tzinfo is None:
|
|
57
|
+
# Naive datetime - assume local time
|
|
58
|
+
local_dt = dt.astimezone() # Adds local timezone
|
|
59
|
+
return local_dt.astimezone(UTC)
|
|
60
|
+
elif dt.tzinfo == UTC:
|
|
61
|
+
return dt
|
|
62
|
+
else:
|
|
63
|
+
return dt.astimezone(UTC)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def apply_common_settings(
|
|
67
|
+
settings: Any | None,
|
|
68
|
+
settings_class: type[Any],
|
|
69
|
+
model_config: Any,
|
|
70
|
+
) -> Any | None:
|
|
71
|
+
"""Apply common settings (temperature, max_tokens) to model settings.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
settings: Existing settings instance or None
|
|
75
|
+
settings_class: Settings class to instantiate if needed
|
|
76
|
+
model_config: ModelConfig with temperature and max_tokens
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Updated settings instance or None if no settings to apply
|
|
80
|
+
"""
|
|
81
|
+
if model_config.temperature is None and model_config.max_tokens is None:
|
|
82
|
+
return settings
|
|
83
|
+
|
|
84
|
+
if settings is None:
|
|
85
|
+
settings_dict = settings_class()
|
|
86
|
+
else:
|
|
87
|
+
settings_dict = settings
|
|
88
|
+
|
|
89
|
+
if model_config.temperature is not None:
|
|
90
|
+
settings_dict["temperature"] = model_config.temperature
|
|
91
|
+
|
|
92
|
+
if model_config.max_tokens is not None:
|
|
93
|
+
settings_dict["max_tokens"] = model_config.max_tokens
|
|
94
|
+
|
|
95
|
+
return settings_dict
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_model(
|
|
99
|
+
model_config: "ModelConfig",
|
|
100
|
+
app_config: "AppConfig | None" = None,
|
|
101
|
+
) -> Any:
|
|
16
102
|
"""
|
|
17
|
-
|
|
18
|
-
and always executes after the last call.
|
|
103
|
+
Get a model instance for the specified configuration.
|
|
19
104
|
|
|
20
105
|
Args:
|
|
21
|
-
|
|
106
|
+
model_config: ModelConfig with provider, model, and settings
|
|
107
|
+
app_config: AppConfig for provider base URLs (defaults to global Config)
|
|
22
108
|
|
|
23
109
|
Returns:
|
|
24
|
-
|
|
110
|
+
A configured model instance
|
|
25
111
|
"""
|
|
112
|
+
from pydantic_ai.models.openai import OpenAIChatModel, OpenAIChatModelSettings
|
|
113
|
+
from pydantic_ai.providers.ollama import OllamaProvider
|
|
114
|
+
from pydantic_ai.providers.openai import OpenAIProvider
|
|
115
|
+
|
|
116
|
+
if app_config is None:
|
|
117
|
+
from haiku.rag.config import Config
|
|
118
|
+
|
|
119
|
+
app_config = Config
|
|
120
|
+
|
|
121
|
+
provider = model_config.provider
|
|
122
|
+
model = model_config.name
|
|
123
|
+
|
|
124
|
+
if provider == "ollama":
|
|
125
|
+
model_settings = None
|
|
126
|
+
|
|
127
|
+
# Apply thinking control for gpt-oss
|
|
128
|
+
if model == "gpt-oss" and model_config.enable_thinking is not None:
|
|
129
|
+
if model_config.enable_thinking is False:
|
|
130
|
+
model_settings = OpenAIChatModelSettings(openai_reasoning_effort="low")
|
|
131
|
+
else:
|
|
132
|
+
model_settings = OpenAIChatModelSettings(openai_reasoning_effort="high")
|
|
133
|
+
|
|
134
|
+
model_settings = apply_common_settings(
|
|
135
|
+
model_settings, OpenAIChatModelSettings, model_config
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Use model-level base_url if set, otherwise fall back to providers config
|
|
139
|
+
base_url = model_config.base_url or f"{app_config.providers.ollama.base_url}/v1"
|
|
140
|
+
|
|
141
|
+
return OpenAIChatModel(
|
|
142
|
+
model_name=model,
|
|
143
|
+
provider=OllamaProvider(base_url=base_url),
|
|
144
|
+
settings=model_settings,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
elif provider == "openai":
|
|
148
|
+
openai_settings: Any = None
|
|
149
|
+
|
|
150
|
+
# Apply thinking control
|
|
151
|
+
if model_config.enable_thinking is not None:
|
|
152
|
+
if model_config.enable_thinking is False:
|
|
153
|
+
openai_settings = OpenAIChatModelSettings(openai_reasoning_effort="low")
|
|
154
|
+
else:
|
|
155
|
+
openai_settings = OpenAIChatModelSettings(
|
|
156
|
+
openai_reasoning_effort="high"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
openai_settings = apply_common_settings(
|
|
160
|
+
openai_settings, OpenAIChatModelSettings, model_config
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Use model-level base_url if set (for vLLM, LM Studio, etc.)
|
|
164
|
+
if model_config.base_url:
|
|
165
|
+
return OpenAIChatModel(
|
|
166
|
+
model_name=model,
|
|
167
|
+
provider=OpenAIProvider(base_url=model_config.base_url),
|
|
168
|
+
settings=openai_settings,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return OpenAIChatModel(model_name=model, settings=openai_settings)
|
|
172
|
+
|
|
173
|
+
elif provider == "anthropic":
|
|
174
|
+
from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings
|
|
175
|
+
|
|
176
|
+
anthropic_settings: Any = None
|
|
177
|
+
|
|
178
|
+
# Apply thinking control
|
|
179
|
+
if model_config.enable_thinking is not None:
|
|
180
|
+
if model_config.enable_thinking:
|
|
181
|
+
anthropic_settings = AnthropicModelSettings(
|
|
182
|
+
anthropic_thinking={"type": "enabled", "budget_tokens": 4096}
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
anthropic_settings = AnthropicModelSettings(
|
|
186
|
+
anthropic_thinking={"type": "disabled"}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
anthropic_settings = apply_common_settings(
|
|
190
|
+
anthropic_settings, AnthropicModelSettings, model_config
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return AnthropicModel(model_name=model, settings=anthropic_settings)
|
|
194
|
+
|
|
195
|
+
elif provider == "gemini":
|
|
196
|
+
from pydantic_ai.models.google import GoogleModel, GoogleModelSettings
|
|
197
|
+
|
|
198
|
+
gemini_settings: Any = None
|
|
26
199
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
200
|
+
# Apply thinking control
|
|
201
|
+
if model_config.enable_thinking is not None:
|
|
202
|
+
gemini_settings = GoogleModelSettings(
|
|
203
|
+
google_thinking_config={
|
|
204
|
+
"include_thoughts": model_config.enable_thinking
|
|
205
|
+
}
|
|
206
|
+
)
|
|
30
207
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
last_call = asyncio.get_event_loop().time()
|
|
208
|
+
gemini_settings = apply_common_settings(
|
|
209
|
+
gemini_settings, GoogleModelSettings, model_config
|
|
210
|
+
)
|
|
35
211
|
|
|
36
|
-
|
|
37
|
-
task.cancel()
|
|
212
|
+
return GoogleModel(model_name=model, settings=gemini_settings)
|
|
38
213
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if asyncio.get_event_loop().time() - last_call >= wait: # type: ignore
|
|
42
|
-
await func(*args, **kwargs)
|
|
214
|
+
elif provider == "groq":
|
|
215
|
+
from pydantic_ai.models.groq import GroqModel, GroqModelSettings
|
|
43
216
|
|
|
44
|
-
|
|
217
|
+
groq_settings: Any = None
|
|
45
218
|
|
|
46
|
-
|
|
219
|
+
# Apply thinking control
|
|
220
|
+
if model_config.enable_thinking is not None:
|
|
221
|
+
if model_config.enable_thinking:
|
|
222
|
+
groq_settings = GroqModelSettings(groq_reasoning_format="parsed")
|
|
223
|
+
else:
|
|
224
|
+
groq_settings = GroqModelSettings(groq_reasoning_format="hidden")
|
|
47
225
|
|
|
48
|
-
|
|
226
|
+
groq_settings = apply_common_settings(
|
|
227
|
+
groq_settings, GroqModelSettings, model_config
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return GroqModel(model_name=model, settings=groq_settings)
|
|
231
|
+
|
|
232
|
+
elif provider == "bedrock":
|
|
233
|
+
from pydantic_ai.models.bedrock import (
|
|
234
|
+
BedrockConverseModel,
|
|
235
|
+
BedrockModelSettings,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
bedrock_settings: Any = None
|
|
239
|
+
|
|
240
|
+
# Apply thinking control for Claude models
|
|
241
|
+
if model_config.enable_thinking is not None:
|
|
242
|
+
additional_fields: dict[str, Any] = {}
|
|
243
|
+
if model.startswith("anthropic.claude"):
|
|
244
|
+
if model_config.enable_thinking:
|
|
245
|
+
additional_fields = {
|
|
246
|
+
"thinking": {"type": "enabled", "budget_tokens": 4096}
|
|
247
|
+
}
|
|
248
|
+
else:
|
|
249
|
+
additional_fields = {"thinking": {"type": "disabled"}}
|
|
250
|
+
elif "gpt" in model or "o1" in model or "o3" in model:
|
|
251
|
+
# OpenAI models on Bedrock
|
|
252
|
+
additional_fields = {
|
|
253
|
+
"reasoning_effort": "high"
|
|
254
|
+
if model_config.enable_thinking
|
|
255
|
+
else "low"
|
|
256
|
+
}
|
|
257
|
+
elif "qwen" in model:
|
|
258
|
+
# Qwen models on Bedrock
|
|
259
|
+
additional_fields = {
|
|
260
|
+
"reasoning_config": "high"
|
|
261
|
+
if model_config.enable_thinking
|
|
262
|
+
else "low"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if additional_fields:
|
|
266
|
+
bedrock_settings = BedrockModelSettings(
|
|
267
|
+
bedrock_additional_model_requests_fields=additional_fields
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
bedrock_settings = apply_common_settings(
|
|
271
|
+
bedrock_settings, BedrockModelSettings, model_config
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return BedrockConverseModel(model_name=model, settings=bedrock_settings)
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
# For any other provider, use string format and let Pydantic AI handle it
|
|
278
|
+
return f"{provider}:{model}"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def format_bytes(num_bytes: int) -> str:
|
|
282
|
+
"""Format bytes as human-readable string."""
|
|
283
|
+
size = float(num_bytes)
|
|
284
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
285
|
+
if size < 1024.0:
|
|
286
|
+
return f"{size:.1f} {unit}"
|
|
287
|
+
size /= 1024.0
|
|
288
|
+
return f"{size:.1f} PB"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def format_citations(citations: "list[Citation]") -> str:
|
|
292
|
+
"""Format citations as plain text with preserved formatting.
|
|
293
|
+
|
|
294
|
+
Used by things like the MCP server where Rich renderables are not available.
|
|
295
|
+
"""
|
|
296
|
+
if not citations:
|
|
297
|
+
return ""
|
|
298
|
+
|
|
299
|
+
lines = ["## Citations\n"]
|
|
300
|
+
|
|
301
|
+
for c in citations:
|
|
302
|
+
# Header line
|
|
303
|
+
header = f"[{c.document_id}:{c.chunk_id}]"
|
|
304
|
+
|
|
305
|
+
# Location info
|
|
306
|
+
location_parts = []
|
|
307
|
+
if c.page_numbers:
|
|
308
|
+
if len(c.page_numbers) == 1:
|
|
309
|
+
location_parts.append(f"p. {c.page_numbers[0]}")
|
|
310
|
+
else:
|
|
311
|
+
location_parts.append(f"pp. {c.page_numbers[0]}-{c.page_numbers[-1]}")
|
|
312
|
+
if c.headings:
|
|
313
|
+
location_parts.append(f"Section: {c.headings[-1]}")
|
|
314
|
+
|
|
315
|
+
source = c.document_uri
|
|
316
|
+
if c.document_title:
|
|
317
|
+
source = f"{c.document_title} ({c.document_uri})"
|
|
318
|
+
if location_parts:
|
|
319
|
+
source += f" - {', '.join(location_parts)}"
|
|
320
|
+
|
|
321
|
+
lines.append(f"{header} {source}")
|
|
322
|
+
lines.append(c.content)
|
|
323
|
+
lines.append("")
|
|
324
|
+
|
|
325
|
+
return "\n".join(lines)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def format_citations_rich(citations: "list[Citation]") -> "list[RenderableType]":
|
|
329
|
+
"""Format citations as Rich renderables.
|
|
330
|
+
|
|
331
|
+
Returns a list of Rich Panel objects for direct console printing,
|
|
332
|
+
with content rendered as markdown for syntax highlighting.
|
|
333
|
+
"""
|
|
334
|
+
from rich.markdown import Markdown
|
|
335
|
+
from rich.panel import Panel
|
|
336
|
+
from rich.text import Text
|
|
337
|
+
|
|
338
|
+
if not citations:
|
|
339
|
+
return []
|
|
340
|
+
|
|
341
|
+
renderables: list[RenderableType] = []
|
|
342
|
+
renderables.append(Text("Citations", style="bold"))
|
|
343
|
+
|
|
344
|
+
for c in citations:
|
|
345
|
+
# Build header with IDs
|
|
346
|
+
header = Text()
|
|
347
|
+
header.append("doc: ", style="dim")
|
|
348
|
+
header.append(c.document_id, style="cyan")
|
|
349
|
+
header.append(" chunk: ", style="dim")
|
|
350
|
+
header.append(c.chunk_id, style="cyan")
|
|
351
|
+
|
|
352
|
+
# Location info for subtitle
|
|
353
|
+
location_parts = []
|
|
354
|
+
if c.page_numbers:
|
|
355
|
+
if len(c.page_numbers) == 1:
|
|
356
|
+
location_parts.append(f"p. {c.page_numbers[0]}")
|
|
357
|
+
else:
|
|
358
|
+
location_parts.append(f"pp. {c.page_numbers[0]}-{c.page_numbers[-1]}")
|
|
359
|
+
if c.headings:
|
|
360
|
+
location_parts.append(f"Section: {c.headings[-1]}")
|
|
361
|
+
|
|
362
|
+
subtitle = c.document_uri
|
|
363
|
+
if c.document_title:
|
|
364
|
+
subtitle = f"{c.document_title} ({c.document_uri})"
|
|
365
|
+
if location_parts:
|
|
366
|
+
subtitle += f" - {', '.join(location_parts)}"
|
|
367
|
+
panel = Panel(
|
|
368
|
+
Markdown(c.content),
|
|
369
|
+
title=header,
|
|
370
|
+
subtitle=subtitle,
|
|
371
|
+
subtitle_align="left",
|
|
372
|
+
border_style="dim",
|
|
373
|
+
)
|
|
374
|
+
renderables.append(panel)
|
|
375
|
+
|
|
376
|
+
return renderables
|
|
49
377
|
|
|
50
378
|
|
|
51
379
|
def get_default_data_dir() -> Path:
|
|
@@ -70,6 +398,21 @@ def get_default_data_dir() -> Path:
|
|
|
70
398
|
return data_path
|
|
71
399
|
|
|
72
400
|
|
|
401
|
+
def build_prompt(base_prompt: str, config: "AppConfig") -> str:
|
|
402
|
+
"""Build a prompt with domain_preamble prepended if configured.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
base_prompt: The base prompt to use
|
|
406
|
+
config: AppConfig with prompts.domain_preamble
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Prompt with domain_preamble prepended if configured
|
|
410
|
+
"""
|
|
411
|
+
if config.prompts.domain_preamble:
|
|
412
|
+
return f"{config.prompts.domain_preamble}\n\n{base_prompt}"
|
|
413
|
+
return base_prompt
|
|
414
|
+
|
|
415
|
+
|
|
73
416
|
async def is_up_to_date() -> tuple[bool, Version, Version]:
|
|
74
417
|
"""Check whether haiku.rag is current.
|
|
75
418
|
|
|
@@ -91,121 +434,3 @@ async def is_up_to_date() -> tuple[bool, Version, Version]:
|
|
|
91
434
|
# If no network connection, do not raise alarms.
|
|
92
435
|
pypi_version = running_version
|
|
93
436
|
return running_version >= pypi_version, running_version, pypi_version
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def text_to_docling_document(text: str, name: str = "content.md"):
|
|
97
|
-
"""Convert text content to a DoclingDocument.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
text: The text content to convert.
|
|
101
|
-
name: The name to use for the document stream (defaults to "content.md").
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
A DoclingDocument created from the text content.
|
|
105
|
-
"""
|
|
106
|
-
try:
|
|
107
|
-
import docling # noqa: F401
|
|
108
|
-
except ImportError as e:
|
|
109
|
-
raise ImportError(
|
|
110
|
-
"Docling is required for document conversion. "
|
|
111
|
-
"Install with: pip install haiku.rag-slim[docling]"
|
|
112
|
-
) from e
|
|
113
|
-
|
|
114
|
-
from docling.document_converter import DocumentConverter
|
|
115
|
-
from docling_core.types.io import DocumentStream
|
|
116
|
-
|
|
117
|
-
bytes_io = BytesIO(text.encode("utf-8"))
|
|
118
|
-
doc_stream = DocumentStream(name=name, stream=bytes_io)
|
|
119
|
-
converter = DocumentConverter()
|
|
120
|
-
result = converter.convert(doc_stream)
|
|
121
|
-
return result.document
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def load_callable(path: str):
|
|
125
|
-
"""Load a callable from a dotted path or file path.
|
|
126
|
-
|
|
127
|
-
Supported formats:
|
|
128
|
-
- "package.module:func" or "package.module.func"
|
|
129
|
-
- "path/to/file.py:func"
|
|
130
|
-
|
|
131
|
-
Returns the loaded callable. Raises ValueError on failure.
|
|
132
|
-
"""
|
|
133
|
-
if not path:
|
|
134
|
-
raise ValueError("Empty callable path provided")
|
|
135
|
-
|
|
136
|
-
module_part = None
|
|
137
|
-
func_name = None
|
|
138
|
-
|
|
139
|
-
if ":" in path:
|
|
140
|
-
module_part, func_name = path.split(":", 1)
|
|
141
|
-
else:
|
|
142
|
-
# split by last dot for module.attr
|
|
143
|
-
if "." in path:
|
|
144
|
-
module_part, func_name = path.rsplit(".", 1)
|
|
145
|
-
else:
|
|
146
|
-
raise ValueError(
|
|
147
|
-
"Invalid callable path format. Use 'module:func' or 'module.func' or 'file.py:func'."
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# Try file path first
|
|
151
|
-
mod: ModuleType | None = None
|
|
152
|
-
module_path = Path(module_part)
|
|
153
|
-
if module_path.suffix == ".py" and module_path.exists():
|
|
154
|
-
spec = importlib.util.spec_from_file_location(module_path.stem, module_path)
|
|
155
|
-
if spec and spec.loader:
|
|
156
|
-
mod = importlib.util.module_from_spec(spec)
|
|
157
|
-
spec.loader.exec_module(mod)
|
|
158
|
-
else:
|
|
159
|
-
# Import as a module path
|
|
160
|
-
try:
|
|
161
|
-
mod = importlib.import_module(module_part)
|
|
162
|
-
except Exception as e:
|
|
163
|
-
raise ValueError(f"Failed to import module '{module_part}': {e}")
|
|
164
|
-
|
|
165
|
-
if not hasattr(mod, func_name):
|
|
166
|
-
raise ValueError(f"Callable '{func_name}' not found in module '{module_part}'")
|
|
167
|
-
func = getattr(mod, func_name)
|
|
168
|
-
if not callable(func):
|
|
169
|
-
raise ValueError(
|
|
170
|
-
f"Attribute '{func_name}' in module '{module_part}' is not callable"
|
|
171
|
-
)
|
|
172
|
-
return func
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def prefetch_models():
|
|
176
|
-
"""Prefetch runtime models (Docling + Ollama as configured)."""
|
|
177
|
-
import httpx
|
|
178
|
-
|
|
179
|
-
from haiku.rag.config import Config
|
|
180
|
-
|
|
181
|
-
try:
|
|
182
|
-
from docling.utils.model_downloader import download_models
|
|
183
|
-
|
|
184
|
-
download_models()
|
|
185
|
-
except ImportError:
|
|
186
|
-
# Docling not installed, skip downloading docling models
|
|
187
|
-
pass
|
|
188
|
-
|
|
189
|
-
# Collect Ollama models from config
|
|
190
|
-
required_models: set[str] = set()
|
|
191
|
-
if Config.embeddings.provider == "ollama":
|
|
192
|
-
required_models.add(Config.embeddings.model)
|
|
193
|
-
if Config.qa.provider == "ollama":
|
|
194
|
-
required_models.add(Config.qa.model)
|
|
195
|
-
if Config.research.provider == "ollama":
|
|
196
|
-
required_models.add(Config.research.model)
|
|
197
|
-
if Config.reranking.provider == "ollama":
|
|
198
|
-
required_models.add(Config.reranking.model)
|
|
199
|
-
|
|
200
|
-
if not required_models:
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
base_url = Config.providers.ollama.base_url
|
|
204
|
-
|
|
205
|
-
with httpx.Client(timeout=None) as client:
|
|
206
|
-
for model in sorted(required_models):
|
|
207
|
-
with client.stream(
|
|
208
|
-
"POST", f"{base_url}/api/pull", json={"model": model}
|
|
209
|
-
) as r:
|
|
210
|
-
for _ in r.iter_lines():
|
|
211
|
-
pass
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: haiku.rag-slim
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.24.0
|
|
4
|
+
Summary: Opinionated agentic RAG powered by LanceDB, Pydantic AI, and Docling - Minimal dependencies
|
|
5
5
|
Author-email: Yiorgis Gozadinos <ggozadinos@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
License-File: LICENSE
|
|
@@ -17,16 +17,15 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
|
-
Requires-Dist: docling-core==2.
|
|
20
|
+
Requires-Dist: docling-core==2.57.0
|
|
21
21
|
Requires-Dist: httpx>=0.28.1
|
|
22
|
-
Requires-Dist: lancedb==0.
|
|
22
|
+
Requires-Dist: lancedb==0.26.0
|
|
23
23
|
Requires-Dist: pathspec>=0.12.1
|
|
24
|
-
Requires-Dist: pydantic-ai-slim[ag-ui,fastmcp,logfire,openai]>=1.
|
|
25
|
-
Requires-Dist: pydantic>=2.12.
|
|
24
|
+
Requires-Dist: pydantic-ai-slim[ag-ui,fastmcp,logfire,openai]>=1.39.0
|
|
25
|
+
Requires-Dist: pydantic>=2.12.5
|
|
26
26
|
Requires-Dist: python-dotenv>=1.2.1
|
|
27
27
|
Requires-Dist: pyyaml>=6.0.3
|
|
28
28
|
Requires-Dist: rich>=14.2.0
|
|
29
|
-
Requires-Dist: tiktoken>=0.12.0
|
|
30
29
|
Requires-Dist: typer<0.20.0,>=0.19.2
|
|
31
30
|
Requires-Dist: watchfiles>=1.1.1
|
|
32
31
|
Provides-Extra: anthropic
|
|
@@ -34,13 +33,17 @@ Requires-Dist: pydantic-ai-slim[anthropic]; extra == 'anthropic'
|
|
|
34
33
|
Provides-Extra: bedrock
|
|
35
34
|
Requires-Dist: pydantic-ai-slim[bedrock]; extra == 'bedrock'
|
|
36
35
|
Provides-Extra: cohere
|
|
37
|
-
Requires-Dist: cohere>=5.
|
|
36
|
+
Requires-Dist: cohere>=5.20.1; extra == 'cohere'
|
|
38
37
|
Provides-Extra: docling
|
|
39
|
-
Requires-Dist: docling==2.
|
|
38
|
+
Requires-Dist: docling==2.65.0; extra == 'docling'
|
|
39
|
+
Requires-Dist: opencv-python-headless>=4.12.0.88; extra == 'docling'
|
|
40
40
|
Provides-Extra: google
|
|
41
41
|
Requires-Dist: pydantic-ai-slim[google]; extra == 'google'
|
|
42
42
|
Provides-Extra: groq
|
|
43
43
|
Requires-Dist: pydantic-ai-slim[groq]; extra == 'groq'
|
|
44
|
+
Provides-Extra: inspector
|
|
45
|
+
Requires-Dist: textual-image>=0.8.5; extra == 'inspector'
|
|
46
|
+
Requires-Dist: textual>=7.0.0; extra == 'inspector'
|
|
44
47
|
Provides-Extra: mistral
|
|
45
48
|
Requires-Dist: pydantic-ai-slim[mistral]; extra == 'mistral'
|
|
46
49
|
Provides-Extra: mxbai
|
|
@@ -48,14 +51,14 @@ Requires-Dist: mxbai-rerank>=0.1.6; extra == 'mxbai'
|
|
|
48
51
|
Provides-Extra: vertexai
|
|
49
52
|
Requires-Dist: pydantic-ai-slim[vertexai]; extra == 'vertexai'
|
|
50
53
|
Provides-Extra: voyageai
|
|
51
|
-
Requires-Dist: voyageai>=0.3.
|
|
54
|
+
Requires-Dist: voyageai>=0.3.7; extra == 'voyageai'
|
|
52
55
|
Provides-Extra: zeroentropy
|
|
53
|
-
Requires-Dist: zeroentropy>=0.1.
|
|
56
|
+
Requires-Dist: zeroentropy>=0.1.0a7; extra == 'zeroentropy'
|
|
54
57
|
Description-Content-Type: text/markdown
|
|
55
58
|
|
|
56
59
|
# haiku.rag-slim
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
Opinionated agentic RAG powered by LanceDB, Pydantic AI, and Docling - Core package with minimal dependencies.
|
|
59
62
|
|
|
60
63
|
`haiku.rag-slim` is the core package for users who want to install only the dependencies they need. Document processing (docling), and reranker support are all optional extras.
|
|
61
64
|
|