llama-stack-haystack 1.1.1__tar.gz → 1.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.
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/llama_stack-v1.1.1] - 2025-08-22
4
+
5
+
6
+ ### 🧹 Chores
7
+
8
+ - Standardize readmes - part 2 (#2205)
9
+
10
+ ### 🌀 Miscellaneous
11
+
12
+ - Fix: Remove openai pin in llama-stack integration (#2210)
13
+
3
14
  ## [integrations/llama_stack-v1.1.0] - 2025-08-08
4
15
 
5
16
 
@@ -12,7 +23,7 @@
12
23
  ### 🐛 Bug Fixes
13
24
 
14
25
  - Update the command for Llama Stack server (#2097)
15
- - Pin openai to `<=1.99.1` avoid errors in Llama Stack integration (#2169)
26
+ - Pin openai to `<=1.99.2` avoid errors in Llama Stack integration (#2169)
16
27
 
17
28
 
18
29
  ## [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.2.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
 
@@ -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"
@@ -2,12 +2,12 @@
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, Dict, 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
13
 
@@ -41,7 +41,7 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
41
41
 
42
42
  messages = [ChatMessage.from_user("What's Natural Language Processing?")]
43
43
 
44
- client = LlamaStackChatGenerator(model="llama3.2:3b")
44
+ client = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
45
45
  response = client.run(messages)
46
46
  print(response)
47
47
 
@@ -49,7 +49,7 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
49
49
  is a branch of artificial intelligence
50
50
  >>that focuses on enabling computers to understand, interpret, and generate human language in a way that is
51
51
  >>meaningful and useful.')], _role=<ChatRole.ASSISTANT: 'assistant'>, _name=None,
52
- >>_meta={'model': 'llama3.2:3b', 'index': 0, 'finish_reason': 'stop',
52
+ >>_meta={'model': 'ollama/llama3.2:3b', 'index': 0, 'finish_reason': 'stop',
53
53
  >>'usage': {'prompt_tokens': 15, 'completion_tokens': 36, 'total_tokens': 51}})]}
54
54
  """
55
55
 
@@ -62,7 +62,7 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
62
62
  streaming_callback: Optional[StreamingCallbackT] = None,
63
63
  generation_kwargs: Optional[Dict[str, Any]] = None,
64
64
  timeout: Optional[int] = None,
65
- tools: Optional[Union[List[Tool], Toolset]] = None,
65
+ tools: Optional[ToolsType] = None,
66
66
  tools_strict: bool = False,
67
67
  max_retries: Optional[int] = None,
68
68
  http_client_kwargs: Optional[Dict[str, Any]] = None,
@@ -98,8 +98,8 @@ class LlamaStackChatGenerator(OpenAIChatGenerator):
98
98
  Timeout for client calls using OpenAI API. If not set, it defaults to either the
99
99
  `OPENAI_TIMEOUT` environment variable, or 30 seconds.
100
100
  :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.
101
+ A list of Tool and/or Toolset objects, or a single Toolset for which the model can prepare calls.
102
+ Each tool should have a unique name.
103
103
  :param tools_strict:
104
104
  Whether to enable strict schema adherence for tool calls. If set to `True`, the model will follow exactly
105
105
  the schema provided in the `parameters` field of the tool definition, but this may increase latency.
@@ -5,7 +5,7 @@ import pytest
5
5
  import pytz
6
6
  from haystack.components.generators.utils import print_streaming_chunk
7
7
  from haystack.dataclasses import ChatMessage, ChatRole, StreamingChunk, ToolCall
8
- from haystack.tools import Tool
8
+ from haystack.tools import Tool, Toolset
9
9
  from openai.types.chat import ChatCompletion, ChatCompletionMessage
10
10
  from openai.types.chat.chat_completion import Choice
11
11
 
@@ -25,6 +25,11 @@ def weather(city: str):
25
25
  return f"The weather in {city} is sunny and 32°C"
26
26
 
27
27
 
28
+ def population(city: str):
29
+ """Get population for a given city."""
30
+ return f"The population of {city} is 2.2 million"
31
+
32
+
28
33
  @pytest.fixture
29
34
  def tools():
30
35
  tool_parameters = {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
@@ -38,6 +43,25 @@ def tools():
38
43
  return [tool]
39
44
 
40
45
 
46
+ @pytest.fixture
47
+ def mixed_tools():
48
+ """Fixture that returns a mixed list of Tool and Toolset."""
49
+ weather_tool = Tool(
50
+ name="weather",
51
+ description="useful to determine the weather in a given location",
52
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
53
+ function=weather,
54
+ )
55
+ population_tool = Tool(
56
+ name="population",
57
+ description="useful to determine the population of a given location",
58
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
59
+ function=population,
60
+ )
61
+ toolset = Toolset([population_tool])
62
+ return [weather_tool, toolset]
63
+
64
+
41
65
  @pytest.fixture
42
66
  def mock_chat_completion():
43
67
  """
@@ -46,7 +70,7 @@ def mock_chat_completion():
46
70
  with patch("openai.resources.chat.completions.Completions.create") as mock_chat_completion_create:
47
71
  completion = ChatCompletion(
48
72
  id="foo",
49
- model="llama3.2:3b",
73
+ model="ollama/llama3.2:3b",
50
74
  object="chat.completion",
51
75
  choices=[
52
76
  Choice(
@@ -66,27 +90,27 @@ def mock_chat_completion():
66
90
 
67
91
  class TestLlamaStackChatGenerator:
68
92
  def test_init_default(self):
69
- component = LlamaStackChatGenerator(model="llama3.2:3b")
70
- assert component.model == "llama3.2:3b"
93
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
94
+ assert component.model == "ollama/llama3.2:3b"
71
95
  assert component.api_base_url == "http://localhost:8321/v1/openai/v1"
72
96
  assert component.streaming_callback is None
73
97
  assert not component.generation_kwargs
74
98
 
75
99
  def test_init_with_parameters(self):
76
100
  component = LlamaStackChatGenerator(
77
- model="llama3.2:3b",
101
+ model="ollama/llama3.2:3b",
78
102
  streaming_callback=print_streaming_chunk,
79
103
  api_base_url="test-base-url",
80
104
  generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
81
105
  )
82
- assert component.model == "llama3.2:3b"
106
+ assert component.model == "ollama/llama3.2:3b"
83
107
  assert component.streaming_callback is print_streaming_chunk
84
108
  assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"}
85
109
 
86
110
  def test_to_dict_default(
87
111
  self,
88
112
  ):
89
- component = LlamaStackChatGenerator(model="llama3.2:3b")
113
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
90
114
  data = component.to_dict()
91
115
 
92
116
  assert (
@@ -95,7 +119,7 @@ class TestLlamaStackChatGenerator:
95
119
  )
96
120
 
97
121
  expected_params = {
98
- "model": "llama3.2:3b",
122
+ "model": "ollama/llama3.2:3b",
99
123
  "streaming_callback": None,
100
124
  "api_base_url": "http://localhost:8321/v1/openai/v1",
101
125
  "generation_kwargs": {},
@@ -113,7 +137,7 @@ class TestLlamaStackChatGenerator:
113
137
  self,
114
138
  ):
115
139
  component = LlamaStackChatGenerator(
116
- model="llama3.2:3b",
140
+ model="ollama/llama3.2:3b",
117
141
  streaming_callback=print_streaming_chunk,
118
142
  api_base_url="test-base-url",
119
143
  generation_kwargs={"max_tokens": 10, "some_test_param": "test-params"},
@@ -131,7 +155,7 @@ class TestLlamaStackChatGenerator:
131
155
  )
132
156
 
133
157
  expected_params = {
134
- "model": "llama3.2:3b",
158
+ "model": "ollama/llama3.2:3b",
135
159
  "api_base_url": "test-base-url",
136
160
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
137
161
  "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
@@ -153,7 +177,7 @@ class TestLlamaStackChatGenerator:
153
177
  "haystack_integrations.components.generators.llama_stack.chat.chat_generator.LlamaStackChatGenerator"
154
178
  ),
155
179
  "init_parameters": {
156
- "model": "llama3.2:3b",
180
+ "model": "ollama/llama3.2:3b",
157
181
  "api_base_url": "test-base-url",
158
182
  "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk",
159
183
  "generation_kwargs": {"max_tokens": 10, "some_test_param": "test-params"},
@@ -165,7 +189,7 @@ class TestLlamaStackChatGenerator:
165
189
  },
166
190
  }
167
191
  component = LlamaStackChatGenerator.from_dict(data)
168
- assert component.model == "llama3.2:3b"
192
+ assert component.model == "ollama/llama3.2:3b"
169
193
  assert component.streaming_callback is print_streaming_chunk
170
194
  assert component.api_base_url == "test-base-url"
171
195
  assert component.generation_kwargs == {"max_tokens": 10, "some_test_param": "test-params"}
@@ -175,8 +199,33 @@ class TestLlamaStackChatGenerator:
175
199
  assert component.max_retries == 10
176
200
  assert not component.tools_strict
177
201
 
202
+ def test_init_with_mixed_tools(self):
203
+ def tool_fn(city: str) -> str:
204
+ return city
205
+
206
+ weather_tool = Tool(
207
+ name="weather",
208
+ description="Weather lookup",
209
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
210
+ function=tool_fn,
211
+ )
212
+ population_tool = Tool(
213
+ name="population",
214
+ description="Population lookup",
215
+ parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
216
+ function=tool_fn,
217
+ )
218
+ toolset = Toolset([population_tool])
219
+
220
+ generator = LlamaStackChatGenerator(
221
+ model="ollama/llama3.2:3b",
222
+ tools=[weather_tool, toolset],
223
+ )
224
+
225
+ assert generator.tools == [weather_tool, toolset]
226
+
178
227
  def test_run(self, chat_messages, mock_chat_completion): # noqa: ARG002
179
- component = LlamaStackChatGenerator(model="llama3.2:3b")
228
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
180
229
  response = component.run(chat_messages)
181
230
 
182
231
  # check that the component returns the correct ChatMessage response
@@ -188,7 +237,7 @@ class TestLlamaStackChatGenerator:
188
237
 
189
238
  def test_run_with_params(self, chat_messages, mock_chat_completion):
190
239
  component = LlamaStackChatGenerator(
191
- model="llama3.2:3b",
240
+ model="ollama/llama3.2:3b",
192
241
  generation_kwargs={"max_tokens": 10, "temperature": 0.5},
193
242
  )
194
243
  response = component.run(chat_messages)
@@ -208,7 +257,7 @@ class TestLlamaStackChatGenerator:
208
257
  @pytest.mark.integration
209
258
  def test_live_run(self):
210
259
  chat_messages = [ChatMessage.from_user("What's the capital of France")]
211
- component = LlamaStackChatGenerator(model="llama3.2:3b")
260
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b")
212
261
  results = component.run(chat_messages)
213
262
  assert len(results["replies"]) == 1
214
263
  message: ChatMessage = results["replies"][0]
@@ -228,7 +277,7 @@ class TestLlamaStackChatGenerator:
228
277
  self.responses += chunk.content if chunk.content else ""
229
278
 
230
279
  callback = Callback()
231
- component = LlamaStackChatGenerator(model="llama3.2:3b", streaming_callback=callback)
280
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b", streaming_callback=callback)
232
281
  results = component.run([ChatMessage.from_user("What's the capital of France?")])
233
282
 
234
283
  assert len(results["replies"]) == 1
@@ -244,7 +293,7 @@ class TestLlamaStackChatGenerator:
244
293
  @pytest.mark.integration
245
294
  def test_live_run_with_tools(self, tools):
246
295
  chat_messages = [ChatMessage.from_user("What's the weather like in Paris?")]
247
- component = LlamaStackChatGenerator(model="llama3.2:3b", tools=tools)
296
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b", tools=tools)
248
297
  results = component.run(chat_messages)
249
298
  assert len(results["replies"]) == 1
250
299
  message = results["replies"][0]
@@ -263,7 +312,7 @@ class TestLlamaStackChatGenerator:
263
312
  Integration test that the LlamaStackChatGenerator component can run with tools and get a response.
264
313
  """
265
314
  initial_messages = [ChatMessage.from_user("What's the weather like in Paris?")]
266
- component = LlamaStackChatGenerator(model="llama3.2:3b", tools=tools)
315
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b", tools=tools)
267
316
  results = component.run(messages=initial_messages, generation_kwargs={"tool_choice": "auto"})
268
317
 
269
318
  assert len(results["replies"]) > 0, "No replies received"
@@ -298,3 +347,63 @@ class TestLlamaStackChatGenerator:
298
347
  assert not final_message.tool_call
299
348
  assert len(final_message.text) > 0
300
349
  assert "paris" in final_message.text.lower()
350
+
351
+ @pytest.mark.integration
352
+ def test_live_run_with_mixed_tools(self, mixed_tools):
353
+ """
354
+ Integration test that verifies LlamaStackChatGenerator works with mixed Tool and Toolset.
355
+ This tests that the LLM can correctly invoke tools from both a standalone Tool and a Toolset.
356
+ """
357
+ initial_messages = [
358
+ ChatMessage.from_user("What's the weather like in Paris and what is the population of Berlin?")
359
+ ]
360
+ component = LlamaStackChatGenerator(model="ollama/llama3.2:3b", tools=mixed_tools)
361
+ results = component.run(messages=initial_messages)
362
+
363
+ assert len(results["replies"]) > 0, "No replies received"
364
+
365
+ # Find the message with tool calls
366
+ tool_call_message = None
367
+ for message in results["replies"]:
368
+ if message.tool_calls:
369
+ tool_call_message = message
370
+ break
371
+
372
+ assert tool_call_message is not None, "No message with tool call found"
373
+ assert isinstance(tool_call_message, ChatMessage), "Tool message is not a ChatMessage instance"
374
+ assert ChatMessage.is_from(tool_call_message, ChatRole.ASSISTANT), "Tool message is not from the assistant"
375
+
376
+ tool_calls = tool_call_message.tool_calls
377
+ assert len(tool_calls) == 2, f"Expected 2 tool calls, got {len(tool_calls)}"
378
+
379
+ # Verify we got calls to both weather and population tools
380
+ tool_names = {tc.tool_name for tc in tool_calls}
381
+ assert "weather" in tool_names, "Expected 'weather' tool call"
382
+ assert "population" in tool_names, "Expected 'population' tool call"
383
+
384
+ # Verify tool call details
385
+ for tool_call in tool_calls:
386
+ assert tool_call.id, "Tool call does not contain value for 'id' key"
387
+ assert tool_call.tool_name in ["weather", "population"]
388
+ assert "city" in tool_call.arguments
389
+ assert tool_call.arguments["city"] in ["Paris", "Berlin"]
390
+ assert tool_call_message.meta["finish_reason"] == "tool_calls"
391
+
392
+ # Mock the response we'd get from ToolInvoker
393
+ tool_result_messages = []
394
+ for tool_call in tool_calls:
395
+ if tool_call.tool_name == "weather":
396
+ result = "The weather in Paris is sunny and 32°C"
397
+ else: # population
398
+ result = "The population of Berlin is 2.2 million"
399
+ tool_result_messages.append(ChatMessage.from_tool(tool_result=result, origin=tool_call))
400
+
401
+ new_messages = [*initial_messages, tool_call_message, *tool_result_messages]
402
+ results = component.run(new_messages)
403
+
404
+ assert len(results["replies"]) == 1
405
+ final_message = results["replies"][0]
406
+ assert not final_message.tool_call
407
+ assert len(final_message.text) > 0
408
+ assert "paris" in final_message.text.lower()
409
+ assert "berlin" in final_message.text.lower()