chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,28 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock
import pytest
from azure.ai.agents.models import Agent as AzureAIAgentModel
from azure.ai.projects.aio import AIProjectClient
@pytest.fixture
def ai_project_client() -> AsyncMock:
client = AsyncMock(spec=AIProjectClient)
agents_mock = MagicMock()
client.agents = agents_mock
return client
@pytest.fixture
def ai_agent_definition() -> AsyncMock:
definition = AsyncMock(spec=AzureAIAgentModel)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
return definition
@@ -0,0 +1,464 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock
from azure.ai.agents.models import (
MessageDelta,
MessageDeltaChunk,
MessageDeltaImageFileContent,
MessageDeltaImageFileContentObject,
MessageDeltaTextContent,
MessageDeltaTextContentObject,
MessageDeltaTextFileCitationAnnotation,
MessageDeltaTextFileCitationAnnotationObject,
MessageDeltaTextFilePathAnnotation,
MessageDeltaTextFilePathAnnotationObject,
MessageDeltaTextUrlCitationAnnotation,
MessageDeltaTextUrlCitationDetails,
MessageImageFileContent,
MessageImageFileDetails,
MessageTextContent,
MessageTextDetails,
MessageTextFileCitationAnnotation,
MessageTextFileCitationDetails,
MessageTextFilePathAnnotation,
MessageTextFilePathDetails,
MessageTextUrlCitationAnnotation,
MessageTextUrlCitationDetails,
RequiredFunctionToolCall,
RunStep,
RunStepBingCustomSearchToolCall,
RunStepBingGroundingToolCall,
RunStepDeltaFunction,
RunStepDeltaFunctionToolCall,
RunStepDeltaToolCallObject,
RunStepFunctionToolCall,
RunStepFunctionToolCallDetails,
ThreadMessage,
)
from semantic_kernel.agents.azure_ai.agent_content_generation import (
THREAD_MESSAGE_ID,
generate_annotation_content,
generate_bing_grounding_content,
generate_code_interpreter_content,
generate_function_call_content,
generate_function_result_content,
generate_message_content,
generate_streaming_annotation_content,
generate_streaming_code_interpreter_content,
generate_streaming_function_content,
generate_streaming_message_content,
get_function_call_contents,
get_message_contents,
)
from semantic_kernel.contents.annotation_content import AnnotationContent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.file_reference_content import FileReferenceContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.streaming_annotation_content import StreamingAnnotationContent
from semantic_kernel.contents.streaming_file_reference_content import StreamingFileReferenceContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
def test_get_message_contents_all_types():
chat_msg = ChatMessageContent(role=AuthorRole.USER, content="")
chat_msg.items.append(TextContent(text="hello world"))
chat_msg.items.append(ImageContent(uri="http://example.com/image.png"))
chat_msg.items.append(FileReferenceContent(file_id="file123"))
chat_msg.items.append(FunctionResultContent(id="func1", result={"a": 1}))
results = get_message_contents(chat_msg)
assert len(results) == 4
assert results[0]["type"] == "text"
assert results[1]["type"] == "image_url"
assert results[2]["type"] == "image_file"
assert results[3]["type"] == "text"
def test_generate_message_content_text_and_image():
thread_msg = ThreadMessage(
content=[],
role="user",
)
image = MessageImageFileContent(image_file=MessageImageFileDetails(file_id="test_file_id"))
text = MessageTextContent(
text=MessageTextDetails(
value="some text",
annotations=[
MessageTextFileCitationAnnotation(
text="text",
file_citation=MessageTextFileCitationDetails(file_id="file_id", quote="some quote"),
start_index=0,
end_index=9,
),
MessageTextFilePathAnnotation(
text="text again",
file_path=MessageTextFilePathDetails(file_id="file_id_2"),
start_index=1,
end_index=10,
),
MessageTextUrlCitationAnnotation(
text="text",
url_citation=MessageTextUrlCitationDetails(title="some title", url="http://example.com"),
start_index=1,
end_index=10,
),
],
)
)
thread_msg.content = [image, text]
step = RunStep(id="step_id", run_id="run_id", thread_id="thread_id", agent_id="agent_id")
out = generate_message_content("assistant", thread_msg, step)
assert len(out.items) == 5
assert isinstance(out.items[0], FileReferenceContent)
assert isinstance(out.items[1], TextContent)
assert isinstance(out.items[2], AnnotationContent)
assert isinstance(out.items[3], AnnotationContent)
assert isinstance(out.items[4], AnnotationContent)
assert out.items[0].file_id == "test_file_id"
assert out.items[1].text == "some text"
assert out.items[2].file_id == "file_id"
assert out.items[2].quote == "text"
assert out.items[2].start_index == 0
assert out.items[2].end_index == 9
assert out.items[2].citation_type == "file_citation"
assert out.items[3].file_id == "file_id_2"
assert out.items[3].quote == "text again"
assert out.items[3].start_index == 1
assert out.items[3].end_index == 10
assert out.items[3].citation_type == "file_path"
assert out.items[4].url == "http://example.com"
assert out.items[4].quote == "text"
assert out.items[4].start_index == 1
assert out.items[4].end_index == 10
assert out.items[4].title == "some title"
assert out.items[4].citation_type == "url_citation"
assert out.metadata["step_id"] == "step_id"
assert out.role == AuthorRole.USER
def test_generate_annotation_content():
message_text_file_path_ann = MessageTextFilePathAnnotation(
text="some text",
file_path=MessageTextFilePathDetails(file_id="file123"),
start_index=0,
end_index=9,
)
message_text_file_citation_ann = MessageTextFileCitationAnnotation(
text="some text",
file_citation=MessageTextFileCitationDetails(file_id="file123"),
start_index=0,
end_index=9,
)
for fake_ann in [message_text_file_path_ann, message_text_file_citation_ann]:
out = generate_annotation_content(fake_ann)
assert out.file_id == "file123"
assert out.quote == "some text"
assert out.start_index == 0
assert out.end_index == 9
def test_generate_streaming_message_content_text_annotations():
message_delta_image_file_content = MessageDeltaImageFileContent(
index=0,
image_file=MessageDeltaImageFileContentObject(file_id="image_file"),
)
MessageDeltaTextFileCitationAnnotation, MessageDeltaTextFilePathAnnotation
message_delta_text_content = MessageDeltaTextContent(
index=0,
text=MessageDeltaTextContentObject(
value="some text",
annotations=[
MessageDeltaTextFileCitationAnnotation(
index=0,
file_citation=MessageDeltaTextFileCitationAnnotationObject(file_id="file123", quote="some text"),
start_index=0,
end_index=9,
text="some text",
),
MessageDeltaTextFilePathAnnotation(
index=0,
file_path=MessageDeltaTextFilePathAnnotationObject(file_id="file123"),
start_index=1,
end_index=10,
text="some text",
),
MessageDeltaTextUrlCitationAnnotation(
index=0,
url_citation=MessageDeltaTextUrlCitationDetails(
title="some title",
url="http://example.com",
),
start_index=2,
end_index=11,
),
],
),
)
delta = MessageDeltaChunk(
id="chunk123",
delta=MessageDelta(role="user", content=[message_delta_image_file_content, message_delta_text_content]),
)
out = generate_streaming_message_content("assistant", delta)
assert out is not None
assert out.content == "some text"
assert len(out.items) == 5
assert out.items[0].file_id == "image_file"
assert isinstance(out.items[0], StreamingFileReferenceContent)
assert isinstance(out.items[1], StreamingTextContent)
assert isinstance(out.items[2], StreamingAnnotationContent)
assert out.items[2].file_id == "file123"
assert out.items[2].quote == "some text"
assert out.items[2].start_index == 0
assert out.items[2].end_index == 9
assert out.items[2].citation_type == "file_citation"
assert isinstance(out.items[3], StreamingAnnotationContent)
assert out.items[3].file_id == "file123"
assert out.items[3].quote == "some text"
assert out.items[3].start_index == 1
assert out.items[3].end_index == 10
assert out.items[3].citation_type == "file_path"
assert isinstance(out.items[4], StreamingAnnotationContent)
assert out.items[4].url == "http://example.com"
assert out.items[4].title == "some title"
assert out.items[4].start_index == 2
assert out.items[4].end_index == 11
assert out.items[4].citation_type == "url_citation"
def test_generate_annotation_content_url_annotation_without_indices():
ann = MessageTextUrlCitationAnnotation(
text="url text",
url_citation=MessageTextUrlCitationDetails(title="", url="http://ex.com"),
start_index=None,
end_index=None,
)
out = generate_annotation_content(ann)
assert out.file_id is None
assert out.url == "http://ex.com"
assert out.title == "" # preserve empty title
assert out.quote == "url text"
assert out.start_index is None
assert out.end_index is None
assert out.citation_type == "url_citation"
def test_generate_streaming_annotation_content_url_quote_none_and_missing_indices():
ann = MessageDeltaTextUrlCitationAnnotation(
index=0,
url_citation=MessageDeltaTextUrlCitationDetails(title="", url="http://ex.com"),
start_index=None,
end_index=None,
)
out = generate_streaming_annotation_content(ann)
assert out.file_id is None
assert out.url == "http://ex.com"
assert out.title == ""
assert out.quote is None # no .text on URL annotation
assert out.start_index is None
assert out.end_index is None
assert out.citation_type == "url_citation"
def test_generate_streaming_message_content_text_only_no_annotations():
delta = MessageDeltaChunk(
id="c1",
delta=MessageDelta(
role="assistant",
content=[
MessageDeltaTextContent(
index=0,
text=MessageDeltaTextContentObject(value="just text", annotations=[]),
)
],
),
)
out = generate_streaming_message_content("assistant", delta, thread_msg_id="thread_1")
assert out.content == "just text"
assert len(out.items) == 1
assert isinstance(out.items[0], StreamingTextContent)
assert out.items[0].text == "just text"
assert out.metadata.get(THREAD_MESSAGE_ID) == "thread_1"
def test_generate_annotation_content_empty_title_and_url_only():
ann = MessageTextUrlCitationAnnotation(
text=None,
url_citation=MessageTextUrlCitationDetails(title=None, url="http://empty.com"),
start_index=5,
end_index=10,
)
out = generate_annotation_content(ann)
assert out.quote is None # allow None text
assert out.url == "http://empty.com"
assert out.title is None # allow None title
assert out.start_index == 5
assert out.end_index == 10
def test_generate_streaming_annotation_content_file_and_citation_have_text():
file_ann = MessageDeltaTextFileCitationAnnotation(
index=0,
file_citation=MessageDeltaTextFileCitationAnnotationObject(file_id="f1", quote="q1"),
start_index=2,
end_index=4,
text="q1",
)
out = generate_streaming_annotation_content(file_ann)
assert out.file_id == "f1"
assert out.quote == "q1"
assert out.citation_type == "file_citation"
assert out.start_index == 2
assert out.end_index == 4
def test_generate_streaming_function_content_with_function():
step_details = RunStepDeltaToolCallObject(
tool_calls=[
RunStepDeltaFunctionToolCall(
index=0, id="tool123", function=RunStepDeltaFunction(name="some_func", arguments={"arg": "val"})
)
]
)
out = generate_streaming_function_content("my_agent", step_details)
assert out is not None
assert len(out.items) == 1
assert isinstance(out.items[0], FunctionCallContent)
assert out.items[0].function_name == "some_func"
assert out.items[0].arguments == "{'arg': 'val'}"
def test_get_function_call_contents_no_action():
run = type("ThreadRunFake", (), {"required_action": None})()
fc = get_function_call_contents(run, {})
assert fc == []
def test_get_function_call_contents_submit_tool_outputs():
fake_function = MagicMock()
fake_function.name = "test_function"
fake_function.arguments = {"arg": "val"}
fake_tool_call = MagicMock(spec=RequiredFunctionToolCall)
fake_tool_call.id = "tool_id"
fake_tool_call.function = fake_function
run = MagicMock()
run.required_action.submit_tool_outputs.tool_calls = [fake_tool_call]
function_steps = {}
fc = get_function_call_contents(run, function_steps)
assert len(fc) == 1
assert fc[0].id == "tool_id"
assert fc[0].name == "test_function"
assert fc[0].arguments == {"arg": "val"}
def test_generate_function_call_content():
fcc = FunctionCallContent(id="id123", name="func_name", arguments={"x": 1})
msg = generate_function_call_content("my_agent", [fcc])
assert len(msg.items) == 1
assert msg.role == AuthorRole.ASSISTANT
def test_generate_function_result_content():
step = FunctionCallContent(id="123", name="func_name", arguments={"k": "v"})
tool_call = RunStepFunctionToolCall(
id="123",
function=RunStepFunctionToolCallDetails({
"name": "func_name",
"arguments": '{"k": "v"}',
"output": "result_data",
}),
)
msg = generate_function_result_content("my_agent", step, tool_call)
assert len(msg.items) == 1
assert msg.items[0].result == "result_data"
assert msg.role == AuthorRole.TOOL
def test_generate_code_interpreter_content():
msg = generate_code_interpreter_content("my_agent", "some_code()")
assert msg.content == "some_code()"
assert msg.metadata["code"] is True
def test_generate_streaming_code_interpreter_content_no_calls():
step_details = type("Details", (), {"tool_calls": None})
assert generate_streaming_code_interpreter_content("my_agent", step_details) is None
def test_generate_bing_grounding_content():
"""Test generate_bing_grounding_content with RunStepBingGroundingToolCall."""
bing_grounding_tool_call = RunStepBingGroundingToolCall(
id="call_gvgTmSL4hgdxWP4O7LLnwMlt",
bing_grounding={
"requesturl": "https://api.bing.microsoft.com/v7.0/search?q=search",
"response_metadata": "{'market': 'en-US', 'num_docs_retrieved': 5, 'num_docs_actually_used': 5}",
},
)
msg = generate_bing_grounding_content("my_agent", bing_grounding_tool_call)
assert len(msg.items) == 1
assert msg.role == AuthorRole.ASSISTANT
assert isinstance(msg.items[0], FunctionCallContent)
assert msg.items[0].id == "call_gvgTmSL4hgdxWP4O7LLnwMlt"
assert msg.items[0].name == "bing_grounding"
assert msg.items[0].function_name == "bing_grounding"
assert msg.items[0].arguments["requesturl"] == "https://api.bing.microsoft.com/v7.0/search?q=search"
assert msg.items[0].arguments["response_metadata"] == (
"{'market': 'en-US', 'num_docs_retrieved': 5, 'num_docs_actually_used': 5}"
)
def test_generate_bing_custom_search_content():
"""Test generate_bing_grounding_content with RunStepBingCustomSearchToolCall."""
bing_custom_search_tool_call = RunStepBingCustomSearchToolCall(
id="call_abc123def456ghi",
bing_custom_search={
"query": "semantic kernel python",
"custom_config_id": "config_123",
"search_results": "{'num_results': 10, 'top_result': 'Microsoft Semantic Kernel'}",
},
)
msg = generate_bing_grounding_content("my_agent", bing_custom_search_tool_call)
assert len(msg.items) == 1
assert msg.role == AuthorRole.ASSISTANT
assert isinstance(msg.items[0], FunctionCallContent)
assert msg.items[0].id == "call_abc123def456ghi"
assert msg.items[0].name == "bing_custom_search"
assert msg.items[0].function_name == "bing_custom_search"
assert msg.items[0].arguments["query"] == "semantic kernel python"
assert msg.items[0].arguments["custom_config_id"] == "config_123"
assert msg.items[0].arguments["search_results"] == (
"{'num_results': 10, 'top_result': 'Microsoft Semantic Kernel'}"
)
@@ -0,0 +1,662 @@
# Copyright (c) Microsoft. All rights reserved.
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from azure.ai.agents.models import (
MessageTextContent,
MessageTextDetails,
RequiredFunctionToolCall,
RequiredFunctionToolCallDetails,
RunStep,
RunStepCodeInterpreterToolCall,
RunStepCodeInterpreterToolCallDetails,
RunStepFunctionToolCall,
RunStepFunctionToolCallDetails,
RunStepMessageCreationDetails,
RunStepMessageCreationReference,
RunStepToolCallDetails,
SubmitToolOutputsAction,
SubmitToolOutputsDetails,
ThreadMessage,
ThreadRun,
)
from azure.ai.projects.aio import AIProjectClient
from pytest import fixture
from semantic_kernel.agents.azure_ai.agent_thread_actions import AgentThreadActions
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents import FunctionCallContent, FunctionResultContent, TextContent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_plugin import KernelPlugin
from semantic_kernel.kernel import Kernel
@fixture
def mock_client():
mock_thread = AsyncMock()
mock_thread.id = "thread123"
mock_threads = MagicMock()
mock_threads.create = AsyncMock(return_value=mock_thread)
mock_message = AsyncMock()
mock_message.id = "message456"
mock_messages = MagicMock()
mock_messages.create = AsyncMock(return_value="someMessage")
mock_agents = MagicMock()
mock_agents.threads = mock_threads
mock_agents.messages = mock_messages
mock_client = AsyncMock(spec=AIProjectClient)
mock_client.agents = mock_agents
return mock_client
async def test_agent_thread_actions_create_thread(mock_client):
thread_id = await AgentThreadActions.create_thread(mock_client)
assert thread_id == "thread123"
async def test_agent_thread_actions_create_message(mock_client):
msg = ChatMessageContent(role=AuthorRole.USER, content="some content")
out = await AgentThreadActions.create_message(mock_client, "threadXYZ", msg)
assert out == "someMessage"
async def test_agent_thread_actions_create_message_no_content():
class FakeAgentClient:
create_message = AsyncMock(return_value="should_not_be_called")
class FakeClient:
agents = FakeAgentClient()
message = ChatMessageContent(role=AuthorRole.USER, content=" ")
out = await AgentThreadActions.create_message(FakeClient(), "threadXYZ", message)
assert out is None
assert FakeAgentClient.create_message.await_count == 0
async def test_agent_thread_actions_invoke(ai_project_client: AIProjectClient, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
# Properly construct nested mocks without re-spec'ing from a mock
mock_thread_run = ThreadRun(
id="run123",
thread_id="thread123",
status="running",
instructions="test agent",
created_at=int(datetime.now(timezone.utc).timestamp()),
model="model",
)
agent.client.agents.runs = MagicMock()
agent.client.agents.runs.create = AsyncMock(return_value=mock_thread_run)
agent.client.agents.runs.get = AsyncMock(return_value=mock_thread_run)
async def mock_poll_run_status(*args, **kwargs):
yield RunStep(
type="message_creation",
id="msg123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
step_details=RunStepMessageCreationDetails(
message_creation=RunStepMessageCreationReference(
message_id="msg123",
),
),
)
agent.client.agents.run_steps = MagicMock()
agent.client.agents.run_steps.list = mock_poll_run_status
mock_message = ThreadMessage(
id="msg123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
role="assistant",
content=[MessageTextContent(text=MessageTextDetails(value="some message", annotations=[]))],
)
agent.client.agents.messages = MagicMock()
agent.client.agents.messages.get = AsyncMock(return_value=mock_message)
async for is_visible, message in AgentThreadActions.invoke(
agent=agent, thread_id="thread123", kernel=AsyncMock(spec=Kernel)
):
assert str(message.content) == "some message"
break
async def test_agent_thread_actions_invoke_with_requires_action(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
agent.client.agents = MagicMock()
mock_thread_run = ThreadRun(
id="run123",
thread_id="thread123",
status="running",
instructions="test agent",
created_at=int(datetime.now(timezone.utc).timestamp()),
model="model",
)
agent.client.agents = MagicMock()
agent.client.agents.runs = MagicMock()
agent.client.agents.runs.create = AsyncMock(return_value=mock_thread_run)
agent.client.agents.runs.get = AsyncMock(return_value=mock_thread_run)
agent.client.agents.runs.submit_tool_outputs = AsyncMock()
poll_count = 0
async def mock_poll_run_status(*args, **kwargs):
nonlocal poll_count
if poll_count == 0:
mock_thread_run.status = "requires_action"
mock_thread_run.required_action = SubmitToolOutputsAction(
submit_tool_outputs=SubmitToolOutputsDetails(
tool_calls=[
RequiredFunctionToolCall(
id="tool_call_id",
function=RequiredFunctionToolCallDetails(
name="mock_function_call", arguments={"arg": "value"}
),
)
]
)
)
else:
mock_thread_run.status = "completed"
poll_count += 1
return mock_thread_run
def mock_get_function_call_contents(run: ThreadRun, function_steps: dict):
function_call_content = FunctionCallContent(
name="mock_function_call",
arguments={"arg": "value"},
id="tool_call_id",
)
function_steps[function_call_content.id] = function_call_content
return [function_call_content]
mock_run_step_tool_calls = RunStep(
type="tool_calls",
id="tool_step123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
step_details=RunStepToolCallDetails(
tool_calls=[
# 1. This will yield FunctionResultContent
RunStepFunctionToolCall(
id="tool_call_id",
function=RunStepFunctionToolCallDetails({
"name": "mock_function_call",
"arguments": '{"arg": "value"}',
"output": "some output",
}),
),
# 2. This will yield TextContent
RunStepCodeInterpreterToolCall(
id="tool_call_id",
code_interpreter=RunStepCodeInterpreterToolCallDetails(
input="some code",
),
),
]
),
)
mock_run_step_message_creation = RunStep(
type="message_creation",
id="msg_step123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
step_details=RunStepMessageCreationDetails(
message_creation=RunStepMessageCreationReference(message_id="msg123")
),
)
mock_run_steps = [mock_run_step_tool_calls, mock_run_step_message_creation]
async def mock_list_run_steps(*args, **kwargs):
for step in mock_run_steps:
yield step
agent.client.agents.run_steps = MagicMock()
agent.client.agents.run_steps.list = mock_list_run_steps
mock_message = ThreadMessage(
id="msg123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
role="assistant",
content=[MessageTextContent(text=MessageTextDetails(value="some message", annotations=[]))],
)
agent.client.agents.runs.get = AsyncMock(return_value=mock_message)
agent.client.agents.runs.submit_tool_outputs = AsyncMock()
with (
patch.object(AgentThreadActions, "_poll_run_status", side_effect=mock_poll_run_status),
patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.get_function_call_contents",
side_effect=mock_get_function_call_contents,
),
patch.object(AgentThreadActions, "_invoke_function_calls", return_value=[None]),
):
messages = []
async for is_visible, content in AgentThreadActions.invoke(
agent=agent,
thread_id="thread123",
kernel=AsyncMock(spec=Kernel),
):
messages.append((is_visible, content))
assert len(messages) == 3, "There should be three yields in total."
assert isinstance(messages[0][1].items[0], FunctionCallContent)
assert isinstance(messages[1][1].items[0], FunctionResultContent)
assert isinstance(messages[2][1].items[0], TextContent)
agent.client.agents.runs.submit_tool_outputs.assert_awaited_once()
class MockEvent:
def __init__(self, event, data):
self.event = event
self.data = data
def __iter__(self):
return iter((self.event, self.data, None))
class MockRunData:
def __init__(self, id, status, content: str | None = None):
self.id = id
self.status = status
self.content = content
class MockAsyncIterable:
def __init__(self, items):
self.items = items.copy()
def __aiter__(self):
self._iter = iter(self.items)
return self
async def __anext__(self):
try:
return next(self._iter)
except StopIteration:
raise StopAsyncIteration
class MockStream:
def __init__(self, events):
self.events = events
async def __aenter__(self):
return MockAsyncIterable(self.events)
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
async def test_agent_thread_actions_invoke_stream(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
agent.client.agents = AsyncMock()
events = [
MockEvent("thread.run.created", MockRunData(id="run_1", status="queued")),
MockEvent("thread.message.created", MockRunData(id="msg_1", status="created", content="Hello")),
MockEvent("thread.run.in_progress", MockRunData(id="run_1", status="in_progress")),
MockEvent("thread.run.completed", MockRunData(id="run_1", status="completed")),
]
main_run_stream = MockStream(events)
agent.client.agents.create_stream.return_value = main_run_stream
with (
patch.object(AgentThreadActions, "_invoke_function_calls", return_value=None),
patch.object(AgentThreadActions, "_format_tool_outputs", return_value=[{"type": "mock_tool_output"}]),
):
collected_messages = []
async for content in AgentThreadActions.invoke_stream(
agent=agent,
thread_id="thread123",
kernel=AsyncMock(spec=Kernel),
):
collected_messages.append(content)
assert isinstance(content, ChatMessageContent)
assert content.metadata.get("message_id") == "msg_1"
# region Security tests for tools override and function_choice_behavior
async def test_validate_function_choice_behavior_rejects_required():
"""Required FCB is not supported for agent invocations."""
with pytest.raises(AgentInvokeException, match="not supported"):
AgentThreadActions._validate_function_choice_behavior(FunctionChoiceBehavior.Required())
async def test_validate_function_choice_behavior_accepts_auto():
"""Auto FCB should be accepted without error."""
AgentThreadActions._validate_function_choice_behavior(FunctionChoiceBehavior.Auto())
async def test_validate_function_choice_behavior_rejects_none_invoke():
"""NoneInvoke FCB is not supported for agent invocations."""
with pytest.raises(AgentInvokeException, match="not supported"):
AgentThreadActions._validate_function_choice_behavior(FunctionChoiceBehavior.NoneInvoke())
async def test_validate_function_choice_behavior_accepts_none():
"""None (no FCB) should be accepted."""
AgentThreadActions._validate_function_choice_behavior(None)
async def test_validate_function_choice_behavior_rejects_auto_invoke_false():
"""Auto with auto_invoke=False is not supported for agent invocations."""
with pytest.raises(AgentInvokeException, match="auto_invoke"):
AgentThreadActions._validate_function_choice_behavior(FunctionChoiceBehavior.Auto(auto_invoke=False))
async def test_validate_function_choice_behavior_rejects_empty_filters():
"""Empty filters dict should be rejected."""
fcb = FunctionChoiceBehavior.Auto()
fcb.filters = {}
with pytest.raises(AgentInvokeException, match="must not be empty"):
AgentThreadActions._validate_function_choice_behavior(fcb)
async def test_validate_function_choice_behavior_rejects_unknown_filter_keys():
"""Unknown filter keys should be rejected."""
fcb = FunctionChoiceBehavior.Auto()
# Bypass Pydantic validation to simulate a mistyped key reaching the validator
object.__setattr__(fcb, "filters", {"include_functions": ["foo"]})
with pytest.raises(AgentInvokeException, match="Unknown filter key"):
AgentThreadActions._validate_function_choice_behavior(fcb)
async def test_validate_function_choice_behavior_accepts_valid_filters():
"""Valid filter keys should be accepted."""
AgentThreadActions._validate_function_choice_behavior(
FunctionChoiceBehavior.Auto(filters={"included_functions": ["plugin-func"]})
)
async def test_get_tools_with_tools_override(ai_project_client, ai_agent_definition):
"""When tools_override is provided, it should replace agent.definition.tools."""
from azure.ai.agents.models import CodeInterpreterToolDefinition
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
kernel = MagicMock(spec=Kernel)
kernel.get_full_list_of_function_metadata.return_value = []
override_tool = CodeInterpreterToolDefinition()
tools = AgentThreadActions._get_tools(agent=agent, kernel=kernel, tools_override=[override_tool])
# Should contain the override tool, not agent.definition.tools
assert any(
(isinstance(t, CodeInterpreterToolDefinition) or (isinstance(t, dict) and t.get("type") == "code_interpreter"))
for t in tools
)
async def test_get_tools_with_fcb_filters(ai_project_client, ai_agent_definition):
"""When function_choice_behavior has filters, only matching functions should be included."""
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
kernel = MagicMock(spec=Kernel)
# Simulate filtered metadata
mock_metadata = MagicMock()
mock_metadata.fully_qualified_name = "Plugin-AllowedFunc"
mock_metadata.name = "AllowedFunc"
mock_metadata.plugin_name = "Plugin"
mock_metadata.description = "An allowed function"
mock_metadata.parameters = []
mock_metadata.is_prompt = False
mock_metadata.return_parameter = MagicMock()
mock_metadata.return_parameter.description = ""
mock_metadata.return_parameter.type_ = "str"
mock_metadata.additional_properties = {}
kernel.get_list_of_function_metadata.return_value = [mock_metadata]
kernel.get_full_list_of_function_metadata.return_value = []
fcb = FunctionChoiceBehavior.Auto(filters={"included_functions": ["Plugin-AllowedFunc"]})
AgentThreadActions._get_tools(agent=agent, kernel=kernel, function_choice_behavior=fcb)
# Should have called get_list_of_function_metadata with the filters
kernel.get_list_of_function_metadata.assert_called_once_with(fcb.filters)
async def test_get_tools_with_fcb_disable_kernel_functions(ai_project_client, ai_agent_definition):
"""When enable_kernel_functions=False, no kernel functions should be included."""
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
kernel = MagicMock(spec=Kernel)
fcb = FunctionChoiceBehavior.Auto(enable_kernel_functions=False)
AgentThreadActions._get_tools(agent=agent, kernel=kernel, function_choice_behavior=fcb)
# Full list is called for validation, but filtered list should not be called
kernel.get_full_list_of_function_metadata.assert_called_once()
kernel.get_list_of_function_metadata.assert_not_called()
async def test_invoke_function_calls_passes_function_behavior():
"""_invoke_function_calls should pass function_behavior to kernel.invoke_function_call."""
mock_kernel = AsyncMock(spec=Kernel)
mock_kernel.invoke_function_call.return_value = None
fcc = FunctionCallContent(name="Plugin-Func", arguments={}, id="call1")
from semantic_kernel.contents.chat_history import ChatHistory
chat_history = ChatHistory()
fcb = FunctionChoiceBehavior.Auto(filters={"included_functions": ["Plugin-Func"]})
await AgentThreadActions._invoke_function_calls(
kernel=mock_kernel,
fccs=[fcc],
chat_history=chat_history,
arguments=KernelArguments(),
function_choice_behavior=fcb,
)
mock_kernel.invoke_function_call.assert_awaited_once()
call_kwargs = mock_kernel.invoke_function_call.call_args
assert call_kwargs.kwargs.get("function_behavior") is fcb
async def test_invoke_function_calls_passes_disabled_kernel_functions():
"""_invoke_function_calls should pass enable_kernel_functions=False FCB to kernel."""
mock_kernel = AsyncMock(spec=Kernel)
mock_kernel.invoke_function_call.return_value = None
fcc = FunctionCallContent(name="Plugin-Func", arguments={}, id="call1")
from semantic_kernel.contents.chat_history import ChatHistory
chat_history = ChatHistory()
fcb = FunctionChoiceBehavior.Auto(enable_kernel_functions=False)
await AgentThreadActions._invoke_function_calls(
kernel=mock_kernel,
fccs=[fcc],
chat_history=chat_history,
arguments=KernelArguments(),
function_choice_behavior=fcb,
)
mock_kernel.invoke_function_call.assert_awaited_once()
call_kwargs = mock_kernel.invoke_function_call.call_args
passed_behavior = call_kwargs.kwargs.get("function_behavior")
assert passed_behavior is fcb
assert not passed_behavior.enable_kernel_functions
async def test_invoke_function_calls_blocks_disallowed_function():
"""A real Kernel should block a function call not in the FCB allowlist.
This verifies that the enforcement in kernel.invoke_function_call actually
rejects a disallowed function name when filters are provided, rather than
only asserting that the kwarg is forwarded.
"""
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
@kernel_function
def allowed_func() -> str:
return "allowed"
@kernel_function
def disallowed_func() -> str:
return "disallowed"
kernel = Kernel()
kernel.add_plugin(
KernelPlugin(
name="Plugin",
functions=[
KernelFunctionFromMethod(method=allowed_func, plugin_name="Plugin"),
KernelFunctionFromMethod(method=disallowed_func, plugin_name="Plugin"),
],
)
)
fcb = FunctionChoiceBehavior.Auto(filters={"included_functions": ["Plugin-allowed_func"]})
# Call a function NOT in the allowlist
fcc = FunctionCallContent(
name="Plugin-disallowed_func",
plugin_name="Plugin",
function_name="disallowed_func",
arguments={},
id="call1",
)
chat_history = ChatHistory()
result = await kernel.invoke_function_call(
function_call=fcc,
chat_history=chat_history,
function_behavior=fcb,
)
# invoke_function_call catches the FunctionExecutionException and returns None,
# adding an error message to chat_history instead of raising.
assert result is None
assert len(chat_history.messages) == 1
result_item = chat_history.messages[0].items[0]
assert "not part of the provided tools" in str(result_item.result)
async def test_invoke_function_calls_allows_permitted_function():
"""A real Kernel should allow a function call that IS in the FCB allowlist."""
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
@kernel_function
def allowed_func() -> str:
return "ok"
@kernel_function
def other_func() -> str:
return "other"
kernel = Kernel()
kernel.add_plugin(
KernelPlugin(
name="Plugin",
functions=[
KernelFunctionFromMethod(method=allowed_func, plugin_name="Plugin"),
KernelFunctionFromMethod(method=other_func, plugin_name="Plugin"),
],
)
)
fcb = FunctionChoiceBehavior.Auto(filters={"included_functions": ["Plugin-allowed_func"]})
fcc = FunctionCallContent(
name="Plugin-allowed_func",
plugin_name="Plugin",
function_name="allowed_func",
arguments={},
id="call1",
)
chat_history = ChatHistory()
await kernel.invoke_function_call(
function_call=fcc,
chat_history=chat_history,
function_behavior=fcb,
)
# Should succeed — the function result should be in chat_history
assert len(chat_history.messages) == 1
result_item = chat_history.messages[0].items[0]
assert "ok" in str(result_item.result)
async def test_invoke_raises_for_non_auto_fcb(ai_project_client, ai_agent_definition):
"""Calling AgentThreadActions.invoke() with a non-Auto FCB should raise before any API call."""
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
agent.client.agents = AsyncMock()
with pytest.raises(AgentInvokeException, match="not supported"):
async for _ in AgentThreadActions.invoke(
agent=agent,
thread_id="thread123",
kernel=Kernel(),
function_choice_behavior=FunctionChoiceBehavior.Required(),
):
pass
# No API calls should have been made
agent.client.agents.runs.create.assert_not_awaited()
async def test_invoke_stream_raises_for_non_auto_fcb(ai_project_client, ai_agent_definition):
"""Calling AgentThreadActions.invoke_stream() with a non-Auto FCB should raise before any API call."""
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
agent.client.agents = AsyncMock()
with pytest.raises(AgentInvokeException, match="not supported"):
async for _ in AgentThreadActions.invoke_stream(
agent=agent,
thread_id="thread123",
kernel=Kernel(),
function_choice_behavior=FunctionChoiceBehavior.NoneInvoke(),
):
pass
# No API calls should have been made
agent.client.agents.create_stream.assert_not_called()
# endregion
@@ -0,0 +1,478 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
import pytest
from azure.ai.projects.aio import AIProjectClient
from azure.core.credentials_async import AsyncTokenCredential
from semantic_kernel.agents.agent import AgentResponseItem
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent, AzureAIAgentThread
from semantic_kernel.agents.channels.agent_channel import AgentChannel
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentInitializationException, AgentInvokeException
async def test_azure_ai_agent_init(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
assert agent.id == "agent123"
assert agent.name == "agentName"
assert agent.description == "desc"
async def test_azure_ai_agent_init_with_plugins_via_constructor(
ai_project_client, ai_agent_definition, custom_plugin_class
):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition, plugins=[custom_plugin_class()])
assert agent.id == "agent123"
assert agent.name == "agentName"
assert agent.description == "desc"
assert agent.kernel.plugins is not None
assert len(agent.kernel.plugins) == 1
async def test_azure_ai_agent_get_response(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
response = await agent.get_response(messages="message", thread=thread)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content == "content"
assert response.thread is not None
async def test_azure_ai_agent_get_response_exception(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
async def fake_invoke(*args, **kwargs):
yield False, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with (
patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
),
pytest.raises(AgentInvokeException),
):
await agent.get_response(messages="message", thread=thread)
async def test_azure_ai_agent_invoke(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
results = []
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for item in agent.invoke(messages="message", thread=thread):
results.append(item)
assert len(results) == 1
async def test_azure_ai_agent_invoke_yields_visible_assistant_message(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
results = []
assistant_msg = ChatMessageContent(role=AuthorRole.ASSISTANT, content="assistant says hi")
async def fake_invoke(*args, **kwargs):
yield True, assistant_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for item in agent.invoke(messages="message", thread=thread):
results.append(item)
assert len(results) == 1
assert results[0].message is assistant_msg
async def test_azure_ai_agent_invoke_emits_tool_message_via_callback_only(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
callback_results = []
async def handle_callback(msg: ChatMessageContent) -> None:
callback_results.append(msg)
tool_msg = ChatMessageContent(role=AuthorRole.ASSISTANT, content="tool call")
tool_msg.items = [FunctionCallContent(name="tool", arguments="{}")]
async def fake_invoke(*args, **kwargs):
yield False, tool_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for _ in agent.invoke(messages="message", thread=thread, on_intermediate_message=handle_callback):
pass
assert callback_results == [tool_msg]
async def test_azure_ai_agent_invoke_suppresses_tool_message_without_callback(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
tool_msg = ChatMessageContent(role=AuthorRole.ASSISTANT, content="tool call")
tool_msg.items = [FunctionCallContent(name="tool", arguments="{}")]
async def fake_invoke(*args, **kwargs):
yield False, tool_msg # Not visible, no callback
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
results = [item async for item in agent.invoke(messages="message", thread=thread)]
assert results == [] # Tool message should be suppressed
async def test_azure_ai_agent_invoke_stream(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
results = []
async def fake_invoke(*args, **kwargs):
yield ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for item in agent.invoke_stream(messages="message", thread=thread):
results.append(item)
assert len(results) == 1
async def test_azure_ai_agent_invoke_stream_with_on_new_message_callback(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
thread.id = "test_thread_id"
results = []
final_chat_history = ChatHistory()
async def handle_stream_completion(message: ChatMessageContent) -> None:
final_chat_history.add_message(message)
# Fake collected messages
fake_message = StreamingChatMessageContent(role=AuthorRole.ASSISTANT, content="fake content", choice_index=0)
async def fake_invoke(*args, output_messages=None, **kwargs):
if output_messages is not None:
output_messages.append(fake_message)
yield fake_message
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for item in agent.invoke_stream(
messages="message", thread=thread, on_intermediate_message=handle_stream_completion
):
results.append(item)
assert len(results) == 1
assert results[0].message.content == "fake content"
assert len(final_chat_history.messages) == 1
assert final_chat_history.messages[0].content == "fake content"
async def test_azure_ai_agent_invoke_stream_tool_message_only_goes_to_callback(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
thread.id = "test_thread_id"
received_callback_messages = []
async def async_append(msg: ChatMessageContent):
received_callback_messages.append(msg)
tool_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT, content="tool call", items=[FunctionCallContent(name="ToolA", arguments="{}")]
)
streamed_msg = StreamingChatMessageContent(
role=AuthorRole.ASSISTANT, content="assistant streaming...", choice_index=0
)
async def fake_invoke_stream(*args, output_messages=None, **kwargs):
if output_messages is not None:
output_messages.append(tool_msg)
yield streamed_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke_stream,
):
results = []
async for item in agent.invoke_stream(messages="message", thread=thread, on_intermediate_message=async_append):
results.append(item)
assert results == [AgentResponseItem(message=streamed_msg, thread=thread)]
assert received_callback_messages == [tool_msg]
async def test_azure_ai_agent_invoke_stream_tool_message_suppressed_without_callback(
ai_project_client, ai_agent_definition
):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
thread.id = "test_thread_id"
tool_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT,
content="tool result",
items=[FunctionResultContent(id="test-id", name="ToolA", result="result")],
)
streamed_msg = StreamingChatMessageContent(role=AuthorRole.ASSISTANT, content="assistant says hi", choice_index=0)
async def fake_invoke_stream(*args, output_messages=None, **kwargs):
if output_messages is not None:
output_messages.append(tool_msg)
yield streamed_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke_stream,
):
results = []
async for item in agent.invoke_stream(messages="message", thread=thread):
results.append(item)
# Only assistant-visible content should be yielded
assert len(results) == 1
assert results[0].message == streamed_msg
async def test_azure_ai_agent_invoke_stream_mixed_messages(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
thread.id = "test_thread_id"
callback_results = []
async def async_append(msg: ChatMessageContent):
callback_results.append(msg)
tool_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT, content="tool call", items=[FunctionCallContent(name="tool", arguments="{}")]
)
text_msg = StreamingChatMessageContent(role=AuthorRole.ASSISTANT, content="streamed text", choice_index=0)
async def fake_invoke_stream(*args, output_messages: list = None, **kwargs):
if output_messages is not None:
output_messages.append(tool_msg)
yield text_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke_stream,
):
results = []
async for item in agent.invoke_stream(messages="message", thread=thread, on_intermediate_message=async_append):
results.append(item)
assert callback_results == [tool_msg]
assert results == [AgentResponseItem(message=text_msg, thread=thread)]
def test_azure_ai_agent_get_channel_keys(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
keys = list(agent.get_channel_keys())
assert len(keys) >= 2
async def test_azure_ai_agent_create_channel(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
with (
patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.create_thread",
side_effect="t",
),
patch(
"semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgentThread.create",
new_callable=AsyncMock,
),
patch(
"semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgentThread.id",
new_callable=PropertyMock,
) as mock_id,
):
mock_id.return_value = "mock-thread-id"
ch = await agent.create_channel()
assert isinstance(ch, AgentChannel)
assert ch.thread_id == "mock-thread-id"
def test_create_client_with_explicit_endpoint():
credential = MagicMock(spec=AsyncTokenCredential)
with patch("semantic_kernel.agents.azure_ai.azure_ai_agent.AIProjectClient") as mock_client_cls:
mock_client = MagicMock(spec=AIProjectClient)
mock_client_cls.return_value = mock_client
result = AzureAIAgent.create_client(
credential=credential,
endpoint="https://my-endpoint",
extra_arg="extra_value",
)
mock_client_cls.assert_called_once()
_, kwargs = mock_client_cls.call_args
assert kwargs["credential"] is credential
assert kwargs["endpoint"] == "https://my-endpoint"
assert kwargs["extra_arg"] == "extra_value"
assert result is mock_client
def test_create_client_uses_settings_when_endpoint_none():
credential = MagicMock(spec=AsyncTokenCredential)
with (
patch("semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgentSettings") as mock_settings_cls,
patch("semantic_kernel.agents.azure_ai.azure_ai_agent.AIProjectClient") as mock_client_cls,
):
mock_settings = MagicMock()
mock_settings.endpoint = "https://configured-endpoint"
mock_settings_cls.return_value = mock_settings
mock_client = MagicMock(spec=AIProjectClient)
mock_client_cls.return_value = mock_client
result = AzureAIAgent.create_client(credential=credential)
mock_client_cls.assert_called_once()
_, kwargs = mock_client_cls.call_args
assert kwargs["endpoint"] == "https://configured-endpoint"
assert result is mock_client
def test_create_client_raises_if_no_endpoint():
credential = MagicMock(spec=AsyncTokenCredential)
with patch("semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgentSettings") as mock_settings_cls:
mock_settings = MagicMock()
mock_settings.endpoint = None
mock_settings_cls.return_value = mock_settings
try:
AzureAIAgent.create_client(credential=credential)
except AgentInitializationException as e:
assert "Azure AI endpoint" in str(e)
else:
assert False, "Expected AgentInitializationException to be raised"
async def test_azure_ai_agent_get_response_passes_function_choice_behavior(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
await agent.get_response(messages="message", thread=thread, function_choice_behavior=fcb)
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_azure_ai_agent_invoke_passes_function_choice_behavior(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for _ in agent.invoke(messages="message", thread=thread, function_choice_behavior=fcb):
pass
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_azure_ai_agent_invoke_stream_passes_function_choice_behavior(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for _ in agent.invoke_stream(messages="message", thread=thread, function_choice_behavior=fcb):
pass
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_azure_ai_agent_get_response_no_fcb_passes_none(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
await agent.get_response(messages="message", thread=thread)
assert captured_kwargs.get("function_choice_behavior") is None
@@ -0,0 +1,34 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import Field, SecretStr, ValidationError
from semantic_kernel.kernel_pydantic import KernelBaseSettings
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class AzureAIAgentSettings(KernelBaseSettings):
"""Slightly modified to ensure invalid data raises ValidationError."""
env_prefix = "AZURE_AI_AGENT_"
model_deployment_name: str = Field(min_length=1)
project_connection_string: SecretStr = Field(..., min_length=1)
def test_azure_ai_agent_settings_valid():
settings = AzureAIAgentSettings(
model_deployment_name="test_model",
project_connection_string="secret_value",
)
assert settings.model_deployment_name == "test_model"
assert settings.project_connection_string.get_secret_value() == "secret_value"
def test_azure_ai_agent_settings_invalid():
with pytest.raises(ValidationError):
# Should fail due to min_length=1 constraints
AzureAIAgentSettings(
model_deployment_name="", # empty => invalid
project_connection_string="",
)
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft. All rights reserved.
from azure.ai.agents.models import MessageAttachment, MessageRole
from semantic_kernel.agents.azure_ai.azure_ai_agent_utils import AzureAIAgentUtils
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.file_reference_content import FileReferenceContent
from semantic_kernel.contents.utils.author_role import AuthorRole
def test_azure_ai_agent_utils_get_thread_messages_none():
msgs = AzureAIAgentUtils.get_thread_messages([])
assert msgs is None
def test_azure_ai_agent_utils_get_thread_messages():
msg1 = ChatMessageContent(role=AuthorRole.USER, content="Hello!")
msg1.items.append(FileReferenceContent(file_id="file123"))
results = AzureAIAgentUtils.get_thread_messages([msg1])
assert len(results) == 1
assert results[0].content == "Hello!"
assert results[0].role == MessageRole.USER
assert len(results[0].attachments) == 1
assert isinstance(results[0].attachments[0], MessageAttachment)
def test_azure_ai_agent_utils_get_attachments_empty():
msg1 = ChatMessageContent(role=AuthorRole.USER, content="No file items")
atts = AzureAIAgentUtils.get_attachments(msg1)
assert atts == []
def test_azure_ai_agent_utils_get_attachments_file():
msg1 = ChatMessageContent(role=AuthorRole.USER, content="One file item")
msg1.items.append(FileReferenceContent(file_id="file123"))
atts = AzureAIAgentUtils.get_attachments(msg1)
assert len(atts) == 1
assert atts[0].file_id == "file123"
def test_azure_ai_agent_utils_get_metadata():
msg1 = ChatMessageContent(role=AuthorRole.USER, content="has meta", metadata={"k": 123})
meta = AzureAIAgentUtils.get_metadata(msg1)
assert meta["k"] == "123"
def test_azure_ai_agent_utils_get_tool_definition():
gen = AzureAIAgentUtils._get_tool_definition(["file_search", "code_interpreter", "non_existent"])
# file_search & code_interpreter exist, non_existent yields nothing
tools_list = list(gen)
assert len(tools_list) == 2
@@ -0,0 +1,88 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from azure.ai.projects.aio import AIProjectClient
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent
from semantic_kernel.agents.azure_ai.azure_ai_channel import AzureAIChannel
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
async def test_azure_ai_channel_invoke_invalid_agent():
channel = AzureAIChannel(AsyncMock(spec=AIProjectClient), "thread123")
with pytest.raises(AgentChatException):
async for _ in channel.invoke(object()):
pass
async def test_azure_ai_channel_invoke_valid_agent(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
channel = AzureAIChannel(ai_project_client, "thread123")
results = []
async for is_visible, msg in channel.invoke(agent):
results.append((is_visible, msg))
assert len(results) == 1
async def test_azure_ai_channel_invoke_stream_valid_agent(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
channel = AzureAIChannel(ai_project_client, "thread123")
results = []
async for is_visible, msg in channel.invoke_stream(agent, messages=[]):
results.append((is_visible, msg))
assert len(results) == 1
async def test_azure_ai_channel_get_history():
# We need to return an async iterable, so let's do an AsyncMock returning an _async_gen
class FakeAgentClient:
delete_thread = AsyncMock()
# We'll patch get_messages directly below
class FakeClient:
agents = FakeAgentClient()
channel = AzureAIChannel(FakeClient(), "threadXYZ")
async def fake_get_messages(client, thread_id):
# Must produce an async iterable
yield ChatMessageContent(role=AuthorRole.ASSISTANT, content="Previous msg")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.get_messages",
new=fake_get_messages, # direct replacement with a coroutine
):
results = []
async for item in channel.get_history():
results.append(item)
assert len(results) == 1
assert results[0].content == "Previous msg"
# Helper for returning an async generator
async def _async_gen(items):
for i in items:
yield i