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
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 collections.abc import Callable
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 types import ModuleType
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
- def debounce(wait: float) -> Callable:
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
- A decorator to debounce a function, ensuring it is called only after a specified delay
18
- and always executes after the last call.
103
+ Get a model instance for the specified configuration.
19
104
 
20
105
  Args:
21
- wait (float): The debounce delay in seconds.
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
- Callable: The decorated function.
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
- def decorator(func: Callable) -> Callable:
28
- last_call = None
29
- task = None
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
- @wraps(func)
32
- async def debounced(*args, **kwargs):
33
- nonlocal last_call, task
34
- last_call = asyncio.get_event_loop().time()
208
+ gemini_settings = apply_common_settings(
209
+ gemini_settings, GoogleModelSettings, model_config
210
+ )
35
211
 
36
- if task:
37
- task.cancel()
212
+ return GoogleModel(model_name=model, settings=gemini_settings)
38
213
 
39
- async def call_func():
40
- await asyncio.sleep(wait)
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
- task = asyncio.create_task(call_func())
217
+ groq_settings: Any = None
45
218
 
46
- return debounced
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
- return decorator
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.16.0
4
- Summary: Agentic Retrieval Augmented Generation (RAG) with LanceDB - Minimal dependencies
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.50.1
20
+ Requires-Dist: docling-core==2.57.0
21
21
  Requires-Dist: httpx>=0.28.1
22
- Requires-Dist: lancedb==0.25.2
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.11.1
25
- Requires-Dist: pydantic>=2.12.3
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.0.0; extra == 'cohere'
36
+ Requires-Dist: cohere>=5.20.1; extra == 'cohere'
38
37
  Provides-Extra: docling
39
- Requires-Dist: docling==2.61.1; extra == 'docling'
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.5; extra == 'voyageai'
54
+ Requires-Dist: voyageai>=0.3.7; extra == 'voyageai'
52
55
  Provides-Extra: zeroentropy
53
- Requires-Dist: zeroentropy>=0.1.0a6; extra == 'zeroentropy'
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
- Retrieval-Augmented Generation (RAG) library built on LanceDB - Core package with minimal dependencies.
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