ollama-haystack 2.1.2__tar.gz → 2.3.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.
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/CHANGELOG.md +14 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/PKG-INFO +2 -1
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/pyproject.toml +3 -2
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/src/haystack_integrations/components/generators/ollama/chat/chat_generator.py +123 -41
- ollama_haystack-2.3.0/tests/test_chat_generator.py +529 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/tests/test_generator.py +2 -2
- ollama_haystack-2.1.2/tests/test_chat_generator.py +0 -169
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/.gitignore +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/LICENSE.txt +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/README.md +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/examples/chat_generator_example.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/examples/embedders_example.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/examples/generator_example.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/pydoc/config.yml +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/src/haystack_integrations/components/embedders/ollama/__init__.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/src/haystack_integrations/components/embedders/ollama/document_embedder.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/src/haystack_integrations/components/embedders/ollama/text_embedder.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/src/haystack_integrations/components/generators/ollama/__init__.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/src/haystack_integrations/components/generators/ollama/chat/__init__.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/src/haystack_integrations/components/generators/ollama/generator.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/tests/__init__.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/tests/test_document_embedder.py +0 -0
- {ollama_haystack-2.1.2 → ollama_haystack-2.3.0}/tests/test_text_embedder.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/ollama-v2.2.0] - 2025-01-16
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- Ollama - add support for tools (#1294)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [integrations/ollama-v2.1.2] - 2024-12-18
|
|
11
|
+
|
|
12
|
+
### 🐛 Bug Fixes
|
|
13
|
+
|
|
14
|
+
- Make Ollama Chat Generator compatible with new ChatMessage (#1256)
|
|
15
|
+
|
|
16
|
+
|
|
3
17
|
## [integrations/ollama-v2.1.1] - 2024-12-10
|
|
4
18
|
|
|
5
19
|
### 🌀 Miscellaneous
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ollama-haystack
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
|
20
20
|
Requires-Python: >=3.8
|
|
21
21
|
Requires-Dist: haystack-ai
|
|
22
22
|
Requires-Dist: ollama>=0.4.0
|
|
23
|
+
Requires-Dist: pydantic
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
|
|
25
26
|
# ollama-haystack
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
27
27
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
28
28
|
]
|
|
29
|
-
dependencies = ["haystack-ai", "ollama>=0.4.0"]
|
|
29
|
+
dependencies = ["haystack-ai", "ollama>=0.4.0", "pydantic"]
|
|
30
30
|
|
|
31
31
|
[project.urls]
|
|
32
32
|
Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/ollama#readme"
|
|
@@ -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}"
|
|
@@ -164,5 +165,5 @@ markers = [
|
|
|
164
165
|
addopts = ["--import-mode=importlib"]
|
|
165
166
|
|
|
166
167
|
[[tool.mypy.overrides]]
|
|
167
|
-
module = ["haystack.*", "haystack_integrations.*", "pytest.*", "ollama.*"]
|
|
168
|
+
module = ["haystack.*", "haystack_integrations.*", "pytest.*", "ollama.*", "pydantic.*"]
|
|
168
169
|
ignore_missing_imports = true
|
|
@@ -1,12 +1,69 @@
|
|
|
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
|
-
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
|
|
7
|
+
from pydantic.json_schema import JsonSchemaValue
|
|
6
8
|
|
|
7
9
|
from ollama import ChatResponse, Client
|
|
8
10
|
|
|
9
11
|
|
|
12
|
+
def _convert_chatmessage_to_ollama_format(message: ChatMessage) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Convert a ChatMessage to the format expected by Ollama Chat API.
|
|
15
|
+
"""
|
|
16
|
+
text_contents = message.texts
|
|
17
|
+
tool_calls = message.tool_calls
|
|
18
|
+
tool_call_results = message.tool_call_results
|
|
19
|
+
|
|
20
|
+
if not text_contents and not tool_calls and not tool_call_results:
|
|
21
|
+
msg = "A `ChatMessage` must contain at least one `TextContent`, `ToolCall`, or `ToolCallResult`."
|
|
22
|
+
raise ValueError(msg)
|
|
23
|
+
elif len(text_contents) + len(tool_call_results) > 1:
|
|
24
|
+
msg = "A `ChatMessage` can only contain one `TextContent` or one `ToolCallResult`."
|
|
25
|
+
raise ValueError(msg)
|
|
26
|
+
|
|
27
|
+
ollama_msg: Dict[str, Any] = {"role": message._role.value}
|
|
28
|
+
|
|
29
|
+
if tool_call_results:
|
|
30
|
+
# Ollama does not provide a way to communicate errors in tool invocations, so we ignore the error field
|
|
31
|
+
ollama_msg["content"] = tool_call_results[0].result
|
|
32
|
+
return ollama_msg
|
|
33
|
+
|
|
34
|
+
if text_contents:
|
|
35
|
+
ollama_msg["content"] = text_contents[0]
|
|
36
|
+
if tool_calls:
|
|
37
|
+
# Ollama does not support tool call id, so we ignore it
|
|
38
|
+
ollama_msg["tool_calls"] = [
|
|
39
|
+
{"type": "function", "function": {"name": tc.tool_name, "arguments": tc.arguments}} for tc in tool_calls
|
|
40
|
+
]
|
|
41
|
+
return ollama_msg
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _convert_ollama_response_to_chatmessage(ollama_response: "ChatResponse") -> ChatMessage:
|
|
45
|
+
"""
|
|
46
|
+
Converts the non-streaming response from the Ollama API to a ChatMessage with assistant role.
|
|
47
|
+
"""
|
|
48
|
+
response_dict = ollama_response.model_dump()
|
|
49
|
+
|
|
50
|
+
ollama_message = response_dict["message"]
|
|
51
|
+
|
|
52
|
+
text = ollama_message["content"]
|
|
53
|
+
|
|
54
|
+
tool_calls = []
|
|
55
|
+
if ollama_tool_calls := ollama_message.get("tool_calls"):
|
|
56
|
+
for ollama_tc in ollama_tool_calls:
|
|
57
|
+
tool_calls.append(
|
|
58
|
+
ToolCall(tool_name=ollama_tc["function"]["name"], arguments=ollama_tc["function"]["arguments"])
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
message = ChatMessage.from_assistant(text=text, tool_calls=tool_calls)
|
|
62
|
+
|
|
63
|
+
message.meta.update({key: value for key, value in response_dict.items() if key != "message"})
|
|
64
|
+
return message
|
|
65
|
+
|
|
66
|
+
|
|
10
67
|
@component
|
|
11
68
|
class OllamaChatGenerator:
|
|
12
69
|
"""
|
|
@@ -40,6 +97,8 @@ class OllamaChatGenerator:
|
|
|
40
97
|
timeout: int = 120,
|
|
41
98
|
keep_alive: Optional[Union[float, str]] = None,
|
|
42
99
|
streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
|
|
100
|
+
tools: Optional[List[Tool]] = None,
|
|
101
|
+
response_format: Optional[Union[None, Literal["json"], JsonSchemaValue]] = None,
|
|
43
102
|
):
|
|
44
103
|
"""
|
|
45
104
|
:param model:
|
|
@@ -52,9 +111,6 @@ class OllamaChatGenerator:
|
|
|
52
111
|
[Ollama docs](https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values).
|
|
53
112
|
:param timeout:
|
|
54
113
|
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
114
|
:param keep_alive:
|
|
59
115
|
The option that controls how long the model will stay loaded into memory following the request.
|
|
60
116
|
If not set, it will use the default value from the Ollama (5 minutes).
|
|
@@ -63,15 +119,30 @@ class OllamaChatGenerator:
|
|
|
63
119
|
- a number in seconds (such as 3600)
|
|
64
120
|
- any negative number which will keep the model loaded in memory (e.g. -1 or "-1m")
|
|
65
121
|
- '0' which will unload the model immediately after generating a response.
|
|
122
|
+
:param streaming_callback:
|
|
123
|
+
A callback function that is called when a new token is received from the stream.
|
|
124
|
+
The callback function accepts StreamingChunk as an argument.
|
|
125
|
+
:param tools:
|
|
126
|
+
A list of tools for which the model can prepare calls.
|
|
127
|
+
Not all models support tools. For a list of models compatible with tools, see the
|
|
128
|
+
[models page](https://ollama.com/search?c=tools).
|
|
129
|
+
:param response_format:
|
|
130
|
+
The format for structured model outputs. The value can be:
|
|
131
|
+
- None: No specific structure or format is applied to the response. The response is returned as-is.
|
|
132
|
+
- "json": The response is formatted as a JSON object.
|
|
133
|
+
- JSON Schema: The response is formatted as a JSON object that adheres to the specified JSON Schema.
|
|
66
134
|
"""
|
|
67
135
|
|
|
136
|
+
_check_duplicate_tool_names(tools)
|
|
137
|
+
|
|
68
138
|
self.timeout = timeout
|
|
69
139
|
self.generation_kwargs = generation_kwargs or {}
|
|
70
140
|
self.url = url
|
|
71
141
|
self.model = model
|
|
72
142
|
self.keep_alive = keep_alive
|
|
73
143
|
self.streaming_callback = streaming_callback
|
|
74
|
-
|
|
144
|
+
self.tools = tools
|
|
145
|
+
self.response_format = response_format
|
|
75
146
|
self._client = Client(host=self.url, timeout=self.timeout)
|
|
76
147
|
|
|
77
148
|
def to_dict(self) -> Dict[str, Any]:
|
|
@@ -82,6 +153,7 @@ class OllamaChatGenerator:
|
|
|
82
153
|
Dictionary with serialized data.
|
|
83
154
|
"""
|
|
84
155
|
callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None
|
|
156
|
+
serialized_tools = [tool.to_dict() for tool in self.tools] if self.tools else None
|
|
85
157
|
return default_to_dict(
|
|
86
158
|
self,
|
|
87
159
|
model=self.model,
|
|
@@ -90,6 +162,8 @@ class OllamaChatGenerator:
|
|
|
90
162
|
generation_kwargs=self.generation_kwargs,
|
|
91
163
|
timeout=self.timeout,
|
|
92
164
|
streaming_callback=callback_name,
|
|
165
|
+
tools=serialized_tools,
|
|
166
|
+
response_format=self.response_format,
|
|
93
167
|
)
|
|
94
168
|
|
|
95
169
|
@classmethod
|
|
@@ -102,34 +176,14 @@ class OllamaChatGenerator:
|
|
|
102
176
|
:returns:
|
|
103
177
|
Deserialized component.
|
|
104
178
|
"""
|
|
179
|
+
deserialize_tools_inplace(data["init_parameters"], key="tools")
|
|
180
|
+
|
|
105
181
|
init_params = data.get("init_parameters", {})
|
|
106
|
-
|
|
107
|
-
if serialized_callback_handler:
|
|
182
|
+
|
|
183
|
+
if serialized_callback_handler := init_params.get("streaming_callback"):
|
|
108
184
|
data["init_parameters"]["streaming_callback"] = deserialize_callable(serialized_callback_handler)
|
|
109
185
|
return default_from_dict(cls, data)
|
|
110
186
|
|
|
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
187
|
def _build_chunk(self, chunk_response: Any) -> StreamingChunk:
|
|
134
188
|
"""
|
|
135
189
|
Converts the response from the Ollama API to a StreamingChunk.
|
|
@@ -143,23 +197,28 @@ class OllamaChatGenerator:
|
|
|
143
197
|
chunk_message = StreamingChunk(content, meta)
|
|
144
198
|
return chunk_message
|
|
145
199
|
|
|
146
|
-
def _handle_streaming_response(self, response) -> List[
|
|
200
|
+
def _handle_streaming_response(self, response) -> Dict[str, List[Any]]:
|
|
147
201
|
"""
|
|
148
|
-
Handles
|
|
202
|
+
Handles streaming response and converts it to Haystack format
|
|
149
203
|
"""
|
|
150
204
|
chunks: List[StreamingChunk] = []
|
|
151
205
|
for chunk in response:
|
|
152
|
-
chunk_delta
|
|
206
|
+
chunk_delta = self._build_chunk(chunk)
|
|
153
207
|
chunks.append(chunk_delta)
|
|
154
208
|
if self.streaming_callback is not None:
|
|
155
209
|
self.streaming_callback(chunk_delta)
|
|
156
|
-
|
|
210
|
+
|
|
211
|
+
replies = [ChatMessage.from_assistant("".join([c.content for c in chunks]))]
|
|
212
|
+
meta = {key: value for key, value in chunks[0].meta.items() if key != "message"}
|
|
213
|
+
|
|
214
|
+
return {"replies": replies, "meta": [meta]}
|
|
157
215
|
|
|
158
216
|
@component.output_types(replies=List[ChatMessage])
|
|
159
217
|
def run(
|
|
160
218
|
self,
|
|
161
219
|
messages: List[ChatMessage],
|
|
162
220
|
generation_kwargs: Optional[Dict[str, Any]] = None,
|
|
221
|
+
tools: Optional[List[Tool]] = None,
|
|
163
222
|
):
|
|
164
223
|
"""
|
|
165
224
|
Runs an Ollama Model on a given chat history.
|
|
@@ -170,21 +229,44 @@ class OllamaChatGenerator:
|
|
|
170
229
|
Optional arguments to pass to the Ollama generation endpoint, such as temperature,
|
|
171
230
|
top_p, etc. See the
|
|
172
231
|
[Ollama docs](https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values).
|
|
173
|
-
:param
|
|
174
|
-
A
|
|
232
|
+
:param tools:
|
|
233
|
+
A list of tools for which the model can prepare calls. If set, it will override the `tools` parameter set
|
|
234
|
+
during component initialization.
|
|
175
235
|
:returns: A dictionary with the following keys:
|
|
176
236
|
- `replies`: The responses from the model
|
|
177
237
|
"""
|
|
178
238
|
generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
|
|
179
239
|
|
|
180
240
|
stream = self.streaming_callback is not None
|
|
181
|
-
|
|
241
|
+
tools = tools or self.tools
|
|
242
|
+
_check_duplicate_tool_names(tools)
|
|
243
|
+
|
|
244
|
+
if stream and tools:
|
|
245
|
+
msg = "Ollama does not support tools and streaming at the same time. Please choose one."
|
|
246
|
+
raise ValueError(msg)
|
|
247
|
+
|
|
248
|
+
if self.response_format and tools:
|
|
249
|
+
msg = "Ollama does not support tools and response_format at the same time. Please choose one."
|
|
250
|
+
raise ValueError(msg)
|
|
251
|
+
|
|
252
|
+
if self.response_format and stream:
|
|
253
|
+
msg = "Ollama does not support streaming and response_format at the same time. Please choose one."
|
|
254
|
+
raise ValueError(msg)
|
|
255
|
+
|
|
256
|
+
ollama_tools = [{"type": "function", "function": {**t.tool_spec}} for t in tools] if tools else None
|
|
257
|
+
|
|
258
|
+
ollama_messages = [_convert_chatmessage_to_ollama_format(msg) for msg in messages]
|
|
182
259
|
response = self._client.chat(
|
|
183
|
-
model=self.model,
|
|
260
|
+
model=self.model,
|
|
261
|
+
messages=ollama_messages,
|
|
262
|
+
tools=ollama_tools,
|
|
263
|
+
stream=stream,
|
|
264
|
+
keep_alive=self.keep_alive,
|
|
265
|
+
options=generation_kwargs,
|
|
266
|
+
format=self.response_format,
|
|
184
267
|
)
|
|
185
268
|
|
|
186
269
|
if stream:
|
|
187
|
-
|
|
188
|
-
return self._convert_to_streaming_response(chunks)
|
|
270
|
+
return self._handle_streaming_response(response)
|
|
189
271
|
|
|
190
|
-
return {"replies": [
|
|
272
|
+
return {"replies": [_convert_ollama_response_to_chatmessage(response)]}
|
|
@@ -0,0 +1,529 @@
|
|
|
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
|
+
assert component.response_format is None
|
|
169
|
+
|
|
170
|
+
def test_init(self, tools):
|
|
171
|
+
component = OllamaChatGenerator(
|
|
172
|
+
model="llama2",
|
|
173
|
+
url="http://my-custom-endpoint:11434",
|
|
174
|
+
generation_kwargs={"temperature": 0.5},
|
|
175
|
+
timeout=5,
|
|
176
|
+
keep_alive="10m",
|
|
177
|
+
streaming_callback=print_streaming_chunk,
|
|
178
|
+
tools=tools,
|
|
179
|
+
response_format={"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}}},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
assert component.model == "llama2"
|
|
183
|
+
assert component.url == "http://my-custom-endpoint:11434"
|
|
184
|
+
assert component.generation_kwargs == {"temperature": 0.5}
|
|
185
|
+
assert component.timeout == 5
|
|
186
|
+
assert component.keep_alive == "10m"
|
|
187
|
+
assert component.streaming_callback is print_streaming_chunk
|
|
188
|
+
assert component.tools == tools
|
|
189
|
+
assert component.response_format == {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": {"name": {"type": "string"}, "age": {"type": "number"}},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def test_init_fail_with_duplicate_tool_names(self, tools):
|
|
195
|
+
|
|
196
|
+
duplicate_tools = [tools[0], tools[0]]
|
|
197
|
+
with pytest.raises(ValueError):
|
|
198
|
+
OllamaChatGenerator(tools=duplicate_tools)
|
|
199
|
+
|
|
200
|
+
def test_to_dict(self):
|
|
201
|
+
tool = Tool(
|
|
202
|
+
name="name",
|
|
203
|
+
description="description",
|
|
204
|
+
parameters={"x": {"type": "string"}},
|
|
205
|
+
function=print,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
component = OllamaChatGenerator(
|
|
209
|
+
model="llama2",
|
|
210
|
+
streaming_callback=print_streaming_chunk,
|
|
211
|
+
url="custom_url",
|
|
212
|
+
generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
|
|
213
|
+
tools=[tool],
|
|
214
|
+
keep_alive="5m",
|
|
215
|
+
response_format={"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}}},
|
|
216
|
+
)
|
|
217
|
+
data = component.to_dict()
|
|
218
|
+
assert data == {
|
|
219
|
+
"type": "haystack_integrations.components.generators.ollama.chat.chat_generator.OllamaChatGenerator",
|
|
220
|
+
"init_parameters": {
|
|
221
|
+
"timeout": 120,
|
|
222
|
+
"model": "llama2",
|
|
223
|
+
"url": "custom_url",
|
|
224
|
+
"streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
|
|
225
|
+
"keep_alive": "5m",
|
|
226
|
+
"generation_kwargs": {
|
|
227
|
+
"max_tokens": 10,
|
|
228
|
+
"some_test_param": "test-params",
|
|
229
|
+
},
|
|
230
|
+
"tools": [
|
|
231
|
+
{
|
|
232
|
+
"type": "haystack.tools.tool.Tool",
|
|
233
|
+
"data": {
|
|
234
|
+
"description": "description",
|
|
235
|
+
"function": "builtins.print",
|
|
236
|
+
"name": "name",
|
|
237
|
+
"parameters": {
|
|
238
|
+
"x": {
|
|
239
|
+
"type": "string",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
"response_format": {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"properties": {"name": {"type": "string"}, "age": {"type": "number"}},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
def test_from_dict(self):
|
|
253
|
+
tool = Tool(
|
|
254
|
+
name="name",
|
|
255
|
+
description="description",
|
|
256
|
+
parameters={"x": {"type": "string"}},
|
|
257
|
+
function=print,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
data = {
|
|
261
|
+
"type": "haystack_integrations.components.generators.ollama.chat.chat_generator.OllamaChatGenerator",
|
|
262
|
+
"init_parameters": {
|
|
263
|
+
"timeout": 120,
|
|
264
|
+
"model": "llama2",
|
|
265
|
+
"url": "custom_url",
|
|
266
|
+
"keep_alive": "5m",
|
|
267
|
+
"streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
|
|
268
|
+
"generation_kwargs": {
|
|
269
|
+
"max_tokens": 10,
|
|
270
|
+
"some_test_param": "test-params",
|
|
271
|
+
},
|
|
272
|
+
"tools": [
|
|
273
|
+
{
|
|
274
|
+
"type": "haystack.tools.tool.Tool",
|
|
275
|
+
"data": {
|
|
276
|
+
"description": "description",
|
|
277
|
+
"function": "builtins.print",
|
|
278
|
+
"name": "name",
|
|
279
|
+
"parameters": {
|
|
280
|
+
"x": {
|
|
281
|
+
"type": "string",
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
"response_format": {
|
|
288
|
+
"type": "object",
|
|
289
|
+
"properties": {"name": {"type": "string"}, "age": {"type": "number"}},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
component = OllamaChatGenerator.from_dict(data)
|
|
294
|
+
assert component.model == "llama2"
|
|
295
|
+
assert component.streaming_callback is print_streaming_chunk
|
|
296
|
+
assert component.url == "custom_url"
|
|
297
|
+
assert component.keep_alive == "5m"
|
|
298
|
+
assert component.generation_kwargs == {
|
|
299
|
+
"max_tokens": 10,
|
|
300
|
+
"some_test_param": "test-params",
|
|
301
|
+
}
|
|
302
|
+
assert component.timeout == 120
|
|
303
|
+
assert component.tools == [tool]
|
|
304
|
+
assert component.response_format == {
|
|
305
|
+
"type": "object",
|
|
306
|
+
"properties": {"name": {"type": "string"}, "age": {"type": "number"}},
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@patch("haystack_integrations.components.generators.ollama.chat.chat_generator.Client")
|
|
310
|
+
def test_run(self, mock_client):
|
|
311
|
+
generator = OllamaChatGenerator()
|
|
312
|
+
|
|
313
|
+
mock_response = ChatResponse(
|
|
314
|
+
model="llama3.2",
|
|
315
|
+
created_at="2023-12-12T14:13:43.416799Z",
|
|
316
|
+
message={
|
|
317
|
+
"role": "assistant",
|
|
318
|
+
"content": "Fine. How can I help you today?",
|
|
319
|
+
},
|
|
320
|
+
done=True,
|
|
321
|
+
total_duration=5191566416,
|
|
322
|
+
load_duration=2154458,
|
|
323
|
+
prompt_eval_count=26,
|
|
324
|
+
prompt_eval_duration=383809000,
|
|
325
|
+
eval_count=298,
|
|
326
|
+
eval_duration=4799921000,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
mock_client_instance = mock_client.return_value
|
|
330
|
+
mock_client_instance.chat.return_value = mock_response
|
|
331
|
+
|
|
332
|
+
result = generator.run(messages=[ChatMessage.from_user("Hello! How are you today?")])
|
|
333
|
+
|
|
334
|
+
mock_client_instance.chat.assert_called_once_with(
|
|
335
|
+
model="orca-mini",
|
|
336
|
+
messages=[{"role": "user", "content": "Hello! How are you today?"}],
|
|
337
|
+
stream=False,
|
|
338
|
+
tools=None,
|
|
339
|
+
options={},
|
|
340
|
+
keep_alive=None,
|
|
341
|
+
format=None,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
assert "replies" in result
|
|
345
|
+
assert len(result["replies"]) == 1
|
|
346
|
+
assert result["replies"][0].text == "Fine. How can I help you today?"
|
|
347
|
+
assert result["replies"][0].role == "assistant"
|
|
348
|
+
|
|
349
|
+
@patch("haystack_integrations.components.generators.ollama.chat.chat_generator.Client")
|
|
350
|
+
def test_run_streaming(self, mock_client):
|
|
351
|
+
streaming_callback_called = False
|
|
352
|
+
|
|
353
|
+
def streaming_callback(_: StreamingChunk) -> None:
|
|
354
|
+
nonlocal streaming_callback_called
|
|
355
|
+
streaming_callback_called = True
|
|
356
|
+
|
|
357
|
+
generator = OllamaChatGenerator(streaming_callback=streaming_callback)
|
|
358
|
+
|
|
359
|
+
mock_response = iter(
|
|
360
|
+
[
|
|
361
|
+
ChatResponse(
|
|
362
|
+
model="llama3.2",
|
|
363
|
+
created_at="2023-12-12T14:13:43.416799Z",
|
|
364
|
+
message={"role": "assistant", "content": "first chunk "},
|
|
365
|
+
done=False,
|
|
366
|
+
),
|
|
367
|
+
ChatResponse(
|
|
368
|
+
model="llama3.2",
|
|
369
|
+
created_at="2023-12-12T14:13:43.416799Z",
|
|
370
|
+
message={"role": "assistant", "content": "second chunk"},
|
|
371
|
+
done=True,
|
|
372
|
+
total_duration=4883583458,
|
|
373
|
+
load_duration=1334875,
|
|
374
|
+
prompt_eval_count=26,
|
|
375
|
+
prompt_eval_duration=342546000,
|
|
376
|
+
eval_count=282,
|
|
377
|
+
eval_duration=4535599000,
|
|
378
|
+
),
|
|
379
|
+
]
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
mock_client_instance = mock_client.return_value
|
|
383
|
+
mock_client_instance.chat.return_value = mock_response
|
|
384
|
+
|
|
385
|
+
result = generator.run(messages=[ChatMessage.from_user("irrelevant")])
|
|
386
|
+
|
|
387
|
+
assert streaming_callback_called
|
|
388
|
+
|
|
389
|
+
assert "replies" in result
|
|
390
|
+
assert len(result["replies"]) == 1
|
|
391
|
+
assert result["replies"][0].text == "first chunk second chunk"
|
|
392
|
+
assert result["replies"][0].role == "assistant"
|
|
393
|
+
|
|
394
|
+
def test_run_fail_with_tools_and_streaming(self, tools):
|
|
395
|
+
component = OllamaChatGenerator(tools=tools, streaming_callback=print_streaming_chunk)
|
|
396
|
+
|
|
397
|
+
with pytest.raises(ValueError):
|
|
398
|
+
message = ChatMessage.from_user("irrelevant")
|
|
399
|
+
component.run([message])
|
|
400
|
+
|
|
401
|
+
@pytest.mark.integration
|
|
402
|
+
def test_live_run(self):
|
|
403
|
+
chat_generator = OllamaChatGenerator(model="llama3.2:3b")
|
|
404
|
+
|
|
405
|
+
user_questions_and_assistant_answers = [
|
|
406
|
+
("What's the capital of France?", "Paris"),
|
|
407
|
+
("What is the capital of Canada?", "Ottawa"),
|
|
408
|
+
("What is the capital of Ghana?", "Accra"),
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
for question, answer in user_questions_and_assistant_answers:
|
|
412
|
+
message = ChatMessage.from_user(question)
|
|
413
|
+
|
|
414
|
+
response = chat_generator.run([message])
|
|
415
|
+
|
|
416
|
+
assert isinstance(response, dict)
|
|
417
|
+
assert isinstance(response["replies"], list)
|
|
418
|
+
assert answer in response["replies"][0].text
|
|
419
|
+
|
|
420
|
+
@pytest.mark.integration
|
|
421
|
+
def test_run_with_chat_history(self):
|
|
422
|
+
chat_generator = OllamaChatGenerator(model="llama3.2:3b")
|
|
423
|
+
|
|
424
|
+
chat_messages = [
|
|
425
|
+
ChatMessage.from_user("What is the largest city in the United Kingdom by population?"),
|
|
426
|
+
ChatMessage.from_assistant("London is the largest city in the United Kingdom by population"),
|
|
427
|
+
ChatMessage.from_user("And what is the second largest?"),
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
response = chat_generator.run(chat_messages)
|
|
431
|
+
|
|
432
|
+
assert isinstance(response, dict)
|
|
433
|
+
assert isinstance(response["replies"], list)
|
|
434
|
+
|
|
435
|
+
assert any(city in response["replies"][-1].text for city in ["Manchester", "Birmingham", "Glasgow"])
|
|
436
|
+
|
|
437
|
+
@pytest.mark.integration
|
|
438
|
+
def test_run_model_unavailable(self):
|
|
439
|
+
component = OllamaChatGenerator(model="unknown_model")
|
|
440
|
+
|
|
441
|
+
with pytest.raises(ResponseError):
|
|
442
|
+
message = ChatMessage.from_user("irrelevant")
|
|
443
|
+
component.run([message])
|
|
444
|
+
|
|
445
|
+
@pytest.mark.integration
|
|
446
|
+
def test_run_with_streaming(self):
|
|
447
|
+
streaming_callback = Mock()
|
|
448
|
+
chat_generator = OllamaChatGenerator(model="llama3.2:3b", streaming_callback=streaming_callback)
|
|
449
|
+
|
|
450
|
+
chat_messages = [
|
|
451
|
+
ChatMessage.from_user("What is the largest city in the United Kingdom by population?"),
|
|
452
|
+
ChatMessage.from_assistant("London is the largest city in the United Kingdom by population"),
|
|
453
|
+
ChatMessage.from_user("And what is the second largest?"),
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
response = chat_generator.run(chat_messages)
|
|
457
|
+
|
|
458
|
+
streaming_callback.assert_called()
|
|
459
|
+
|
|
460
|
+
assert isinstance(response, dict)
|
|
461
|
+
assert isinstance(response["replies"], list)
|
|
462
|
+
assert any(city in response["replies"][-1].text for city in ["Manchester", "Birmingham", "Glasgow"])
|
|
463
|
+
|
|
464
|
+
@pytest.mark.integration
|
|
465
|
+
def test_run_with_tools(self, tools):
|
|
466
|
+
chat_generator = OllamaChatGenerator(model="llama3.2:3b", tools=tools)
|
|
467
|
+
|
|
468
|
+
message = ChatMessage.from_user("What is the weather in Paris?")
|
|
469
|
+
response = chat_generator.run([message])
|
|
470
|
+
|
|
471
|
+
assert len(response["replies"]) == 1
|
|
472
|
+
message = response["replies"][0]
|
|
473
|
+
|
|
474
|
+
assert message.tool_calls
|
|
475
|
+
tool_call = message.tool_call
|
|
476
|
+
assert isinstance(tool_call, ToolCall)
|
|
477
|
+
assert tool_call.tool_name == "weather"
|
|
478
|
+
assert tool_call.arguments == {"city": "Paris"}
|
|
479
|
+
|
|
480
|
+
@pytest.mark.integration
|
|
481
|
+
def test_run_with_response_format(self):
|
|
482
|
+
response_format = {
|
|
483
|
+
"type": "object",
|
|
484
|
+
"properties": {"capital": {"type": "string"}, "population": {"type": "number"}},
|
|
485
|
+
}
|
|
486
|
+
chat_generator = OllamaChatGenerator(model="llama3.2:3b", response_format=response_format)
|
|
487
|
+
|
|
488
|
+
message = ChatMessage.from_user("What's the capital of France and its population?")
|
|
489
|
+
response = chat_generator.run([message])
|
|
490
|
+
|
|
491
|
+
assert isinstance(response, dict)
|
|
492
|
+
assert isinstance(response["replies"], list)
|
|
493
|
+
|
|
494
|
+
# Parse the response text as JSON and verify its structure
|
|
495
|
+
response_data = json.loads(response["replies"][0].text)
|
|
496
|
+
assert isinstance(response_data, dict)
|
|
497
|
+
assert "capital" in response_data
|
|
498
|
+
assert isinstance(response_data["capital"], str)
|
|
499
|
+
assert "population" in response_data
|
|
500
|
+
assert isinstance(response_data["population"], (int, float))
|
|
501
|
+
assert response_data["capital"] == "Paris"
|
|
502
|
+
|
|
503
|
+
def test_run_with_streaming_and_format(self):
|
|
504
|
+
response_format = {
|
|
505
|
+
"type": "object",
|
|
506
|
+
"properties": {"answer": {"type": "string"}},
|
|
507
|
+
}
|
|
508
|
+
streaming_callback = Mock()
|
|
509
|
+
chat_generator = OllamaChatGenerator(
|
|
510
|
+
model="llama3.2:3b", streaming_callback=streaming_callback, response_format=response_format
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
chat_messages = [
|
|
514
|
+
ChatMessage.from_user("What is the largest city in the United Kingdom by population?"),
|
|
515
|
+
ChatMessage.from_assistant("London is the largest city in the United Kingdom by population"),
|
|
516
|
+
ChatMessage.from_user("And what is the second largest?"),
|
|
517
|
+
]
|
|
518
|
+
with pytest.raises(ValueError):
|
|
519
|
+
chat_generator.run([chat_messages])
|
|
520
|
+
|
|
521
|
+
def test_run_with_tools_and_format(self, tools):
|
|
522
|
+
response_format = {
|
|
523
|
+
"type": "object",
|
|
524
|
+
"properties": {"capital": {"type": "string"}, "population": {"type": "number"}},
|
|
525
|
+
}
|
|
526
|
+
chat_generator = OllamaChatGenerator(model="llama3.2:3b", tools=tools, response_format=response_format)
|
|
527
|
+
message = ChatMessage.from_user("What's the weather in Paris?")
|
|
528
|
+
with pytest.raises(ValueError):
|
|
529
|
+
chat_generator.run([message])
|
|
@@ -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
|
|
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
|