Files
microsoft--semantic-kernel/python/tests/unit/agents/test_agent.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

420 lines
14 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import sys
import uuid
from typing import ClassVar
from unittest.mock import AsyncMock
import pytest
from semantic_kernel.agents.agent import AGENT_TYPE_REGISTRY, AgentRegistry, DeclarativeSpecMixin, register_agent_type
from semantic_kernel.exceptions.agent_exceptions import AgentInitializationException
from semantic_kernel.kernel import Kernel
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from semantic_kernel.agents import Agent
from semantic_kernel.agents.channels.agent_channel import AgentChannel
from semantic_kernel.functions.kernel_arguments import KernelArguments
class MockChatHistory:
"""Minimal mock for ChatHistory to hold messages."""
def __init__(self, messages=None):
self.messages = messages if messages is not None else []
class MockChannel(AgentChannel):
"""Mock channel for testing get_channel_keys and create_channel."""
class MockAgent(Agent):
"""A mock agent for testing purposes."""
channel_type: ClassVar[type[AgentChannel]] = MockChannel
def __init__(self, name: str = "Test-Agent", description: str = "A test agent", id: str = None):
args = {
"name": name,
"description": description,
}
if id is not None:
args["id"] = id
super().__init__(**args)
async def create_channel(self) -> AgentChannel:
return AsyncMock(spec=AgentChannel)
@override
async def get_response(self, *args, **kwargs):
raise NotImplementedError
@override
async def invoke(self, *args, **kwargs):
raise NotImplementedError
@override
async def invoke_stream(self, *args, **kwargs):
raise NotImplementedError
class MockAgentWithoutChannelType(MockAgent):
channel_type = None
async def test_agent_initialization():
name = "TestAgent"
description = "A test agent"
id_value = str(uuid.uuid4())
agent = MockAgent(name=name, description=description, id=id_value)
assert agent.name == name
assert agent.description == description
assert agent.id == id_value
async def test_agent_default_id():
agent = MockAgent()
assert agent.id is not None
assert isinstance(uuid.UUID(agent.id), uuid.UUID)
def test_get_channel_keys():
agent = MockAgent()
keys = agent.get_channel_keys()
assert len(list(keys)) == 1, "Should return a single key"
async def test_create_channel():
agent = MockAgent()
channel = await agent.create_channel()
assert isinstance(channel, AgentChannel)
async def test_agent_equality():
id_value = str(uuid.uuid4())
agent1 = MockAgent(name="TestAgent", description="A test agent", id=id_value)
agent2 = MockAgent(name="TestAgent", description="A test agent", id=id_value)
assert agent1 == agent2
agent3 = MockAgent(name="TestAgent", description="A different description", id=id_value)
assert agent1 != agent3
agent4 = MockAgent(name="AnotherAgent", description="A test agent", id=id_value)
assert agent1 != agent4
async def test_agent_equality_different_type():
agent = MockAgent(name="TestAgent", description="A test agent", id=str(uuid.uuid4()))
non_agent = "Not an agent"
assert agent != non_agent
async def test_agent_hash():
id_value = str(uuid.uuid4())
agent1 = MockAgent(name="TestAgent", description="A test agent", id=id_value)
agent2 = MockAgent(name="TestAgent", description="A test agent", id=id_value)
assert hash(agent1) == hash(agent2)
agent3 = MockAgent(name="TestAgent", description="A different description", id=id_value)
assert hash(agent1) != hash(agent3)
def test_get_channel_keys_no_channel_type():
agent = MockAgentWithoutChannelType()
with pytest.raises(NotImplementedError):
list(agent.get_channel_keys())
def test_merge_arguments_both_none():
agent = MockAgent()
merged = agent._merge_arguments(None)
assert isinstance(merged, KernelArguments)
assert len(merged) == 0, "If both arguments are None, should return an empty KernelArguments object"
def test_merge_arguments_agent_none_override_not_none():
agent = MockAgent()
override = KernelArguments(settings={"key": "override"}, param1="val1")
merged = agent._merge_arguments(override)
assert merged is override, "If agent.arguments is None, just return override_args"
def test_merge_arguments_override_none_agent_not_none():
agent = MockAgent()
agent.arguments = KernelArguments(settings={"key": "base"}, param1="baseVal")
merged = agent._merge_arguments(None)
assert merged is agent.arguments, "If override_args is None, should return the agent's arguments"
def test_merge_arguments_both_not_none():
agent = MockAgent()
agent.arguments = KernelArguments(settings={"key1": "val1", "common": "base"}, param1="baseVal")
override = KernelArguments(settings={"key2": "override_val", "common": "override"}, param2="override_param")
merged = agent._merge_arguments(override)
assert merged.execution_settings["key1"] == "val1", "Should retain original setting from agent"
assert merged.execution_settings["key2"] == "override_val", "Should include new setting from override"
assert merged.execution_settings["common"] == "override", "Override should take precedence"
assert merged["param1"] == "baseVal", "Should retain base param from agent"
assert merged["param2"] == "override_param", "Should include param from override"
# region Declarative Spec tests
class DummyPlugin:
def __init__(self):
self.functions = {"dummy_function": lambda: "result"}
def get(self, name):
return self.functions.get(name)
class DummyAgentSettings:
azure_ai_search_connection_id = "test-conn-id"
azure_ai_search_index_name = "test-index"
class DummyKernel:
def __init__(self):
self.plugins = {}
def add_plugin(self, plugin):
name = plugin.__class__.__name__
self.plugins[name] = plugin
async def test_resolve_placeholders_with_short_and_long_keys():
class DummyDeclarativeSpec:
@classmethod
def resolve_placeholders(cls, yaml_str, settings=None, extras=None):
import re
pattern = re.compile(r"\$\{([^}]+)\}")
field_mapping = {
"AzureAISearchConnectionId": "conn-123",
"AzureAI:AzureAISearchConnectionId": "conn-123-override",
}
if extras:
field_mapping.update(extras)
def replacer(match):
full_key = match.group(1)
section, _, key = full_key.partition(":")
if section != "AzureAI":
return match.group(0)
return str(field_mapping.get(key) or field_mapping.get(full_key) or match.group(0))
return pattern.sub(replacer, yaml_str)
spec = "connection: ${AzureAI:AzureAISearchConnectionId}"
resolved = DummyDeclarativeSpec.resolve_placeholders(spec)
assert resolved == "connection: conn-123"
async def test_validate_tools_succeeds_with_valid_plugin():
class DummyDeclarativeSpec:
@classmethod
def _validate_tools(cls, tools_list, kernel):
for tool in tools_list:
tool_id = tool.get("id")
if not tool_id or tool.get("type") != "function":
continue
plugin_name, function_name = tool_id.split(".")
plugin = kernel.plugins.get(plugin_name)
if not plugin:
raise ValueError(f"Plugin {plugin_name} missing")
if function_name not in plugin.functions:
raise ValueError(f"Function {function_name} missing in plugin {plugin_name}")
kernel = DummyKernel()
plugin = DummyPlugin()
kernel.add_plugin(plugin)
DummyDeclarativeSpec._validate_tools([{"id": "DummyPlugin.dummy_function", "type": "function"}], kernel)
async def test_validate_tools_raises_on_missing_plugin():
class DummyDeclarativeSpec:
@classmethod
def _validate_tools(cls, tools_list, kernel):
for tool in tools_list:
tool_id = tool.get("id")
if not tool_id or tool.get("type") != "function":
continue
plugin_name, function_name = tool_id.split(".")
plugin = kernel.plugins.get(plugin_name)
if not plugin:
raise ValueError(f"Plugin {plugin_name} missing")
if function_name not in plugin.functions:
raise ValueError(f"Function {function_name} missing in plugin {plugin_name}")
kernel = DummyKernel()
with pytest.raises(ValueError, match="Plugin DummyPlugin missing"):
DummyDeclarativeSpec._validate_tools([{"id": "DummyPlugin.dummy_function", "type": "function"}], kernel)
def test_normalize_spec_fields_creates_kernel_and_extracts_fields():
data = {
"name": "TestAgent",
"description": "An agent",
"instructions": "Use this.",
"model": {"options": {"temperature": 0.7}},
}
fields, kernel = DeclarativeSpecMixin._normalize_spec_fields(data)
assert isinstance(kernel, Kernel)
assert fields["name"] == "TestAgent"
assert isinstance(fields["arguments"], KernelArguments)
def test_normalize_spec_fields_adds_plugins_to_kernel():
plugin = DummyPlugin()
data = {"name": "PluginAgent"}
_, kernel = DeclarativeSpecMixin._normalize_spec_fields(data, plugins=[plugin])
assert "DummyPlugin" in kernel.plugins
def test_normalize_spec_fields_parses_prompt_template_and_overwrites_instructions():
data = {"name": "T", "prompt_template": {"template": "new instructions", "template_format": "semantic-kernel"}}
fields, _ = DeclarativeSpecMixin._normalize_spec_fields(data)
assert fields["instructions"] == "new instructions"
def test_validate_tools_success(custom_plugin_class):
kernel = Kernel()
kernel.add_plugin(custom_plugin_class())
tools_list = [{"id": "CustomPlugin.getLightStatus", "type": "function"}]
DeclarativeSpecMixin._validate_tools(tools_list, kernel)
def test_validate_tools_fails_on_invalid_format():
kernel = Kernel()
with pytest.raises(AgentInitializationException, match="format"):
DeclarativeSpecMixin._validate_tools([{"id": "badformat", "type": "function"}], kernel)
def test_validate_tools_fails_on_missing_plugin():
kernel = Kernel()
with pytest.raises(AgentInitializationException, match="not found in kernel"):
DeclarativeSpecMixin._validate_tools([{"id": "MissingPlugin.foo", "type": "function"}], kernel)
def test_validate_tools_fails_on_missing_function():
plugin = DummyPlugin()
kernel = Kernel()
kernel.add_plugin(plugin)
with pytest.raises(AgentInitializationException, match="not found in plugin"):
DeclarativeSpecMixin._validate_tools([{"id": "DummyPlugin.bar", "type": "function"}], kernel)
@register_agent_type("test_agent")
class TestAgent(DeclarativeSpecMixin, Agent):
@classmethod
def resolve_placeholders(cls, yaml_str, settings=None, extras=None):
return yaml_str
@classmethod
async def _from_dict(cls, data, kernel, **kwargs):
return cls(
name=data.get("name"),
description=data.get("description"),
instructions=data.get("instructions"),
kernel=kernel,
)
async def get_response(self, messages, instructions_override=None):
return "Test response"
async def invoke(self, messages, **kwargs):
return "invoke result"
async def invoke_stream(self, messages, **kwargs):
yield "streamed result"
async def test_register_type_and_create_from_yaml_success():
yaml_str = """
type: test_agent
name: TestAgent
"""
agent = await AgentRegistry.create_from_yaml(yaml_str)
assert agent.__class__.__name__ == "TestAgent"
async def test_create_from_yaml_missing_type():
yaml_str = """
name: InvalidAgent
"""
with pytest.raises(AgentInitializationException, match="Missing 'type'"):
await AgentRegistry.create_from_yaml(yaml_str)
async def test_create_from_yaml_unregistered_type():
yaml_str = """
type: nonexistent_agent
"""
# Ensure unregistered
AGENT_TYPE_REGISTRY.pop("nonexistent_agent", None)
with pytest.raises(AgentInitializationException, match="not registered"):
await AgentRegistry.create_from_yaml(yaml_str)
async def test_create_from_dict_success(test_agent_cls):
data = {"type": "test_agent", "name": "FromDictAgent"}
agent: TestAgent = await AgentRegistry.create_from_dict(data)
assert agent.name == "FromDictAgent"
assert type(agent).__name__ == "TestAgent"
async def test_create_from_dict_missing_type():
data = {"name": "NoType"}
with pytest.raises(AgentInitializationException, match="Missing 'type'"):
await AgentRegistry.create_from_dict(data)
async def test_create_from_dict_type_not_supported():
AGENT_TYPE_REGISTRY.pop("unknown", None)
data = {"type": "unknown"}
with pytest.raises(AgentInitializationException, match="not supported"):
await AgentRegistry.create_from_dict(data)
async def test_create_from_file_reads_and_creates(tmp_path, test_agent_cls):
file_path = tmp_path / "spec.yaml"
file_path.write_text("type: test_agent\nname: FileAgent\n", encoding="utf-8")
agent: TestAgent = await AgentRegistry.create_from_file(str(file_path))
assert agent.name == "FileAgent"
assert type(agent).__name__ == "TestAgent"
async def test_create_from_file_raises_on_bad_path():
with pytest.raises(AgentInitializationException, match="Failed to read agent spec file"):
await AgentRegistry.create_from_file("/nonexistent/path/spec.yaml")
# endregion