test_azure.py
1 # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 2 # 3 # SPDX-License-Identifier: Apache-2.0 4 5 import contextlib 6 import json 7 import os 8 from typing import Any 9 10 import pytest 11 from openai import OpenAIError 12 from pydantic import BaseModel 13 14 from haystack import Pipeline, component 15 from haystack.components.generators.chat import AzureOpenAIChatGenerator 16 from haystack.components.generators.utils import print_streaming_chunk 17 from haystack.dataclasses import ChatMessage, ToolCall 18 from haystack.tools import ComponentTool, Tool 19 from haystack.tools.toolset import Toolset 20 from haystack.utils.auth import Secret 21 from haystack.utils.azure import default_azure_ad_token_provider 22 23 24 class CalendarEvent(BaseModel): 25 event_name: str 26 event_date: str 27 event_location: str 28 29 30 @pytest.fixture 31 def calendar_event_model(): 32 return CalendarEvent 33 34 35 def get_weather(city: str) -> dict[str, Any]: 36 weather_info = { 37 "Berlin": {"weather": "mostly sunny", "temperature": 7, "unit": "celsius"}, 38 "Paris": {"weather": "mostly cloudy", "temperature": 8, "unit": "celsius"}, 39 "Rome": {"weather": "sunny", "temperature": 14, "unit": "celsius"}, 40 } 41 return weather_info.get(city, {"weather": "unknown", "temperature": 0, "unit": "celsius"}) 42 43 44 @component 45 class MessageExtractor: 46 @component.output_types(messages=list[str], meta=dict[str, Any]) 47 def run(self, messages: list[ChatMessage], meta: dict[str, Any] | None = None) -> dict[str, Any]: 48 """ 49 Extracts the text content of ChatMessage objects 50 51 :param messages: List of Haystack ChatMessage objects 52 :param meta: Optional metadata to include in the response. 53 :returns: 54 A dictionary with keys "messages" and "meta". 55 """ 56 if meta is None: 57 meta = {} 58 return {"messages": [m.text for m in messages], "meta": meta} 59 60 61 @pytest.fixture 62 def tools(): 63 weather_tool = Tool( 64 name="weather", 65 description="useful to determine the weather in a given location", 66 parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, 67 function=get_weather, 68 ) 69 # We add a tool that has a more complex parameter signature 70 message_extractor_tool = ComponentTool( 71 component=MessageExtractor(), 72 name="message_extractor", 73 description="Useful for returning the text content of ChatMessage objects", 74 ) 75 return [weather_tool, message_extractor_tool] 76 77 78 class TestAzureOpenAIChatGenerator: 79 def test_supported_models(self) -> None: 80 """SUPPORTED_MODELS is a non-empty list of strings.""" 81 models = AzureOpenAIChatGenerator.SUPPORTED_MODELS 82 assert isinstance(models, list) 83 assert len(models) > 0 84 assert all(isinstance(m, str) for m in models) 85 86 def test_init_default(self, monkeypatch): 87 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 88 component = AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint") 89 assert component.client.api_key == "test-api-key" 90 assert component.azure_deployment == "gpt-4.1-mini" 91 assert component.streaming_callback is None 92 assert not component.generation_kwargs 93 94 def test_init_fail_wo_api_key(self, monkeypatch): 95 monkeypatch.delenv("AZURE_OPENAI_API_KEY", raising=False) 96 monkeypatch.delenv("AZURE_OPENAI_AD_TOKEN", raising=False) 97 with pytest.raises(OpenAIError): 98 AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint") 99 100 def test_init_with_parameters(self, tools): 101 component = AzureOpenAIChatGenerator( 102 api_key=Secret.from_token("test-api-key"), 103 azure_endpoint="some-non-existing-endpoint", 104 streaming_callback=print_streaming_chunk, 105 generation_kwargs={"max_completion_tokens": 10, "some_test_param": "test-params"}, 106 tools=tools, 107 tools_strict=True, 108 azure_ad_token_provider=default_azure_ad_token_provider, 109 ) 110 assert component.client.api_key == "test-api-key" 111 assert component.azure_deployment == "gpt-4.1-mini" 112 assert component.streaming_callback is print_streaming_chunk 113 assert component.generation_kwargs == {"max_completion_tokens": 10, "some_test_param": "test-params"} 114 assert component.tools == tools 115 assert component.tools_strict 116 assert component.azure_ad_token_provider is not None 117 assert component.max_retries == 5 118 119 def test_init_with_0_max_retries(self, tools): 120 """Tests that the max_retries init param is set correctly if equal 0""" 121 component = AzureOpenAIChatGenerator( 122 api_key=Secret.from_token("test-api-key"), 123 azure_endpoint="some-non-existing-endpoint", 124 streaming_callback=print_streaming_chunk, 125 generation_kwargs={"max_completion_tokens": 10, "some_test_param": "test-params"}, 126 tools=tools, 127 tools_strict=True, 128 azure_ad_token_provider=default_azure_ad_token_provider, 129 max_retries=0, 130 ) 131 assert component.client.api_key == "test-api-key" 132 assert component.azure_deployment == "gpt-4.1-mini" 133 assert component.streaming_callback is print_streaming_chunk 134 assert component.generation_kwargs == {"max_completion_tokens": 10, "some_test_param": "test-params"} 135 assert component.tools == tools 136 assert component.tools_strict 137 assert component.azure_ad_token_provider is not None 138 assert component.max_retries == 0 139 140 def test_to_dict_default(self, monkeypatch): 141 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 142 component = AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint") 143 data = component.to_dict() 144 assert data == { 145 "type": "haystack.components.generators.chat.azure.AzureOpenAIChatGenerator", 146 "init_parameters": { 147 "api_key": {"env_vars": ["AZURE_OPENAI_API_KEY"], "strict": False, "type": "env_var"}, 148 "azure_ad_token": {"env_vars": ["AZURE_OPENAI_AD_TOKEN"], "strict": False, "type": "env_var"}, 149 "api_version": "2024-12-01-preview", 150 "azure_endpoint": "some-non-existing-endpoint", 151 "azure_deployment": "gpt-4.1-mini", 152 "organization": None, 153 "streaming_callback": None, 154 "generation_kwargs": {}, 155 "timeout": 30.0, 156 "max_retries": 5, 157 "default_headers": {}, 158 "tools": None, 159 "tools_strict": False, 160 "azure_ad_token_provider": None, 161 "http_client_kwargs": None, 162 }, 163 } 164 165 def test_to_dict_with_parameters(self, monkeypatch, calendar_event_model): 166 monkeypatch.setenv("ENV_VAR", "test-api-key") 167 component = AzureOpenAIChatGenerator( 168 api_key=Secret.from_env_var("ENV_VAR", strict=False), 169 azure_ad_token=Secret.from_env_var("ENV_VAR1", strict=False), 170 azure_endpoint="some-non-existing-endpoint", 171 streaming_callback=print_streaming_chunk, 172 timeout=2.5, 173 max_retries=10, 174 generation_kwargs={ 175 "max_completion_tokens": 10, 176 "some_test_param": "test-params", 177 "response_format": calendar_event_model, 178 }, 179 azure_ad_token_provider=default_azure_ad_token_provider, 180 http_client_kwargs={"proxy": "http://localhost:8080"}, 181 ) 182 data = component.to_dict() 183 assert data == { 184 "type": "haystack.components.generators.chat.azure.AzureOpenAIChatGenerator", 185 "init_parameters": { 186 "api_key": {"env_vars": ["ENV_VAR"], "strict": False, "type": "env_var"}, 187 "azure_ad_token": {"env_vars": ["ENV_VAR1"], "strict": False, "type": "env_var"}, 188 "api_version": "2024-12-01-preview", 189 "azure_endpoint": "some-non-existing-endpoint", 190 "azure_deployment": "gpt-4.1-mini", 191 "organization": None, 192 "streaming_callback": "haystack.components.generators.utils.print_streaming_chunk", 193 "timeout": 2.5, 194 "max_retries": 10, 195 "generation_kwargs": { 196 "max_completion_tokens": 10, 197 "some_test_param": "test-params", 198 "response_format": { 199 "type": "json_schema", 200 "json_schema": { 201 "name": "CalendarEvent", 202 "strict": True, 203 "schema": { 204 "properties": { 205 "event_name": {"title": "Event Name", "type": "string"}, 206 "event_date": {"title": "Event Date", "type": "string"}, 207 "event_location": {"title": "Event Location", "type": "string"}, 208 }, 209 "required": ["event_name", "event_date", "event_location"], 210 "title": "CalendarEvent", 211 "type": "object", 212 "additionalProperties": False, 213 }, 214 }, 215 }, 216 }, 217 "tools": None, 218 "tools_strict": False, 219 "default_headers": {}, 220 "azure_ad_token_provider": "haystack.utils.azure.default_azure_ad_token_provider", 221 "http_client_kwargs": {"proxy": "http://localhost:8080"}, 222 }, 223 } 224 225 def test_from_dict(self, monkeypatch): 226 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 227 monkeypatch.setenv("AZURE_OPENAI_AD_TOKEN", "test-ad-token") 228 data = { 229 "type": "haystack.components.generators.chat.azure.AzureOpenAIChatGenerator", 230 "init_parameters": { 231 "api_key": {"env_vars": ["AZURE_OPENAI_API_KEY"], "strict": False, "type": "env_var"}, 232 "azure_ad_token": {"env_vars": ["AZURE_OPENAI_AD_TOKEN"], "strict": False, "type": "env_var"}, 233 "api_version": "2024-12-01-preview", 234 "azure_endpoint": "some-non-existing-endpoint", 235 "azure_deployment": "gpt-4.1-mini", 236 "organization": None, 237 "streaming_callback": None, 238 "generation_kwargs": {}, 239 "timeout": 30.0, 240 "max_retries": 5, 241 "default_headers": {}, 242 "tools": [ 243 { 244 "type": "haystack.tools.tool.Tool", 245 "data": { 246 "description": "description", 247 "function": "builtins.print", 248 "name": "name", 249 "parameters": {"x": {"type": "string"}}, 250 }, 251 } 252 ], 253 "tools_strict": False, 254 "http_client_kwargs": None, 255 }, 256 } 257 258 generator = AzureOpenAIChatGenerator.from_dict(data) 259 assert isinstance(generator, AzureOpenAIChatGenerator) 260 261 assert generator.api_key == Secret.from_env_var("AZURE_OPENAI_API_KEY", strict=False) 262 assert generator.azure_ad_token == Secret.from_env_var("AZURE_OPENAI_AD_TOKEN", strict=False) 263 assert generator.api_version == "2024-12-01-preview" 264 assert generator.azure_endpoint == "some-non-existing-endpoint" 265 assert generator.azure_deployment == "gpt-4.1-mini" 266 assert generator.organization is None 267 assert generator.streaming_callback is None 268 assert generator.generation_kwargs == {} 269 assert generator.timeout == 30.0 270 assert generator.max_retries == 5 271 assert generator.default_headers == {} 272 assert generator.tools == [ 273 Tool(name="name", description="description", parameters={"x": {"type": "string"}}, function=print) 274 ] 275 assert generator.tools_strict is False 276 assert generator.http_client_kwargs is None 277 278 def test_pipeline_serialization_deserialization(self, tmp_path, monkeypatch): 279 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 280 generator = AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint") 281 p = Pipeline() 282 p.add_component(instance=generator, name="generator") 283 284 assert p.to_dict() == { 285 "metadata": {}, 286 "max_runs_per_component": 100, 287 "connection_type_validation": True, 288 "components": { 289 "generator": { 290 "type": "haystack.components.generators.chat.azure.AzureOpenAIChatGenerator", 291 "init_parameters": { 292 "azure_endpoint": "some-non-existing-endpoint", 293 "azure_deployment": "gpt-4.1-mini", 294 "organization": None, 295 "api_version": "2024-12-01-preview", 296 "streaming_callback": None, 297 "generation_kwargs": {}, 298 "timeout": 30.0, 299 "max_retries": 5, 300 "api_key": {"type": "env_var", "env_vars": ["AZURE_OPENAI_API_KEY"], "strict": False}, 301 "azure_ad_token": {"type": "env_var", "env_vars": ["AZURE_OPENAI_AD_TOKEN"], "strict": False}, 302 "default_headers": {}, 303 "tools": None, 304 "tools_strict": False, 305 "azure_ad_token_provider": None, 306 "http_client_kwargs": None, 307 }, 308 } 309 }, 310 "connections": [], 311 } 312 p_str = p.dumps() 313 q = Pipeline.loads(p_str) 314 assert p.to_dict() == q.to_dict(), "Pipeline serialization/deserialization w/ AzureOpenAIChatGenerator failed." 315 316 def test_azure_chat_generator_with_toolset_initialization(self, tools, monkeypatch): 317 """Test that the AzureOpenAIChatGenerator can be initialized with a Toolset.""" 318 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 319 toolset = Toolset(tools) 320 generator = AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint", tools=toolset) 321 assert generator.tools == toolset 322 323 def test_from_dict_with_toolset(self, tools, monkeypatch): 324 """Test that the AzureOpenAIChatGenerator can be deserialized from a dictionary with a Toolset.""" 325 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 326 toolset = Toolset(tools) 327 component = AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint", tools=toolset) 328 data = component.to_dict() 329 330 deserialized_component = AzureOpenAIChatGenerator.from_dict(data) 331 332 assert isinstance(deserialized_component.tools, Toolset) 333 assert len(deserialized_component.tools) == len(tools) 334 assert all(isinstance(tool, Tool) for tool in deserialized_component.tools) 335 336 @pytest.mark.integration 337 @pytest.mark.skipif( 338 not os.environ.get("AZURE_OPENAI_API_KEY", None) or not os.environ.get("AZURE_OPENAI_ENDPOINT", None), 339 reason=( 340 "Please export env variables called AZURE_OPENAI_API_KEY containing " 341 "the Azure OpenAI key, AZURE_OPENAI_ENDPOINT containing " 342 "the Azure OpenAI endpoint URL to run this test." 343 ), 344 ) 345 def test_live_run(self): 346 chat_messages = [ChatMessage.from_user("What's the capital of France")] 347 component = AzureOpenAIChatGenerator(organization="HaystackCI") 348 results = component.run(chat_messages) 349 assert len(results["replies"]) == 1 350 message: ChatMessage = results["replies"][0] 351 assert "Paris" in message.text 352 assert "gpt-4.1-mini" in message.meta["model"] 353 assert message.meta["finish_reason"] == "stop" 354 355 @pytest.mark.integration 356 @pytest.mark.skipif( 357 not os.environ.get("AZURE_OPENAI_API_KEY", None) or not os.environ.get("AZURE_OPENAI_ENDPOINT", None), 358 reason=( 359 "Please export env variables called AZURE_OPENAI_API_KEY containing " 360 "the Azure OpenAI key, AZURE_OPENAI_ENDPOINT containing " 361 "the Azure OpenAI endpoint URL to run this test." 362 ), 363 ) 364 def test_live_run_with_tools(self, tools): 365 chat_messages = [ChatMessage.from_user("What's the weather like in Paris?")] 366 component = AzureOpenAIChatGenerator(organization="HaystackCI", tools=tools) 367 results = component.run(chat_messages) 368 assert len(results["replies"]) == 1 369 message = results["replies"][0] 370 371 assert not message.texts 372 assert not message.text 373 assert message.tool_calls 374 tool_call = message.tool_call 375 assert isinstance(tool_call, ToolCall) 376 assert tool_call.tool_name == "weather" 377 assert tool_call.arguments == {"city": "Paris"} 378 assert message.meta["finish_reason"] == "tool_calls" 379 380 @pytest.mark.skipif( 381 not os.environ.get("AZURE_OPENAI_API_KEY", None), 382 reason="Export an env var called AZURE_OPENAI_API_KEY containing the Azure OpenAI API key to run this test.", 383 ) 384 @pytest.mark.integration 385 def test_live_run_with_response_format(self): 386 class CalendarEvent(BaseModel): 387 event_name: str 388 event_date: str 389 event_location: str 390 391 chat_messages = [ 392 ChatMessage.from_user("The marketing summit takes place on October12th at the Hilton Hotel downtown.") 393 ] 394 component = AzureOpenAIChatGenerator( 395 api_version="2024-08-01-preview", generation_kwargs={"response_format": CalendarEvent} 396 ) 397 results = component.run(chat_messages) 398 assert len(results["replies"]) == 1 399 message: ChatMessage = results["replies"][0] 400 msg = json.loads(message.text) 401 assert "Marketing Summit" in msg["event_name"] 402 assert isinstance(msg["event_date"], str) 403 assert isinstance(msg["event_location"], str) 404 405 assert message.meta["finish_reason"] == "stop" 406 407 def test_to_dict_with_toolset(self, tools, monkeypatch): 408 """Test that the AzureOpenAIChatGenerator can be serialized to a dictionary with a Toolset.""" 409 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 410 toolset = Toolset(tools[:1]) 411 component = AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint", tools=toolset) 412 data = component.to_dict() 413 414 expected_tools_data = { 415 "type": "haystack.tools.toolset.Toolset", 416 "data": { 417 "tools": [ 418 { 419 "type": "haystack.tools.tool.Tool", 420 "data": { 421 "name": "weather", 422 "description": "useful to determine the weather in a given location", 423 "parameters": { 424 "type": "object", 425 "properties": {"city": {"type": "string"}}, 426 "required": ["city"], 427 }, 428 "function": "generators.chat.test_azure.get_weather", 429 "outputs_to_string": None, 430 "inputs_from_state": None, 431 "outputs_to_state": None, 432 }, 433 } 434 ] 435 }, 436 } 437 assert data["init_parameters"]["tools"] == expected_tools_data 438 439 def test_warm_up_with_tools(self, monkeypatch): 440 """Test that warm_up() calls warm_up on tools and is idempotent.""" 441 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 442 443 # Create a mock tool that tracks if warm_up() was called 444 class MockTool(Tool): 445 warm_up_call_count = 0 # Class variable to track calls 446 447 def __init__(self): 448 super().__init__( 449 name="mock_tool", 450 description="A mock tool for testing", 451 parameters={"x": {"type": "string"}}, 452 function=lambda x: x, 453 ) 454 455 def warm_up(self): 456 MockTool.warm_up_call_count += 1 457 458 # Reset the class variable before test 459 MockTool.warm_up_call_count = 0 460 mock_tool = MockTool() 461 462 # Create AzureOpenAIChatGenerator with the mock tool 463 component = AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint", tools=[mock_tool]) 464 465 # Verify initial state - warm_up not called yet 466 assert MockTool.warm_up_call_count == 0 467 assert not component._is_warmed_up 468 469 # Call warm_up() on the generator 470 component.warm_up() 471 472 # Assert that the tool's warm_up() was called 473 assert MockTool.warm_up_call_count == 1 474 assert component._is_warmed_up 475 476 # Call warm_up() again and verify it's idempotent (only warms up once) 477 component.warm_up() 478 479 # The tool's warm_up should still only have been called once 480 assert MockTool.warm_up_call_count == 1 481 assert component._is_warmed_up 482 483 def test_warm_up_with_no_tools(self, monkeypatch): 484 """Test that warm_up() works when no tools are provided.""" 485 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 486 487 component = AzureOpenAIChatGenerator(azure_endpoint="some-non-existing-endpoint") 488 489 # Verify initial state 490 assert not component._is_warmed_up 491 assert component.tools is None 492 493 # Call warm_up() - should not raise an error 494 component.warm_up() 495 496 # Verify the component is warmed up 497 assert component._is_warmed_up 498 499 # Call warm_up() again - should be idempotent 500 component.warm_up() 501 assert component._is_warmed_up 502 503 def test_warm_up_with_multiple_tools(self, monkeypatch): 504 """Test that warm_up() works with multiple tools.""" 505 monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-api-key") 506 507 # Track warm_up calls 508 warm_up_calls = [] 509 510 class MockTool(Tool): 511 def __init__(self, tool_name): 512 super().__init__( 513 name=tool_name, 514 description=f"Mock tool {tool_name}", 515 parameters={"type": "object", "properties": {"x": {"type": "string"}}, "required": ["x"]}, 516 function=lambda x: f"{tool_name} result: {x}", 517 ) 518 519 def warm_up(self): 520 warm_up_calls.append(self.name) 521 522 mock_tool1 = MockTool("tool1") 523 mock_tool2 = MockTool("tool2") 524 525 # Use a LIST of tools, not a Toolset 526 component = AzureOpenAIChatGenerator( 527 azure_endpoint="some-non-existing-endpoint", tools=[mock_tool1, mock_tool2] 528 ) 529 530 # Call warm_up() 531 component.warm_up() 532 533 # Assert that both tools' warm_up() were called 534 assert "tool1" in warm_up_calls 535 assert "tool2" in warm_up_calls 536 assert component._is_warmed_up 537 538 # Test idempotency - warm_up should not call tools again 539 initial_count = len(warm_up_calls) 540 component.warm_up() 541 assert len(warm_up_calls) == initial_count 542 543 544 class TestAzureOpenAIChatGeneratorAsync: 545 def test_init_should_also_create_async_client_with_same_args(self, tools): 546 component = AzureOpenAIChatGenerator( 547 api_key=Secret.from_token("test-api-key"), 548 azure_endpoint="some-non-existing-endpoint", 549 streaming_callback=print_streaming_chunk, 550 generation_kwargs={"max_completion_tokens": 10, "some_test_param": "test-params"}, 551 tools=tools, 552 tools_strict=True, 553 ) 554 assert component.async_client.api_key == "test-api-key" 555 assert component.azure_deployment == "gpt-4.1-mini" 556 assert component.streaming_callback is print_streaming_chunk 557 assert component.generation_kwargs == {"max_completion_tokens": 10, "some_test_param": "test-params"} 558 assert component.tools == tools 559 assert component.tools_strict 560 561 @pytest.mark.integration 562 @pytest.mark.skipif( 563 not os.environ.get("AZURE_OPENAI_API_KEY", None) or not os.environ.get("AZURE_OPENAI_ENDPOINT", None), 564 reason=( 565 "Please export env variables called AZURE_OPENAI_API_KEY containing " 566 "the Azure OpenAI key, AZURE_OPENAI_ENDPOINT containing " 567 "the Azure OpenAI endpoint URL to run this test." 568 ), 569 ) 570 @pytest.mark.asyncio 571 async def test_live_run_async(self): 572 component = AzureOpenAIChatGenerator(generation_kwargs={"n": 1}) 573 chat_messages = [ChatMessage.from_user("What's the capital of France")] 574 results = await component.run_async(chat_messages) 575 assert len(results["replies"]) == 1 576 message: ChatMessage = results["replies"][0] 577 assert "Paris" in message.text 578 assert "gpt-4.1-mini" in message.meta["model"] 579 assert message.meta["finish_reason"] == "stop" 580 # Close async client; suppress RuntimeError if the event loop is already closed 581 with contextlib.suppress(RuntimeError): 582 await component.async_client.close() 583 584 @pytest.mark.integration 585 @pytest.mark.skipif( 586 not os.environ.get("AZURE_OPENAI_API_KEY", None) or not os.environ.get("AZURE_OPENAI_ENDPOINT", None), 587 reason=( 588 "Please export env variables called AZURE_OPENAI_API_KEY containing " 589 "the Azure OpenAI key, AZURE_OPENAI_ENDPOINT containing " 590 "the Azure OpenAI endpoint URL to run this test." 591 ), 592 ) 593 @pytest.mark.asyncio 594 async def test_live_run_with_tools_async(self, tools): 595 component = AzureOpenAIChatGenerator(tools=tools) 596 chat_messages = [ChatMessage.from_user("What's the weather like in Paris?")] 597 results = await component.run_async(chat_messages) 598 assert len(results["replies"]) == 1 599 message = results["replies"][0] 600 601 assert not message.texts 602 assert not message.text 603 assert message.tool_calls 604 tool_call = message.tool_call 605 assert isinstance(tool_call, ToolCall) 606 assert tool_call.tool_name == "weather" 607 assert tool_call.arguments == {"city": "Paris"} 608 assert message.meta["finish_reason"] == "tool_calls" 609 610 # Close async client; suppress RuntimeError if the event loop is already closed 611 with contextlib.suppress(RuntimeError): 612 await component.async_client.close() 613 614 # additional tests intentionally omitted as they are covered by test_openai.py