llama-stack-haystack 1.1.1__tar.gz → 1.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.
Files changed (16) hide show
  1. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/CHANGELOG.md +23 -1
  2. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/PKG-INFO +2 -2
  3. llama_stack_haystack-1.3.0/examples/chatgenerator_with_structured_outputs.py +35 -0
  4. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/examples/llama-stack-with-tools.py +1 -1
  5. llama_stack_haystack-1.3.0/pydoc/config_docusaurus.yml +28 -0
  6. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/pyproject.toml +4 -4
  7. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/src/haystack_integrations/components/generators/llama_stack/chat/chat_generator.py +37 -12
  8. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/tests/test_llama_stack_chat_generator.py +217 -20
  9. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/.gitignore +0 -0
  10. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/LICENSE.txt +0 -0
  11. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/README.md +0 -0
  12. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/pydoc/config.yml +0 -0
  13. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/src/haystack_integrations/components/generators/llama_stack/__init__.py +0 -0
  14. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/src/haystack_integrations/components/generators/llama_stack/chat/__init__.py +0 -0
  15. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/src/haystack_integrations/components/generators/py.typed +0 -0
  16. {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/tests/__init__.py +0 -0
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/llama_stack-v1.2.0] - 2025-10-24
4
+
5
+ ### 🚀 Features
6
+
7
+ - `LlamaStackChatGenerator` update tools param to ToolsType (#2436)
8
+
9
+ ### 📚 Documentation
10
+
11
+ - Add pydoc configurations for Docusaurus (#2411)
12
+
13
+
14
+ ## [integrations/llama_stack-v1.1.1] - 2025-08-22
15
+
16
+
17
+ ### 🧹 Chores
18
+
19
+ - Standardize readmes - part 2 (#2205)
20
+
21
+ ### 🌀 Miscellaneous
22
+
23
+ - Fix: Remove openai pin in llama-stack integration (#2210)
24
+
3
25
  ## [integrations/llama_stack-v1.1.0] - 2025-08-08
4
26
 
5
27
 
@@ -12,7 +34,7 @@
12
34
  ### 🐛 Bug Fixes
13
35
 
14
36
  - Update the command for Llama Stack server (#2097)
15
- - Pin openai to `<=1.99.1` avoid errors in Llama Stack integration (#2169)
37
+ - Pin openai to `<=1.99.2` avoid errors in Llama Stack integration (#2169)
16
38
 
17
39
 
18
40
  ## [integrations/llama_stack-v1.0.0] - 2025-07-11
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llama-stack-haystack
3
- Version: 1.1.1
3
+ Version: 1.3.0
4
4
  Summary: Use Llama Stack framework for text generation
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/llama-stack#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: Implementation :: CPython
17
17
  Classifier: Programming Language :: Python :: Implementation :: PyPy
18
18
  Requires-Python: >=3.12
19
- Requires-Dist: haystack-ai>=2.14
19
+ Requires-Dist: haystack-ai>=2.19.0
20
20
  Requires-Dist: llama-stack>=0.2.17
21
21
  Description-Content-Type: text/markdown
22
22
 
@@ -0,0 +1,35 @@
1
+ # SPDX-FileCopyrightText: 2024-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+
6
+ # This example demonstrates how to use the LlamaStackChatGenerator component
7
+ # with structured outputs.
8
+ # To run this example, you will need to
9
+ # set up Llama Stack Server and have a model available
10
+
11
+ from haystack.dataclasses import ChatMessage
12
+ from pydantic import BaseModel
13
+
14
+ from haystack_integrations.components.generators.llama_stack import LlamaStackChatGenerator
15
+
16
+
17
+ class NobelPrizeInfo(BaseModel):
18
+ recipient_name: str
19
+ award_year: int
20
+ category: str
21
+ achievement_description: str
22
+ nationality: str
23
+
24
+
25
+ chat_messages = [
26
+ ChatMessage.from_user(
27
+ "In 2021, American scientist David Julius received the Nobel Prize in"
28
+ " Physiology or Medicine for his groundbreaking discoveries on how the human body"
29
+ " senses temperature and touch."
30
+ )
31
+ ]
32
+ component = LlamaStackChatGenerator(generation_kwargs={"response_format": NobelPrizeInfo})
33
+ results = component.run(chat_messages)
34
+
35
+ # print(results)
@@ -34,7 +34,7 @@ weather_tool = Tool(
34
34
  tool_invoker = ToolInvoker(tools=[weather_tool])
35
35
 
36
36
  client = LlamaStackChatGenerator(
37
- model="llama3.2:3b", # model name varies depending on the inference provider used for the Llama Stack Server.
37
+ model="ollama/llama3.2:3b", # model depends on the inference provider used for the Llama Stack Server.
38
38
  api_base_url="http://localhost:8321/v1/openai/v1",
39
39
  )
40
40
  messages = [ChatMessage.from_user("What's the weather in Tokyo?")]
@@ -0,0 +1,28 @@
1
+ loaders:
2
+ - ignore_when_discovered:
3
+ - __init__
4
+ modules:
5
+ - haystack_integrations.components.generators.llama_stack.chat.chat_generator
6
+ search_path:
7
+ - ../src
8
+ type: haystack_pydoc_tools.loaders.CustomPythonLoader
9
+ processors:
10
+ - do_not_filter_modules: false
11
+ documented_only: true
12
+ expression: null
13
+ skip_empty_modules: true
14
+ type: filter
15
+ - type: smart
16
+ - type: crossref
17
+ renderer:
18
+ description: Llama Stack integration for Haystack
19
+ id: integrations-llama-stack
20
+ markdown:
21
+ add_member_class_prefix: false
22
+ add_method_class_prefix: true
23
+ classdef_code_block: false
24
+ descriptive_class_title: false
25
+ descriptive_module_title: true
26
+ filename: llama_stack.md
27
+ title: Llama Stack
28
+ type: haystack_pydoc_tools.renderers.DocusaurusRenderer
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: Implementation :: CPython",
21
21
  "Programming Language :: Python :: Implementation :: PyPy",
22
22
  ]
23
- dependencies = ["haystack-ai>=2.14", "llama-stack>=0.2.17"]
23
+ dependencies = ["haystack-ai>=2.19.0", "llama-stack>=0.2.17"]
24
24
 
25
25
  [project.urls]
26
26
  Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/llama-stack#readme"
@@ -60,8 +60,8 @@ dependencies = [
60
60
  [tool.hatch.envs.test.scripts]
61
61
  unit = 'pytest -m "not integration" {args:tests}'
62
62
  integration = 'pytest -m "integration" {args:tests}'
63
- all = 'pytest {args:tests}'
64
- cov-retry = 'all --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x'
63
+ all = 'pytest --reruns 3 --reruns-delay 30 -x {args:tests}'
64
+ cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
65
65
 
66
66
  types = "mypy -p haystack_integrations.components.generators.llama_stack {args}"
67
67
 
@@ -73,7 +73,7 @@ disallow_incomplete_defs = true
73
73
 
74
74
 
75
75
  [tool.ruff]
76
- target-version = "py38"
76
+ target-version = "py39"
77
77
  line-length = 120
78
78
 
79
79
  [tool.ruff.lint]
@@ -2,14 +2,16 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from typing import Any, Dict, List, Optional, Union
5
+ from typing import Any, Optional
6
6
 
7
7
  from haystack import component, default_from_dict, default_to_dict, logging
8
8
  from haystack.components.generators.chat import OpenAIChatGenerator
9
9
  from haystack.dataclasses import StreamingCallbackT
10
- from haystack.tools import Tool, Toolset, deserialize_tools_or_toolset_inplace, serialize_tools_or_toolset
10
+ from haystack.tools import ToolsType, deserialize_tools_or_toolset_inplace, serialize_tools_or_toolset
11
11
  from haystack.utils import deserialize_callable, serialize_callable
12
12
  from haystack.utils.auth import Secret
13
+ from openai.lib._pydantic import to_strict_json_schema
14
+ from pydantic import BaseModel
13
15
 
14
16
  logger = logging.getLogger(__name__)
15
17
 
@@ -41,7 +43,7 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
41
43
 
42
44
  messages = [ChatMessage.from_user("What's Natural Language Processing?")]
43
45
 
44
- client = LlamaStackChatGenerator(model="llama3.2:3b")
46
+ client = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
45
47
  response = client.run(messages)
46
48
  print(response)
47
49
 
@@ -49,7 +51,7 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
49
51
  is a branch of artificial intelligence
50
52
  >>that focuses on enabling computers to understand, interpret, and generate human language in a way that is
51
53
  >>meaningful and useful.')], _role=<ChatRole.ASSISTANT: 'assistant'>, _name=None,
52
- >>_meta={'model': 'llama3.2:3b', 'index': 0, 'finish_reason': 'stop',
54
+ >>_meta={'model': 'ollama/llama3.2:3b', 'index': 0, 'finish_reason': 'stop',
53
55
  >>'usage': {'prompt_tokens': 15, 'completion_tokens': 36, 'total_tokens': 51}})]}
54
56
  """
55
57
 
@@ -60,12 +62,12 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
60
62
  api_base_url: str = "http://localhost:8321/v1/openai/v1",
61
63
  organization: Optional[str] = None,
62
64
  streaming_callback: Optional[StreamingCallbackT] = None,
63
- generation_kwargs: Optional[Dict[str, Any]] = None,
65
+ generation_kwargs: Optional[dict[str, Any]] = None,
64
66
  timeout: Optional[int] = None,
65
- tools: Optional[Union[List[Tool], Toolset]] = None,
67
+ tools: Optional[ToolsType] = None,
66
68
  tools_strict: bool = False,
67
69
  max_retries: Optional[int] = None,
68
- http_client_kwargs: Optional[Dict[str, Any]] = None,
70
+ http_client_kwargs: Optional[dict[str, Any]] = None,
69
71
  ):
70
72
  """
71
73
  Creates an instance of LlamaStackChatGenerator. To use this chat generator,
@@ -94,12 +96,19 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
94
96
  events as they become available, with the stream terminated by a data: [DONE] message.
95
97
  - `safe_prompt`: Whether to inject a safety prompt before all conversations.
96
98
  - `random_seed`: The seed to use for random sampling.
99
+ - `response_format`: A JSON schema or a Pydantic model that enforces the structure of the model's response.
100
+ If provided, the output will always be validated against this
101
+ format (unless the model returns a tool call).
102
+ For details, see the [OpenAI Structured Outputs documentation](https://platform.openai.com/docs/guides/structured-outputs).
103
+ Notes:
104
+ - For structured outputs with streaming,
105
+ the `response_format` must be a JSON schema and not a Pydantic model.
97
106
  :param timeout:
98
107
  Timeout for client calls using OpenAI API. If not set, it defaults to either the
99
108
  `OPENAI_TIMEOUT` environment variable, or 30 seconds.
100
109
  :param tools:
101
- A list of tools or a Toolset for which the model can prepare calls. This parameter can accept either a
102
- list of `Tool` objects or a `Toolset` instance.
110
+ A list of Tool and/or Toolset objects, or a single Toolset for which the model can prepare calls.
111
+ Each tool should have a unique name.
103
112
  :param tools_strict:
104
113
  Whether to enable strict schema adherence for tool calls. If set to `True`, the model will follow exactly
105
114
  the schema provided in the `parameters` field of the tool definition, but this may increase latency.
@@ -129,7 +138,7 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
129
138
  http_client_kwargs=http_client_kwargs,
130
139
  )
131
140
 
132
- def to_dict(self) -> Dict[str, Any]:
141
+ def to_dict(self) -> dict[str, Any]:
133
142
  """
134
143
  Serialize this component to a dictionary.
135
144
 
@@ -137,13 +146,29 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
137
146
  The serialized component as a dictionary.
138
147
  """
139
148
  callback_name = serialize_callable(self.streaming_callback) if self.streaming_callback else None
149
+ generation_kwargs = self.generation_kwargs.copy()
150
+ response_format = generation_kwargs.get("response_format")
151
+ # If the response format is a Pydantic model, it's converted to openai's json schema format
152
+ # If it's already a json schema, it's left as is
153
+ if response_format and isinstance(response_format, type) and issubclass(response_format, BaseModel):
154
+ json_schema = {
155
+ "type": "json_schema",
156
+ "json_schema": {
157
+ "name": response_format.__name__,
158
+ "strict": True,
159
+ "schema": to_strict_json_schema(response_format),
160
+ },
161
+ }
162
+
163
+ generation_kwargs["response_format"] = json_schema
164
+
140
165
  return default_to_dict(
141
166
  self,
142
167
  model=self.model,
143
168
  streaming_callback=callback_name,
144
169
  api_base_url=self.api_base_url,
145
170
  organization=self.organization,
146
- generation_kwargs=self.generation_kwargs,
171
+ generation_kwargs=generation_kwargs,
147
172
  timeout=self.timeout,
148
173
  max_retries=self.max_retries,
149
174
  tools=serialize_tools_or_toolset(self.tools),
@@ -152,7 +177,7 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
152
177
  )
153
178
 
154
179
  @classmethod
155
- def from_dict(cls, data: Dict[str, Any]) -> "LlamaStackChatGenerator":
180
+ def from_dict(cls, data: dict[str, Any]) -> "LlamaStackChatGenerator":
156
181
  """
157
182
  Deserialize this component from a dictionary.
158
183
 
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from datetime import datetime
2
3
  from unittest.mock import patch
3
4
 
@@ -5,13 +6,25 @@ import pytest
5
6
  import pytz
6
7
  from haystack.components.generators.utils import print_streaming_chunk
7
8
  from haystack.dataclasses import ChatMessage, ChatRole, StreamingChunk, ToolCall
8
- from haystack.tools import Tool
9
+ from haystack.tools import Tool, Toolset
9
10
  from openai.types.chat import ChatCompletion, ChatCompletionMessage
10
11
  from openai.types.chat.chat_completion import Choice
12
+ from pydantic import BaseModel
11
13
 
12
14
  from haystack_integrations.components.generators.llama_stack.chat.chat_generator import LlamaStackChatGenerator
13
15
 
14
16
 
17
+ class CalendarEvent(BaseModel):
18
+ event_name: str
19
+ event_date: str
20
+ event_location: str
21
+
22
+
23
+ @pytest.fixture
24
+ def calendar_event_model():
25
+ return CalendarEvent
26
+
27
+
15
28
  @pytest.fixture
16
29
  def chat_messages():
17
30
  return [
@@ -25,6 +38,11 @@ def weather(city: str):
25
38
  return f"The weather in {city} is sunny and 32°C"
26
39
 
27
40
 
41
+ def population(city: str):
42
+ """Get population for a given city."""
43
+ return f"The population of {city} is 2.2 million"
44
+
45
+
28
46
  @pytest.fixture
29
47
  def tools():
30
48
  tool_parameters = {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
@@ -38,6 +56,25 @@ def tools():
38
56
  return [tool]
39
57
 
40
58
 
59
+ @pytest.fixture
60
+ def mixed_tools():
61
+ """Fixture that returns a mixed list of Tool and Toolset."""
62
+ weather_tool = Tool(
63
+ name="weather",
64
+ description="useful to determine the weather in a given location",
65
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
66
+ function=weather,
67
+ )
68
+ population_tool = Tool(
69
+ name="population",
70
+ description="useful to determine the population of a given location",
71
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
72
+ function=population,
73
+ )
74
+ toolset = Toolset([population_tool])
75
+ return [weather_tool, toolset]
76
+
77
+
41
78
  @pytest.fixture
42
79
  def mock_chat_completion():
43
80
  """
@@ -46,7 +83,7 @@ def mock_chat_completion():
46
83
  with patch("openai.resources.chat.completions.Completions.create") as mock_chat_completion_create:
47
84
  completion = ChatCompletion(
48
85
  id="foo",
49
- model="llama3.2:3b",
86
+ model="ollama/llama3.2:3b",
50
87
  object="chat.completion",
51
88
  choices=[
52
89
  Choice(
@@ -66,27 +103,27 @@ def mock_chat_completion():
66
103
 
67
104
  class TestLlamaStackChatGenerator:
68
105
  def test_init_default(self):
69
- component = LlamaStackChatGenerator(model="llama3.2:3b")
70
- assert component.model == "llama3.2:3b"
106
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
107
+ assert component.model == "ollama/llama3.2:3b"
71
108
  assert component.api_base_url == "http://localhost:8321/v1/openai/v1"
72
109
  assert component.streaming_callback is None
73
110
  assert not component.generation_kwargs
74
111
 
75
112
  def test_init_with_parameters(self):
76
113
  component = LlamaStackChatGenerator(
77
- model="llama3.2:3b",
114
+ model="ollama/llama3.2:3b",
78
115
  streaming_callback=print_streaming_chunk,
79
116
  api_base_url="test-base-url",
80
117
  generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
81
118
  )
82
- assert component.model == "llama3.2:3b"
119
+ assert component.model == "ollama/llama3.2:3b"
83
120
  assert component.streaming_callback is print_streaming_chunk
84
121
  assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"}
85
122
 
86
123
  def test_to_dict_default(
87
124
  self,
88
125
  ):
89
- component = LlamaStackChatGenerator(model="llama3.2:3b")
126
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
90
127
  data = component.to_dict()
91
128
 
92
129
  assert (
@@ -95,7 +132,7 @@ class TestLlamaStackChatGenerator:
95
132
  )
96
133
 
97
134
  expected_params = {
98
- "model": "llama3.2:3b",
135
+ "model": "ollama/llama3.2:3b",
99
136
  "streaming_callback": None,
100
137
  "api_base_url": "http://localhost:8321/v1/openai/v1",
101
138
  "generation_kwargs": {},
@@ -111,12 +148,17 @@ class TestLlamaStackChatGenerator:
111
148
 
112
149
  def test_to_dict_with_parameters(
113
150
  self,
151
+ calendar_event_model,
114
152
  ):
115
153
  component = LlamaStackChatGenerator(
116
- model="llama3.2:3b",
154
+ model="ollama/llama3.2:3b",
117
155
  streaming_callback=print_streaming_chunk,
118
156
  api_base_url="test-base-url",
119
- generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
157
+ generation_kwargs={
158
+ "max_tokens": 10,
159
+ "some_test_param": "test-params",
160
+ "response_format": calendar_event_model,
161
+ },
120
162
  timeout=10,
121
163
  max_retries=10,
122
164
  tools=None,
@@ -131,10 +173,31 @@ class TestLlamaStackChatGenerator:
131
173
  )
132
174
 
133
175
  expected_params = {
134
- "model": "llama3.2:3b",
176
+ "model": "ollama/llama3.2:3b",
135
177
  "api_base_url": "test-base-url",
136
178
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
137
- "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
179
+ "generation_kwargs": {
180
+ "max_tokens": 10,
181
+ "some_test_param": "test-params",
182
+ "response_format": {
183
+ "type": "json_schema",
184
+ "json_schema": {
185
+ "name": "CalendarEvent",
186
+ "strict": True,
187
+ "schema": {
188
+ "properties": {
189
+ "event_name": {"title": "Event Name", "type": "string"},
190
+ "event_date": {"title": "Event Date", "type": "string"},
191
+ "event_location": {"title": "Event Location", "type": "string"},
192
+ },
193
+ "required": ["event_name", "event_date", "event_location"],
194
+ "title": "CalendarEvent",
195
+ "type": "object",
196
+ "additionalProperties": False,
197
+ },
198
+ },
199
+ },
200
+ },
138
201
  "timeout": 10,
139
202
  "max_retries": 10,
140
203
  "tools": None,
@@ -153,7 +216,7 @@ class TestLlamaStackChatGenerator:
153
216
  "haystack_integrations.components.generators.llama_stack.chat.chat_generator.LlamaStackChatGenerator"
154
217
  ),
155
218
  "init_parameters": {
156
- "model": "llama3.2:3b",
219
+ "model": "ollama/llama3.2:3b",
157
220
  "api_base_url": "test-base-url",
158
221
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
159
222
  "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
@@ -165,7 +228,7 @@ class TestLlamaStackChatGenerator:
165
228
  },
166
229
  }
167
230
  component = LlamaStackChatGenerator.from_dict(data)
168
- assert component.model == "llama3.2:3b"
231
+ assert component.model == "ollama/llama3.2:3b"
169
232
  assert component.streaming_callback is print_streaming_chunk
170
233
  assert component.api_base_url == "test-base-url"
171
234
  assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"}
@@ -175,8 +238,33 @@ class TestLlamaStackChatGenerator:
175
238
  assert component.max_retries == 10
176
239
  assert not component.tools_strict
177
240
 
241
+ def test_init_with_mixed_tools(self):
242
+ def tool_fn(city: str) -> str:
243
+ return city
244
+
245
+ weather_tool = Tool(
246
+ name="weather",
247
+ description="Weather lookup",
248
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
249
+ function=tool_fn,
250
+ )
251
+ population_tool = Tool(
252
+ name="population",
253
+ description="Population lookup",
254
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
255
+ function=tool_fn,
256
+ )
257
+ toolset = Toolset([population_tool])
258
+
259
+ generator = LlamaStackChatGenerator(
260
+ model="ollama/llama3.2:3b",
261
+ tools=[weather_tool, toolset],
262
+ )
263
+
264
+ assert generator.tools == [weather_tool, toolset]
265
+
178
266
  def test_run(self, chat_messages, mock_chat_completion): # noqa: ARG002
179
- component = LlamaStackChatGenerator(model="llama3.2:3b")
267
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
180
268
  response = component.run(chat_messages)
181
269
 
182
270
  # check that the component returns the correct ChatMessage response
@@ -188,7 +276,7 @@ class TestLlamaStackChatGenerator:
188
276
 
189
277
  def test_run_with_params(self, chat_messages, mock_chat_completion):
190
278
  component = LlamaStackChatGenerator(
191
- model="llama3.2:3b",
279
+ model="ollama/llama3.2:3b",
192
280
  generation_kwargs={"max_tokens": 10, "temperature": 0.5},
193
281
  )
194
282
  response = component.run(chat_messages)
@@ -208,7 +296,7 @@ class TestLlamaStackChatGenerator:
208
296
  @pytest.mark.integration
209
297
  def test_live_run(self):
210
298
  chat_messages = [ChatMessage.from_user("What's the capital of France")]
211
- component = LlamaStackChatGenerator(model="llama3.2:3b")
299
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
212
300
  results = component.run(chat_messages)
213
301
  assert len(results["replies"]) == 1
214
302
  message: ChatMessage = results["replies"][0]
@@ -228,7 +316,7 @@ class TestLlamaStackChatGenerator:
228
316
  self.responses += chunk.content if chunk.content else ""
229
317
 
230
318
  callback = Callback()
231
- component = LlamaStackChatGenerator(model="llama3.2:3b", streaming_callback=callback)
319
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b", streaming_callback=callback)
232
320
  results = component.run([ChatMessage.from_user("What's the capital of France?")])
233
321
 
234
322
  assert len(results["replies"]) == 1
@@ -244,7 +332,7 @@ class TestLlamaStackChatGenerator:
244
332
  @pytest.mark.integration
245
333
  def test_live_run_with_tools(self, tools):
246
334
  chat_messages = [ChatMessage.from_user("What's the weather like in Paris?")]
247
- component = LlamaStackChatGenerator(model="llama3.2:3b", tools=tools)
335
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b", tools=tools)
248
336
  results = component.run(chat_messages)
249
337
  assert len(results["replies"]) == 1
250
338
  message = results["replies"][0]
@@ -263,7 +351,7 @@ class TestLlamaStackChatGenerator:
263
351
  Integration test that the LlamaStackChatGenerator component can run with tools and get a response.
264
352
  """
265
353
  initial_messages = [ChatMessage.from_user("What's the weather like in Paris?")]
266
- component = LlamaStackChatGenerator(model="llama3.2:3b", tools=tools)
354
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b", tools=tools)
267
355
  results = component.run(messages=initial_messages, generation_kwargs={"tool_choice": "auto"})
268
356
 
269
357
  assert len(results["replies"]) > 0, "No replies received"
@@ -298,3 +386,112 @@ class TestLlamaStackChatGenerator:
298
386
  assert not final_message.tool_call
299
387
  assert len(final_message.text) > 0
300
388
  assert "paris" in final_message.text.lower()
389
+
390
+ @pytest.mark.integration
391
+ def test_live_run_with_mixed_tools(self, mixed_tools):
392
+ """
393
+ Integration test that verifies LlamaStackChatGenerator works with mixed Tool and Toolset.
394
+ This tests that the LLM can correctly invoke tools from both a standalone Tool and a Toolset.
395
+ """
396
+ initial_messages = [
397
+ ChatMessage.from_user("What's the weather like in Paris and what is the population of Berlin?")
398
+ ]
399
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b", tools=mixed_tools)
400
+ results = component.run(messages=initial_messages)
401
+
402
+ assert len(results["replies"]) > 0, "No replies received"
403
+
404
+ # Find the message with tool calls
405
+ tool_call_message = None
406
+ for message in results["replies"]:
407
+ if message.tool_calls:
408
+ tool_call_message = message
409
+ break
410
+
411
+ assert tool_call_message is not None, "No message with tool call found"
412
+ assert isinstance(tool_call_message, ChatMessage), "Tool message is not a ChatMessage instance"
413
+ assert ChatMessage.is_from(tool_call_message, ChatRole.ASSISTANT), "Tool message is not from the assistant"
414
+
415
+ tool_calls = tool_call_message.tool_calls
416
+ assert len(tool_calls) == 2, f"Expected 2 tool calls, got {len(tool_calls)}"
417
+
418
+ # Verify we got calls to both weather and population tools
419
+ tool_names = {tc.tool_name for tc in tool_calls}
420
+ assert "weather" in tool_names, "Expected 'weather' tool call"
421
+ assert "population" in tool_names, "Expected 'population' tool call"
422
+
423
+ # Verify tool call details
424
+ for tool_call in tool_calls:
425
+ assert tool_call.id, "Tool call does not contain value for 'id' key"
426
+ assert tool_call.tool_name in ["weather", "population"]
427
+ assert "city" in tool_call.arguments
428
+ assert tool_call.arguments["city"] in ["Paris", "Berlin"]
429
+ assert tool_call_message.meta["finish_reason"] == "tool_calls"
430
+
431
+ # Mock the response we'd get from ToolInvoker
432
+ tool_result_messages = []
433
+ for tool_call in tool_calls:
434
+ if tool_call.tool_name == "weather":
435
+ result = "The weather in Paris is sunny and 32°C"
436
+ else: # population
437
+ result = "The population of Berlin is 2.2 million"
438
+ tool_result_messages.append(ChatMessage.from_tool(tool_result=result, origin=tool_call))
439
+
440
+ new_messages = [*initial_messages, tool_call_message, *tool_result_messages]
441
+ results = component.run(new_messages)
442
+
443
+ assert len(results["replies"]) == 1
444
+ final_message = results["replies"][0]
445
+ assert not final_message.tool_call
446
+ assert len(final_message.text) > 0
447
+ assert "paris" in final_message.text.lower()
448
+ assert "berlin" in final_message.text.lower()
449
+
450
+ @pytest.mark.integration
451
+ def test_live_run_with_response_format_json_schema(self):
452
+ response_schema = {
453
+ "type": "json_schema",
454
+ "json_schema": {
455
+ "name": "CapitalCity",
456
+ "strict": True,
457
+ "schema": {
458
+ "title": "CapitalCity",
459
+ "type": "object",
460
+ "properties": {
461
+ "city": {"title": "City", "type": "string"},
462
+ "country": {"title": "Country", "type": "string"},
463
+ },
464
+ "required": ["city", "country"],
465
+ "additionalProperties": False,
466
+ },
467
+ },
468
+ }
469
+
470
+ chat_messages = [ChatMessage.from_user("What's the capital of France?")]
471
+ comp = LlamaStackChatGenerator(
472
+ model="ollama/llama3.2:3b", generation_kwargs={"response_format": response_schema}
473
+ )
474
+ results = comp.run(chat_messages)
475
+ assert len(results["replies"]) == 1
476
+ message: ChatMessage = results["replies"][0]
477
+ msg = json.loads(message.text)
478
+ assert "Paris" in msg["city"]
479
+ assert isinstance(msg["country"], str)
480
+ assert "France" in msg["country"]
481
+ assert message.meta["finish_reason"] == "stop"
482
+
483
+ @pytest.mark.integration
484
+ def test_live_run_with_response_format_pydantic_model(self, calendar_event_model):
485
+ chat_messages = [
486
+ ChatMessage.from_user("The marketing summit takes place on October 12th at the Hilton Hotel downtown.")
487
+ ]
488
+ component = LlamaStackChatGenerator(
489
+ model="ollama/llama3.2:3b", generation_kwargs={"response_format": calendar_event_model}
490
+ )
491
+ results = component.run(chat_messages)
492
+ assert len(results["replies"]) == 1
493
+ message: ChatMessage = results["replies"][0]
494
+ msg = json.loads(message.text)
495
+ assert "Marketing Summit" in msg["event_name"]
496
+ assert isinstance(msg["event_date"], str)
497
+ assert isinstance(msg["event_location"], str)