ollama-haystack 2.2.0__tar.gz → 2.4.0__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.2.0 → ollama_haystack-2.4.0}/CHANGELOG.md +14 -0
  2. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/PKG-INFO +4 -2
  3. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/pyproject.toml +4 -8
  4. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/src/haystack_integrations/components/generators/ollama/chat/chat_generator.py +74 -3
  5. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/tests/test_chat_generator.py +102 -4
  6. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/.gitignore +0 -0
  7. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/LICENSE.txt +0 -0
  8. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/README.md +0 -0
  9. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/examples/chat_generator_example.py +0 -0
  10. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/examples/embedders_example.py +0 -0
  11. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/examples/generator_example.py +0 -0
  12. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/pydoc/config.yml +0 -0
  13. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/src/haystack_integrations/components/embedders/ollama/__init__.py +0 -0
  14. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/src/haystack_integrations/components/embedders/ollama/document_embedder.py +0 -0
  15. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/src/haystack_integrations/components/embedders/ollama/text_embedder.py +0 -0
  16. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/src/haystack_integrations/components/generators/ollama/__init__.py +0 -0
  17. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/src/haystack_integrations/components/generators/ollama/chat/__init__.py +0 -0
  18. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/src/haystack_integrations/components/generators/ollama/generator.py +0 -0
  19. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/tests/__init__.py +0 -0
  20. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/tests/test_document_embedder.py +0 -0
  21. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/tests/test_generator.py +0 -0
  22. {ollama_haystack-2.2.0 → ollama_haystack-2.4.0}/tests/test_text_embedder.py +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/ollama-v2.3.0] - 2025-01-29
4
+
5
+ ### 🚀 Features
6
+
7
+ - Add `response_format` param to `OllamaChatGenerator` (#1326)
8
+
9
+
10
+ ## [integrations/ollama-v2.2.0] - 2025-01-16
11
+
12
+ ### 🚀 Features
13
+
14
+ - Ollama - add support for tools (#1294)
15
+
16
+
3
17
  ## [integrations/ollama-v2.1.2] - 2024-12-18
4
18
 
5
19
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ollama-haystack
3
- Version: 2.2.0
3
+ Version: 2.4.0
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
@@ -15,11 +15,13 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
- Requires-Python: >=3.8
21
+ Requires-Python: >=3.9
21
22
  Requires-Dist: haystack-ai
22
23
  Requires-Dist: ollama>=0.4.0
24
+ Requires-Dist: pydantic
23
25
  Description-Content-Type: text/markdown
24
26
 
25
27
  # ollama-haystack
@@ -7,7 +7,7 @@ name = "ollama-haystack"
7
7
  dynamic = ["version"]
8
8
  description = 'An integration between the Ollama LLM framework and Haystack'
9
9
  readme = "README.md"
10
- requires-python = ">=3.8"
10
+ requires-python = ">=3.9"
11
11
  license = "Apache-2.0"
12
12
  keywords = []
13
13
  authors = [
@@ -23,10 +23,11 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.10",
24
24
  "Programming Language :: Python :: 3.11",
25
25
  "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
26
27
  "Programming Language :: Python :: Implementation :: CPython",
27
28
  "Programming Language :: Python :: Implementation :: PyPy",
28
29
  ]
29
- dependencies = ["haystack-ai", "ollama>=0.4.0"]
30
+ dependencies = ["haystack-ai", "ollama>=0.4.0", "pydantic"]
30
31
 
31
32
  [project.urls]
32
33
  Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/ollama#readme"
@@ -51,7 +52,6 @@ dependencies = [
51
52
  "pytest",
52
53
  "pytest-rerunfailures",
53
54
  "haystack-pydoc-tools",
54
- "jsonschema", # needed for Tool
55
55
  ]
56
56
  [tool.hatch.envs.default.scripts]
57
57
  test = "pytest {args:tests}"
@@ -62,10 +62,6 @@ cov = ["test-cov", "cov-report"]
62
62
  cov-retry = ["test-cov-retry", "cov-report"]
63
63
  docs = ["pydoc-markdown pydoc/config.yml"]
64
64
 
65
- [[tool.hatch.envs.all.matrix]]
66
- python = ["3.9", "3.10", "3.11", "3.12"]
67
-
68
-
69
65
  [tool.hatch.envs.lint]
70
66
  installer = "uv"
71
67
  detached = true
@@ -165,5 +161,5 @@ markers = [
165
161
  addopts = ["--import-mode=importlib"]
166
162
 
167
163
  [[tool.mypy.overrides]]
168
- module = ["haystack.*", "haystack_integrations.*", "pytest.*", "ollama.*"]
164
+ module = ["haystack.*", "haystack_integrations.*", "pytest.*", "ollama.*", "pydantic.*"]
169
165
  ignore_missing_imports = true
@@ -1,9 +1,10 @@
1
- from typing import Any, Callable, Dict, List, Optional, Union
1
+ 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
5
  from haystack.tools import Tool, _check_duplicate_tool_names, deserialize_tools_inplace
6
6
  from haystack.utils.callable_serialization import deserialize_callable, serialize_callable
7
+ from pydantic.json_schema import JsonSchemaValue
7
8
 
8
9
  from ollama import ChatResponse, Client
9
10
 
@@ -40,6 +41,60 @@ def _convert_chatmessage_to_ollama_format(message: ChatMessage) -> Dict[str, Any
40
41
  return ollama_msg
41
42
 
42
43
 
44
+ def _convert_ollama_meta_to_openai_format(intput_response_dict: Dict) -> Dict:
45
+ """
46
+ Internal function to convert Ollama metadata to OpenAI format.
47
+
48
+ All fields that are not part of the OpenAI metadata are left unchanged in the returned dict.
49
+
50
+ Example Ollama metadata:
51
+ {
52
+ 'model': 'phi4:14b-q4_K_M',
53
+ 'created_at': '2025-03-09T18:38:33.004185821Z',
54
+ 'done': True,
55
+ 'done_reason': 'stop',
56
+ 'total_duration': 86627206961,
57
+ 'load_duration': 23585622554,
58
+ 'prompt_eval_count': 26,
59
+ 'prompt_eval_duration': 3426000000,
60
+ 'eval_count': 298,
61
+ 'eval_duration': 4799921000
62
+ }
63
+
64
+ Example OpenAI metadata:
65
+ {
66
+ 'model': 'phi4:14b-q4_K_M',
67
+ 'finish_reason': 'stop',
68
+ 'usage': {
69
+ 'completion_tokens': 298,
70
+ 'prompt_tokens': 26,
71
+ 'total_tokens': 324,
72
+ }
73
+ 'completion_start_time': '2025-03-09T18:38:33.004185821Z',
74
+ 'done': True,
75
+ 'total_duration': 86627206961,
76
+ 'load_duration': 23585622554,
77
+ 'prompt_eval_duration': 3426000000,
78
+ 'eval_duration': 4799921000,
79
+ }
80
+ """
81
+ meta = {key: value for key, value in intput_response_dict.items() if key != "message"}
82
+
83
+ if "done_reason" in meta:
84
+ meta["finish_reason"] = meta.pop("done_reason")
85
+ if "created_at" in meta:
86
+ meta["completion_start_time"] = meta.pop("created_at")
87
+ if "eval_count" in meta and "prompt_eval_count" in meta:
88
+ eval_count = meta.pop("eval_count")
89
+ prompt_eval_count = meta.pop("prompt_eval_count")
90
+ meta["usage"] = {
91
+ "completion_tokens": eval_count,
92
+ "prompt_tokens": prompt_eval_count,
93
+ "total_tokens": eval_count + prompt_eval_count,
94
+ }
95
+ return meta
96
+
97
+
43
98
  def _convert_ollama_response_to_chatmessage(ollama_response: "ChatResponse") -> ChatMessage:
44
99
  """
45
100
  Converts the non-streaming response from the Ollama API to a ChatMessage with assistant role.
@@ -59,7 +114,7 @@ def _convert_ollama_response_to_chatmessage(ollama_response: "ChatResponse") ->
59
114
 
60
115
  message = ChatMessage.from_assistant(text=text, tool_calls=tool_calls)
61
116
 
62
- message.meta.update({key: value for key, value in response_dict.items() if key != "message"})
117
+ message._meta = _convert_ollama_meta_to_openai_format(response_dict)
63
118
  return message
64
119
 
65
120
 
@@ -97,6 +152,7 @@ class OllamaChatGenerator:
97
152
  keep_alive: Optional[Union[float, str]] = None,
98
153
  streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
99
154
  tools: Optional[List[Tool]] = None,
155
+ response_format: Optional[Union[None, Literal["json"], JsonSchemaValue]] = None,
100
156
  ):
101
157
  """
102
158
  :param model:
@@ -124,6 +180,11 @@ class OllamaChatGenerator:
124
180
  A list of tools for which the model can prepare calls.
125
181
  Not all models support tools. For a list of models compatible with tools, see the
126
182
  [models page](https://ollama.com/search?c=tools).
183
+ :param response_format:
184
+ The format for structured model outputs. The value can be:
185
+ - None: No specific structure or format is applied to the response. The response is returned as-is.
186
+ - "json": The response is formatted as a JSON object.
187
+ - JSON Schema: The response is formatted as a JSON object that adheres to the specified JSON Schema.
127
188
  """
128
189
 
129
190
  _check_duplicate_tool_names(tools)
@@ -135,7 +196,7 @@ class OllamaChatGenerator:
135
196
  self.keep_alive = keep_alive
136
197
  self.streaming_callback = streaming_callback
137
198
  self.tools = tools
138
-
199
+ self.response_format = response_format
139
200
  self._client = Client(host=self.url, timeout=self.timeout)
140
201
 
141
202
  def to_dict(self) -> Dict[str, Any]:
@@ -156,6 +217,7 @@ class OllamaChatGenerator:
156
217
  timeout=self.timeout,
157
218
  streaming_callback=callback_name,
158
219
  tools=serialized_tools,
220
+ response_format=self.response_format,
159
221
  )
160
222
 
161
223
  @classmethod
@@ -237,6 +299,14 @@ class OllamaChatGenerator:
237
299
  msg = "Ollama does not support tools and streaming at the same time. Please choose one."
238
300
  raise ValueError(msg)
239
301
 
302
+ if self.response_format and tools:
303
+ msg = "Ollama does not support tools and response_format at the same time. Please choose one."
304
+ raise ValueError(msg)
305
+
306
+ if self.response_format and stream:
307
+ msg = "Ollama does not support streaming and response_format at the same time. Please choose one."
308
+ raise ValueError(msg)
309
+
240
310
  ollama_tools = [{"type": "function", "function": {**t.tool_spec}} for t in tools] if tools else None
241
311
 
242
312
  ollama_messages = [_convert_chatmessage_to_ollama_format(msg) for msg in messages]
@@ -247,6 +317,7 @@ class OllamaChatGenerator:
247
317
  stream=stream,
248
318
  keep_alive=self.keep_alive,
249
319
  options=generation_kwargs,
320
+ format=self.response_format,
250
321
  )
251
322
 
252
323
  if stream:
@@ -97,13 +97,12 @@ def test_convert_chatmessage_to_ollama_invalid():
97
97
 
98
98
 
99
99
  def test_convert_ollama_response_to_chatmessage():
100
- model = "some_model"
101
-
102
100
  ollama_response = ChatResponse(
103
- model=model,
101
+ model="some_model",
104
102
  created_at="2023-12-12T14:13:43.416799Z",
105
103
  message={"role": "assistant", "content": "Hello! How are you today?"},
106
104
  done=True,
105
+ done_reason="stop",
107
106
  total_duration=5191566416,
108
107
  load_duration=2154458,
109
108
  prompt_eval_count=26,
@@ -117,6 +116,22 @@ def test_convert_ollama_response_to_chatmessage():
117
116
  assert observed.role == "assistant"
118
117
  assert observed.text == "Hello! How are you today?"
119
118
 
119
+ assert observed.meta == {
120
+ "finish_reason": "stop",
121
+ "usage": {
122
+ "completion_tokens": 298,
123
+ "prompt_tokens": 26,
124
+ "total_tokens": 324,
125
+ },
126
+ "completion_start_time": "2023-12-12T14:13:43.416799Z",
127
+ "load_duration": 2154458,
128
+ "total_duration": 5191566416,
129
+ "eval_duration": 4799921000,
130
+ "prompt_eval_duration": 383809000,
131
+ "done": True,
132
+ "model": "some_model",
133
+ }
134
+
120
135
 
121
136
  def test_convert_ollama_response_to_chatmessage_with_tools():
122
137
  model = "some_model"
@@ -165,6 +180,7 @@ class TestOllamaChatGenerator:
165
180
  assert component.streaming_callback is None
166
181
  assert component.tools is None
167
182
  assert component.keep_alive is None
183
+ assert component.response_format is None
168
184
 
169
185
  def test_init(self, tools):
170
186
  component = OllamaChatGenerator(
@@ -175,6 +191,7 @@ class TestOllamaChatGenerator:
175
191
  keep_alive="10m",
176
192
  streaming_callback=print_streaming_chunk,
177
193
  tools=tools,
194
+ response_format={"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}}},
178
195
  )
179
196
 
180
197
  assert component.model == "llama2"
@@ -184,6 +201,10 @@ class TestOllamaChatGenerator:
184
201
  assert component.keep_alive == "10m"
185
202
  assert component.streaming_callback is print_streaming_chunk
186
203
  assert component.tools == tools
204
+ assert component.response_format == {
205
+ "type": "object",
206
+ "properties": {"name": {"type": "string"}, "age": {"type": "number"}},
207
+ }
187
208
 
188
209
  def test_init_fail_with_duplicate_tool_names(self, tools):
189
210
 
@@ -206,9 +227,11 @@ class TestOllamaChatGenerator:
206
227
  generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
207
228
  tools=[tool],
208
229
  keep_alive="5m",
230
+ response_format={"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}}},
209
231
  )
210
232
  data = component.to_dict()
211
- assert data == {
233
+
234
+ expected_dict = {
212
235
  "type": "haystack_integrations.components.generators.ollama.chat.chat_generator.OllamaChatGenerator",
213
236
  "init_parameters": {
214
237
  "timeout": 120,
@@ -235,9 +258,24 @@ class TestOllamaChatGenerator:
235
258
  },
236
259
  },
237
260
  ],
261
+ "response_format": {
262
+ "type": "object",
263
+ "properties": {"name": {"type": "string"}, "age": {"type": "number"}},
264
+ },
238
265
  },
239
266
  }
240
267
 
268
+ # add outputs_to_string, inputs_from_state and outputs_to_state tool parameters for compatibility with
269
+ # haystack-ai>=2.12.0
270
+ if hasattr(tool, "outputs_to_string"):
271
+ expected_dict["init_parameters"]["tools"][0]["data"]["outputs_to_string"] = tool.outputs_to_string
272
+ if hasattr(tool, "inputs_from_state"):
273
+ expected_dict["init_parameters"]["tools"][0]["data"]["inputs_from_state"] = tool.inputs_from_state
274
+ if hasattr(tool, "outputs_to_state"):
275
+ expected_dict["init_parameters"]["tools"][0]["data"]["outputs_to_state"] = tool.outputs_to_state
276
+
277
+ assert data == expected_dict
278
+
241
279
  def test_from_dict(self):
242
280
  tool = Tool(
243
281
  name="name",
@@ -273,6 +311,10 @@ class TestOllamaChatGenerator:
273
311
  },
274
312
  },
275
313
  ],
314
+ "response_format": {
315
+ "type": "object",
316
+ "properties": {"name": {"type": "string"}, "age": {"type": "number"}},
317
+ },
276
318
  },
277
319
  }
278
320
  component = OllamaChatGenerator.from_dict(data)
@@ -286,6 +328,10 @@ class TestOllamaChatGenerator:
286
328
  }
287
329
  assert component.timeout == 120
288
330
  assert component.tools == [tool]
331
+ assert component.response_format == {
332
+ "type": "object",
333
+ "properties": {"name": {"type": "string"}, "age": {"type": "number"}},
334
+ }
289
335
 
290
336
  @patch("haystack_integrations.components.generators.ollama.chat.chat_generator.Client")
291
337
  def test_run(self, mock_client):
@@ -319,6 +365,7 @@ class TestOllamaChatGenerator:
319
365
  tools=None,
320
366
  options={},
321
367
  keep_alive=None,
368
+ format=None,
322
369
  )
323
370
 
324
371
  assert "replies" in result
@@ -456,3 +503,54 @@ class TestOllamaChatGenerator:
456
503
  assert isinstance(tool_call, ToolCall)
457
504
  assert tool_call.tool_name == "weather"
458
505
  assert tool_call.arguments == {"city": "Paris"}
506
+
507
+ @pytest.mark.integration
508
+ def test_run_with_response_format(self):
509
+ response_format = {
510
+ "type": "object",
511
+ "properties": {"capital": {"type": "string"}, "population": {"type": "number"}},
512
+ }
513
+ chat_generator = OllamaChatGenerator(model="llama3.2:3b", response_format=response_format)
514
+
515
+ message = ChatMessage.from_user("What's the capital of France and its population?")
516
+ response = chat_generator.run([message])
517
+
518
+ assert isinstance(response, dict)
519
+ assert isinstance(response["replies"], list)
520
+
521
+ # Parse the response text as JSON and verify its structure
522
+ response_data = json.loads(response["replies"][0].text)
523
+ assert isinstance(response_data, dict)
524
+ assert "capital" in response_data
525
+ assert isinstance(response_data["capital"], str)
526
+ assert "population" in response_data
527
+ assert isinstance(response_data["population"], (int, float))
528
+ assert response_data["capital"] == "Paris"
529
+
530
+ def test_run_with_streaming_and_format(self):
531
+ response_format = {
532
+ "type": "object",
533
+ "properties": {"answer": {"type": "string"}},
534
+ }
535
+ streaming_callback = Mock()
536
+ chat_generator = OllamaChatGenerator(
537
+ model="llama3.2:3b", streaming_callback=streaming_callback, response_format=response_format
538
+ )
539
+
540
+ chat_messages = [
541
+ ChatMessage.from_user("What is the largest city in the United Kingdom by population?"),
542
+ ChatMessage.from_assistant("London is the largest city in the United Kingdom by population"),
543
+ ChatMessage.from_user("And what is the second largest?"),
544
+ ]
545
+ with pytest.raises(ValueError):
546
+ chat_generator.run([chat_messages])
547
+
548
+ def test_run_with_tools_and_format(self, tools):
549
+ response_format = {
550
+ "type": "object",
551
+ "properties": {"capital": {"type": "string"}, "population": {"type": "number"}},
552
+ }
553
+ chat_generator = OllamaChatGenerator(model="llama3.2:3b", tools=tools, response_format=response_format)
554
+ message = ChatMessage.from_user("What's the weather in Paris?")
555
+ with pytest.raises(ValueError):
556
+ chat_generator.run([message])