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.
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/CHANGELOG.md +23 -1
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/PKG-INFO +2 -2
- llama_stack_haystack-1.3.0/examples/chatgenerator_with_structured_outputs.py +35 -0
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/examples/llama-stack-with-tools.py +1 -1
- llama_stack_haystack-1.3.0/pydoc/config_docusaurus.yml +28 -0
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/pyproject.toml +4 -4
- {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
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/tests/test_llama_stack_chat_generator.py +217 -20
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/.gitignore +0 -0
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/LICENSE.txt +0 -0
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/README.md +0 -0
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/pydoc/config.yml +0 -0
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/src/haystack_integrations/components/generators/llama_stack/__init__.py +0 -0
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/src/haystack_integrations/components/generators/llama_stack/chat/__init__.py +0 -0
- {llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/src/haystack_integrations/components/generators/py.typed +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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)
|
{llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/examples/llama-stack-with-tools.py
RENAMED
|
@@ -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
|
|
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.
|
|
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 = '
|
|
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 = "
|
|
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,
|
|
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
|
|
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[
|
|
65
|
+
generation_kwargs: Optional[dict[str, Any]] = None,
|
|
64
66
|
timeout: Optional[int] = None,
|
|
65
|
-
tools: Optional[
|
|
67
|
+
tools: Optional[ToolsType] = None,
|
|
66
68
|
tools_strict: bool = False,
|
|
67
69
|
max_retries: Optional[int] = None,
|
|
68
|
-
http_client_kwargs: Optional[
|
|
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
|
|
102
|
-
|
|
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) ->
|
|
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=
|
|
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:
|
|
180
|
+
def from_dict(cls, data: dict[str, Any]) -> "LlamaStackChatGenerator":
|
|
156
181
|
"""
|
|
157
182
|
Deserialize this component from a dictionary.
|
|
158
183
|
|
{llama_stack_haystack-1.1.1 → llama_stack_haystack-1.3.0}/tests/test_llama_stack_chat_generator.py
RENAMED
|
@@ -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={
|
|
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": {
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|