/ test / components / generators / chat / test_azure.py
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