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.
Files changed (22) hide show
  1. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/CHANGELOG.md +34 -0
  2. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/PKG-INFO +1 -1
  3. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/generators/ollama/chat/chat_generator.py +30 -10
  4. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/generators/ollama/generator.py +17 -6
  5. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/test_chat_generator.py +68 -3
  6. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/test_generator.py +24 -2
  7. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/.gitignore +0 -0
  8. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/LICENSE.txt +0 -0
  9. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/README.md +0 -0
  10. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/examples/chat_generator_example.py +0 -0
  11. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/examples/embedders_example.py +0 -0
  12. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/examples/generator_example.py +0 -0
  13. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/pydoc/config.yml +0 -0
  14. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/pyproject.toml +0 -0
  15. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/embedders/ollama/__init__.py +0 -0
  16. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/embedders/ollama/document_embedder.py +0 -0
  17. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/embedders/ollama/text_embedder.py +0 -0
  18. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/generators/ollama/__init__.py +0 -0
  19. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/src/haystack_integrations/components/generators/ollama/chat/__init__.py +0 -0
  20. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/__init__.py +0 -0
  21. {ollama_haystack-2.4.0 → ollama_haystack-2.4.2}/tests/test_document_embedder.py +0 -0
  22. {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.0
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, deserialize_tools_inplace
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
- deserialize_tools_inplace(data["init_parameters"], key="tools")
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(self, response) -> Dict[str, List[Any]]:
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 self.streaming_callback is not None:
263
- self.streaming_callback(chunk_delta)
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
- replies = [ChatMessage.from_assistant("".join([c.content for c in chunks]))]
266
- meta = {key: value for key, value in chunks[0].meta.items() if key != "message"}
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": replies, "meta": [meta]}
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 = self.streaming_callback is not None
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(self, response) -> List[StreamingChunk]:
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 self.streaming_callback is not None:
149
- self.streaming_callback(chunk_delta)
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
- stream = self.streaming_callback is not None
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, prompt=prompt, stream=stream, keep_alive=self.keep_alive, options=generation_kwargs
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(city in response["replies"][-1].text for city in ["Manchester", "Birmingham", "Glasgow"])
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(city in response["replies"][-1].text for city in ["Manchester", "Birmingham", "Glasgow"])
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"] == "Paris"
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 test_ollama_generator_run_streaming(self):
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 "Amsterdam" in results["replies"][0]
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