ollama-haystack 2.1.2__tar.gz → 2.2.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 (23) hide show
  1. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/CHANGELOG.md +7 -0
  2. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/PKG-INFO +1 -1
  3. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/pyproject.toml +1 -0
  4. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/src/haystack_integrations/components/generators/ollama/chat/chat_generator.py +104 -39
  5. ollama_haystack-2.2.0/tests/test_chat_generator.py +458 -0
  6. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/tests/test_generator.py +2 -2
  7. ollama_haystack-2.1.2/tests/test_chat_generator.py +0 -169
  8. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/.gitignore +0 -0
  9. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/LICENSE.txt +0 -0
  10. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/README.md +0 -0
  11. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/examples/chat_generator_example.py +0 -0
  12. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/examples/embedders_example.py +0 -0
  13. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/examples/generator_example.py +0 -0
  14. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/pydoc/config.yml +0 -0
  15. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/src/haystack_integrations/components/embedders/ollama/__init__.py +0 -0
  16. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/src/haystack_integrations/components/embedders/ollama/document_embedder.py +0 -0
  17. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/src/haystack_integrations/components/embedders/ollama/text_embedder.py +0 -0
  18. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/src/haystack_integrations/components/generators/ollama/__init__.py +0 -0
  19. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/src/haystack_integrations/components/generators/ollama/chat/__init__.py +0 -0
  20. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/src/haystack_integrations/components/generators/ollama/generator.py +0 -0
  21. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/tests/__init__.py +0 -0
  22. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/tests/test_document_embedder.py +0 -0
  23. {ollama_haystack-2.1.2 → ollama_haystack-2.2.0}/tests/test_text_embedder.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/ollama-v2.1.2] - 2024-12-18
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - Make Ollama Chat Generator compatible with new ChatMessage (#1256)
8
+
9
+
3
10
  ## [integrations/ollama-v2.1.1] - 2024-12-10
4
11
 
5
12
  ### 🌀 Miscellaneous
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ollama-haystack
3
- Version: 2.1.2
3
+ Version: 2.2.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
@@ -51,6 +51,7 @@ dependencies = [
51
51
  "pytest",
52
52
  "pytest-rerunfailures",
53
53
  "haystack-pydoc-tools",
54
+ "jsonschema", # needed for Tool
54
55
  ]
55
56
  [tool.hatch.envs.default.scripts]
56
57
  test = "pytest {args:tests}"
@@ -1,12 +1,68 @@
1
1
  from typing import Any, Callable, Dict, List, Optional, Union
2
2
 
3
3
  from haystack import component, default_from_dict, default_to_dict
4
- from haystack.dataclasses import ChatMessage, StreamingChunk
4
+ from haystack.dataclasses import ChatMessage, StreamingChunk, ToolCall
5
+ from haystack.tools import Tool, _check_duplicate_tool_names, deserialize_tools_inplace
5
6
  from haystack.utils.callable_serialization import deserialize_callable, serialize_callable
6
7
 
7
8
  from ollama import ChatResponse, Client
8
9
 
9
10
 
11
+ def _convert_chatmessage_to_ollama_format(message: ChatMessage) -> Dict[str, Any]:
12
+ """
13
+ Convert a ChatMessage to the format expected by Ollama Chat API.
14
+ """
15
+ text_contents = message.texts
16
+ tool_calls = message.tool_calls
17
+ tool_call_results = message.tool_call_results
18
+
19
+ if not text_contents and not tool_calls and not tool_call_results:
20
+ msg = "A `ChatMessage` must contain at least one `TextContent`, `ToolCall`, or `ToolCallResult`."
21
+ raise ValueError(msg)
22
+ elif len(text_contents) + len(tool_call_results) > 1:
23
+ msg = "A `ChatMessage` can only contain one `TextContent` or one `ToolCallResult`."
24
+ raise ValueError(msg)
25
+
26
+ ollama_msg: Dict[str, Any] = {"role": message._role.value}
27
+
28
+ if tool_call_results:
29
+ # Ollama does not provide a way to communicate errors in tool invocations, so we ignore the error field
30
+ ollama_msg["content"] = tool_call_results[0].result
31
+ return ollama_msg
32
+
33
+ if text_contents:
34
+ ollama_msg["content"] = text_contents[0]
35
+ if tool_calls:
36
+ # Ollama does not support tool call id, so we ignore it
37
+ ollama_msg["tool_calls"] = [
38
+ {"type": "function", "function": {"name": tc.tool_name, "arguments": tc.arguments}} for tc in tool_calls
39
+ ]
40
+ return ollama_msg
41
+
42
+
43
+ def _convert_ollama_response_to_chatmessage(ollama_response: "ChatResponse") -> ChatMessage:
44
+ """
45
+ Converts the non-streaming response from the Ollama API to a ChatMessage with assistant role.
46
+ """
47
+ response_dict = ollama_response.model_dump()
48
+
49
+ ollama_message = response_dict["message"]
50
+
51
+ text = ollama_message["content"]
52
+
53
+ tool_calls = []
54
+ if ollama_tool_calls := ollama_message.get("tool_calls"):
55
+ for ollama_tc in ollama_tool_calls:
56
+ tool_calls.append(
57
+ ToolCall(tool_name=ollama_tc["function"]["name"], arguments=ollama_tc["function"]["arguments"])
58
+ )
59
+
60
+ message = ChatMessage.from_assistant(text=text, tool_calls=tool_calls)
61
+
62
+ message.meta.update({key: value for key, value in response_dict.items() if key != "message"})
63
+ return message
64
+
65
+
10
66
  @component
11
67
  class OllamaChatGenerator:
12
68
  """
@@ -40,6 +96,7 @@ class OllamaChatGenerator:
40
96
  timeout: int = 120,
41
97
  keep_alive: Optional[Union[float, str]] = None,
42
98
  streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
99
+ tools: Optional[List[Tool]] = None,
43
100
  ):
44
101
  """
45
102
  :param model:
@@ -52,9 +109,6 @@ class OllamaChatGenerator:
52
109
  [Ollama docs](https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values).
53
110
  :param timeout:
54
111
  The number of seconds before throwing a timeout error from the Ollama API.
55
- :param streaming_callback:
56
- A callback function that is called when a new token is received from the stream.
57
- The callback function accepts StreamingChunk as an argument.
58
112
  :param keep_alive:
59
113
  The option that controls how long the model will stay loaded into memory following the request.
60
114
  If not set, it will use the default value from the Ollama (5 minutes).
@@ -63,14 +117,24 @@ class OllamaChatGenerator:
63
117
  - a number in seconds (such as 3600)
64
118
  - any negative number which will keep the model loaded in memory (e.g. -1 or "-1m")
65
119
  - '0' which will unload the model immediately after generating a response.
120
+ :param streaming_callback:
121
+ A callback function that is called when a new token is received from the stream.
122
+ The callback function accepts StreamingChunk as an argument.
123
+ :param tools:
124
+ A list of tools for which the model can prepare calls.
125
+ Not all models support tools. For a list of models compatible with tools, see the
126
+ [models page](https://ollama.com/search?c=tools).
66
127
  """
67
128
 
129
+ _check_duplicate_tool_names(tools)
130
+
68
131
  self.timeout = timeout
69
132
  self.generation_kwargs = generation_kwargs or {}
70
133
  self.url = url
71
134
  self.model = model
72
135
  self.keep_alive = keep_alive
73
136
  self.streaming_callback = streaming_callback
137
+ self.tools = tools
74
138
 
75
139
  self._client = Client(host=self.url, timeout=self.timeout)
76
140
 
@@ -82,6 +146,7 @@ class OllamaChatGenerator:
82
146
  Dictionary with serialized data.
83
147
  """
84
148
  callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None
149
+ serialized_tools = [tool.to_dict() for tool in self.tools] if self.tools else None
85
150
  return default_to_dict(
86
151
  self,
87
152
  model=self.model,
@@ -90,6 +155,7 @@ class OllamaChatGenerator:
90
155
  generation_kwargs=self.generation_kwargs,
91
156
  timeout=self.timeout,
92
157
  streaming_callback=callback_name,
158
+ tools=serialized_tools,
93
159
  )
94
160
 
95
161
  @classmethod
@@ -102,34 +168,14 @@ class OllamaChatGenerator:
102
168
  :returns:
103
169
  Deserialized component.
104
170
  """
171
+ deserialize_tools_inplace(data["init_parameters"], key="tools")
172
+
105
173
  init_params = data.get("init_parameters", {})
106
- serialized_callback_handler = init_params.get("streaming_callback")
107
- if serialized_callback_handler:
174
+
175
+ if serialized_callback_handler := init_params.get("streaming_callback"):
108
176
  data["init_parameters"]["streaming_callback"] = deserialize_callable(serialized_callback_handler)
109
177
  return default_from_dict(cls, data)
110
178
 
111
- def _message_to_dict(self, message: ChatMessage) -> Dict[str, str]:
112
- return {"role": message.role.value, "content": message.text}
113
-
114
- def _build_message_from_ollama_response(self, ollama_response: ChatResponse) -> ChatMessage:
115
- """
116
- Converts the non-streaming response from the Ollama API to a ChatMessage.
117
- """
118
- response_dict = ollama_response.model_dump()
119
- message = ChatMessage.from_assistant(response_dict["message"]["content"])
120
- message.meta.update({key: value for key, value in response_dict.items() if key != "message"})
121
- return message
122
-
123
- def _convert_to_streaming_response(self, chunks: List[StreamingChunk]) -> Dict[str, List[Any]]:
124
- """
125
- Converts a list of chunks response required Haystack format.
126
- """
127
-
128
- replies = [ChatMessage.from_assistant("".join([c.content for c in chunks]))]
129
- meta = {key: value for key, value in chunks[0].meta.items() if key != "message"}
130
-
131
- return {"replies": replies, "meta": [meta]}
132
-
133
179
  def _build_chunk(self, chunk_response: Any) -> StreamingChunk:
134
180
  """
135
181
  Converts the response from the Ollama API to a StreamingChunk.
@@ -143,23 +189,28 @@ class OllamaChatGenerator:
143
189
  chunk_message = StreamingChunk(content, meta)
144
190
  return chunk_message
145
191
 
146
- def _handle_streaming_response(self, response) -> List[StreamingChunk]:
192
+ def _handle_streaming_response(self, response) -> Dict[str, List[Any]]:
147
193
  """
148
- Handles Streaming response cases
194
+ Handles streaming response and converts it to Haystack format
149
195
  """
150
196
  chunks: List[StreamingChunk] = []
151
197
  for chunk in response:
152
- chunk_delta: StreamingChunk = self._build_chunk(chunk)
198
+ chunk_delta = self._build_chunk(chunk)
153
199
  chunks.append(chunk_delta)
154
200
  if self.streaming_callback is not None:
155
201
  self.streaming_callback(chunk_delta)
156
- return chunks
202
+
203
+ replies = [ChatMessage.from_assistant("".join([c.content for c in chunks]))]
204
+ meta = {key: value for key, value in chunks[0].meta.items() if key != "message"}
205
+
206
+ return {"replies": replies, "meta": [meta]}
157
207
 
158
208
  @component.output_types(replies=List[ChatMessage])
159
209
  def run(
160
210
  self,
161
211
  messages: List[ChatMessage],
162
212
  generation_kwargs: Optional[Dict[str, Any]] = None,
213
+ tools: Optional[List[Tool]] = None,
163
214
  ):
164
215
  """
165
216
  Runs an Ollama Model on a given chat history.
@@ -170,21 +221,35 @@ class OllamaChatGenerator:
170
221
  Optional arguments to pass to the Ollama generation endpoint, such as temperature,
171
222
  top_p, etc. See the
172
223
  [Ollama docs](https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values).
173
- :param streaming_callback:
174
- A callback function that will be called with each response chunk in streaming mode.
224
+ :param tools:
225
+ A list of tools for which the model can prepare calls. If set, it will override the `tools` parameter set
226
+ during component initialization.
175
227
  :returns: A dictionary with the following keys:
176
228
  - `replies`: The responses from the model
177
229
  """
178
230
  generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
179
231
 
180
232
  stream = self.streaming_callback is not None
181
- messages = [self._message_to_dict(message) for message in messages]
233
+ tools = tools or self.tools
234
+ _check_duplicate_tool_names(tools)
235
+
236
+ if stream and tools:
237
+ msg = "Ollama does not support tools and streaming at the same time. Please choose one."
238
+ raise ValueError(msg)
239
+
240
+ ollama_tools = [{"type": "function", "function": {**t.tool_spec}} for t in tools] if tools else None
241
+
242
+ ollama_messages = [_convert_chatmessage_to_ollama_format(msg) for msg in messages]
182
243
  response = self._client.chat(
183
- model=self.model, messages=messages, stream=stream, keep_alive=self.keep_alive, options=generation_kwargs
244
+ model=self.model,
245
+ messages=ollama_messages,
246
+ tools=ollama_tools,
247
+ stream=stream,
248
+ keep_alive=self.keep_alive,
249
+ options=generation_kwargs,
184
250
  )
185
251
 
186
252
  if stream:
187
- chunks: List[StreamingChunk] = self._handle_streaming_response(response)
188
- return self._convert_to_streaming_response(chunks)
253
+ return self._handle_streaming_response(response)
189
254
 
190
- return {"replies": [self._build_message_from_ollama_response(response)]}
255
+ return {"replies": [_convert_ollama_response_to_chatmessage(response)]}
@@ -0,0 +1,458 @@
1
+ import json
2
+ from unittest.mock import Mock, patch
3
+
4
+ import pytest
5
+ from haystack.components.generators.utils import print_streaming_chunk
6
+ from haystack.dataclasses import (
7
+ ChatMessage,
8
+ ChatRole,
9
+ StreamingChunk,
10
+ TextContent,
11
+ ToolCall,
12
+ )
13
+ from haystack.tools import Tool
14
+ from ollama._types import ChatResponse, ResponseError
15
+
16
+ from haystack_integrations.components.generators.ollama.chat.chat_generator import (
17
+ OllamaChatGenerator,
18
+ _convert_chatmessage_to_ollama_format,
19
+ _convert_ollama_response_to_chatmessage,
20
+ )
21
+
22
+
23
+ @pytest.fixture
24
+ def tools():
25
+ tool_parameters = {
26
+ "type": "object",
27
+ "properties": {"city": {"type": "string"}},
28
+ "required": ["city"],
29
+ }
30
+ tool = Tool(
31
+ name="weather",
32
+ description="useful to determine the weather in a given location",
33
+ parameters=tool_parameters,
34
+ function=lambda x: x,
35
+ )
36
+
37
+ return [tool]
38
+
39
+
40
+ def test_convert_chatmessage_to_ollama_format():
41
+ message = ChatMessage.from_system("You are good assistant")
42
+ assert _convert_chatmessage_to_ollama_format(message) == {
43
+ "role": "system",
44
+ "content": "You are good assistant",
45
+ }
46
+
47
+ message = ChatMessage.from_user("I have a question")
48
+ assert _convert_chatmessage_to_ollama_format(message) == {
49
+ "role": "user",
50
+ "content": "I have a question",
51
+ }
52
+
53
+ message = ChatMessage.from_assistant(text="I have an answer", meta={"finish_reason": "stop"})
54
+ assert _convert_chatmessage_to_ollama_format(message) == {
55
+ "role": "assistant",
56
+ "content": "I have an answer",
57
+ }
58
+
59
+ message = ChatMessage.from_assistant(
60
+ tool_calls=[ToolCall(id="123", tool_name="weather", arguments={"city": "Paris"})]
61
+ )
62
+ assert _convert_chatmessage_to_ollama_format(message) == {
63
+ "role": "assistant",
64
+ "tool_calls": [
65
+ {
66
+ "type": "function",
67
+ "function": {"name": "weather", "arguments": {"city": "Paris"}},
68
+ }
69
+ ],
70
+ }
71
+
72
+ tool_result = json.dumps({"weather": "sunny", "temperature": "25"})
73
+ message = ChatMessage.from_tool(
74
+ tool_result=tool_result,
75
+ origin=ToolCall(tool_name="weather", arguments={"city": "Paris"}),
76
+ )
77
+ assert _convert_chatmessage_to_ollama_format(message) == {
78
+ "role": "tool",
79
+ "content": tool_result,
80
+ }
81
+
82
+
83
+ def test_convert_chatmessage_to_ollama_invalid():
84
+ message = ChatMessage(_role=ChatRole.ASSISTANT, _content=[])
85
+ with pytest.raises(ValueError):
86
+ _convert_chatmessage_to_ollama_format(message)
87
+
88
+ message = ChatMessage(
89
+ _role=ChatRole.ASSISTANT,
90
+ _content=[
91
+ TextContent(text="I have an answer"),
92
+ TextContent(text="I have another answer"),
93
+ ],
94
+ )
95
+ with pytest.raises(ValueError):
96
+ _convert_chatmessage_to_ollama_format(message)
97
+
98
+
99
+ def test_convert_ollama_response_to_chatmessage():
100
+ model = "some_model"
101
+
102
+ ollama_response = ChatResponse(
103
+ model=model,
104
+ created_at="2023-12-12T14:13:43.416799Z",
105
+ message={"role": "assistant", "content": "Hello! How are you today?"},
106
+ done=True,
107
+ total_duration=5191566416,
108
+ load_duration=2154458,
109
+ prompt_eval_count=26,
110
+ prompt_eval_duration=383809000,
111
+ eval_count=298,
112
+ eval_duration=4799921000,
113
+ )
114
+
115
+ observed = _convert_ollama_response_to_chatmessage(ollama_response)
116
+
117
+ assert observed.role == "assistant"
118
+ assert observed.text == "Hello! How are you today?"
119
+
120
+
121
+ def test_convert_ollama_response_to_chatmessage_with_tools():
122
+ model = "some_model"
123
+
124
+ ollama_response = ChatResponse(
125
+ model=model,
126
+ created_at="2023-12-12T14:13:43.416799Z",
127
+ message={
128
+ "role": "assistant",
129
+ "content": "",
130
+ "tool_calls": [
131
+ {
132
+ "function": {
133
+ "name": "get_current_weather",
134
+ "arguments": {"format": "celsius", "location": "Paris, FR"},
135
+ }
136
+ }
137
+ ],
138
+ },
139
+ done=True,
140
+ total_duration=5191566416,
141
+ load_duration=2154458,
142
+ prompt_eval_count=26,
143
+ prompt_eval_duration=383809000,
144
+ eval_count=298,
145
+ eval_duration=4799921000,
146
+ )
147
+
148
+ observed = _convert_ollama_response_to_chatmessage(ollama_response)
149
+
150
+ assert observed.role == "assistant"
151
+ assert observed.text == ""
152
+ assert observed.tool_call == ToolCall(
153
+ tool_name="get_current_weather",
154
+ arguments={"format": "celsius", "location": "Paris, FR"},
155
+ )
156
+
157
+
158
+ class TestOllamaChatGenerator:
159
+ def test_init_default(self):
160
+ component = OllamaChatGenerator()
161
+ assert component.model == "orca-mini"
162
+ assert component.url == "http://localhost:11434"
163
+ assert component.generation_kwargs == {}
164
+ assert component.timeout == 120
165
+ assert component.streaming_callback is None
166
+ assert component.tools is None
167
+ assert component.keep_alive is None
168
+
169
+ def test_init(self, tools):
170
+ component = OllamaChatGenerator(
171
+ model="llama2",
172
+ url="http://my-custom-endpoint:11434",
173
+ generation_kwargs={"temperature": 0.5},
174
+ timeout=5,
175
+ keep_alive="10m",
176
+ streaming_callback=print_streaming_chunk,
177
+ tools=tools,
178
+ )
179
+
180
+ assert component.model == "llama2"
181
+ assert component.url == "http://my-custom-endpoint:11434"
182
+ assert component.generation_kwargs == {"temperature": 0.5}
183
+ assert component.timeout == 5
184
+ assert component.keep_alive == "10m"
185
+ assert component.streaming_callback is print_streaming_chunk
186
+ assert component.tools == tools
187
+
188
+ def test_init_fail_with_duplicate_tool_names(self, tools):
189
+
190
+ duplicate_tools = [tools[0], tools[0]]
191
+ with pytest.raises(ValueError):
192
+ OllamaChatGenerator(tools=duplicate_tools)
193
+
194
+ def test_to_dict(self):
195
+ tool = Tool(
196
+ name="name",
197
+ description="description",
198
+ parameters={"x": {"type": "string"}},
199
+ function=print,
200
+ )
201
+
202
+ component = OllamaChatGenerator(
203
+ model="llama2",
204
+ streaming_callback=print_streaming_chunk,
205
+ url="custom_url",
206
+ generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
207
+ tools=[tool],
208
+ keep_alive="5m",
209
+ )
210
+ data = component.to_dict()
211
+ assert data == {
212
+ "type": "haystack_integrations.components.generators.ollama.chat.chat_generator.OllamaChatGenerator",
213
+ "init_parameters": {
214
+ "timeout": 120,
215
+ "model": "llama2",
216
+ "url": "custom_url",
217
+ "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
218
+ "keep_alive": "5m",
219
+ "generation_kwargs": {
220
+ "max_tokens": 10,
221
+ "some_test_param": "test-params",
222
+ },
223
+ "tools": [
224
+ {
225
+ "type": "haystack.tools.tool.Tool",
226
+ "data": {
227
+ "description": "description",
228
+ "function": "builtins.print",
229
+ "name": "name",
230
+ "parameters": {
231
+ "x": {
232
+ "type": "string",
233
+ },
234
+ },
235
+ },
236
+ },
237
+ ],
238
+ },
239
+ }
240
+
241
+ def test_from_dict(self):
242
+ tool = Tool(
243
+ name="name",
244
+ description="description",
245
+ parameters={"x": {"type": "string"}},
246
+ function=print,
247
+ )
248
+
249
+ data = {
250
+ "type": "haystack_integrations.components.generators.ollama.chat.chat_generator.OllamaChatGenerator",
251
+ "init_parameters": {
252
+ "timeout": 120,
253
+ "model": "llama2",
254
+ "url": "custom_url",
255
+ "keep_alive": "5m",
256
+ "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
257
+ "generation_kwargs": {
258
+ "max_tokens": 10,
259
+ "some_test_param": "test-params",
260
+ },
261
+ "tools": [
262
+ {
263
+ "type": "haystack.tools.tool.Tool",
264
+ "data": {
265
+ "description": "description",
266
+ "function": "builtins.print",
267
+ "name": "name",
268
+ "parameters": {
269
+ "x": {
270
+ "type": "string",
271
+ },
272
+ },
273
+ },
274
+ },
275
+ ],
276
+ },
277
+ }
278
+ component = OllamaChatGenerator.from_dict(data)
279
+ assert component.model == "llama2"
280
+ assert component.streaming_callback is print_streaming_chunk
281
+ assert component.url == "custom_url"
282
+ assert component.keep_alive == "5m"
283
+ assert component.generation_kwargs == {
284
+ "max_tokens": 10,
285
+ "some_test_param": "test-params",
286
+ }
287
+ assert component.timeout == 120
288
+ assert component.tools == [tool]
289
+
290
+ @patch("haystack_integrations.components.generators.ollama.chat.chat_generator.Client")
291
+ def test_run(self, mock_client):
292
+ generator = OllamaChatGenerator()
293
+
294
+ mock_response = ChatResponse(
295
+ model="llama3.2",
296
+ created_at="2023-12-12T14:13:43.416799Z",
297
+ message={
298
+ "role": "assistant",
299
+ "content": "Fine. How can I help you today?",
300
+ },
301
+ done=True,
302
+ total_duration=5191566416,
303
+ load_duration=2154458,
304
+ prompt_eval_count=26,
305
+ prompt_eval_duration=383809000,
306
+ eval_count=298,
307
+ eval_duration=4799921000,
308
+ )
309
+
310
+ mock_client_instance = mock_client.return_value
311
+ mock_client_instance.chat.return_value = mock_response
312
+
313
+ result = generator.run(messages=[ChatMessage.from_user("Hello! How are you today?")])
314
+
315
+ mock_client_instance.chat.assert_called_once_with(
316
+ model="orca-mini",
317
+ messages=[{"role": "user", "content": "Hello! How are you today?"}],
318
+ stream=False,
319
+ tools=None,
320
+ options={},
321
+ keep_alive=None,
322
+ )
323
+
324
+ assert "replies" in result
325
+ assert len(result["replies"]) == 1
326
+ assert result["replies"][0].text == "Fine. How can I help you today?"
327
+ assert result["replies"][0].role == "assistant"
328
+
329
+ @patch("haystack_integrations.components.generators.ollama.chat.chat_generator.Client")
330
+ def test_run_streaming(self, mock_client):
331
+ streaming_callback_called = False
332
+
333
+ def streaming_callback(_: StreamingChunk) -> None:
334
+ nonlocal streaming_callback_called
335
+ streaming_callback_called = True
336
+
337
+ generator = OllamaChatGenerator(streaming_callback=streaming_callback)
338
+
339
+ mock_response = iter(
340
+ [
341
+ ChatResponse(
342
+ model="llama3.2",
343
+ created_at="2023-12-12T14:13:43.416799Z",
344
+ message={"role": "assistant", "content": "first chunk "},
345
+ done=False,
346
+ ),
347
+ ChatResponse(
348
+ model="llama3.2",
349
+ created_at="2023-12-12T14:13:43.416799Z",
350
+ message={"role": "assistant", "content": "second chunk"},
351
+ done=True,
352
+ total_duration=4883583458,
353
+ load_duration=1334875,
354
+ prompt_eval_count=26,
355
+ prompt_eval_duration=342546000,
356
+ eval_count=282,
357
+ eval_duration=4535599000,
358
+ ),
359
+ ]
360
+ )
361
+
362
+ mock_client_instance = mock_client.return_value
363
+ mock_client_instance.chat.return_value = mock_response
364
+
365
+ result = generator.run(messages=[ChatMessage.from_user("irrelevant")])
366
+
367
+ assert streaming_callback_called
368
+
369
+ assert "replies" in result
370
+ assert len(result["replies"]) == 1
371
+ assert result["replies"][0].text == "first chunk second chunk"
372
+ assert result["replies"][0].role == "assistant"
373
+
374
+ def test_run_fail_with_tools_and_streaming(self, tools):
375
+ component = OllamaChatGenerator(tools=tools, streaming_callback=print_streaming_chunk)
376
+
377
+ with pytest.raises(ValueError):
378
+ message = ChatMessage.from_user("irrelevant")
379
+ component.run([message])
380
+
381
+ @pytest.mark.integration
382
+ def test_live_run(self):
383
+ chat_generator = OllamaChatGenerator(model="llama3.2:3b")
384
+
385
+ user_questions_and_assistant_answers = [
386
+ ("What's the capital of France?", "Paris"),
387
+ ("What is the capital of Canada?", "Ottawa"),
388
+ ("What is the capital of Ghana?", "Accra"),
389
+ ]
390
+
391
+ for question, answer in user_questions_and_assistant_answers:
392
+ message = ChatMessage.from_user(question)
393
+
394
+ response = chat_generator.run([message])
395
+
396
+ assert isinstance(response, dict)
397
+ assert isinstance(response["replies"], list)
398
+ assert answer in response["replies"][0].text
399
+
400
+ @pytest.mark.integration
401
+ def test_run_with_chat_history(self):
402
+ chat_generator = OllamaChatGenerator(model="llama3.2:3b")
403
+
404
+ chat_messages = [
405
+ ChatMessage.from_user("What is the largest city in the United Kingdom by population?"),
406
+ ChatMessage.from_assistant("London is the largest city in the United Kingdom by population"),
407
+ ChatMessage.from_user("And what is the second largest?"),
408
+ ]
409
+
410
+ response = chat_generator.run(chat_messages)
411
+
412
+ assert isinstance(response, dict)
413
+ assert isinstance(response["replies"], list)
414
+
415
+ assert any(city in response["replies"][-1].text for city in ["Manchester", "Birmingham", "Glasgow"])
416
+
417
+ @pytest.mark.integration
418
+ def test_run_model_unavailable(self):
419
+ component = OllamaChatGenerator(model="unknown_model")
420
+
421
+ with pytest.raises(ResponseError):
422
+ message = ChatMessage.from_user("irrelevant")
423
+ component.run([message])
424
+
425
+ @pytest.mark.integration
426
+ def test_run_with_streaming(self):
427
+ streaming_callback = Mock()
428
+ chat_generator = OllamaChatGenerator(model="llama3.2:3b", streaming_callback=streaming_callback)
429
+
430
+ chat_messages = [
431
+ ChatMessage.from_user("What is the largest city in the United Kingdom by population?"),
432
+ ChatMessage.from_assistant("London is the largest city in the United Kingdom by population"),
433
+ ChatMessage.from_user("And what is the second largest?"),
434
+ ]
435
+
436
+ response = chat_generator.run(chat_messages)
437
+
438
+ streaming_callback.assert_called()
439
+
440
+ assert isinstance(response, dict)
441
+ assert isinstance(response["replies"], list)
442
+ assert any(city in response["replies"][-1].text for city in ["Manchester", "Birmingham", "Glasgow"])
443
+
444
+ @pytest.mark.integration
445
+ def test_run_with_tools(self, tools):
446
+ chat_generator = OllamaChatGenerator(model="llama3.2:3b", tools=tools)
447
+
448
+ message = ChatMessage.from_user("What is the weather in Paris?")
449
+ response = chat_generator.run([message])
450
+
451
+ assert len(response["replies"]) == 1
452
+ message = response["replies"][0]
453
+
454
+ assert message.tool_calls
455
+ tool_call = message.tool_call
456
+ assert isinstance(tool_call, ToolCall)
457
+ assert tool_call.tool_name == "weather"
458
+ assert tool_call.arguments == {"city": "Paris"}
@@ -19,7 +19,7 @@ class TestOllamaGenerator:
19
19
  ("What is the capital of Ghana?", "Accra"),
20
20
  ]
21
21
 
22
- component = OllamaGenerator()
22
+ component = OllamaGenerator(model="llama3.2:3b")
23
23
 
24
24
  for prompt, answer in prompts_and_answers:
25
25
  results = component.run(prompt=prompt)
@@ -147,7 +147,7 @@ class TestOllamaGenerator:
147
147
  return chunk
148
148
 
149
149
  callback = Callback()
150
- component = OllamaGenerator(streaming_callback=callback)
150
+ component = OllamaGenerator(model="llama3.2:3b", streaming_callback=callback)
151
151
  results = component.run(prompt="What's the capital of Netherlands?")
152
152
 
153
153
  assert len(results["replies"]) == 1
@@ -1,169 +0,0 @@
1
- from typing import List
2
- from unittest.mock import Mock
3
-
4
- import pytest
5
- from haystack.components.generators.utils import print_streaming_chunk
6
- from haystack.dataclasses import ChatMessage
7
- from ollama._types import ChatResponse, ResponseError
8
-
9
- from haystack_integrations.components.generators.ollama import OllamaChatGenerator
10
-
11
-
12
- @pytest.fixture
13
- def chat_messages() -> List[ChatMessage]:
14
- return [
15
- ChatMessage.from_user("Tell me about why Super Mario is the greatest superhero"),
16
- ChatMessage.from_assistant(
17
- "Super Mario has prevented Bowser from destroying the world", {"something": "something"}
18
- ),
19
- ]
20
-
21
-
22
- class TestOllamaChatGenerator:
23
- def test_init_default(self):
24
- component = OllamaChatGenerator()
25
- assert component.model == "orca-mini"
26
- assert component.url == "http://localhost:11434"
27
- assert component.generation_kwargs == {}
28
- assert component.timeout == 120
29
- assert component.keep_alive is None
30
-
31
- def test_init(self):
32
- component = OllamaChatGenerator(
33
- model="llama2",
34
- url="http://my-custom-endpoint:11434",
35
- generation_kwargs={"temperature": 0.5},
36
- keep_alive="10m",
37
- timeout=5,
38
- )
39
-
40
- assert component.model == "llama2"
41
- assert component.url == "http://my-custom-endpoint:11434"
42
- assert component.generation_kwargs == {"temperature": 0.5}
43
- assert component.timeout == 5
44
- assert component.keep_alive == "10m"
45
-
46
- def test_to_dict(self):
47
- component = OllamaChatGenerator(
48
- model="llama2",
49
- streaming_callback=print_streaming_chunk,
50
- url="custom_url",
51
- generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
52
- keep_alive="5m",
53
- )
54
- data = component.to_dict()
55
- assert data == {
56
- "type": "haystack_integrations.components.generators.ollama.chat.chat_generator.OllamaChatGenerator",
57
- "init_parameters": {
58
- "timeout": 120,
59
- "model": "llama2",
60
- "keep_alive": "5m",
61
- "url": "custom_url",
62
- "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
63
- "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
64
- },
65
- }
66
-
67
- def test_from_dict(self):
68
- data = {
69
- "type": "haystack_integrations.components.generators.ollama.chat.chat_generator.OllamaChatGenerator",
70
- "init_parameters": {
71
- "timeout": 120,
72
- "model": "llama2",
73
- "url": "custom_url",
74
- "keep_alive": "5m",
75
- "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
76
- "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
77
- },
78
- }
79
- component = OllamaChatGenerator.from_dict(data)
80
- assert component.model == "llama2"
81
- assert component.streaming_callback is print_streaming_chunk
82
- assert component.url == "custom_url"
83
- assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"}
84
- assert component.keep_alive == "5m"
85
-
86
- def test_build_message_from_ollama_response(self):
87
- model = "some_model"
88
-
89
- ollama_response = ChatResponse(
90
- model=model,
91
- created_at="2023-12-12T14:13:43.416799Z",
92
- message={"role": "assistant", "content": "Hello! How are you today?"},
93
- done=True,
94
- total_duration=5191566416,
95
- load_duration=2154458,
96
- prompt_eval_count=26,
97
- prompt_eval_duration=383809000,
98
- eval_count=298,
99
- eval_duration=4799921000,
100
- )
101
-
102
- observed = OllamaChatGenerator(model=model)._build_message_from_ollama_response(ollama_response)
103
-
104
- assert observed.role == "assistant"
105
- assert observed.text == "Hello! How are you today?"
106
-
107
- @pytest.mark.integration
108
- def test_run(self):
109
- chat_generator = OllamaChatGenerator()
110
-
111
- user_questions_and_assistant_answers = [
112
- ("What's the capital of France?", "Paris"),
113
- ("What is the capital of Canada?", "Ottawa"),
114
- ("What is the capital of Ghana?", "Accra"),
115
- ]
116
-
117
- for question, answer in user_questions_and_assistant_answers:
118
- message = ChatMessage.from_user(question)
119
-
120
- response = chat_generator.run([message])
121
-
122
- assert isinstance(response, dict)
123
- assert isinstance(response["replies"], list)
124
- assert answer in response["replies"][0].text
125
-
126
- @pytest.mark.integration
127
- def test_run_with_chat_history(self):
128
- chat_generator = OllamaChatGenerator()
129
-
130
- chat_history = [
131
- ChatMessage.from_user("What is the largest city in the United Kingdom by population?"),
132
- ChatMessage.from_assistant("London is the largest city in the United Kingdom by population"),
133
- ChatMessage.from_user("And what is the second largest?"),
134
- ]
135
-
136
- response = chat_generator.run(chat_history)
137
-
138
- assert isinstance(response, dict)
139
- assert isinstance(response["replies"], list)
140
- assert "Manchester" in response["replies"][-1].text or "Glasgow" in response["replies"][-1].text
141
-
142
- @pytest.mark.integration
143
- def test_run_model_unavailable(self):
144
- component = OllamaChatGenerator(model="Alistair_and_Stefano_are_great")
145
-
146
- with pytest.raises(ResponseError):
147
- message = ChatMessage.from_user(
148
- "Based on your infinite wisdom, can you tell me why Alistair and Stefano are so great?"
149
- )
150
- component.run([message])
151
-
152
- @pytest.mark.integration
153
- def test_run_with_streaming(self):
154
- streaming_callback = Mock()
155
- chat_generator = OllamaChatGenerator(streaming_callback=streaming_callback)
156
-
157
- chat_history = [
158
- ChatMessage.from_user("What is the largest city in the United Kingdom by population?"),
159
- ChatMessage.from_assistant("London is the largest city in the United Kingdom by population"),
160
- ChatMessage.from_user("And what is the second largest?"),
161
- ]
162
-
163
- response = chat_generator.run(chat_history)
164
-
165
- streaming_callback.assert_called()
166
-
167
- assert isinstance(response, dict)
168
- assert isinstance(response["replies"], list)
169
- assert "Manchester" in response["replies"][-1].text or "Glasgow" in response["replies"][-1].text