-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathtest_azure_ai_client.py
More file actions
563 lines (416 loc) · 22.3 KB
/
Copy pathtest_azure_ai_client.py
File metadata and controls
563 lines (416 loc) · 22.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import (
ChatClientProtocol,
ChatMessage,
ChatOptions,
Role,
TextContent,
)
from agent_framework.exceptions import ServiceInitializationError
from azure.ai.projects.models import (
ResponseTextFormatConfigurationJsonSchema,
)
from pydantic import BaseModel, ConfigDict, ValidationError
from agent_framework_azure_ai import AzureAIClient, AzureAISettings
def create_test_azure_ai_client(
mock_project_client: MagicMock,
agent_name: str | None = None,
agent_version: str | None = None,
conversation_id: str | None = None,
azure_ai_settings: AzureAISettings | None = None,
should_close_client: bool = False,
use_latest_version: bool | None = None,
) -> AzureAIClient:
"""Helper function to create AzureAIClient instances for testing, bypassing normal validation."""
if azure_ai_settings is None:
azure_ai_settings = AzureAISettings(env_file_path="test.env")
# Create client instance directly
client = object.__new__(AzureAIClient)
# Set attributes directly
client.project_client = mock_project_client
client.credential = None
client.agent_name = agent_name
client.agent_version = agent_version
client.use_latest_version = use_latest_version
client.model_id = azure_ai_settings.model_deployment_name
client.conversation_id = conversation_id
client._should_close_client = should_close_client # type: ignore
client.additional_properties = {}
client.middleware = None
# Mock the OpenAI client attribute
mock_openai_client = MagicMock()
mock_openai_client.conversations = MagicMock()
mock_openai_client.conversations.create = AsyncMock()
client.client = mock_openai_client
return client
def test_azure_ai_settings_init(azure_ai_unit_test_env: dict[str, str]) -> None:
"""Test AzureAISettings initialization."""
settings = AzureAISettings()
assert settings.project_endpoint == azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"]
assert settings.model_deployment_name == azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]
def test_azure_ai_settings_init_with_explicit_values() -> None:
"""Test AzureAISettings initialization with explicit values."""
settings = AzureAISettings(
project_endpoint="https://custom-endpoint.com/",
model_deployment_name="custom-model",
)
assert settings.project_endpoint == "https://custom-endpoint.com/"
assert settings.model_deployment_name == "custom-model"
def test_azure_ai_client_init_with_project_client(mock_project_client: MagicMock) -> None:
"""Test AzureAIClient initialization with existing project_client."""
with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
mock_settings.return_value.project_endpoint = None
mock_settings.return_value.model_deployment_name = "test-model"
client = AzureAIClient(
project_client=mock_project_client,
agent_name="test-agent",
agent_version="1.0",
)
assert client.project_client is mock_project_client
assert client.agent_name == "test-agent"
assert client.agent_version == "1.0"
assert not client._should_close_client # type: ignore
assert isinstance(client, ChatClientProtocol)
def test_azure_ai_client_init_auto_create_client(
azure_ai_unit_test_env: dict[str, str],
mock_azure_credential: MagicMock,
) -> None:
"""Test AzureAIClient initialization with auto-created project_client."""
with patch("agent_framework_azure_ai._client.AIProjectClient") as mock_ai_project_client:
mock_project_client = MagicMock()
mock_ai_project_client.return_value = mock_project_client
client = AzureAIClient(
project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"],
model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
async_credential=mock_azure_credential,
agent_name="test-agent",
)
assert client.project_client is mock_project_client
assert client.agent_name == "test-agent"
assert client._should_close_client # type: ignore
# Verify AIProjectClient was called with correct parameters
mock_ai_project_client.assert_called_once()
def test_azure_ai_client_init_missing_project_endpoint() -> None:
"""Test AzureAIClient initialization when project_endpoint is missing and no project_client provided."""
with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
mock_settings.return_value.project_endpoint = None
mock_settings.return_value.model_deployment_name = "test-model"
with pytest.raises(ServiceInitializationError, match="Azure AI project endpoint is required"):
AzureAIClient(async_credential=MagicMock())
def test_azure_ai_client_init_missing_credential(azure_ai_unit_test_env: dict[str, str]) -> None:
"""Test AzureAIClient.__init__ when async_credential is missing and no project_client provided."""
with pytest.raises(
ServiceInitializationError, match="Azure credential is required when project_client is not provided"
):
AzureAIClient(
project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"],
model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
)
def test_azure_ai_client_init_validation_error(mock_azure_credential: MagicMock) -> None:
"""Test that ValidationError in AzureAISettings is properly handled."""
with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
mock_settings.side_effect = ValidationError.from_exception_data("test", [])
with pytest.raises(ServiceInitializationError, match="Failed to create Azure AI settings"):
AzureAIClient(async_credential=mock_azure_credential)
async def test_azure_ai_client_get_agent_reference_or_create_existing_version(
mock_project_client: MagicMock,
) -> None:
"""Test _get_agent_reference_or_create when agent_version is already provided."""
client = create_test_azure_ai_client(mock_project_client, agent_name="existing-agent", agent_version="1.0")
agent_ref = await client._get_agent_reference_or_create({}, None) # type: ignore
assert agent_ref == {"name": "existing-agent", "version": "1.0", "type": "agent_reference"}
async def test_azure_ai_client_get_agent_reference_or_create_new_agent(
mock_project_client: MagicMock,
azure_ai_unit_test_env: dict[str, str],
) -> None:
"""Test _get_agent_reference_or_create when creating a new agent."""
azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"])
client = create_test_azure_ai_client(
mock_project_client, agent_name="new-agent", azure_ai_settings=azure_ai_settings
)
# Mock agent creation response
mock_agent = MagicMock()
mock_agent.name = "new-agent"
mock_agent.version = "1.0"
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
run_options = {"model": azure_ai_settings.model_deployment_name}
agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
assert agent_ref == {"name": "new-agent", "version": "1.0", "type": "agent_reference"}
assert client.agent_name == "new-agent"
assert client.agent_version == "1.0"
async def test_azure_ai_client_get_agent_reference_missing_model(
mock_project_client: MagicMock,
) -> None:
"""Test _get_agent_reference_or_create when model is missing for agent creation."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
with pytest.raises(ServiceInitializationError, match="Model deployment name is required for agent creation"):
await client._get_agent_reference_or_create({}, None) # type: ignore
async def test_azure_ai_client_prepare_input_with_system_messages(
mock_project_client: MagicMock,
) -> None:
"""Test _prepare_input converts system/developer messages to instructions."""
client = create_test_azure_ai_client(mock_project_client)
messages = [
ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="You are a helpful assistant.")]),
ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="System response")]),
]
result_messages, instructions = client._prepare_input(messages) # type: ignore
assert len(result_messages) == 2
assert result_messages[0].role == Role.USER
assert result_messages[1].role == Role.ASSISTANT
assert instructions == "You are a helpful assistant."
async def test_azure_ai_client_prepare_input_no_system_messages(
mock_project_client: MagicMock,
) -> None:
"""Test _prepare_input with no system/developer messages."""
client = create_test_azure_ai_client(mock_project_client)
messages = [
ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="Hi there!")]),
]
result_messages, instructions = client._prepare_input(messages) # type: ignore
assert len(result_messages) == 2
assert instructions is None
async def test_azure_ai_client_prepare_options_basic(mock_project_client: MagicMock) -> None:
"""Test prepare_options basic functionality."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
chat_options = ChatOptions()
with (
patch.object(client.__class__.__bases__[0], "prepare_options", return_value={"model": "test-model"}),
patch.object(
client,
"_get_agent_reference_or_create",
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
),
):
run_options = await client.prepare_options(messages, chat_options)
assert "extra_body" in run_options
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
async def test_azure_ai_client_initialize_client(mock_project_client: MagicMock) -> None:
"""Test initialize_client method."""
client = create_test_azure_ai_client(mock_project_client)
mock_openai_client = MagicMock()
mock_project_client.get_openai_client = AsyncMock(return_value=mock_openai_client)
await client.initialize_client()
assert client.client is mock_openai_client
mock_project_client.get_openai_client.assert_called_once()
def test_azure_ai_client_update_agent_name(mock_project_client: MagicMock) -> None:
"""Test _update_agent_name method."""
client = create_test_azure_ai_client(mock_project_client)
# Test updating agent name when current is None
with patch.object(client, "_update_agent_name") as mock_update:
mock_update.return_value = None
client._update_agent_name("new-agent") # type: ignore
mock_update.assert_called_once_with("new-agent")
# Test behavior when agent name is updated
assert client.agent_name is None # Should remain None since we didn't actually update
client.agent_name = "test-agent" # Manually set for the test
# Test with None input
with patch.object(client, "_update_agent_name") as mock_update:
mock_update.return_value = None
client._update_agent_name(None) # type: ignore
mock_update.assert_called_once_with(None)
async def test_azure_ai_client_async_context_manager(mock_project_client: MagicMock) -> None:
"""Test async context manager functionality."""
client = create_test_azure_ai_client(mock_project_client, should_close_client=True)
mock_project_client.close = AsyncMock()
async with client as ctx_client:
assert ctx_client is client
# Should call close after exiting context
mock_project_client.close.assert_called_once()
async def test_azure_ai_client_close_method(mock_project_client: MagicMock) -> None:
"""Test close method."""
client = create_test_azure_ai_client(mock_project_client, should_close_client=True)
mock_project_client.close = AsyncMock()
await client.close()
mock_project_client.close.assert_called_once()
async def test_azure_ai_client_close_client_when_should_close_false(mock_project_client: MagicMock) -> None:
"""Test _close_client_if_needed when should_close_client is False."""
client = create_test_azure_ai_client(mock_project_client, should_close_client=False)
mock_project_client.close = AsyncMock()
await client._close_client_if_needed() # type: ignore
# Should not call close when should_close_client is False
mock_project_client.close.assert_not_called()
async def test_azure_ai_client_agent_creation_with_instructions(
mock_project_client: MagicMock,
) -> None:
"""Test agent creation with combined instructions."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
# Mock agent creation response
mock_agent = MagicMock()
mock_agent.name = "test-agent"
mock_agent.version = "1.0"
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
run_options = {"model": "test-model", "instructions": "Option instructions. "}
messages_instructions = "Message instructions. "
await client._get_agent_reference_or_create(run_options, messages_instructions) # type: ignore
# Verify agent was created with combined instructions
call_args = mock_project_client.agents.create_version.call_args
assert call_args[1]["definition"].instructions == "Message instructions. Option instructions. "
async def test_azure_ai_client_agent_creation_with_tools(
mock_project_client: MagicMock,
) -> None:
"""Test agent creation with tools."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
# Mock agent creation response
mock_agent = MagicMock()
mock_agent.name = "test-agent"
mock_agent.version = "1.0"
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
test_tools = [{"type": "function", "function": {"name": "test_tool"}}]
run_options = {"model": "test-model", "tools": test_tools}
await client._get_agent_reference_or_create(run_options, None) # type: ignore
# Verify agent was created with tools
call_args = mock_project_client.agents.create_version.call_args
assert call_args[1]["definition"].tools == test_tools
async def test_azure_ai_client_use_latest_version_existing_agent(
mock_project_client: MagicMock,
) -> None:
"""Test _get_agent_reference_or_create when use_latest_version=True and agent exists."""
client = create_test_azure_ai_client(mock_project_client, agent_name="existing-agent", use_latest_version=True)
# Mock existing agent response
mock_existing_agent = MagicMock()
mock_existing_agent.name = "existing-agent"
mock_existing_agent.versions.latest.version = "2.5"
mock_project_client.agents.get = AsyncMock(return_value=mock_existing_agent)
run_options = {"model": "test-model"}
agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
# Verify existing agent was retrieved and used
mock_project_client.agents.get.assert_called_once_with("existing-agent")
mock_project_client.agents.create_version.assert_not_called()
assert agent_ref == {"name": "existing-agent", "version": "2.5", "type": "agent_reference"}
assert client.agent_name == "existing-agent"
assert client.agent_version == "2.5"
async def test_azure_ai_client_use_latest_version_agent_not_found(
mock_project_client: MagicMock,
) -> None:
"""Test _get_agent_reference_or_create when use_latest_version=True but agent doesn't exist."""
from azure.core.exceptions import ResourceNotFoundError
client = create_test_azure_ai_client(mock_project_client, agent_name="non-existing-agent", use_latest_version=True)
# Mock ResourceNotFoundError when trying to retrieve agent
mock_project_client.agents.get = AsyncMock(side_effect=ResourceNotFoundError("Agent not found"))
# Mock agent creation response for fallback
mock_created_agent = MagicMock()
mock_created_agent.name = "non-existing-agent"
mock_created_agent.version = "1.0"
mock_project_client.agents.create_version = AsyncMock(return_value=mock_created_agent)
run_options = {"model": "test-model"}
agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
# Verify retrieval was attempted and creation was used as fallback
mock_project_client.agents.get.assert_called_once_with("non-existing-agent")
mock_project_client.agents.create_version.assert_called_once()
assert agent_ref == {"name": "non-existing-agent", "version": "1.0", "type": "agent_reference"}
assert client.agent_name == "non-existing-agent"
assert client.agent_version == "1.0"
async def test_azure_ai_client_use_latest_version_false(
mock_project_client: MagicMock,
) -> None:
"""Test _get_agent_reference_or_create when use_latest_version=False (default behavior)."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", use_latest_version=False)
# Mock agent creation response
mock_created_agent = MagicMock()
mock_created_agent.name = "test-agent"
mock_created_agent.version = "1.0"
mock_project_client.agents.create_version = AsyncMock(return_value=mock_created_agent)
run_options = {"model": "test-model"}
agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
# Verify retrieval was not attempted and creation was used directly
mock_project_client.agents.get.assert_not_called()
mock_project_client.agents.create_version.assert_called_once()
assert agent_ref == {"name": "test-agent", "version": "1.0", "type": "agent_reference"}
async def test_azure_ai_client_use_latest_version_with_existing_agent_version(
mock_project_client: MagicMock,
) -> None:
"""Test that use_latest_version is ignored when agent_version is already provided."""
client = create_test_azure_ai_client(
mock_project_client, agent_name="test-agent", agent_version="3.0", use_latest_version=True
)
agent_ref = await client._get_agent_reference_or_create({}, None) # type: ignore
# Verify neither retrieval nor creation was attempted since version is already set
mock_project_client.agents.get.assert_not_called()
mock_project_client.agents.create_version.assert_not_called()
assert agent_ref == {"name": "test-agent", "version": "3.0", "type": "agent_reference"}
class ResponseFormatModel(BaseModel):
"""Test Pydantic model for response format testing."""
name: str
value: int
description: str
model_config = ConfigDict(extra="forbid")
async def test_azure_ai_client_agent_creation_with_response_format(
mock_project_client: MagicMock,
) -> None:
"""Test agent creation with response_format configuration."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
# Mock agent creation response
mock_agent = MagicMock()
mock_agent.name = "test-agent"
mock_agent.version = "1.0"
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
run_options = {"model": "test-model", "response_format": ResponseFormatModel}
await client._get_agent_reference_or_create(run_options, None) # type: ignore
# Verify agent was created with response format configuration
call_args = mock_project_client.agents.create_version.call_args
created_definition = call_args[1]["definition"]
# Check that text format configuration was set
assert hasattr(created_definition, "text")
assert created_definition.text is not None
# Check that the format is a ResponseTextFormatConfigurationJsonSchema
assert hasattr(created_definition.text, "format")
format_config = created_definition.text.format
assert isinstance(format_config, ResponseTextFormatConfigurationJsonSchema)
# Check the schema name matches the model class name
assert format_config.name == "ResponseFormatModel"
# Check that schema was generated correctly
assert format_config.schema is not None
schema = format_config.schema
assert "properties" in schema
assert "name" in schema["properties"]
assert "value" in schema["properties"]
assert "description" in schema["properties"]
async def test_azure_ai_client_prepare_options_excludes_response_format(
mock_project_client: MagicMock,
) -> None:
"""Test that prepare_options excludes response_format from final run options."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
chat_options = ChatOptions()
with (
patch.object(
client.__class__.__bases__[0],
"prepare_options",
return_value={"model": "test-model", "response_format": ResponseFormatModel},
),
patch.object(
client,
"_get_agent_reference_or_create",
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
),
):
run_options = await client.prepare_options(messages, chat_options)
# response_format should be excluded from final run options
assert "response_format" not in run_options
# But extra_body should contain agent reference
assert "extra_body" in run_options
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
@pytest.fixture
def mock_project_client() -> MagicMock:
"""Fixture that provides a mock AIProjectClient."""
mock_client = MagicMock()
# Mock agents property
mock_client.agents = MagicMock()
mock_client.agents.create_version = AsyncMock()
# Mock conversations property
mock_client.conversations = MagicMock()
mock_client.conversations.create = AsyncMock()
# Mock telemetry property
mock_client.telemetry = MagicMock()
mock_client.telemetry.get_application_insights_connection_string = AsyncMock()
# Mock get_openai_client method
mock_client.get_openai_client = AsyncMock()
# Mock close method
mock_client.close = AsyncMock()
return mock_client