openrouter-haystack 0.2.2__tar.gz → 0.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.
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/CHANGELOG.md +12 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/PKG-INFO +2 -2
- openrouter_haystack-0.3.0/examples/openrouter_with_structured_outputs.py +35 -0
- openrouter_haystack-0.3.0/pydoc/config_docusaurus.yml +28 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/pyproject.toml +2 -2
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/src/haystack_integrations/components/generators/openrouter/chat/chat_generator.py +54 -28
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/tests/test_openrouter_chat_generator.py +129 -1
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/tests/test_openrouter_chat_generator_async.py +64 -1
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/.gitignore +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/LICENSE.txt +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/README.md +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/examples/openrouter_with_tools_example.py +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/pydoc/config.yml +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/src/haystack_integrations/components/generators/openrouter/__init__.py +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/src/haystack_integrations/components/generators/openrouter/chat/__init__.py +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/src/haystack_integrations/components/generators/py.typed +0 -0
- {openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/tests/__init__.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/openrouter-v0.2.2] - 2025-09-23
|
|
4
|
+
|
|
5
|
+
### 🐛 Bug Fixes
|
|
6
|
+
|
|
7
|
+
- Chore: Fix linting in tests for Openrouter integration (#2261)
|
|
8
|
+
- Update OpenRouterChatGenerator to work with `haystack-ai>=2.18.0` (#2295)
|
|
9
|
+
|
|
10
|
+
### 🧹 Chores
|
|
11
|
+
|
|
12
|
+
- Standardize readmes - part 2 (#2205)
|
|
13
|
+
|
|
14
|
+
|
|
3
15
|
## [integrations/openrouter-v0.2.1] - 2025-08-07
|
|
4
16
|
|
|
5
17
|
### 🚀 Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrouter-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/openrouter#readme
|
|
5
5
|
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
6
6
|
Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/openrouter
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
|
-
Requires-Dist: haystack-ai>=2.
|
|
21
|
+
Requires-Dist: haystack-ai>=2.19.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
24
|
# openrouter-haystack
|
|
@@ -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 OpenRouterChatGenerator component
|
|
7
|
+
# with structured outputs.
|
|
8
|
+
# To run this example, you will need to
|
|
9
|
+
# set `OPENROUTER_API_KEY` environment variable
|
|
10
|
+
|
|
11
|
+
from haystack.dataclasses import ChatMessage
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from haystack_integrations.components.generators.openrouter import OpenRouterChatGenerator
|
|
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 = OpenRouterChatGenerator(generation_kwargs={"response_format": NobelPrizeInfo})
|
|
33
|
+
results = component.run(chat_messages)
|
|
34
|
+
|
|
35
|
+
# print(results)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
loaders:
|
|
2
|
+
- ignore_when_discovered:
|
|
3
|
+
- __init__
|
|
4
|
+
modules:
|
|
5
|
+
- haystack_integrations.components.generators.openrouter.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: OpenRouter integration for Haystack
|
|
19
|
+
id: integrations-openrouter
|
|
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: openrouter.md
|
|
27
|
+
title: OpenRouter
|
|
28
|
+
type: haystack_pydoc_tools.renderers.DocusaurusRenderer
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
24
24
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
25
25
|
]
|
|
26
|
-
dependencies = ["haystack-ai>=2.
|
|
26
|
+
dependencies = ["haystack-ai>=2.19.0"]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/openrouter#readme"
|
|
@@ -154,4 +154,4 @@ addopts = "--strict-markers"
|
|
|
154
154
|
markers = [
|
|
155
155
|
"integration: integration tests",
|
|
156
156
|
]
|
|
157
|
-
log_cli = true
|
|
157
|
+
log_cli = true
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
from typing import Any, Dict,
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
6
|
|
|
7
7
|
from haystack import component, default_to_dict, logging
|
|
8
8
|
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
9
9
|
from haystack.dataclasses import ChatMessage, StreamingCallbackT
|
|
10
|
-
from haystack.tools import
|
|
10
|
+
from haystack.tools import ToolsType, _check_duplicate_tool_names, flatten_tools_or_toolsets, serialize_tools_or_toolset
|
|
11
11
|
from haystack.utils import serialize_callable
|
|
12
12
|
from haystack.utils.auth import Secret
|
|
13
13
|
|
|
@@ -64,7 +64,7 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
|
|
|
64
64
|
streaming_callback: Optional[StreamingCallbackT] = None,
|
|
65
65
|
api_base_url: Optional[str] = "https://openrouter.ai/api/v1",
|
|
66
66
|
generation_kwargs: Optional[Dict[str, Any]] = None,
|
|
67
|
-
tools: Optional[
|
|
67
|
+
tools: Optional[ToolsType] = None,
|
|
68
68
|
timeout: Optional[float] = None,
|
|
69
69
|
extra_headers: Optional[Dict[str, Any]] = None,
|
|
70
70
|
max_retries: Optional[int] = None,
|
|
@@ -98,6 +98,14 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
|
|
|
98
98
|
events as they become available, with the stream terminated by a data: [DONE] message.
|
|
99
99
|
- `safe_prompt`: Whether to inject a safety prompt before all conversations.
|
|
100
100
|
- `random_seed`: The seed to use for random sampling.
|
|
101
|
+
- `response_format`: A JSON schema or a Pydantic model that enforces the structure of the model's response.
|
|
102
|
+
If provided, the output will always be validated against this
|
|
103
|
+
format (unless the model returns a tool call).
|
|
104
|
+
For details, see the [OpenAI Structured Outputs documentation](https://platform.openai.com/docs/guides/structured-outputs).
|
|
105
|
+
Notes:
|
|
106
|
+
- This parameter accepts Pydantic models and JSON schemas for latest models starting from GPT-4o.
|
|
107
|
+
- For structured outputs with streaming,
|
|
108
|
+
the `response_format` must be a JSON schema and not a Pydantic model.
|
|
101
109
|
:param tools:
|
|
102
110
|
A list of tools or a Toolset for which the model can prepare calls. This parameter can accept either a
|
|
103
111
|
list of `Tool` objects or a `Toolset` instance.
|
|
@@ -148,7 +156,7 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
|
|
|
148
156
|
api_base_url=self.api_base_url,
|
|
149
157
|
generation_kwargs=self.generation_kwargs,
|
|
150
158
|
api_key=self.api_key.to_dict(),
|
|
151
|
-
tools=
|
|
159
|
+
tools=serialize_tools_or_toolset(self.tools),
|
|
152
160
|
extra_headers=self.extra_headers,
|
|
153
161
|
timeout=self.timeout,
|
|
154
162
|
max_retries=self.max_retries,
|
|
@@ -158,46 +166,64 @@ class OpenRouterChatGenerator(OpenAIChatGenerator):
|
|
|
158
166
|
def _prepare_api_call(
|
|
159
167
|
self,
|
|
160
168
|
*,
|
|
161
|
-
messages:
|
|
169
|
+
messages: list[ChatMessage],
|
|
162
170
|
streaming_callback: Optional[StreamingCallbackT] = None,
|
|
163
|
-
generation_kwargs: Optional[
|
|
164
|
-
tools: Optional[
|
|
171
|
+
generation_kwargs: Optional[dict[str, Any]] = None,
|
|
172
|
+
tools: Optional[ToolsType] = None,
|
|
165
173
|
tools_strict: Optional[bool] = None,
|
|
166
|
-
) ->
|
|
174
|
+
) -> dict[str, Any]:
|
|
167
175
|
# update generation kwargs by merging with the generation kwargs passed to the run method
|
|
168
176
|
generation_kwargs = {**self.generation_kwargs, **(generation_kwargs or {})}
|
|
169
177
|
extra_headers = {**(self.extra_headers or {})}
|
|
170
178
|
|
|
179
|
+
is_streaming = streaming_callback is not None
|
|
180
|
+
num_responses = generation_kwargs.pop("n", 1)
|
|
181
|
+
|
|
182
|
+
if is_streaming and num_responses > 1:
|
|
183
|
+
msg = "Cannot stream multiple responses, please set n=1."
|
|
184
|
+
raise ValueError(msg)
|
|
185
|
+
response_format = generation_kwargs.pop("response_format", None)
|
|
186
|
+
|
|
171
187
|
# adapt ChatMessage(s) to the format expected by the OpenAI API
|
|
172
188
|
openai_formatted_messages = [message.to_openai_dict_format() for message in messages]
|
|
173
189
|
|
|
174
|
-
|
|
175
|
-
if isinstance(tools, Toolset):
|
|
176
|
-
tools = list(tools)
|
|
190
|
+
flattened_tools = flatten_tools_or_toolsets(tools or self.tools)
|
|
177
191
|
tools_strict = tools_strict if tools_strict is not None else self.tools_strict
|
|
178
|
-
_check_duplicate_tool_names(
|
|
192
|
+
_check_duplicate_tool_names(flattened_tools)
|
|
179
193
|
|
|
180
194
|
openai_tools = {}
|
|
181
|
-
if
|
|
182
|
-
tool_definitions = [
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
if flattened_tools:
|
|
196
|
+
tool_definitions = []
|
|
197
|
+
for t in flattened_tools:
|
|
198
|
+
function_spec = {**t.tool_spec}
|
|
199
|
+
if tools_strict:
|
|
200
|
+
function_spec["strict"] = True
|
|
201
|
+
function_spec["parameters"]["additionalProperties"] = False
|
|
202
|
+
tool_definitions.append({"type": "function", "function": function_spec})
|
|
186
203
|
openai_tools = {"tools": tool_definitions}
|
|
187
204
|
|
|
188
|
-
|
|
189
|
-
num_responses = generation_kwargs.pop("n", 1)
|
|
190
|
-
if is_streaming and num_responses > 1:
|
|
191
|
-
msg = "Cannot stream multiple responses, please set n=1."
|
|
192
|
-
raise ValueError(msg)
|
|
193
|
-
|
|
194
|
-
return {
|
|
205
|
+
base_args = {
|
|
195
206
|
"model": self.model,
|
|
196
|
-
"messages": openai_formatted_messages,
|
|
197
|
-
"stream": streaming_callback is not None,
|
|
207
|
+
"messages": openai_formatted_messages,
|
|
198
208
|
"n": num_responses,
|
|
199
209
|
**openai_tools,
|
|
200
|
-
"extra_body": {**generation_kwargs},
|
|
201
210
|
"extra_headers": {**extra_headers},
|
|
202
|
-
"
|
|
211
|
+
"extra_body": {**generation_kwargs},
|
|
203
212
|
}
|
|
213
|
+
|
|
214
|
+
if response_format and not is_streaming:
|
|
215
|
+
# for structured outputs without streaming, we use openai's parse endpoint
|
|
216
|
+
# Note: `stream` cannot be passed to chat.completions.parse
|
|
217
|
+
# we pass a key `openai_endpoint` as a hint to the run method to use the parse endpoint
|
|
218
|
+
# this key will be removed before the API call is made
|
|
219
|
+
return {**base_args, "response_format": response_format, "openai_endpoint": "parse"}
|
|
220
|
+
|
|
221
|
+
# for structured outputs with streaming, we use openai's create endpoint
|
|
222
|
+
# we pass a key `openai_endpoint` as a hint to the run method to use the create endpoint
|
|
223
|
+
# this key will be removed before the API call is made
|
|
224
|
+
final_args = {**base_args, "stream": is_streaming, "openai_endpoint": "create"}
|
|
225
|
+
|
|
226
|
+
# We only set the response_format parameter if it's not None since None is not a valid value in the API.
|
|
227
|
+
if response_format:
|
|
228
|
+
final_args["response_format"] = response_format
|
|
229
|
+
return final_args
|
{openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/tests/test_openrouter_chat_generator.py
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
from unittest.mock import patch
|
|
@@ -8,7 +9,7 @@ from haystack import Pipeline
|
|
|
8
9
|
from haystack.components.generators.utils import print_streaming_chunk
|
|
9
10
|
from haystack.components.tools import ToolInvoker
|
|
10
11
|
from haystack.dataclasses import ChatMessage, ChatRole, StreamingChunk, ToolCall
|
|
11
|
-
from haystack.tools import Tool
|
|
12
|
+
from haystack.tools import Tool, Toolset
|
|
12
13
|
from haystack.utils.auth import Secret
|
|
13
14
|
from openai import OpenAIError
|
|
14
15
|
from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
|
|
@@ -16,10 +17,22 @@ from openai.types.chat.chat_completion import Choice
|
|
|
16
17
|
from openai.types.chat.chat_completion_chunk import Choice as ChoiceChunk
|
|
17
18
|
from openai.types.chat.chat_completion_chunk import ChoiceDelta, ChoiceDeltaToolCall, ChoiceDeltaToolCallFunction
|
|
18
19
|
from openai.types.completion_usage import CompletionTokensDetails, CompletionUsage, PromptTokensDetails
|
|
20
|
+
from pydantic import BaseModel
|
|
19
21
|
|
|
20
22
|
from haystack_integrations.components.generators.openrouter.chat.chat_generator import OpenRouterChatGenerator
|
|
21
23
|
|
|
22
24
|
|
|
25
|
+
class CalendarEvent(BaseModel):
|
|
26
|
+
event_name: str
|
|
27
|
+
event_date: str
|
|
28
|
+
event_location: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def calendar_event_model():
|
|
33
|
+
return CalendarEvent
|
|
34
|
+
|
|
35
|
+
|
|
23
36
|
class CollectorCallback:
|
|
24
37
|
"""
|
|
25
38
|
Callback to collect streaming chunks for testing purposes.
|
|
@@ -440,6 +453,41 @@ class TestOpenRouterChatGenerator:
|
|
|
440
453
|
== results["tool_invoker"]["tool_messages"][0].tool_call_result.result
|
|
441
454
|
)
|
|
442
455
|
|
|
456
|
+
@pytest.mark.skipif(
|
|
457
|
+
not os.environ.get("OPENROUTER_API_KEY", None),
|
|
458
|
+
reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.",
|
|
459
|
+
)
|
|
460
|
+
@pytest.mark.integration
|
|
461
|
+
def test_live_run_with_response_format_json_schema(self):
|
|
462
|
+
response_schema = {
|
|
463
|
+
"type": "json_schema",
|
|
464
|
+
"json_schema": {
|
|
465
|
+
"name": "CapitalCity",
|
|
466
|
+
"strict": True,
|
|
467
|
+
"schema": {
|
|
468
|
+
"title": "CapitalCity",
|
|
469
|
+
"type": "object",
|
|
470
|
+
"properties": {
|
|
471
|
+
"city": {"title": "City", "type": "string"},
|
|
472
|
+
"country": {"title": "Country", "type": "string"},
|
|
473
|
+
},
|
|
474
|
+
"required": ["city", "country"],
|
|
475
|
+
"additionalProperties": False,
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
chat_messages = [ChatMessage.from_user("What's the capital of France?")]
|
|
481
|
+
comp = OpenRouterChatGenerator(generation_kwargs={"response_format": response_schema})
|
|
482
|
+
results = comp.run(chat_messages)
|
|
483
|
+
assert len(results["replies"]) == 1
|
|
484
|
+
message: ChatMessage = results["replies"][0]
|
|
485
|
+
msg = json.loads(message.text)
|
|
486
|
+
assert "Paris" in msg["city"]
|
|
487
|
+
assert isinstance(msg["country"], str)
|
|
488
|
+
assert "France" in msg["country"]
|
|
489
|
+
assert message.meta["finish_reason"] == "stop"
|
|
490
|
+
|
|
443
491
|
def test_serde_in_pipeline(self, monkeypatch):
|
|
444
492
|
"""
|
|
445
493
|
Test serialization/deserialization of OpenRouterChatGenerator in a Pipeline,
|
|
@@ -539,6 +587,86 @@ class TestOpenRouterChatGenerator:
|
|
|
539
587
|
assert loaded_generator.tools[0].description == generator.tools[0].description
|
|
540
588
|
assert loaded_generator.tools[0].parameters == generator.tools[0].parameters
|
|
541
589
|
|
|
590
|
+
@pytest.mark.skipif(
|
|
591
|
+
not os.environ.get("OPENROUTER_API_KEY", None),
|
|
592
|
+
reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.",
|
|
593
|
+
)
|
|
594
|
+
@pytest.mark.integration
|
|
595
|
+
def test_live_run_with_response_format_pydantic_model(self, calendar_event_model):
|
|
596
|
+
chat_messages = [
|
|
597
|
+
ChatMessage.from_user("The marketing summit takes place on October12th at the Hilton Hotel downtown.")
|
|
598
|
+
]
|
|
599
|
+
component = OpenRouterChatGenerator(generation_kwargs={"response_format": calendar_event_model})
|
|
600
|
+
results = component.run(chat_messages)
|
|
601
|
+
assert len(results["replies"]) == 1
|
|
602
|
+
message: ChatMessage = results["replies"][0]
|
|
603
|
+
msg = json.loads(message.text)
|
|
604
|
+
assert "Marketing Summit" in msg["event_name"]
|
|
605
|
+
assert isinstance(msg["event_date"], str)
|
|
606
|
+
assert isinstance(msg["event_location"], str)
|
|
607
|
+
|
|
608
|
+
@pytest.mark.skipif(
|
|
609
|
+
not os.environ.get("OPENROUTER_API_KEY", None),
|
|
610
|
+
reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.",
|
|
611
|
+
)
|
|
612
|
+
@pytest.mark.integration
|
|
613
|
+
def test_integration_mixing_tools_and_toolset(self):
|
|
614
|
+
"""Test mixing Tool list and Toolset at runtime."""
|
|
615
|
+
|
|
616
|
+
def weather_function(city: str) -> str:
|
|
617
|
+
"""Get weather information for a city."""
|
|
618
|
+
return f"Weather in {city}: 22°C, sunny"
|
|
619
|
+
|
|
620
|
+
def time_function(city: str) -> str:
|
|
621
|
+
"""Get current time in a city."""
|
|
622
|
+
return f"Current time in {city}: 14:30"
|
|
623
|
+
|
|
624
|
+
def echo_function(text: str) -> str:
|
|
625
|
+
"""Echo a text."""
|
|
626
|
+
return text
|
|
627
|
+
|
|
628
|
+
# Create tools
|
|
629
|
+
weather_tool = Tool(
|
|
630
|
+
name="weather",
|
|
631
|
+
description="Get weather information for a city",
|
|
632
|
+
parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
|
|
633
|
+
function=weather_function,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
time_tool = Tool(
|
|
637
|
+
name="time",
|
|
638
|
+
description="Get current time in a city",
|
|
639
|
+
parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
|
|
640
|
+
function=time_function,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
echo_tool = Tool(
|
|
644
|
+
name="echo",
|
|
645
|
+
description="Echo a text",
|
|
646
|
+
parameters={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]},
|
|
647
|
+
function=echo_function,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
# Create Toolset with weather and time tools
|
|
651
|
+
toolset = Toolset([weather_tool, time_tool])
|
|
652
|
+
|
|
653
|
+
# Initialize with no tools, we'll pass them at runtime
|
|
654
|
+
component = OpenRouterChatGenerator()
|
|
655
|
+
|
|
656
|
+
# Pass mixed list: echo_tool (individual) and toolset (weather + time) at runtime
|
|
657
|
+
# This tests that both individual tools and toolsets can be combined
|
|
658
|
+
messages = [ChatMessage.from_user("Echo this: Hello World")]
|
|
659
|
+
results = component.run(messages, tools=[echo_tool, toolset])
|
|
660
|
+
|
|
661
|
+
assert len(results["replies"]) == 1
|
|
662
|
+
message = results["replies"][0]
|
|
663
|
+
|
|
664
|
+
# Should be able to use echo_tool from the runtime mixed list
|
|
665
|
+
assert message.tool_calls is not None
|
|
666
|
+
tool_call = message.tool_calls[0]
|
|
667
|
+
assert tool_call.tool_name == "echo"
|
|
668
|
+
assert tool_call.arguments == {"text": "Hello World"}
|
|
669
|
+
|
|
542
670
|
|
|
543
671
|
class TestChatCompletionChunkConversion:
|
|
544
672
|
def test_handle_stream_response(self):
|
|
@@ -9,7 +9,7 @@ from haystack.dataclasses import (
|
|
|
9
9
|
ChatRole,
|
|
10
10
|
StreamingChunk,
|
|
11
11
|
)
|
|
12
|
-
from haystack.tools import Tool
|
|
12
|
+
from haystack.tools import Tool, Toolset
|
|
13
13
|
from openai import AsyncOpenAI
|
|
14
14
|
from openai.types.chat import ChatCompletion, ChatCompletionMessage
|
|
15
15
|
from openai.types.chat.chat_completion import Choice
|
|
@@ -262,3 +262,66 @@ class TestOpenRouterChatGeneratorAsync:
|
|
|
262
262
|
assert tool_call.tool_name == "weather"
|
|
263
263
|
assert tool_call.arguments == {"city": "Paris"}
|
|
264
264
|
assert tool_message.meta["finish_reason"] == "tool_calls"
|
|
265
|
+
|
|
266
|
+
@pytest.mark.skipif(
|
|
267
|
+
not os.environ.get("OPENROUTER_API_KEY", None),
|
|
268
|
+
reason="Export an env var called OPENROUTER_API_KEY containing the OpenRouter API key to run this test.",
|
|
269
|
+
)
|
|
270
|
+
@pytest.mark.integration
|
|
271
|
+
@pytest.mark.asyncio
|
|
272
|
+
async def test_integration_mixing_tools_and_toolset_async(self):
|
|
273
|
+
"""Test mixing Tool list and Toolset at runtime in async mode."""
|
|
274
|
+
|
|
275
|
+
def weather_function(city: str) -> str:
|
|
276
|
+
"""Get weather information for a city."""
|
|
277
|
+
return f"Weather in {city}: 22°C, sunny"
|
|
278
|
+
|
|
279
|
+
def time_function(city: str) -> str:
|
|
280
|
+
"""Get current time in a city."""
|
|
281
|
+
return f"Current time in {city}: 14:30"
|
|
282
|
+
|
|
283
|
+
def echo_function(text: str) -> str:
|
|
284
|
+
"""Echo a text."""
|
|
285
|
+
return text
|
|
286
|
+
|
|
287
|
+
# Create tools
|
|
288
|
+
weather_tool = Tool(
|
|
289
|
+
name="weather",
|
|
290
|
+
description="Get weather information for a city",
|
|
291
|
+
parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
|
|
292
|
+
function=weather_function,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
time_tool = Tool(
|
|
296
|
+
name="time",
|
|
297
|
+
description="Get current time in a city",
|
|
298
|
+
parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
|
|
299
|
+
function=time_function,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
echo_tool = Tool(
|
|
303
|
+
name="echo",
|
|
304
|
+
description="Echo a text",
|
|
305
|
+
parameters={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]},
|
|
306
|
+
function=echo_function,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Create Toolset with weather and time tools
|
|
310
|
+
toolset = Toolset([weather_tool, time_tool])
|
|
311
|
+
|
|
312
|
+
# Initialize with no tools, we'll pass them at runtime
|
|
313
|
+
component = OpenRouterChatGenerator()
|
|
314
|
+
|
|
315
|
+
# Pass mixed list: echo_tool (individual) and toolset (weather + time) at runtime
|
|
316
|
+
# This tests that both individual tools and toolsets can be combined
|
|
317
|
+
messages = [ChatMessage.from_user("Echo this: Hello World")]
|
|
318
|
+
results = await component.run_async(messages, tools=[echo_tool, toolset])
|
|
319
|
+
|
|
320
|
+
assert len(results["replies"]) == 1
|
|
321
|
+
message = results["replies"][0]
|
|
322
|
+
|
|
323
|
+
# Should be able to use echo_tool from the runtime mixed list
|
|
324
|
+
assert message.tool_calls is not None
|
|
325
|
+
tool_call = message.tool_calls[0]
|
|
326
|
+
assert tool_call.tool_name == "echo"
|
|
327
|
+
assert tool_call.arguments == {"text": "Hello World"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openrouter_haystack-0.2.2 → openrouter_haystack-0.3.0}/examples/openrouter_with_tools_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|