1220 lines
46 KiB
Python
1220 lines
46 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import os
|
|
import tempfile
|
|
from collections.abc import Callable
|
|
from copy import deepcopy
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Union
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from semantic_kernel import Kernel
|
|
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
|
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
|
|
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
|
|
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
|
from semantic_kernel.const import METADATA_EXCEPTION_KEY
|
|
from semantic_kernel.contents import ChatMessageContent
|
|
from semantic_kernel.contents.chat_history import ChatHistory
|
|
from semantic_kernel.contents.function_call_content import FunctionCallContent
|
|
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
|
|
from semantic_kernel.exceptions import KernelFunctionAlreadyExistsError, KernelServiceNotFoundError
|
|
from semantic_kernel.exceptions.content_exceptions import FunctionCallInvalidArgumentsException
|
|
from semantic_kernel.exceptions.kernel_exceptions import (
|
|
KernelFunctionNotFoundError,
|
|
KernelInvokeException,
|
|
KernelPluginNotFoundError,
|
|
OperationCancelledException,
|
|
)
|
|
from semantic_kernel.exceptions.template_engine_exceptions import TemplateSyntaxError
|
|
from semantic_kernel.filters.filter_types import FilterTypes
|
|
from semantic_kernel.functions.function_result import FunctionResult
|
|
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
|
from semantic_kernel.functions.kernel_function import KernelFunction
|
|
from semantic_kernel.functions.kernel_function_decorator import kernel_function
|
|
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
|
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
|
from semantic_kernel.functions.kernel_plugin import KernelPlugin
|
|
from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate
|
|
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
|
|
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
|
|
from semantic_kernel.services.ai_service_selector import AIServiceSelector
|
|
|
|
|
|
# region Init
|
|
def test_init():
|
|
kernel = Kernel()
|
|
assert kernel is not None
|
|
assert kernel.ai_service_selector is not None
|
|
assert kernel.plugins is not None
|
|
assert kernel.services is not None
|
|
assert kernel.retry_mechanism is not None
|
|
assert kernel.function_invocation_filters is not None
|
|
assert kernel.prompt_rendering_filters is not None
|
|
|
|
|
|
def test_kernel_init_with_ai_service_selector():
|
|
ai_service_selector = AIServiceSelector()
|
|
kernel = Kernel(ai_service_selector=ai_service_selector)
|
|
assert kernel.ai_service_selector is not None
|
|
|
|
|
|
def test_kernel_init_with_services(service: AIServiceClientBase):
|
|
kernel = Kernel(services=service)
|
|
assert kernel.services is not None
|
|
assert kernel.services["service"] is not None
|
|
|
|
|
|
def test_kernel_init_with_services_dict(service: AIServiceClientBase):
|
|
kernel = Kernel(services={"service": service})
|
|
assert kernel.services is not None
|
|
assert kernel.services["service"] is not None
|
|
|
|
|
|
def test_kernel_init_with_services_list(service: AIServiceClientBase):
|
|
kernel = Kernel(services=[service])
|
|
assert kernel.services is not None
|
|
assert kernel.services["service"] is not None
|
|
|
|
|
|
def test_kernel_init_with_plugins():
|
|
plugins = {"plugin": KernelPlugin(name="plugin")}
|
|
kernel = Kernel(plugins=plugins)
|
|
assert kernel.plugins is not None
|
|
|
|
|
|
def test_kernel_init_with_kernel_plugin_instance():
|
|
plugin = KernelPlugin(name="plugin")
|
|
kernel = Kernel(plugins=plugin)
|
|
assert kernel.plugins is not None
|
|
|
|
|
|
def test_kernel_init_with_kernel_plugin_list():
|
|
plugin = [KernelPlugin(name="plugin")]
|
|
kernel = Kernel(plugins=plugin)
|
|
assert kernel.plugins is not None
|
|
|
|
|
|
# endregion
|
|
# region Invoke Functions
|
|
|
|
|
|
async def test_invoke_function(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
|
|
await kernel.invoke(mock_function, KernelArguments())
|
|
|
|
assert mock_function.call_count == 1
|
|
|
|
|
|
async def test_invoke_function_with_cancellation(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
|
|
with (
|
|
patch.object(Kernel, "invoke", AsyncMock(side_effect=OperationCancelledException("Operation cancelled"))),
|
|
pytest.raises(OperationCancelledException),
|
|
):
|
|
result = await kernel.invoke(function=mock_function, arguments=KernelArguments())
|
|
|
|
assert result is None
|
|
|
|
Kernel.invoke.assert_called_once_with(function=mock_function, arguments=KernelArguments())
|
|
|
|
|
|
async def test_invoke_functions_by_name(kernel: Kernel, create_mock_function):
|
|
mock_function = kernel.add_function(plugin_name="test", function=create_mock_function(name="test_function"))
|
|
|
|
result = await kernel.invoke(function_name="test_function", plugin_name="test", arguments=KernelArguments())
|
|
assert str(result) == "test"
|
|
|
|
assert mock_function.call_count == 1
|
|
|
|
async for response in kernel.invoke_stream(function_name="test_function", plugin_name="test"):
|
|
assert response[0].text == "test"
|
|
|
|
|
|
async def test_invoke_functions_by_name_return_function_results(kernel: Kernel, create_mock_function):
|
|
mock_function = kernel.add_function(plugin_name="test", function=create_mock_function(name="test_function"))
|
|
|
|
result = await kernel.invoke(function_name="test_function", plugin_name="test", arguments=KernelArguments())
|
|
assert str(result) == "test"
|
|
|
|
assert mock_function.call_count == 1
|
|
|
|
async for _ in kernel.invoke_stream(
|
|
function_name="test_function", plugin_name="test", return_function_results=True
|
|
):
|
|
assert isinstance(result, FunctionResult)
|
|
|
|
|
|
async def test_invoke_function_fail(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
kernel.add_plugin(KernelPlugin(name="test", functions=[mock_function]))
|
|
|
|
with pytest.raises(KernelFunctionNotFoundError):
|
|
await kernel.invoke(arguments=KernelArguments())
|
|
|
|
with pytest.raises(KernelFunctionNotFoundError):
|
|
async for _ in kernel.invoke_stream(arguments=KernelArguments()):
|
|
pass
|
|
|
|
|
|
async def test_invoke_function_cancelled(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
mock_function._invoke_internal = AsyncMock(side_effect=OperationCancelledException("Operation cancelled"))
|
|
kernel.add_plugin(KernelPlugin(name="test", functions=[mock_function]))
|
|
|
|
result = await kernel.invoke(mock_function, arguments=KernelArguments())
|
|
assert result is None
|
|
|
|
|
|
async def test_invoke_stream_function(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
mock_function = kernel.add_function(plugin_name="test", function=mock_function)
|
|
|
|
async for part in kernel.invoke_stream(mock_function, input="test"):
|
|
assert part[0].text == "test"
|
|
|
|
assert mock_function.call_count == 1
|
|
|
|
|
|
async def test_invoke_stream_functions_throws_exception(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
kernel.add_plugin(KernelPlugin(name="test", functions=[mock_function]))
|
|
functions = [mock_function]
|
|
|
|
function_result_with_exception = FunctionResult(
|
|
value="", function=mock_function.metadata, output=None, metadata={METADATA_EXCEPTION_KEY: "Test Exception"}
|
|
)
|
|
|
|
with patch("semantic_kernel.kernel.Kernel.invoke_stream", return_value=AsyncMock()) as mocked_invoke_stream:
|
|
mocked_invoke_stream.return_value.__aiter__.return_value = [function_result_with_exception]
|
|
|
|
async for part in kernel.invoke_stream(functions, input="test"):
|
|
assert METADATA_EXCEPTION_KEY in part.metadata, "Expected exception metadata in the FunctionResult."
|
|
assert part.metadata[METADATA_EXCEPTION_KEY] == "Test Exception", "The exception message does not match."
|
|
break
|
|
|
|
|
|
async def test_invoke_prompt(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
with patch(
|
|
"semantic_kernel.functions.kernel_function_from_prompt.KernelFunctionFromPrompt._invoke_internal",
|
|
return_value=FunctionResult(function=mock_function.metadata, value="test"),
|
|
) as mock_invoke:
|
|
await kernel.invoke_prompt(prompt="test", plugin_name="test", function_name="test", arguments=KernelArguments())
|
|
mock_invoke.invoke.call_count == 1
|
|
|
|
|
|
async def test_invoke_prompt_no_prompt_error(kernel: Kernel):
|
|
with pytest.raises(TemplateSyntaxError):
|
|
await kernel.invoke_prompt(
|
|
function_name="test_function",
|
|
plugin_name="test_plugin",
|
|
prompt="",
|
|
)
|
|
|
|
|
|
async def test_invoke_prompt_stream_no_prompt_throws(kernel: Kernel):
|
|
with pytest.raises(TemplateSyntaxError):
|
|
async for _ in kernel.invoke_prompt_stream(prompt=""):
|
|
pass
|
|
|
|
|
|
async def test_invoke_prompt_stream(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
with (
|
|
patch(
|
|
"semantic_kernel.kernel.Kernel.invoke_stream",
|
|
) as mock_stream_invoke,
|
|
):
|
|
mock_stream_invoke.return_value.__aiter__.return_value = [
|
|
FunctionResult(function=mock_function.metadata, value="test")
|
|
]
|
|
async for response in kernel.invoke_prompt_stream(prompt="test"):
|
|
assert response.value == "test"
|
|
|
|
|
|
async def test_invoke_prompt_stream_returns_function_results(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
with (
|
|
patch(
|
|
"semantic_kernel.kernel.Kernel.invoke_stream",
|
|
) as mock_stream_invoke,
|
|
):
|
|
mock_stream_invoke.return_value.__aiter__.return_value = [
|
|
FunctionResult(function=mock_function.metadata, value="test")
|
|
]
|
|
async for response in kernel.invoke_prompt_stream(prompt="test", return_function_results=True):
|
|
assert isinstance(response, FunctionResult)
|
|
|
|
|
|
async def test_invoke_prompt_stream_raises_exception(kernel: Kernel, create_mock_function):
|
|
mock_function = create_mock_function(name="test_function")
|
|
with (
|
|
patch(
|
|
"semantic_kernel.kernel.Kernel.invoke_stream",
|
|
) as mock_stream_invoke,
|
|
pytest.raises(KernelInvokeException),
|
|
):
|
|
mock_stream_invoke.return_value.__aiter__.return_value = [
|
|
FunctionResult(
|
|
function=mock_function.metadata, value="", metadata={METADATA_EXCEPTION_KEY: KernelInvokeException()}
|
|
)
|
|
]
|
|
async for _ in kernel.invoke_prompt_stream(prompt="test"):
|
|
pass
|
|
|
|
|
|
async def test_invoke_function_call(kernel: Kernel, get_tool_call_mock):
|
|
tool_call_mock = get_tool_call_mock
|
|
result_mock = MagicMock(spec=ChatMessageContent)
|
|
result_mock.items = [tool_call_mock]
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
|
|
func_mock = AsyncMock(spec=KernelFunction)
|
|
func_meta = KernelFunctionMetadata(name="function", is_prompt=False)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_result = FunctionResult(value="Function result", function=func_meta)
|
|
func_mock.invoke = MagicMock(return_value=func_result)
|
|
|
|
arguments = KernelArguments()
|
|
|
|
with (
|
|
patch("semantic_kernel.kernel.logger", autospec=True),
|
|
patch("semantic_kernel.kernel.Kernel.get_list_of_function_metadata", return_value=[func_meta]),
|
|
):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
arguments=arguments,
|
|
function_call_count=1,
|
|
request_index=0,
|
|
function_behavior=FunctionChoiceBehavior.Auto(filters={"included_functions": ["function"]}),
|
|
)
|
|
|
|
|
|
class LightsPlugin:
|
|
lights = [
|
|
{"id": 1, "name": "Table Lamp", "is_on": False},
|
|
{"id": 2, "name": "Porch light", "is_on": False},
|
|
{"id": 3, "name": "Chandelier", "is_on": False},
|
|
]
|
|
|
|
@kernel_function(
|
|
name="get_lights_states",
|
|
description="Gets a list of lights and their current state",
|
|
)
|
|
def get_lights_states(self) -> list[dict]:
|
|
"""Return a list of lights and their states (read-only)."""
|
|
return self.lights # returns by reference, but deep-copy fix will snapshot it
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kernel_invoke_deep_copy_preserves_previous_state():
|
|
kernel = Kernel()
|
|
plugin = LightsPlugin()
|
|
kernel.add_plugin(plugin, plugin_name="lights")
|
|
|
|
func_call = FunctionCallContent(
|
|
plugin_name="lights", function_name="get_lights_states", name="get_lights_states", arguments={}
|
|
)
|
|
|
|
chat_history = ChatHistory()
|
|
|
|
# 1st invocation: capture snapshot of the list
|
|
_ = await kernel.invoke_function_call(
|
|
function_call=func_call,
|
|
chat_history=chat_history,
|
|
arguments=None,
|
|
function_call_count=1,
|
|
request_index=0,
|
|
function_behavior=FunctionChoiceBehavior.Auto(),
|
|
)
|
|
snapshot1 = chat_history.messages[-1].items[0].result
|
|
|
|
# Mutate the plugin's internal state after the first call
|
|
plugin.lights.append({"id": 4, "name": "Desk lamp", "is_on": True})
|
|
|
|
# The snapshot from the first invocation did not change
|
|
assert snapshot1 == [
|
|
{"id": 1, "name": "Table Lamp", "is_on": False},
|
|
{"id": 2, "name": "Porch light", "is_on": False},
|
|
{"id": 3, "name": "Chandelier", "is_on": False},
|
|
]
|
|
|
|
# 2nd invocation: should pick up the new item
|
|
_ = await kernel.invoke_function_call(
|
|
function_call=func_call,
|
|
chat_history=chat_history,
|
|
arguments=None,
|
|
function_call_count=1,
|
|
request_index=1,
|
|
function_behavior=FunctionChoiceBehavior.Auto(),
|
|
)
|
|
snapshot2 = chat_history.messages[-1].items[0].result
|
|
|
|
# The new invoke reflects the mutated state
|
|
assert snapshot2[-1] == {"id": 4, "name": "Desk lamp", "is_on": True}
|
|
|
|
|
|
# region Clone safety with unpickleable async context
|
|
|
|
|
|
class _AsyncGenPlugin:
|
|
"""Plugin holding an async generator to emulate MCP-style async internals.
|
|
|
|
Deep-copying objects that reference async generators typically fails with
|
|
`TypeError: cannot pickle 'async_generator' object`.
|
|
"""
|
|
|
|
def __init__(self):
|
|
async def _agen():
|
|
yield "tick"
|
|
|
|
# Store an async generator object on the instance to make it unpickleable.
|
|
self._unpickleable_async_gen = _agen()
|
|
|
|
@kernel_function(name="do", description="Return OK to validate plugin wiring")
|
|
async def do(self) -> str:
|
|
return "ok"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kernel_clone_with_unpickleable_plugin_does_not_raise():
|
|
kernel = Kernel()
|
|
plugin_instance = _AsyncGenPlugin()
|
|
kernel.add_plugin(plugin_instance)
|
|
|
|
# Sanity: naive deepcopy of plugins should raise due to async generator state
|
|
with pytest.raises(TypeError):
|
|
deepcopy(kernel.plugins)
|
|
|
|
# Clone should succeed and preserve function usability
|
|
cloned = kernel.clone()
|
|
|
|
func = cloned.get_function(plugin_instance.__class__.__name__, "do")
|
|
result = await func.invoke(cloned)
|
|
assert result is not None
|
|
assert result.value == "ok"
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
async def test_invoke_function_call_throws_during_invoke(kernel: Kernel, get_tool_call_mock):
|
|
tool_call_mock = get_tool_call_mock
|
|
result_mock = MagicMock(spec=ChatMessageContent)
|
|
result_mock.items = [tool_call_mock]
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
chat_history_mock.messages = [MagicMock(spec=StreamingChatMessageContent)]
|
|
|
|
func_mock = AsyncMock(spec=KernelFunction)
|
|
func_meta = KernelFunctionMetadata(name="function", is_prompt=False)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_result = FunctionResult(value="Function result", function=func_meta)
|
|
func_mock.invoke = MagicMock(return_value=func_result)
|
|
|
|
arguments = KernelArguments()
|
|
|
|
with (
|
|
patch("semantic_kernel.kernel.logger", autospec=True),
|
|
patch("semantic_kernel.kernel.Kernel.get_list_of_function_metadata", return_value=[func_meta]),
|
|
patch("semantic_kernel.kernel.Kernel.get_function", return_value=func_mock),
|
|
):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
arguments=arguments,
|
|
function_call_count=1,
|
|
request_index=0,
|
|
function_behavior=FunctionChoiceBehavior.Auto(),
|
|
)
|
|
|
|
|
|
async def test_invoke_function_call_non_allowed_func_throws(kernel: Kernel, get_tool_call_mock):
|
|
tool_call_mock = get_tool_call_mock
|
|
result_mock = MagicMock(spec=ChatMessageContent)
|
|
result_mock.items = [tool_call_mock]
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
|
|
func_mock = AsyncMock(spec=KernelFunction)
|
|
func_meta = KernelFunctionMetadata(name="function", is_prompt=False)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_result = FunctionResult(value="Function result", function=func_meta)
|
|
func_mock.invoke = MagicMock(return_value=func_result)
|
|
|
|
arguments = KernelArguments()
|
|
|
|
with patch("semantic_kernel.kernel.logger", autospec=True):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
arguments=arguments,
|
|
function_call_count=1,
|
|
request_index=0,
|
|
function_behavior=FunctionChoiceBehavior.Auto(filters={"included_functions": ["unknown"]}),
|
|
)
|
|
|
|
|
|
async def test_invoke_function_call_no_name_throws(kernel: Kernel, get_tool_call_mock):
|
|
tool_call_mock = get_tool_call_mock
|
|
tool_call_mock.name = None
|
|
result_mock = MagicMock(spec=ChatMessageContent)
|
|
result_mock.items = [tool_call_mock]
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
|
|
func_mock = AsyncMock(spec=KernelFunction)
|
|
func_meta = KernelFunctionMetadata(name="function", is_prompt=False)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_result = FunctionResult(value="Function result", function=func_meta)
|
|
func_mock.invoke = MagicMock(return_value=func_result)
|
|
|
|
arguments = KernelArguments()
|
|
|
|
with (
|
|
patch("semantic_kernel.kernel.logger", autospec=True),
|
|
):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
arguments=arguments,
|
|
function_call_count=1,
|
|
request_index=0,
|
|
function_behavior=FunctionChoiceBehavior.Auto(),
|
|
)
|
|
|
|
|
|
async def test_invoke_function_call_not_enough_parsed_args(kernel: Kernel, get_tool_call_mock):
|
|
tool_call_mock = get_tool_call_mock
|
|
tool_call_mock.to_kernel_arguments.return_value = {}
|
|
result_mock = MagicMock(spec=ChatMessageContent)
|
|
result_mock.items = [tool_call_mock]
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
|
|
func_mock = AsyncMock(spec=KernelFunction)
|
|
func_meta = KernelFunctionMetadata(name="function", is_prompt=False)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_mock.parameters = [KernelParameterMetadata(name="param1", is_required=True)]
|
|
func_result = FunctionResult(value="Function result", function=func_meta)
|
|
func_mock.invoke = MagicMock(return_value=func_result)
|
|
|
|
arguments = KernelArguments()
|
|
|
|
with (
|
|
patch("semantic_kernel.kernel.logger", autospec=True),
|
|
patch("semantic_kernel.kernel.Kernel.get_function", return_value=func_mock),
|
|
):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
arguments=arguments,
|
|
function_call_count=1,
|
|
request_index=0,
|
|
function_behavior=FunctionChoiceBehavior.Auto(),
|
|
)
|
|
|
|
|
|
async def test_invoke_function_call_with_continuation_on_malformed_arguments(kernel: Kernel, get_tool_call_mock):
|
|
tool_call_mock = MagicMock(spec=FunctionCallContent)
|
|
tool_call_mock.to_kernel_arguments.side_effect = FunctionCallInvalidArgumentsException("Malformed arguments")
|
|
tool_call_mock.name = "test-function"
|
|
tool_call_mock.function_name = "function"
|
|
tool_call_mock.plugin_name = "test"
|
|
tool_call_mock.arguments = {"arg_name": "arg_value"}
|
|
tool_call_mock.ai_model_id = None
|
|
tool_call_mock.metadata = {}
|
|
tool_call_mock.index = 0
|
|
tool_call_mock.to_kernel_arguments.return_value = {"arg_name": "arg_value"}
|
|
tool_call_mock.id = "test_id"
|
|
result_mock = MagicMock(spec=ChatMessageContent)
|
|
result_mock.items = [tool_call_mock]
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
|
|
func_mock = MagicMock(spec=KernelFunction)
|
|
func_meta = KernelFunctionMetadata(name="function", is_prompt=False)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_result = FunctionResult(value="Function result", function=func_meta)
|
|
func_mock.invoke = AsyncMock(return_value=func_result)
|
|
arguments = KernelArguments()
|
|
|
|
with (
|
|
patch("semantic_kernel.kernel.logger", autospec=True) as logger_mock,
|
|
patch("semantic_kernel.kernel.Kernel.get_function", return_value=func_mock),
|
|
):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
arguments=arguments,
|
|
function_call_count=1,
|
|
request_index=0,
|
|
function_behavior=FunctionChoiceBehavior.Auto(),
|
|
)
|
|
|
|
logger_mock.info.assert_any_call(
|
|
"Received invalid arguments for function test-function: Malformed arguments. Trying tool call again."
|
|
)
|
|
|
|
add_message_calls = chat_history_mock.add_message.call_args_list
|
|
assert any(
|
|
call[1]["message"].items[0].result
|
|
== "The tool call arguments are malformed. Arguments must be in JSON format. Please try again." # noqa: E501
|
|
and call[1]["message"].items[0].id == "test_id"
|
|
and call[1]["message"].items[0].name == "test-function"
|
|
for call in add_message_calls
|
|
), "Expected call to add_message not found with the expected message content and metadata."
|
|
|
|
|
|
async def test_invoke_function_call_with_missing_or_unexpected_args(kernel: Kernel):
|
|
tool_call_mock = MagicMock(spec=FunctionCallContent)
|
|
tool_call_mock.to_kernel_arguments.return_value = {"unexpected_arg": "value"}
|
|
tool_call_mock.name = "test-function"
|
|
tool_call_mock.function_name = "function"
|
|
tool_call_mock.plugin_name = "test"
|
|
tool_call_mock.arguments = {"unexpected_arg": "value"}
|
|
tool_call_mock.ai_model_id = None
|
|
tool_call_mock.metadata = {}
|
|
tool_call_mock.index = 0
|
|
tool_call_mock.id = "test_id"
|
|
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
|
|
# One required parameter called "required_arg"
|
|
required_param = KernelParameterMetadata(name="required_arg", is_required=True)
|
|
func_meta = KernelFunctionMetadata(name="function", is_prompt=False, parameters=[required_param])
|
|
func_mock = MagicMock(spec=KernelFunction)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_mock.parameters = [required_param]
|
|
|
|
with (
|
|
patch("semantic_kernel.kernel.logger", autospec=True) as logger_mock,
|
|
patch("semantic_kernel.kernel.Kernel.get_function", return_value=func_mock),
|
|
):
|
|
result = await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
arguments=KernelArguments(),
|
|
function_call_count=1,
|
|
request_index=0,
|
|
function_behavior=FunctionChoiceBehavior.Auto(),
|
|
)
|
|
|
|
assert result is None
|
|
|
|
logger_msg = (
|
|
"Missing required argument(s): ['required_arg']. "
|
|
"Received unexpected argument(s): ['unexpected_arg']. "
|
|
"Please revise the arguments to match the function signature."
|
|
)
|
|
logger_mock.info.assert_called_with(logger_msg)
|
|
|
|
add_message_calls = chat_history_mock.add_message.call_args_list
|
|
assert any(
|
|
call[1]["message"].items[0].result.startswith("Missing required argument")
|
|
and call[1]["message"].items[0].id == "test_id"
|
|
for call in add_message_calls
|
|
), "Expected fallback message not found in chat history."
|
|
|
|
|
|
async def test_invoke_function_call_with_filters_blocks_unallowed_function(kernel: Kernel):
|
|
"""Verify that when function_behavior has filters, an unallowed function is blocked."""
|
|
tool_call_mock = MagicMock(spec=FunctionCallContent)
|
|
tool_call_mock.name = "HttpPlugin-GetAsync"
|
|
tool_call_mock.function_name = "GetAsync"
|
|
tool_call_mock.plugin_name = "HttpPlugin"
|
|
tool_call_mock.arguments = {"url": "http://169.254.169.254/"}
|
|
tool_call_mock.ai_model_id = None
|
|
tool_call_mock.metadata = {}
|
|
tool_call_mock.index = 0
|
|
tool_call_mock.id = "test_id"
|
|
|
|
chat_history = ChatHistory()
|
|
|
|
safe_func_meta = KernelFunctionMetadata(name="safe_function", is_prompt=False, plugin_name="SafePlugin")
|
|
function_behavior = FunctionChoiceBehavior.Auto(filters={"included_plugins": ["SafePlugin"]})
|
|
|
|
with patch("semantic_kernel.kernel.Kernel.get_list_of_function_metadata", return_value=[safe_func_meta]):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history,
|
|
function_behavior=function_behavior,
|
|
)
|
|
|
|
# The function should have been blocked — an error message should be in chat history
|
|
assert len(chat_history.messages) == 1
|
|
assert "not allowed" in str(chat_history.messages[0].items[0].result) or "not part of the provided" in str(
|
|
chat_history.messages[0].items[0].result
|
|
)
|
|
|
|
|
|
async def test_invoke_function_call_with_filters_allows_matching_function(kernel: Kernel, get_tool_call_mock):
|
|
"""Verify that when function_behavior has filters, an allowed function proceeds (not blocked)."""
|
|
tool_call_mock = get_tool_call_mock
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
|
|
func_meta = KernelFunctionMetadata(
|
|
name="function", is_prompt=False, plugin_name="test", fully_qualified_name="test-function"
|
|
)
|
|
|
|
func_mock = AsyncMock(spec=KernelFunction)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_mock.parameters = []
|
|
func_result = FunctionResult(value="ok", function=func_meta)
|
|
func_mock.invoke = AsyncMock(return_value=func_result)
|
|
|
|
function_behavior = FunctionChoiceBehavior.Auto(filters={"included_plugins": ["test"]})
|
|
|
|
with (
|
|
patch("semantic_kernel.kernel.logger", autospec=True) as logger_mock,
|
|
patch("semantic_kernel.kernel.Kernel.get_list_of_function_metadata", return_value=[func_meta]),
|
|
patch("semantic_kernel.kernel.Kernel.get_function", return_value=func_mock),
|
|
):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
function_behavior=function_behavior,
|
|
)
|
|
|
|
# The debug message for missing function_behavior should NOT have been logged
|
|
debug_calls = [call[0][0] for call in logger_mock.debug.call_args_list] if logger_mock.debug.called else []
|
|
assert not any("without function_behavior" in msg for msg in debug_calls)
|
|
# The exception logger should NOT have been called (function was allowed)
|
|
logger_mock.exception.assert_not_called()
|
|
|
|
|
|
async def test_invoke_function_call_without_function_behavior_logs_debug(kernel: Kernel, get_tool_call_mock):
|
|
"""Verify that calling invoke_function_call without function_behavior logs a debug message."""
|
|
tool_call_mock = get_tool_call_mock
|
|
chat_history_mock = MagicMock(spec=ChatHistory)
|
|
|
|
func_mock = AsyncMock(spec=KernelFunction)
|
|
func_meta = KernelFunctionMetadata(name="function", is_prompt=False)
|
|
func_mock.metadata = func_meta
|
|
func_mock.name = "function"
|
|
func_mock.parameters = []
|
|
func_result = FunctionResult(value="Function result", function=func_meta)
|
|
func_mock.invoke = AsyncMock(return_value=func_result)
|
|
|
|
with (
|
|
patch("semantic_kernel.kernel.logger", autospec=True) as logger_mock,
|
|
patch("semantic_kernel.kernel.Kernel.get_function", return_value=func_mock),
|
|
):
|
|
await kernel.invoke_function_call(
|
|
function_call=tool_call_mock,
|
|
chat_history=chat_history_mock,
|
|
# function_behavior intentionally omitted
|
|
)
|
|
|
|
logger_mock.debug.assert_called()
|
|
debug_calls = [call[0][0] for call in logger_mock.debug.call_args_list]
|
|
assert any("without function_behavior" in msg for msg in debug_calls)
|
|
|
|
|
|
# endregion
|
|
# region Plugins
|
|
|
|
|
|
def test_add_plugin_from_directory(kernel: Kernel):
|
|
# import plugins
|
|
plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins")
|
|
# path to plugins directory
|
|
plugin = kernel.add_plugin(plugin_name="TestPlugin", parent_directory=plugins_directory)
|
|
assert plugin is not None
|
|
assert len(plugin.functions) == 2
|
|
func = plugin.functions["TestFunction"]
|
|
assert func is not None
|
|
func_handlebars = plugin.functions["TestFunctionHandlebars"]
|
|
assert func_handlebars is not None
|
|
|
|
|
|
def test_add_plugin_from_directory_with_encoding(kernel: Kernel):
|
|
"""Test kernel.add_plugin with custom encoding parameter."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Create a plugin directory with UTF-8 content
|
|
plugin_dir = os.path.join(temp_dir, "test_encoding_plugin")
|
|
os.makedirs(plugin_dir)
|
|
|
|
function_dir = os.path.join(plugin_dir, "test_function")
|
|
os.makedirs(function_dir)
|
|
|
|
prompt_path = os.path.join(function_dir, "skprompt.txt")
|
|
config_path = os.path.join(function_dir, "config.json")
|
|
|
|
# UTF-8 content with international characters
|
|
# Hello World Test
|
|
prompt_content = """Multi-language assistant:
|
|
Chinese: 你好世界!
|
|
Japanese: こんにちは世界!
|
|
Question: {{$input}}
|
|
"""
|
|
|
|
config_content = """{
|
|
"schema": 1,
|
|
"description": "Test encoding function",
|
|
"input_variables": [
|
|
{
|
|
"name": "input",
|
|
"description": "User's question",
|
|
"required": true
|
|
}
|
|
]
|
|
}"""
|
|
|
|
# Write files with UTF-8 encoding
|
|
with open(prompt_path, "w", encoding="utf-8") as f:
|
|
f.write(prompt_content)
|
|
with open(config_path, "w", encoding="utf-8") as f:
|
|
f.write(config_content)
|
|
|
|
# Test with explicit encoding
|
|
plugin = kernel.add_plugin(parent_directory=temp_dir, plugin_name="test_encoding_plugin", encoding="utf-8")
|
|
|
|
assert plugin is not None
|
|
assert plugin.name == "test_encoding_plugin"
|
|
assert "test_function" in plugin.functions
|
|
|
|
function = plugin.functions["test_function"]
|
|
template = function.prompt_template.prompt_template_config.template
|
|
# Assert "Hello World"
|
|
assert "你好世界" in template
|
|
assert "こんにちは世界" in template
|
|
assert function.description == "Test encoding function"
|
|
|
|
|
|
def test_plugin_no_plugin(kernel: Kernel):
|
|
with pytest.raises(ValueError):
|
|
kernel.add_plugin(plugin_name="test")
|
|
|
|
|
|
def test_plugin_name_from_class_name(kernel: Kernel):
|
|
kernel.add_plugin(" ", None)
|
|
assert "str" in kernel.plugins
|
|
|
|
|
|
def test_plugin_name_from_name_attribute(kernel: Kernel):
|
|
@dataclass
|
|
class TestPlugin:
|
|
name: str = "test_plugin"
|
|
|
|
kernel.add_plugin(TestPlugin(), None)
|
|
assert "test_plugin" in kernel.plugins
|
|
|
|
|
|
def test_plugin_name_not_string_error(kernel: Kernel):
|
|
with pytest.raises(TypeError):
|
|
kernel.add_plugin(" ", plugin_name=Path(__file__).parent)
|
|
|
|
|
|
def test_plugins_add_plugins(kernel: Kernel):
|
|
plugin1 = KernelPlugin(name="TestPlugin")
|
|
plugin2 = KernelPlugin(name="TestPlugin2")
|
|
|
|
kernel.add_plugins([plugin1, plugin2])
|
|
assert len(kernel.plugins) == 2
|
|
|
|
|
|
def test_add_function_from_prompt(kernel: Kernel):
|
|
prompt = """
|
|
Write a short story about two Corgis on an adventure.
|
|
The story must be:
|
|
- G rated
|
|
- Have a positive message
|
|
- No sexism, racism or other bias/bigotry
|
|
- Be exactly {{$paragraph_count}} paragraphs long
|
|
- Be written in this language: {{$language}}
|
|
- The two names of the corgis are {{GenerateNames.generate_names}}
|
|
"""
|
|
|
|
kernel.add_function(
|
|
prompt=prompt,
|
|
function_name="TestFunction",
|
|
plugin_name="TestPlugin",
|
|
description="Write a short story.",
|
|
execution_settings=PromptExecutionSettings(
|
|
extension_data={"max_tokens": 500, "temperature": 0.5, "top_p": 0.5}
|
|
),
|
|
)
|
|
func = kernel.get_function("TestPlugin", "TestFunction")
|
|
assert func.name == "TestFunction"
|
|
assert func.description == "Write a short story."
|
|
assert len(func.parameters) == 2
|
|
|
|
|
|
def test_add_function_not_provided(kernel: Kernel):
|
|
with pytest.raises(ValueError):
|
|
kernel.add_function(function_name="TestFunction", plugin_name="TestPlugin")
|
|
|
|
|
|
def test_add_function_from_prompt_different_values(kernel: Kernel):
|
|
template = """
|
|
Write a short story about two Corgis on an adventure.
|
|
The story must be:
|
|
- G rated
|
|
- Have a positive message
|
|
- No sexism, racism or other bias/bigotry
|
|
- Be exactly {{$paragraph_count}} paragraphs long
|
|
- Be written in this language: {{$language}}
|
|
- The two names of the corgis are {{GenerateNames.generate_names}}
|
|
"""
|
|
prompt = "test"
|
|
|
|
kernel.add_function(
|
|
prompt=prompt,
|
|
function_name="TestFunction",
|
|
plugin_name="TestPlugin",
|
|
description="Write a short story.",
|
|
template_format="handlebars",
|
|
prompt_template_config=PromptTemplateConfig(
|
|
template=template,
|
|
),
|
|
execution_settings=PromptExecutionSettings(
|
|
extension_data={"max_tokens": 500, "temperature": 0.5, "top_p": 0.5}
|
|
),
|
|
)
|
|
func = kernel.get_function("TestPlugin", "TestFunction")
|
|
assert func.name == "TestFunction"
|
|
assert func.description == "Write a short story."
|
|
assert isinstance(func.prompt_template, KernelPromptTemplate)
|
|
assert len(func.parameters) == 2
|
|
|
|
|
|
def test_add_functions(kernel: Kernel):
|
|
@kernel_function(name="func1")
|
|
def func1(arg1: str) -> str:
|
|
return "test"
|
|
|
|
@kernel_function(name="func2")
|
|
def func2(arg1: str) -> str:
|
|
return "test"
|
|
|
|
plugin = kernel.add_functions(plugin_name="test", functions=[func1, func2])
|
|
assert len(plugin.functions) == 2
|
|
|
|
|
|
def test_add_functions_to_existing(kernel: Kernel):
|
|
kernel.add_plugin(KernelPlugin(name="test"))
|
|
|
|
@kernel_function(name="func1")
|
|
def func1(arg1: str) -> str:
|
|
return "test"
|
|
|
|
@kernel_function(name="func2")
|
|
def func2(arg1: str) -> str:
|
|
return "test"
|
|
|
|
plugin = kernel.add_functions(plugin_name="test", functions=[func1, func2])
|
|
assert len(plugin.functions) == 2
|
|
|
|
|
|
def test_import_plugin_from_openapi(kernel: Kernel):
|
|
openapi_spec_file = os.path.join(
|
|
os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAPIPlugin", "akv-openapi.yaml"
|
|
)
|
|
|
|
kernel.add_plugin_from_openapi(
|
|
plugin_name="TestOpenAPIPlugin",
|
|
openapi_document_path=openapi_spec_file,
|
|
)
|
|
plugin = kernel.get_plugin(plugin_name="TestOpenAPIPlugin")
|
|
assert plugin is not None
|
|
assert plugin.name == "TestOpenAPIPlugin"
|
|
assert plugin.functions.get("GetSecret") is not None
|
|
assert plugin.functions.get("SetSecret") is not None
|
|
|
|
|
|
def test_get_plugin(kernel: Kernel):
|
|
kernel.add_plugin(KernelPlugin(name="TestPlugin"))
|
|
plugin = kernel.get_plugin("TestPlugin")
|
|
assert plugin is not None
|
|
|
|
|
|
def test_get_plugin_not_found(kernel: Kernel):
|
|
with pytest.raises(KernelPluginNotFoundError):
|
|
kernel.get_plugin("TestPlugin2")
|
|
|
|
|
|
def test_get_function(kernel: Kernel, custom_plugin_class):
|
|
kernel.add_plugin(custom_plugin_class(), "TestPlugin")
|
|
func = kernel.get_function("TestPlugin", "getLightStatus")
|
|
assert func
|
|
|
|
|
|
def test_func_plugin_not_found(kernel: Kernel):
|
|
with pytest.raises(KernelPluginNotFoundError):
|
|
kernel.get_function("TestPlugin", "TestFunction")
|
|
|
|
|
|
def test_func_function_not_found(kernel: Kernel, custom_plugin_class):
|
|
kernel.add_plugin(custom_plugin_class(), "TestPlugin")
|
|
with pytest.raises(KernelFunctionNotFoundError):
|
|
kernel.get_function("TestPlugin", "TestFunction")
|
|
|
|
|
|
def test_get_function_from_fqn(kernel: Kernel, custom_plugin_class):
|
|
kernel.add_plugin(custom_plugin_class(), "TestPlugin")
|
|
func = kernel.get_function_from_fully_qualified_function_name("TestPlugin-getLightStatus")
|
|
assert func
|
|
|
|
|
|
def test_get_function_from_fqn_wo_plugin(kernel: Kernel, custom_plugin_class):
|
|
kernel.add_plugin(custom_plugin_class(), "TestPlugin")
|
|
func = kernel.get_function_from_fully_qualified_function_name("getLightStatus")
|
|
assert func
|
|
|
|
|
|
def test_get_function_bare_name_single_match(kernel: Kernel, custom_plugin_class):
|
|
kernel.add_plugin(custom_plugin_class(), "TestPlugin")
|
|
func = kernel.get_function(None, "getLightStatus")
|
|
assert func
|
|
|
|
|
|
def test_get_function_bare_name_ambiguous_warns(kernel: Kernel, caplog):
|
|
import logging
|
|
|
|
class PluginA:
|
|
@kernel_function(name="check_permissions")
|
|
def check_permissions(self) -> str:
|
|
return "a"
|
|
|
|
class PluginB:
|
|
@kernel_function(name="check_permissions")
|
|
def check_permissions(self) -> str:
|
|
return "b"
|
|
|
|
kernel.add_plugin(PluginA(), "PluginA")
|
|
kernel.add_plugin(PluginB(), "PluginB")
|
|
|
|
with caplog.at_level(logging.WARNING, logger="semantic_kernel.functions.kernel_function_extension"):
|
|
func = kernel.get_function(None, "check_permissions")
|
|
|
|
# Warn-only: resolves to the first-registered match, but logs the ambiguity.
|
|
assert func is kernel.get_function("PluginA", "check_permissions")
|
|
assert "ambiguous" in caplog.text
|
|
assert "PluginA" in caplog.text and "PluginB" in caplog.text
|
|
|
|
|
|
# endregion
|
|
# region Services
|
|
|
|
|
|
def test_kernel_add_service(kernel: Kernel, service: AIServiceClientBase):
|
|
kernel.add_service(service)
|
|
assert kernel.services == {"service": service}
|
|
|
|
|
|
def test_kernel_add_service_twice(kernel_with_service: Kernel, service: AIServiceClientBase):
|
|
with pytest.raises(KernelFunctionAlreadyExistsError):
|
|
kernel_with_service.add_service(service)
|
|
assert kernel_with_service.services == {"service": service}
|
|
|
|
|
|
def test_kernel_add_multiple_services(kernel_with_service: Kernel, service: AIServiceClientBase):
|
|
service2 = AIServiceClientBase(service_id="service2", ai_model_id="ai_model_id")
|
|
kernel_with_service.add_service(service2)
|
|
assert kernel_with_service.services["service2"] == service2
|
|
assert len(kernel_with_service.services) == 2
|
|
|
|
|
|
def test_kernel_remove_service(kernel_with_service: Kernel):
|
|
kernel_with_service.remove_service("service")
|
|
assert kernel_with_service.services == {}
|
|
|
|
|
|
def test_kernel_remove_service_error(kernel_with_service: Kernel):
|
|
with pytest.raises(KernelServiceNotFoundError):
|
|
kernel_with_service.remove_service("service2")
|
|
|
|
|
|
def test_kernel_remove_all_service(kernel_with_service: Kernel):
|
|
kernel_with_service.remove_all_services()
|
|
assert kernel_with_service.services == {}
|
|
|
|
|
|
def test_get_default_service(kernel_with_default_service: Kernel):
|
|
service_get = kernel_with_default_service.get_service()
|
|
assert service_get == kernel_with_default_service.services["default"]
|
|
|
|
|
|
def test_get_default_service_with_type(kernel_with_default_service: Kernel):
|
|
service_get = kernel_with_default_service.get_service(type=AIServiceClientBase)
|
|
assert service_get == kernel_with_default_service.services["default"]
|
|
|
|
|
|
def test_get_service(kernel_with_service: Kernel):
|
|
service_get = kernel_with_service.get_service("service")
|
|
assert service_get == kernel_with_service.services["service"]
|
|
|
|
|
|
def test_get_service_by_type(kernel_with_service: Kernel):
|
|
service_get = kernel_with_service.get_service(type=AIServiceClientBase)
|
|
assert service_get == kernel_with_service.services["service"]
|
|
|
|
|
|
def test_get_service_by_type_not_found(kernel_with_service: Kernel):
|
|
with pytest.raises(KernelServiceNotFoundError):
|
|
kernel_with_service.get_service(type=ChatCompletionClientBase)
|
|
|
|
|
|
def test_get_default_service_by_type(kernel_with_default_service: Kernel):
|
|
service_get = kernel_with_default_service.get_services_by_type(AIServiceClientBase)
|
|
assert service_get["default"] == kernel_with_default_service.services["default"]
|
|
|
|
|
|
def test_get_services_by_type(kernel_with_service: Kernel):
|
|
service_get = kernel_with_service.get_services_by_type(AIServiceClientBase)
|
|
assert service_get["service"] == kernel_with_service.services["service"]
|
|
|
|
|
|
def test_get_service_with_id_not_found(kernel_with_service: Kernel):
|
|
with pytest.raises(KernelServiceNotFoundError):
|
|
kernel_with_service.get_service("service2", type=AIServiceClientBase)
|
|
|
|
|
|
def test_get_service_with_type(kernel_with_service: Kernel):
|
|
service_get = kernel_with_service.get_service("service", type=AIServiceClientBase)
|
|
assert service_get == kernel_with_service.services["service"]
|
|
|
|
|
|
def test_get_service_with_multiple_types(kernel_with_service: Kernel):
|
|
service_get = kernel_with_service.get_service("service", type=(AIServiceClientBase, ChatCompletionClientBase))
|
|
assert service_get == kernel_with_service.services["service"]
|
|
|
|
|
|
def test_get_service_with_multiple_types_union(kernel_with_service: Kernel):
|
|
"""This is valid syntax only in python 3.10+. It is skipped for older versions."""
|
|
service_get = kernel_with_service.get_service("service", type=Union[AIServiceClientBase, ChatCompletionClientBase])
|
|
assert service_get == kernel_with_service.services["service"]
|
|
|
|
|
|
def test_get_service_with_type_not_found(kernel_with_service: Kernel):
|
|
with pytest.raises(KernelServiceNotFoundError):
|
|
kernel_with_service.get_service("service", type=ChatCompletionClientBase)
|
|
|
|
|
|
def test_get_service_no_id(kernel_with_service: Kernel):
|
|
service_get = kernel_with_service.get_service()
|
|
assert service_get == kernel_with_service.services["service"]
|
|
|
|
|
|
def test_instantiate_prompt_execution_settings_through_kernel(kernel_with_service: Kernel):
|
|
settings = kernel_with_service.get_prompt_execution_settings_from_service_id("service")
|
|
assert settings.service_id == "service"
|
|
|
|
|
|
# endregion
|
|
# region experimental class decorator
|
|
|
|
|
|
def test_experimental_class_has_decorator_and_flag(experimental_plugin_class):
|
|
assert hasattr(experimental_plugin_class, "is_experimental")
|
|
assert experimental_plugin_class.is_experimental
|
|
assert "This class is marked as 'experimental' and may change in the future" in experimental_plugin_class.__doc__
|
|
|
|
|
|
# endregion
|
|
|
|
# region copy and clone
|
|
|
|
|
|
def test_kernel_model_dump(
|
|
kernel: Kernel,
|
|
custom_plugin_class: type,
|
|
auto_function_invocation_filter: Callable,
|
|
):
|
|
kernel.add_plugin(custom_plugin_class(), "TestPlugin")
|
|
kernel.add_filter(FilterTypes.AUTO_FUNCTION_INVOCATION, auto_function_invocation_filter)
|
|
|
|
kernel_dict = kernel.model_dump()
|
|
|
|
assert kernel_dict is not None
|
|
assert kernel_dict["plugins"] is not None and len(kernel_dict["plugins"]) > 0
|
|
assert (
|
|
kernel_dict["auto_function_invocation_filters"] is not None
|
|
and len(kernel_dict["auto_function_invocation_filters"]) > 0
|
|
)
|
|
|
|
|
|
def test_kernel_deep_copy(
|
|
kernel: Kernel,
|
|
custom_plugin_class: type,
|
|
auto_function_invocation_filter: Callable,
|
|
):
|
|
kernel.add_plugin(custom_plugin_class(), "TestPlugin")
|
|
kernel.add_filter(FilterTypes.AUTO_FUNCTION_INVOCATION, auto_function_invocation_filter)
|
|
|
|
kernel_copy = kernel.model_copy(deep=True)
|
|
|
|
assert kernel_copy is not None
|
|
assert kernel_copy.plugins is not None and len(kernel_copy.plugins) > 0
|
|
assert (
|
|
kernel_copy.auto_function_invocation_filters is not None
|
|
and len(kernel_copy.auto_function_invocation_filters) > 0
|
|
)
|
|
|
|
|
|
def test_kernel_model_dump_fail_with_services(kernel: Kernel):
|
|
open_ai_chat_completion = OpenAIChatCompletion(ai_model_id="abc", api_key="abc")
|
|
kernel.add_service(open_ai_chat_completion)
|
|
|
|
with pytest.raises(TypeError):
|
|
# This will fail because OpenAIChatCompletion is not serializable, more specifically,
|
|
# the client is not serializable
|
|
kernel.model_dump(deep=True)
|
|
|
|
|
|
def test_kernel_deep_copy_fail_with_services(kernel: Kernel):
|
|
open_ai_chat_completion = OpenAIChatCompletion(ai_model_id="abc", api_key="abc")
|
|
kernel.add_service(open_ai_chat_completion)
|
|
|
|
with pytest.raises(TypeError):
|
|
# This will fail because OpenAIChatCompletion is not serializable, more specifically,
|
|
# the client is not serializable
|
|
kernel.model_copy(deep=True)
|
|
|
|
|
|
def test_kernel_clone(
|
|
kernel: Kernel,
|
|
custom_plugin_class: type,
|
|
auto_function_invocation_filter: Callable,
|
|
):
|
|
kernel.add_service(OpenAIChatCompletion(ai_model_id="abc", api_key="abc"))
|
|
kernel.add_plugin(custom_plugin_class(), "TestPlugin")
|
|
kernel.add_filter(FilterTypes.AUTO_FUNCTION_INVOCATION, auto_function_invocation_filter)
|
|
|
|
kernel_clone = kernel.clone()
|
|
|
|
# Assert the clone has all the same properties as the original kernel
|
|
assert kernel_clone is not None
|
|
assert kernel_clone.plugins is not None and len(kernel_clone.plugins) > 0
|
|
assert (
|
|
kernel_clone.auto_function_invocation_filters is not None
|
|
and len(kernel_clone.auto_function_invocation_filters) > 0
|
|
)
|
|
assert kernel_clone.services is not None and len(kernel_clone.services) > 0
|
|
|
|
# Assert the clone is a deep copy
|
|
kernel_clone.plugins["TestPlugin"].functions["getLightStatus"].metadata.name = "getLightStatus2"
|
|
assert kernel.plugins["TestPlugin"].functions["getLightStatus"].metadata.name == "getLightStatus"
|
|
|
|
kernel_clone.plugins.clear()
|
|
kernel_clone.remove_filter(filter_type=FilterTypes.AUTO_FUNCTION_INVOCATION, position=0)
|
|
kernel_clone.remove_all_services()
|
|
|
|
assert kernel.plugins is not None and len(kernel.plugins) > 0
|
|
assert kernel.auto_function_invocation_filters is not None and len(kernel.auto_function_invocation_filters) > 0
|
|
assert kernel.services is not None and len(kernel.services) > 0
|
|
|
|
|
|
# endregion
|