ollama-haystack 2.4.0__tar.gz → 2.4.2__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.
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/CHANGELOG.md +34 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/PKG-INFO +1 -1
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/generators/ollama/chat/chat_generator.py +30 -10
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/generators/ollama/generator.py +17 -6
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/test_chat_generator.py +68 -3
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/test_generator.py +24 -2
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/.gitignore +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/LICENSE.txt +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/README.md +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/examples/chat_generator_example.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/examples/embedders_example.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/examples/generator_example.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/pydoc/config.yml +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/pyproject.toml +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/embedders/ollama/__init__.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/embedders/ollama/document_embedder.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/embedders/ollama/text_embedder.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/generators/ollama/__init__.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/generators/ollama/chat/__init__.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/__init__.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/test_document_embedder.py +0 -0
- {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/test_text_embedder.py +0 -0
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/ollama-v2.4.1] - 2025-04-10
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- Add `streaming_callback` to run methods of OllamaGenerator and OllamaChatGenerator (#1636)
|
|
8
|
+
|
|
9
|
+
### 🧹 Chores
|
|
10
|
+
|
|
11
|
+
- Update ChatGenerators with `deserialize_tools_or_toolset_inplace` (#1623)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [integrations/ollama-v2.4.0] - 2025-04-04
|
|
15
|
+
|
|
16
|
+
### 🚀 Features
|
|
17
|
+
|
|
18
|
+
- Adapt Ollama metadata to OpenAI format; support Ollama in Langfuse (#1577)
|
|
19
|
+
|
|
20
|
+
### 🧪 Testing
|
|
21
|
+
|
|
22
|
+
- Update tool serialization in tests to include `inputs_from_state` and `outputs_to_state` (#1581)
|
|
23
|
+
|
|
24
|
+
### ⚙️ CI
|
|
25
|
+
|
|
26
|
+
- Review testing workflows (#1541)
|
|
27
|
+
|
|
28
|
+
### 🧹 Chores
|
|
29
|
+
|
|
30
|
+
- Remove Python 3.8 support (#1421)
|
|
31
|
+
|
|
32
|
+
### 🌀 Miscellaneous
|
|
33
|
+
|
|
34
|
+
- Chore: remove `jsonschema` dependency from `default` environment (#1368)
|
|
35
|
+
- Test: Update tests to check for `outputs_to_string` in Tool when running haystack-ai>=2.12 (#1585)
|
|
36
|
+
|
|
3
37
|
## [integrations/ollama-v2.3.0] - 2025-01-29
|
|
4
38
|
|
|
5
39
|
### 🚀 Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ollama-haystack
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: An integration between the Ollama LLM framework and Haystack
|
|
5
5
|
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/ollama#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
@@ -2,8 +2,15 @@ from typing import Any, Callable, Dict, List, Literal, Optional, Union
|
|
|
2
2
|
|
|
3
3
|
from haystack import component, default_from_dict, default_to_dict
|
|
4
4
|
from haystack.dataclasses import ChatMessage, StreamingChunk, ToolCall
|
|
5
|
-
from haystack.tools import Tool, _check_duplicate_tool_names
|
|
5
|
+
from haystack.tools import Tool, _check_duplicate_tool_names
|
|
6
6
|
from haystack.utils.callable_serialization import deserialize_callable, serialize_callable
|
|
7
|
+
|
|
8
|
+
# Compatibility with Haystack 2.12.0 and 2.13.0 - remove after 2.13.0 is released
|
|
9
|
+
try:
|
|
10
|
+
from haystack.tools import deserialize_tools_or_toolset_inplace
|
|
11
|
+
except ImportError:
|
|
12
|
+
from haystack.tools import deserialize_tools_inplace as deserialize_tools_or_toolset_inplace
|
|
13
|
+
|
|
7
14
|
from pydantic.json_schema import JsonSchemaValue
|
|
8
15
|
|
|
9
16
|
from ollama import ChatResponse, Client
|
|
@@ -230,7 +237,7 @@ class OllamaChatGenerator:
|
|
|
230
237
|
:returns:
|
|
231
238
|
Deserialized component.
|
|
232
239
|
"""
|
|
233
|
-
|
|
240
|
+
deserialize_tools_or_toolset_inplace(data["init_parameters"], key="tools")
|
|
234
241
|
|
|
235
242
|
init_params = data.get("init_parameters", {})
|
|
236
243
|
|
|
@@ -251,7 +258,9 @@ class OllamaChatGenerator:
|
|
|
251
258
|
chunk_message = StreamingChunk(content, meta)
|
|
252
259
|
return chunk_message
|
|
253
260
|
|
|
254
|
-
def _handle_streaming_response(
|
|
261
|
+
def _handle_streaming_response(
|
|
262
|
+
self, response: Any, streaming_callback: Optional[Callable[[StreamingChunk], None]]
|
|
263
|
+
) -> Dict[str, List[Any]]:
|
|
255
264
|
"""
|
|
256
265
|
Handles streaming response and converts it to Haystack format
|
|
257
266
|
"""
|
|
@@ -259,13 +268,19 @@ class OllamaChatGenerator:
|
|
|
259
268
|
for chunk in response:
|
|
260
269
|
chunk_delta = self._build_chunk(chunk)
|
|
261
270
|
chunks.append(chunk_delta)
|
|
262
|
-
if
|
|
263
|
-
|
|
271
|
+
if streaming_callback is not None:
|
|
272
|
+
streaming_callback(chunk_delta)
|
|
273
|
+
|
|
274
|
+
# Create the ChatMessage with the combined content
|
|
275
|
+
combined_text = "".join([c.content for c in chunks])
|
|
276
|
+
reply = ChatMessage.from_assistant(combined_text)
|
|
264
277
|
|
|
265
|
-
|
|
266
|
-
|
|
278
|
+
# Convert the last chunk's metadata to OpenAI format and attach it to the ChatMessage
|
|
279
|
+
if chunks:
|
|
280
|
+
meta = _convert_ollama_meta_to_openai_format(chunks[-1].meta)
|
|
281
|
+
reply._meta = meta
|
|
267
282
|
|
|
268
|
-
return {"replies":
|
|
283
|
+
return {"replies": [reply]}
|
|
269
284
|
|
|
270
285
|
@component.output_types(replies=List[ChatMessage])
|
|
271
286
|
def run(
|
|
@@ -273,6 +288,8 @@ class OllamaChatGenerator:
|
|
|
273
288
|
messages: List[ChatMessage],
|
|
274
289
|
generation_kwargs: Optional[Dict[str, Any]] = None,
|
|
275
290
|
tools: Optional[List[Tool]] = None,
|
|
291
|
+
*,
|
|
292
|
+
streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
|
|
276
293
|
):
|
|
277
294
|
"""
|
|
278
295
|
Runs an Ollama Model on a given chat history.
|
|
@@ -286,12 +303,15 @@ class OllamaChatGenerator:
|
|
|
286
303
|
:param tools:
|
|
287
304
|
A list of tools for which the model can prepare calls. If set, it will override the `tools` parameter set
|
|
288
305
|
during component initialization.
|
|
306
|
+
:param streaming_callback:
|
|
307
|
+
A callback function that is called when a new token is received from the stream.
|
|
289
308
|
:returns: A dictionary with the following keys:
|
|
290
309
|
- `replies`: The responses from the model
|
|
291
310
|
"""
|
|
292
311
|
generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
|
|
312
|
+
resolved_streaming_callback = streaming_callback or self.streaming_callback
|
|
293
313
|
|
|
294
|
-
stream =
|
|
314
|
+
stream = resolved_streaming_callback is not None
|
|
295
315
|
tools = tools or self.tools
|
|
296
316
|
_check_duplicate_tool_names(tools)
|
|
297
317
|
|
|
@@ -321,6 +341,6 @@ class OllamaChatGenerator:
|
|
|
321
341
|
)
|
|
322
342
|
|
|
323
343
|
if stream:
|
|
324
|
-
return self._handle_streaming_response(response)
|
|
344
|
+
return self._handle_streaming_response(response, resolved_streaming_callback)
|
|
325
345
|
|
|
326
346
|
return {"replies": [_convert_ollama_response_to_chatmessage(response)]}
|
|
@@ -137,7 +137,9 @@ class OllamaGenerator:
|
|
|
137
137
|
|
|
138
138
|
return {"replies": replies, "meta": [meta]}
|
|
139
139
|
|
|
140
|
-
def _handle_streaming_response(
|
|
140
|
+
def _handle_streaming_response(
|
|
141
|
+
self, response: Any, streaming_callback: Optional[Callable[[StreamingChunk], None]]
|
|
142
|
+
) -> List[StreamingChunk]:
|
|
141
143
|
"""
|
|
142
144
|
Handles Streaming response cases
|
|
143
145
|
"""
|
|
@@ -145,8 +147,8 @@ class OllamaGenerator:
|
|
|
145
147
|
for chunk in response:
|
|
146
148
|
chunk_delta: StreamingChunk = self._build_chunk(chunk)
|
|
147
149
|
chunks.append(chunk_delta)
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
+
if streaming_callback is not None:
|
|
151
|
+
streaming_callback(chunk_delta)
|
|
150
152
|
return chunks
|
|
151
153
|
|
|
152
154
|
def _build_chunk(self, chunk_response: Any) -> StreamingChunk:
|
|
@@ -165,6 +167,8 @@ class OllamaGenerator:
|
|
|
165
167
|
self,
|
|
166
168
|
prompt: str,
|
|
167
169
|
generation_kwargs: Optional[Dict[str, Any]] = None,
|
|
170
|
+
*,
|
|
171
|
+
streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
|
|
168
172
|
):
|
|
169
173
|
"""
|
|
170
174
|
Runs an Ollama Model on the given prompt.
|
|
@@ -175,20 +179,27 @@ class OllamaGenerator:
|
|
|
175
179
|
Optional arguments to pass to the Ollama generation endpoint, such as temperature,
|
|
176
180
|
top_p, and others. See the available arguments in
|
|
177
181
|
[Ollama docs](https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values).
|
|
182
|
+
:param streaming_callback:
|
|
183
|
+
A callback function that is called when a new token is received from the stream.
|
|
178
184
|
:returns: A dictionary with the following keys:
|
|
179
185
|
- `replies`: The responses from the model
|
|
180
186
|
- `meta`: The metadata collected during the run
|
|
181
187
|
"""
|
|
182
188
|
generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
|
|
183
189
|
|
|
184
|
-
|
|
190
|
+
resolved_streaming_callback = streaming_callback or self.streaming_callback
|
|
191
|
+
stream = resolved_streaming_callback is not None
|
|
185
192
|
|
|
186
193
|
response = self._client.generate(
|
|
187
|
-
model=self.model,
|
|
194
|
+
model=self.model,
|
|
195
|
+
prompt=prompt,
|
|
196
|
+
stream=stream,
|
|
197
|
+
keep_alive=self.keep_alive,
|
|
198
|
+
options=generation_kwargs,
|
|
188
199
|
)
|
|
189
200
|
|
|
190
201
|
if stream:
|
|
191
|
-
chunks: List[StreamingChunk] = self._handle_streaming_response(response)
|
|
202
|
+
chunks: List[StreamingChunk] = self._handle_streaming_response(response, resolved_streaming_callback)
|
|
192
203
|
return self._convert_to_streaming_response(chunks)
|
|
193
204
|
|
|
194
205
|
return self._convert_to_response(response)
|
|
@@ -418,6 +418,67 @@ class TestOllamaChatGenerator:
|
|
|
418
418
|
assert result["replies"][0].text == "first chunk second chunk"
|
|
419
419
|
assert result["replies"][0].role == "assistant"
|
|
420
420
|
|
|
421
|
+
# Verify metadata is properly processed and includes usage information
|
|
422
|
+
assert hasattr(result["replies"][0], "_meta")
|
|
423
|
+
assert result["replies"][0]._meta["done"] is True
|
|
424
|
+
assert "usage" in result["replies"][0]._meta
|
|
425
|
+
assert result["replies"][0]._meta["usage"]["prompt_tokens"] == 26
|
|
426
|
+
assert result["replies"][0]._meta["usage"]["completion_tokens"] == 282
|
|
427
|
+
assert result["replies"][0]._meta["usage"]["total_tokens"] == 308
|
|
428
|
+
|
|
429
|
+
@patch("haystack_integrations.components.generators.ollama.chat.chat_generator.Client")
|
|
430
|
+
def test_run_streaming_at_runtime(self, mock_client):
|
|
431
|
+
streaming_callback_called = False
|
|
432
|
+
|
|
433
|
+
def streaming_callback(_: StreamingChunk) -> None:
|
|
434
|
+
nonlocal streaming_callback_called
|
|
435
|
+
streaming_callback_called = True
|
|
436
|
+
|
|
437
|
+
generator = OllamaChatGenerator(streaming_callback=None)
|
|
438
|
+
|
|
439
|
+
mock_response = iter(
|
|
440
|
+
[
|
|
441
|
+
ChatResponse(
|
|
442
|
+
model="llama3.2",
|
|
443
|
+
created_at="2023-12-12T14:13:43.416799Z",
|
|
444
|
+
message={"role": "assistant", "content": "first chunk "},
|
|
445
|
+
done=False,
|
|
446
|
+
),
|
|
447
|
+
ChatResponse(
|
|
448
|
+
model="llama3.2",
|
|
449
|
+
created_at="2023-12-12T14:13:43.416799Z",
|
|
450
|
+
message={"role": "assistant", "content": "second chunk"},
|
|
451
|
+
done=True,
|
|
452
|
+
total_duration=4883583458,
|
|
453
|
+
load_duration=1334875,
|
|
454
|
+
prompt_eval_count=26,
|
|
455
|
+
prompt_eval_duration=342546000,
|
|
456
|
+
eval_count=282,
|
|
457
|
+
eval_duration=4535599000,
|
|
458
|
+
),
|
|
459
|
+
]
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
mock_client_instance = mock_client.return_value
|
|
463
|
+
mock_client_instance.chat.return_value = mock_response
|
|
464
|
+
|
|
465
|
+
result = generator.run(messages=[ChatMessage.from_user("irrelevant")], streaming_callback=streaming_callback)
|
|
466
|
+
|
|
467
|
+
assert streaming_callback_called
|
|
468
|
+
|
|
469
|
+
assert "replies" in result
|
|
470
|
+
assert len(result["replies"]) == 1
|
|
471
|
+
assert result["replies"][0].text == "first chunk second chunk"
|
|
472
|
+
assert result["replies"][0].role == "assistant"
|
|
473
|
+
|
|
474
|
+
# Verify metadata is properly processed and includes usage information
|
|
475
|
+
assert hasattr(result["replies"][0], "_meta")
|
|
476
|
+
assert result["replies"][0]._meta["done"] is True
|
|
477
|
+
assert "usage" in result["replies"][0]._meta
|
|
478
|
+
assert result["replies"][0]._meta["usage"]["prompt_tokens"] == 26
|
|
479
|
+
assert result["replies"][0]._meta["usage"]["completion_tokens"] == 282
|
|
480
|
+
assert result["replies"][0]._meta["usage"]["total_tokens"] == 308
|
|
481
|
+
|
|
421
482
|
def test_run_fail_with_tools_and_streaming(self, tools):
|
|
422
483
|
component = OllamaChatGenerator(tools=tools, streaming_callback=print_streaming_chunk)
|
|
423
484
|
|
|
@@ -459,7 +520,9 @@ class TestOllamaChatGenerator:
|
|
|
459
520
|
assert isinstance(response, dict)
|
|
460
521
|
assert isinstance(response["replies"], list)
|
|
461
522
|
|
|
462
|
-
assert any(
|
|
523
|
+
assert any(
|
|
524
|
+
city.lower() in response["replies"][-1].text.lower() for city in ["Manchester", "Birmingham", "Glasgow"]
|
|
525
|
+
)
|
|
463
526
|
|
|
464
527
|
@pytest.mark.integration
|
|
465
528
|
def test_run_model_unavailable(self):
|
|
@@ -486,7 +549,9 @@ class TestOllamaChatGenerator:
|
|
|
486
549
|
|
|
487
550
|
assert isinstance(response, dict)
|
|
488
551
|
assert isinstance(response["replies"], list)
|
|
489
|
-
assert any(
|
|
552
|
+
assert any(
|
|
553
|
+
city.lower() in response["replies"][-1].text.lower() for city in ["Manchester", "Birmingham", "Glasgow"]
|
|
554
|
+
)
|
|
490
555
|
|
|
491
556
|
@pytest.mark.integration
|
|
492
557
|
def test_run_with_tools(self, tools):
|
|
@@ -525,7 +590,7 @@ class TestOllamaChatGenerator:
|
|
|
525
590
|
assert isinstance(response_data["capital"], str)
|
|
526
591
|
assert "population" in response_data
|
|
527
592
|
assert isinstance(response_data["population"], (int, float))
|
|
528
|
-
assert response_data["capital"] == "
|
|
593
|
+
assert response_data["capital"].lower() == "paris"
|
|
529
594
|
|
|
530
595
|
def test_run_with_streaming_and_format(self):
|
|
531
596
|
response_format = {
|
|
@@ -135,7 +135,7 @@ class TestOllamaGenerator:
|
|
|
135
135
|
assert component.keep_alive == "5m"
|
|
136
136
|
|
|
137
137
|
@pytest.mark.integration
|
|
138
|
-
def
|
|
138
|
+
def test_ollama_generator_streaming(self):
|
|
139
139
|
class Callback:
|
|
140
140
|
def __init__(self):
|
|
141
141
|
self.responses = ""
|
|
@@ -151,7 +151,29 @@ class TestOllamaGenerator:
|
|
|
151
151
|
results = component.run(prompt="What's the capital of Netherlands?")
|
|
152
152
|
|
|
153
153
|
assert len(results["replies"]) == 1
|
|
154
|
-
assert "
|
|
154
|
+
assert "amsterdam" in results["replies"][0].lower()
|
|
155
|
+
assert len(results["meta"]) == 1
|
|
156
|
+
assert callback.responses == results["replies"][0]
|
|
157
|
+
assert callback.count_calls > 1
|
|
158
|
+
|
|
159
|
+
@pytest.mark.integration
|
|
160
|
+
def test_ollama_generator_streaming_in_run(self):
|
|
161
|
+
class Callback:
|
|
162
|
+
def __init__(self):
|
|
163
|
+
self.responses = ""
|
|
164
|
+
self.count_calls = 0
|
|
165
|
+
|
|
166
|
+
def __call__(self, chunk):
|
|
167
|
+
self.responses += chunk.content
|
|
168
|
+
self.count_calls += 1
|
|
169
|
+
return chunk
|
|
170
|
+
|
|
171
|
+
callback = Callback()
|
|
172
|
+
component = OllamaGenerator(model="llama3.2:3b", streaming_callback=None)
|
|
173
|
+
results = component.run(prompt="What's the capital of Netherlands?", streaming_callback=callback)
|
|
174
|
+
|
|
175
|
+
assert len(results["replies"]) == 1
|
|
176
|
+
assert "amsterdam" in results["replies"][0].lower()
|
|
155
177
|
assert len(results["meta"]) == 1
|
|
156
178
|
assert callback.responses == results["replies"][0]
|
|
157
179
|
assert callback.count_calls > 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|