Python: Pydantic-ify the KernelFunction class. Fix code paths related to tool_calls. (#4863)

### Motivation and Context

Solves #4864

The KernelFunction was configured as a Python class without regards to
using Pydantic fields and its validations. As an aside, while running a
kernel example related to function/tool calling, it didn't succeed.

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

This PR cleans up the KernelFunction by creating Pydantic fields. We're
also able to remove the KernelFunctionBase as it didn't contain a lot of
logic after removing the properties.

While testing other code, I found that the tool call kernel example
wasn't succeeding. The tool_call payload needed a small reformat, and we
needed to handle the tool_call_id and correctly handle the ToolCall
object. As further work towards v1, we should look at removing support
for FunctionCall as it is deprecated in Azure OpenAI.

We now see:

```
User:> What is 3+3?
Mosscap:> Ah, the timeless question of arithmetic! The sum of 3 and 3 is undoubtedly a splendid number. 
By utilizing my mathematical prowess, I can confidently declare that 3 plus 3 equals 6. 
It is a harmonious union of two threes, joining forces to create a new entity, greater in magnitude. 
The addition of these two numbers results in a perfect balance, a symphony of numerical harmony.
```

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [X] The code builds clean without any errors or warnings
- [X] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [X] All unit tests pass, and I have added new tests where possible
- [X] I didn't break anyone 😄

---------

Co-authored-by: Evan Mattson <[email protected]>
This commit is contained in:
Evan Mattson
2024-02-05 16:46:58 +00:00
committed by GitHub
co-authored by Evan Mattson
parent 8931ab63b6
commit 7268d13ee8
36 changed files with 389 additions and 403 deletions
@@ -216,7 +216,7 @@
"source": [
"async def setup_chat_with_memory(\n",
" kernel: sk.Kernel,\n",
") -> Tuple[sk.KernelFunctionBase, sk.KernelContext]:\n",
") -> Tuple[sk.KernelFunction, sk.KernelContext]:\n",
" sk_prompt = \"\"\"\n",
" ChatBot can have a conversation with you about any topic.\n",
" It can give explicit instructions or say 'I don't know' if\n",
@@ -276,7 +276,7 @@
"metadata": {},
"outputs": [],
"source": [
"async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunctionBase, context: sk.KernelContext) -> bool:\n",
"async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction, context: sk.KernelContext) -> bool:\n",
" try:\n",
" user_input = input(\"User:> \")\n",
" context[\"user_input\"] = user_input\n",
@@ -537,7 +537,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.13"
"version": "3.10.12"
}
},
"nbformat": 4,
@@ -314,7 +314,7 @@
"source": [
"async def setup_chat_with_memory(\n",
" kernel: sk.Kernel,\n",
") -> Tuple[sk.KernelFunctionBase, sk.KernelContext]:\n",
") -> Tuple[sk.KernelFunction, sk.KernelContext]:\n",
" sk_prompt = \"\"\"\n",
" ChatBot can have a conversation with you about any topic.\n",
" It can give explicit instructions or say 'I don't know' if\n",
@@ -355,7 +355,7 @@
"metadata": {},
"outputs": [],
"source": [
"async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunctionBase, context: sk.KernelContext) -> bool:\n",
"async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction, context: sk.KernelContext) -> bool:\n",
" try:\n",
" user_input = input(\"User:> \")\n",
" context[\"user_input\"] = user_input\n",
@@ -17,7 +17,7 @@ from semantic_kernel.connectors.ai.open_ai.semantic_functions.open_ai_chat_promp
)
from semantic_kernel.connectors.ai.open_ai.utils import (
chat_completion_with_function_call,
get_function_calling_object,
get_tool_call_object,
)
from semantic_kernel.core_plugins.time_plugin import TimePlugin
@@ -76,7 +76,7 @@ chat_function = kernel.register_semantic_function("ChatBot", "Chat", function_co
# to enable or disable function calling or set the function calling to a specific plugin.
# see the openai_function_calling example for how to use this with a unrelated function definition
filter = {"exclude_plugin": ["ChatBot"]}
functions = get_function_calling_object(kernel, filter)
functions = get_tool_call_object(kernel, filter)
async def chat(context: sk.KernelContext) -> Tuple[bool, sk.KernelContext]:
@@ -11,7 +11,7 @@ from semantic_kernel.connectors.ai.open_ai.semantic_functions.open_ai_chat_promp
)
from semantic_kernel.connectors.ai.open_ai.utils import (
chat_completion_with_function_call,
get_function_calling_object,
get_tool_call_object,
)
from semantic_kernel.core_plugins import MathPlugin
@@ -30,8 +30,9 @@ you will return a full answer to me as soon as possible.
kernel = sk.Kernel()
# Note: the underlying gpt-35/gpt-4 model version needs to be at least version 0613 to support tools.
deployment_name, api_key, endpoint = sk.azure_openai_settings_from_dot_env()
api_version = "2023-07-01-preview"
api_version = "2023-12-01-preview"
kernel.add_chat_service(
"chat-gpt",
sk_oai.AzureChatCompletion(
@@ -61,10 +62,11 @@ prompt_config = sk.PromptTemplateConfig(
max_tokens=2000,
temperature=0.7,
top_p=0.8,
function_call="auto",
functions=get_function_calling_object(kernel, {"exclude_plugin": ["ChatBot"]}),
tool_choice="auto",
tools=get_tool_call_object(kernel, {"exclude_plugin": ["ChatBot"]}),
)
)
prompt_template = OpenAIChatPromptTemplate("{{$user_input}}", kernel.prompt_template_engine, prompt_config)
prompt_template.add_system_message(system_message)
prompt_template.add_user_message("Hi there, who are you?")
@@ -42,7 +42,7 @@ async def search_memory_examples(kernel: sk.Kernel) -> None:
async def setup_chat_with_memory(
kernel: sk.Kernel,
) -> Tuple[sk.KernelFunctionBase, sk.KernelContext]:
) -> Tuple[sk.KernelFunction, sk.KernelContext]:
"""
When using Google PaLM to chat with memories, a chat prompt template is
essential; otherwise, the kernel will send text prompts to the Google PaLM
@@ -90,7 +90,7 @@ async def setup_chat_with_memory(
return chat_func, context
async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunctionBase, context: sk.KernelContext) -> bool:
async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction, context: sk.KernelContext) -> bool:
try:
user_input = input("User:> ")
context["user_input"] = user_input
@@ -33,7 +33,7 @@ async def search_memory_examples(kernel: sk.Kernel) -> None:
async def setup_chat_with_memory(
kernel: sk.Kernel,
) -> Tuple[sk.KernelFunctionBase, sk.KernelContext]:
) -> Tuple[sk.KernelFunction, sk.KernelContext]:
sk_prompt = """
ChatBot can have a conversation with you about any topic.
It can give explicit instructions or say 'I don't know' if
@@ -68,7 +68,7 @@ async def setup_chat_with_memory(
return chat_func, context
async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunctionBase, context: sk.KernelContext) -> bool:
async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction, context: sk.KernelContext) -> bool:
try:
user_input = input("User:> ")
context["user_input"] = user_input
-2
View File
@@ -4,7 +4,6 @@ from semantic_kernel import core_plugins, memory
from semantic_kernel.kernel import Kernel
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.semantic_functions.chat_prompt_template import ChatPromptTemplate
from semantic_kernel.semantic_functions.prompt_template import PromptTemplate
from semantic_kernel.semantic_functions.prompt_template_config import (
@@ -50,7 +49,6 @@ __all__ = [
"ChatPromptTemplate",
"SemanticFunctionConfig",
"ContextVariables",
"KernelFunctionBase",
"KernelContext",
"memory",
"core_plugins",
@@ -4,6 +4,7 @@ from typing import Optional
from semantic_kernel.connectors.ai.open_ai.models.chat.function_call import (
FunctionCall,
)
from semantic_kernel.connectors.ai.open_ai.models.chat.tool_calls import ToolCall
from semantic_kernel.models.chat.chat_message import ChatMessage
@@ -13,5 +14,5 @@ class OpenAIChatMessage(ChatMessage):
name: Optional[str] = None
# TODO: handle tool_calls
function_call: Optional[FunctionCall] = None
tool_calls: Optional[dict] = None
tool_calls: Optional[ToolCall] = None
tool_call_id: Optional[str] = None
@@ -13,9 +13,11 @@ logger: logging.Logger = logging.getLogger(__name__)
class OpenAIChatPromptTemplate(ChatPromptTemplate[OpenAIChatMessage]):
def add_function_response_message(self, name: str, content: Any) -> None:
def add_function_response_message(self, name: str, content: Any, tool_call_id: Optional[str] = None) -> None:
"""Add a function response message to the chat template."""
self.messages.append(OpenAIChatMessage(role="function", name=name, fixed_content=str(content)))
self.messages.append(
OpenAIChatMessage(role="function", name=name, fixed_content=str(content), tool_call_id=tool_call_id)
)
def add_tool_call_response_message(self, tool_call_id: str, content: Any) -> None:
"""Add a tool call response message to the chat template."""
@@ -51,13 +53,17 @@ class OpenAIChatPromptTemplate(ChatPromptTemplate[OpenAIChatMessage]):
function_call = None
tool_calls = kwargs.get("tool_calls")
if tool_calls is not None:
# TODO: update this when tool_calls is implemented
# and allow for multiple tool calls
ids = [tool_call.id for tool_call in tool_calls]
if role == "assistant":
self.messages.append(
OpenAIChatMessage(
role=role,
fixed_content=message,
name=name,
tool_calls=tool_calls,
tool_calls=tool_calls[0],
tool_call_id=ids[0],
)
)
return
@@ -8,17 +8,45 @@ from openai.types.chat import ChatCompletion
from semantic_kernel import Kernel, KernelContext
from semantic_kernel.connectors.ai.open_ai.models.chat.function_call import FunctionCall
from semantic_kernel.connectors.ai.open_ai.models.chat.tool_calls import ToolCall
from semantic_kernel.connectors.ai.open_ai.semantic_functions.open_ai_chat_prompt_template import (
OpenAIChatPromptTemplate,
)
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.kernel_function import KernelFunction
logger: logging.Logger = logging.getLogger(__name__)
def _describe_function(function: KernelFunctionBase) -> Dict[str, str]:
"""Create the object used for function_calling.
def _describe_tool_call(function: KernelFunction) -> Dict[str, str]:
"""Create the object used for the tool call.
Assumes that arguments for semantic functions are optional, for native functions required.
"""
func_view = function.describe()
return {
"type": "function",
"function": {
"name": f"{func_view.plugin_name}-{func_view.name}",
"description": func_view.description,
"parameters": {
"type": "object",
"properties": {
param.name: {
"description": param.description,
"type": param.type_ if param.type_ else "string",
**({"enum": param.enum} if hasattr(param, "enum") else {}), # Added support for enum
}
for param in func_view.parameters
},
"required": [p.name for p in func_view.parameters if p.required],
},
},
}
def _describe_function(function: KernelFunction) -> Dict[str, str]:
"""Create the object used for function_calling.
Assumes that arguments for semantic functions are optional, for native functions required.
"""
func_view = function.describe()
@@ -35,8 +63,10 @@ def _describe_function(function: KernelFunctionBase) -> Dict[str, str]:
}
def get_function_calling_object(kernel: Kernel, filter: Dict[str, List[str]]) -> List[Dict[str, str]]:
"""Create the object used for function_calling.
def get_tool_call_object(kernel: Kernel, filter: Dict[str, List[str]]) -> List[Dict[str, str]]:
"""Create the object used for a tool call.
This is the preferred method to create the tool call object.
args:
kernel: the kernel.
@@ -57,7 +87,39 @@ def get_function_calling_object(kernel: Kernel, filter: Dict[str, List[str]]) ->
"exclude_function": ["plugin1-function1", "plugin2-function2"],
}
will return all functions except plugin1-function1 and plugin2-function2.
caller_function_name: the name of the function that is calling the other functions.
returns:
a filtered list of dictionaries of the functions in the kernel that can be passed to the function calling api.
"""
return get_function_calling_object(kernel, filter, is_tool_call=True)
def get_function_calling_object(
kernel: Kernel, filter: Dict[str, List[str]], is_tool_call: Optional[bool] = False
) -> List[Dict[str, str]]:
"""Create the object used for a function call.
Note: although Azure has deprecated function calling, SK still supports it for the time being.
args:
kernel: the kernel.
filter: a dictionary with keys
exclude_plugin, include_plugin, exclude_function, include_function
and lists of the required filter.
The function name should be in the format "plugin_name-function_name".
Using exclude_plugin and include_plugin at the same time will raise an error.
Using exclude_function and include_function at the same time will raise an error.
If using include_* implies that all other function will be excluded.
Example:
filter = {
"exclude_plugin": ["plugin1", "plugin2"],
"include_function": ["plugin3-function1", "plugin4-function2"],
}
will return only plugin3-function1 and plugin4-function2.
filter = {
"exclude_function": ["plugin1-function1", "plugin2-function2"],
}
will return all functions except plugin1-function1 and plugin2-function2.
is_tool_call: if True, the function will return a list of tool calls, otherwise a list of functions.
returns:
a filtered list of dictionaries of the functions in the kernel that can be passed to the function calling api.
"""
@@ -81,36 +143,53 @@ def get_function_calling_object(kernel: Kernel, filter: Dict[str, List[str]]) ->
for (
plugin_name,
plugin,
) in kernel.plugins.data.items():
) in kernel.plugins.plugins.items():
if plugin_name in exclude_plugin or (include_plugin and plugin_name not in include_plugin):
continue
for function_name, function in plugin.items():
for function_name, function in plugin.functions.items():
current_name = f"{plugin_name}-{function_name}"
if current_name in exclude_function or (include_function and current_name not in include_function):
continue
result.append(_describe_function(function))
result.append(_describe_tool_call(function) if is_tool_call else _describe_function(function))
return result
async def execute_function_call(kernel: Kernel, function_call: FunctionCall, log: Optional[Any] = None) -> str:
if log:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
result = await kernel.run(
kernel.func(**function_call.split_name_dict()),
input_vars=function_call.to_context_variables(),
)
logger.info(f"Function call result: {result}")
async def execute(kernel: Kernel, func: KernelFunction, input_vars: ContextVariables) -> str:
"""Execute a function and return the result.
Args:
kernel (Kernel): the kernel to use.
func (KernelFunction): the function to execute.
input_vars (ContextVariables): the input variables.
Returns:
str: the result of the execution.
"""
result = await kernel.run(func, input_vars=input_vars)
logger.info(f"Execution result: {result}")
return str(result)
async def execute_function_call(kernel: Kernel, function_call: FunctionCall) -> str:
"""Execute a function call and return the result."""
func = kernel.func(**function_call.split_name_dict())
input_vars = function_call.to_context_variables()
return await execute(kernel, func, input_vars)
async def execute_tool_call(kernel: Kernel, tool_call: ToolCall) -> str:
"""Execute a tool call and return the result."""
func = kernel.func(**tool_call.function.split_name_dict())
input_vars = tool_call.function.to_context_variables()
return await execute(kernel, func, input_vars)
async def chat_completion_with_function_call(
kernel: Kernel,
context: KernelContext,
chat_plugin_name: Optional[str] = None,
chat_function_name: Optional[str] = None,
chat_function: Optional[KernelFunctionBase] = None,
*,
log: Optional[Any] = None,
chat_function: Optional[KernelFunction] = None,
**kwargs: Dict[str, Any],
) -> KernelContext:
"""Perform a chat completion with auto-executing function calling.
@@ -127,7 +206,7 @@ async def chat_completion_with_function_call(
kernel: the kernel to use.
context: the context to use.
functions: the function calling object,
make sure to use get_function_calling_object method to create it.
make sure to use get_tool_call_object method to create it.
Optional arguments:
chat_plugin_name: the plugin name of the chat function.
chat_function_name: the function name of the chat function.
@@ -140,8 +219,6 @@ async def chat_completion_with_function_call(
returns:
the context with the result of the chat completion, just like a regular invoke/run_async.
"""
if log:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
# check the number of function calls
max_function_calls = kwargs.get("max_function_calls", 5)
current_call_count = kwargs.get("current_call_count", 0)
@@ -149,9 +226,9 @@ async def chat_completion_with_function_call(
if chat_function is None:
chat_function = kernel.func(plugin_name=chat_plugin_name, function_name=chat_function_name)
assert isinstance(
chat_function._chat_prompt_template, OpenAIChatPromptTemplate
chat_function.chat_prompt_template, OpenAIChatPromptTemplate
), "Please make sure to initialize your chat function with the OpenAIChatPromptTemplate class."
settings = chat_function._chat_prompt_template.prompt_config.execution_settings
settings = chat_function.chat_prompt_template.prompt_config.execution_settings
if current_call_count >= max_function_calls:
settings.functions = []
context = await chat_function.invoke(
@@ -159,13 +236,26 @@ async def chat_completion_with_function_call(
# when the maximum number of function calls is reached, execute the chat function without Functions.
settings=settings,
)
function_call = context.objects.pop("function_call", None)
# if there is no function_call or if the content is not a FunctionCall object, return the context
if function_call is None or not isinstance(function_call, FunctionCall):
if not (results := context.objects.pop("results", None)):
return context
function_call = next(
(
fc
for fc in (results[0].function_call or results[0].tool_calls or [None])
if isinstance(fc, (FunctionCall, ToolCall))
),
None,
)
if function_call:
execute_call = execute_tool_call if isinstance(function_call, ToolCall) else execute_function_call
result = await execute_call(kernel, function_call)
tool_call_id = function_call.id
else:
return context
result = await execute_function_call(kernel, function_call)
# add the result to the chat prompt template
chat_function._chat_prompt_template.add_function_response_message(name=function_call.name, content=str(result))
chat_function.chat_prompt_template.add_function_response_message(
name=function_call.function.name, content=str(result), tool_call_id=tool_call_id
)
# request another completion
return await chat_completion_with_function_call(
kernel,
@@ -15,7 +15,7 @@ from semantic_kernel.connectors.ai.open_ai.const import (
USER_AGENT,
)
from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.plugin_definition import kernel_function, kernel_function_context_parameter
logger: logging.Logger = logging.getLogger(__name__)
@@ -251,7 +251,7 @@ def register_openapi_plugin(
kernel: Kernel,
plugin_name: str,
openapi_document: str,
) -> Dict[str, KernelFunctionBase]:
) -> Dict[str, KernelFunction]:
parser = OpenApiParser()
parsed_doc = parser.parse(openapi_document)
operations = parser.create_rest_api_operations(parsed_doc)
+12 -13
View File
@@ -30,7 +30,6 @@ from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryB
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.plugin_definition.function_view import FunctionView
from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin
from semantic_kernel.plugin_definition.kernel_plugin_collection import (
@@ -122,7 +121,7 @@ class Kernel(KernelBaseModel):
super().__init__(plugins=plugins, prompt_template_engine=prompt_template_engine, memory=memory, **kwargs)
def add_plugin(
self, plugin_name: str, functions: List[KernelFunctionBase], plugin: Optional[KernelPlugin] = None
self, plugin_name: str, functions: List[KernelFunction], plugin: Optional[KernelPlugin] = None
) -> None:
"""
Adds a plugin to the kernel's collection of plugins. If a plugin instance is provided,
@@ -130,7 +129,7 @@ class Kernel(KernelBaseModel):
Args:
plugin_name (str): The name of the plugin
functions (List[KernelFunctionBase]): The functions to add to the plugin
functions (List[KernelFunction]): The functions to add to the plugin
plugin (Optional[KernelPlugin]): An optional pre-defined plugin instance
"""
if plugin is None:
@@ -147,7 +146,7 @@ class Kernel(KernelBaseModel):
plugin_name: Optional[str],
function_name: str,
function_config: SemanticFunctionConfig,
) -> KernelFunctionBase:
) -> KernelFunction:
"""
Creates a semantic function from the plugin name, function name and function config
@@ -157,7 +156,7 @@ class Kernel(KernelBaseModel):
function_config (SemanticFunctionConfig): The function config
Returns:
KernelFunctionBase: The created semantic function
KernelFunction: The created semantic function
Raises:
ValueError: If the plugin name or function name are invalid
@@ -178,7 +177,7 @@ class Kernel(KernelBaseModel):
self,
plugin_name: Optional[str],
kernel_function: Callable,
) -> KernelFunctionBase:
) -> KernelFunction:
"""
Creates a native function from the plugin name and kernel function
@@ -187,7 +186,7 @@ class Kernel(KernelBaseModel):
kernel_function (Callable): The kernel function
Returns:
KernelFunctionBase: The created native function
KernelFunction: The created native function
"""
if not hasattr(kernel_function, "__kernel_function__"):
raise KernelException(
@@ -211,6 +210,7 @@ class Kernel(KernelBaseModel):
function = KernelFunction.from_native_method(kernel_function, plugin_name)
self.add_plugin(plugin_name, [function])
function.set_default_plugin_collection(self.plugins)
return function
@@ -319,9 +319,8 @@ class Kernel(KernelBaseModel):
pipeline_step = 0
for func in functions:
while True:
assert isinstance(func, KernelFunctionBase), (
"All func arguments to Kernel.run*(inputs, func1, func2, ...) "
"must be KernelFunctionBase instances"
assert isinstance(func, KernelFunction), (
"All func arguments to Kernel.run*(inputs, func1, func2, ...) " "must be KernelFunction instances"
)
if context.error_occurred:
@@ -393,7 +392,7 @@ class Kernel(KernelBaseModel):
return context
def func(self, plugin_name: str, function_name: str) -> KernelFunctionBase:
def func(self, plugin_name: str, function_name: str) -> KernelFunction:
if plugin_name not in self.plugins:
raise ValueError(f"Plugin '{plugin_name}' not found")
if function_name not in self.plugins[plugin_name]:
@@ -711,7 +710,7 @@ class Kernel(KernelBaseModel):
plugin_name: str,
function_name: str,
function_config: SemanticFunctionConfig,
) -> KernelFunctionBase:
) -> KernelFunction:
function_type = function_config.prompt_template_config.type
if not function_type == "completion":
raise AIException(
@@ -854,7 +853,7 @@ class Kernel(KernelBaseModel):
plugin_name: Optional[str] = None,
description: Optional[str] = None,
**kwargs: Any,
) -> "KernelFunctionBase":
) -> "KernelFunction":
function_name = function_name if function_name is not None else f"f_{generate_random_ascii_name()}"
config = PromptTemplateConfig(
@@ -1,7 +0,0 @@
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
__all__ = [
"KernelFunctionBase",
"KernelFunction",
]
@@ -152,7 +152,7 @@ class KernelContext(KernelBaseModel, Generic[SemanticTextMemoryT]):
function_name {str} -- The function name.
Returns:
KernelFunctionBase -- The function.
KernelFunction -- The function.
"""
if self.plugins is None:
raise ValueError("The plugin collection hasn't been set")
@@ -7,6 +7,13 @@ import sys
from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
from pydantic import Field, StringConstraints
if sys.version_info >= (3, 9):
from typing import Annotated
else:
from typing_extensions import Annotated
from semantic_kernel.connectors.ai.chat_completion_client_base import (
ChatCompletionClientBase,
)
@@ -15,6 +22,7 @@ from semantic_kernel.connectors.ai.text_completion_client_base import (
TextCompletionClientBase,
)
from semantic_kernel.kernel_exception import KernelException
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.memory.null_memory import NullMemory
from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase
from semantic_kernel.models.contents.chat_message_content import ChatMessageContent
@@ -22,7 +30,6 @@ from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.delegate_handlers import DelegateHandlers
from semantic_kernel.orchestration.delegate_inference import DelegateInference
from semantic_kernel.orchestration.delegate_types import DelegateTypes
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.plugin_definition.function_view import FunctionView
from semantic_kernel.plugin_definition.parameter_view import ParameterView
from semantic_kernel.semantic_functions.chat_prompt_template import ChatPromptTemplate
@@ -54,24 +61,93 @@ def store_results(chat_prompt: ChatPromptTemplate, results: List["ChatMessageCon
return chat_prompt
class KernelFunction(KernelFunctionBase):
class KernelFunction(KernelBaseModel):
"""
Semantic Kernel function.
Attributes:
plugin_name (str): The name of the plugin that contains this function. Must be upper/lower
case letters and underscores with a minimum length of 1.
description (Optional[str]): The description of the function.
name (str): The name of the function. Must be upper/lower case letters and
underscores with a minimum length of 1.
is_semantic (bool): Whether the function is semantic.
stream_function (Optional[Callable[..., Any]]): The stream function for the function.
parameters (List[ParameterView]): The parameters for the function.
delegate_type (DelegateTypes): The delegate type for the function.
function (Callable[..., Any]): The function to call.
plugins (Optional[KernelPluginCollection]): The collection of plugins.
ai_service (Optional[Union[TextCompletionClientBase, ChatCompletionClientBase]]): The AI service.
ai_prompt_execution_settings (PromptExecutionSettings): The AI prompt execution settings.
chat_prompt_template (Optional[ChatPromptTemplate]): The chat prompt template.
"""
# TODO: rebuild with proper pydantic fields
_parameters: List[ParameterView]
_delegate_type: DelegateTypes
_function: Callable[..., Any]
_plugin_collection: Optional["KernelPluginCollection"]
_ai_service: Optional[Union[TextCompletionClientBase, ChatCompletionClientBase]]
_ai_prompt_execution_settings: PromptExecutionSettings
_chat_prompt_template: ChatPromptTemplate
plugin_name: Annotated[str, StringConstraints(pattern=r"^[A-Za-z_]+$", min_length=1)]
description: Optional[str] = Field(default=None)
name: Annotated[str, StringConstraints(pattern=r"^[A-Za-z_]+$", min_length=1)]
is_semantic: bool = Field(...)
stream_function: Optional[Callable[..., Any]] = Field(default=None)
parameters: List[ParameterView] = Field(...)
delegate_type: DelegateTypes = Field(...)
function: Callable[..., Any] = Field(...)
plugins: Optional["KernelPluginCollection"] = Field(default=None)
ai_service: Optional[Union[TextCompletionClientBase, ChatCompletionClientBase]] = Field(default=None)
prompt_execution_settings: PromptExecutionSettings = Field(default_factory=PromptExecutionSettings)
chat_prompt_template: Optional[ChatPromptTemplate] = Field(default=None)
def __init__(
self,
delegate_type: DelegateTypes,
delegate_function: Callable[..., Any],
parameters: List[ParameterView],
description: str,
plugin_name: str,
function_name: str,
is_semantic: bool,
delegate_stream_function: Optional[Callable[..., Any]] = None,
**kwargs: Dict[str, Any],
) -> None:
"""
Initializes a new instance of the KernelFunction class
Args:
delegate_type (DelegateTypes): The delegate type for the function
delegate_function (Callable[..., Any]): The delegate function for the function
parameters (List[ParameterView]): The parameters for the function
description (str): The description for the function
plugin_name (str): The name of the plugin
name (str): The name of the function
is_semantic (bool): Whether the function is semantic
delegate_stream_function (Optional[Callable[..., Any]]): The delegate stream function for the function
kwargs (Dict[str, Any]): Additional keyword arguments
"""
chat_prompt_template = kwargs.pop("chat_prompt_template", None)
super().__init__(
delegate_type=delegate_type,
function=delegate_function,
parameters=parameters,
description=description,
plugin_name=plugin_name,
name=function_name,
is_semantic=is_semantic,
stream_function=delegate_stream_function,
chat_prompt_template=chat_prompt_template,
**kwargs,
)
@staticmethod
def from_native_method(method, plugin_name="", log=None) -> "KernelFunction":
if log:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
def from_native_method(method: Callable[..., Any], plugin_name: str) -> "KernelFunction":
"""
Create a KernelFunction from a native method.
Args:
method (Callable[..., Any]): The method to create the function from
plugin_name (str): The name of the plugin
Returns:
KernelFunction: The kernel function
"""
if method is None:
raise ValueError("Method cannot be `None`")
@@ -126,10 +202,18 @@ class KernelFunction(KernelFunctionBase):
plugin_name: str,
function_name: str,
function_config: SemanticFunctionConfig,
log: Optional[Any] = None,
) -> "KernelFunction":
if log:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
"""
Create a KernelFunction from a semantic configuration.
Args:
plugin_name (str): The name of the plugin
function_name (str): The name of the function
function_config (SemanticFunctionConfig): The function configuration
Returns:
KernelFunction: The kernel function
"""
if function_config is None:
raise ValueError("Function configuration cannot be `None`")
@@ -156,7 +240,8 @@ class KernelFunction(KernelFunctionBase):
messages = await chat_prompt.render_messages(context)
results = await client.complete_chat(messages, prompt_execution_settings)
context.objects["results"] = results
context.variables.update(str(results[0]))
if results[0].content is not None:
context.variables.update(str(results[0]))
# TODO: most of this will be deleted once context is gone, just AIResponse object is then returned.
chat_prompt = store_results(chat_prompt, results)
except Exception as exc:
@@ -207,93 +292,36 @@ class KernelFunction(KernelFunctionBase):
chat_prompt_template=function_config.prompt_template if function_config.has_chat_prompt else None,
)
@property
def name(self) -> str:
return self._name
@property
def plugin_name(self) -> str:
return self._plugin_name
@property
def description(self) -> str:
return self._description
@property
def parameters(self) -> List[ParameterView]:
return self._parameters
@property
def is_semantic(self) -> bool:
return self._is_semantic
@property
def is_native(self) -> bool:
return not self._is_semantic
@property
def prompt_execution_settings(self) -> PromptExecutionSettings:
return self._ai_prompt_execution_settings
def __init__(
self,
delegate_type: DelegateTypes,
delegate_function: Callable[..., Any],
parameters: List[ParameterView],
description: str,
plugin_name: str,
function_name: str,
is_semantic: bool,
log: Optional[Any] = None,
delegate_stream_function: Optional[Callable[..., Any]] = None,
**kwargs: Dict[str, Any],
) -> None:
super().__init__()
if log:
logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.")
self._delegate_type = delegate_type
self._function = delegate_function
self._parameters = parameters
self._description = description
self._plugin_name = plugin_name
self._name = function_name
self._is_semantic = is_semantic
self._stream_function = delegate_stream_function
self._plugin_collection = None
self._ai_service = None
self._ai_prompt_execution_settings = PromptExecutionSettings()
self._chat_prompt_template = kwargs.get("chat_prompt_template", None)
def set_default_plugin_collection(self, plugins: "KernelPluginCollection") -> "KernelFunction":
self._plugin_collection = plugins
self.plugins = plugins
return self
def set_ai_service(self, ai_service: Callable[[], TextCompletionClientBase]) -> "KernelFunction":
if ai_service is None:
raise ValueError("AI LLM service factory cannot be `None`")
self._verify_is_semantic()
self._ai_service = ai_service()
self.ai_service = ai_service()
return self
def set_chat_service(self, chat_service: Callable[[], ChatCompletionClientBase]) -> "KernelFunction":
if chat_service is None:
raise ValueError("Chat LLM service factory cannot be `None`")
self._verify_is_semantic()
self._ai_service = chat_service()
self.ai_service = chat_service()
return self
def set_ai_configuration(self, settings: PromptExecutionSettings) -> "KernelFunction":
if settings is None:
raise ValueError("AI LLM request settings cannot be `None`")
self._verify_is_semantic()
self._ai_prompt_execution_settings = settings
self.prompt_execution_settings = settings
return self
def set_chat_configuration(self, settings: PromptExecutionSettings) -> "KernelFunction":
if settings is None:
raise ValueError("Chat LLM request settings cannot be `None`")
self._verify_is_semantic()
self._ai_prompt_execution_settings = settings
self.prompt_execution_settings = settings
return self
def describe(self) -> FunctionView:
@@ -302,7 +330,7 @@ class KernelFunction(KernelFunctionBase):
plugin_name=self.plugin_name,
description=self.description,
is_semantic=self.is_semantic,
parameters=self._parameters,
parameters=self.parameters,
)
async def __call__(
@@ -374,7 +402,7 @@ class KernelFunction(KernelFunctionBase):
context = KernelContext(
variables=ContextVariables("") if variables is None else variables,
memory=memory if memory is not None else NullMemory.instance,
plugins=self._plugin_collection,
plugins=self.plugins,
)
else:
# If context is passed, we need to merge the variables
@@ -398,7 +426,7 @@ class KernelFunction(KernelFunctionBase):
async def _invoke_semantic(self, context: "KernelContext", settings: PromptExecutionSettings, **kwargs):
self._verify_is_semantic()
self._ensure_context_has_plugins(context)
new_context = await self._function(self._ai_service, settings or self._ai_prompt_execution_settings, context)
new_context = await self.function(self.ai_service, settings or self.prompt_execution_settings, context)
context.variables.merge_or_overwrite(new_context.variables)
return context
@@ -407,16 +435,16 @@ class KernelFunction(KernelFunctionBase):
self._ensure_context_has_plugins(context)
delegate = DelegateHandlers.get_handler(self._delegate_type)
delegate = DelegateHandlers.get_handler(self.delegate_type)
# for python3.9 compatibility (staticmethod is not callable)
if not hasattr(delegate, "__call__"):
delegate = delegate.__func__
new_context = await delegate(self._function, context)
new_context = await delegate(self.function, context)
return new_context
def _verify_is_semantic(self) -> None:
if self._is_semantic:
if self.is_semantic:
return
logger.error("The function is not semantic")
@@ -426,7 +454,7 @@ class KernelFunction(KernelFunctionBase):
)
def _verify_is_native(self) -> None:
if not self._is_semantic:
if not self.is_semantic:
return
logger.error("The function is not native")
@@ -449,7 +477,7 @@ class KernelFunction(KernelFunctionBase):
context = KernelContext(
variables=ContextVariables("") if variables is None else variables,
memory=memory if memory is not None else NullMemory.instance,
plugins=self._plugin_collection,
plugins=self.plugins,
)
else:
# If context is passed, we need to merge the variables
@@ -479,8 +507,8 @@ class KernelFunction(KernelFunctionBase):
async def _invoke_semantic_stream(self, context, settings):
self._verify_is_semantic()
self._ensure_context_has_plugins(context)
async for stream_msg in self._stream_function(
self._ai_service, settings or self._ai_prompt_execution_settings, context
async for stream_msg in self.stream_function(
self.ai_service, settings or self.prompt_execution_settings, context
):
yield stream_msg
@@ -495,7 +523,7 @@ class KernelFunction(KernelFunctionBase):
delegate = delegate.__func__
completion = ""
async for partial in delegate(self._function, context):
async for partial in delegate(self.function, context):
completion += partial
yield partial
@@ -505,7 +533,7 @@ class KernelFunction(KernelFunctionBase):
if context.plugins is not None:
return
context.plugins = self._plugin_collection
context.plugins = self.plugins
def _trace_function_type_Call(self, type: Enum) -> None:
logger.debug(f"Executing function type {type}: {type.name}")
@@ -1,142 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import abstractmethod
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import (
TextCompletionClientBase,
)
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.plugin_definition.function_view import FunctionView
if TYPE_CHECKING:
from semantic_kernel.orchestration.kernel_context import KernelContext
class KernelFunctionBase(KernelBaseModel):
@property
@abstractmethod
def name(self) -> str:
"""
Name of the function.
The name is used by the plugin collection and in
prompt templates; e.g., {{pluginName.functionName}}
"""
pass
@property
@abstractmethod
def plugin_name(self) -> str:
"""
Name of the plugin that contains this function.
The name is used by the plugin collection and in
prompt templates; e.g., {{pluginName.functionName}}"""
pass
@property
@abstractmethod
def description(self) -> str:
"""
Function description.
The description is used in combination with embeddings
when searching for relevant functions."""
pass
@property
@abstractmethod
def is_semantic(self) -> bool:
"""
Whether the function is semantic.
IMPORTANT: native functions might use semantic functions
internally, so when this property is False, executing
the function might still involve AI calls.
"""
pass
@property
@abstractmethod
def is_native(self) -> bool:
"""
Whether the function is native.
IMPORTANT: native functions might use semantic functions
internally, so when this property is True, executing
the function might still involve AI calls.
"""
pass
@property
@abstractmethod
def prompt_execution_settings(self) -> PromptExecutionSettings:
"""AI service settings"""
pass
@abstractmethod
def describe() -> FunctionView:
"""
Returns a description of the function,
including its parameters
Returns:
FunctionView -- The function description.
"""
pass
@abstractmethod
async def invoke(
self,
input: Optional[str] = None,
variables: ContextVariables = None,
context: Optional["KernelContext"] = None,
memory: Optional[SemanticTextMemoryBase] = None,
settings: Optional[PromptExecutionSettings] = None,
**kwargs: Dict[str, Any],
) -> "KernelContext":
"""
Invokes the function with an explicit string input
Keyword Arguments:
input {str} -- The explicit string input (default: {None})
variables {ContextVariables} -- The custom input
context {KernelContext} -- The context to use
memory: {SemanticTextMemoryBase} -- The memory to use
settings {PromptExecutionSettings} -- LLM completion settings
Returns:
KernelContext -- The updated context, potentially a new one if
context switching is implemented.
"""
pass
@abstractmethod
def set_ai_service(self, service_factory: Callable[[], TextCompletionClientBase]) -> "KernelFunctionBase":
"""
Sets the AI service used by the semantic function, passing in a factory
method. The factory allows us to lazily instantiate the client and to
properly handle its disposal
Arguments:
service_factory -- AI service factory
Returns:
KernelFunctionBase -- The function instance
"""
pass
@abstractmethod
def set_ai_configuration(self, settings: PromptExecutionSettings) -> "KernelFunctionBase":
"""
Sets the AI completion settings used with LLM requests
Arguments:
settings {PromptExecutionSettings} -- LLM completion settings
Returns:
KernelFunctionBase -- The function instance
"""
pass
@@ -11,7 +11,7 @@ import regex
from semantic_kernel import Kernel
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.planning.action_planner.action_planner_config import (
ActionPlannerConfig,
)
@@ -37,7 +37,7 @@ class ActionPlanner:
config: ActionPlannerConfig
_stop_sequence: str = "#END-OF-PLAN"
_planner_function: KernelFunctionBase
_planner_function: KernelFunction
_kernel: Kernel
_prompt_template: str
+9 -9
View File
@@ -17,7 +17,7 @@ from semantic_kernel.memory.null_memory import NullMemory
from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.plugin_definition.function_view import FunctionView
from semantic_kernel.plugin_definition.kernel_plugin_collection import (
KernelPluginCollection,
@@ -26,10 +26,10 @@ from semantic_kernel.plugin_definition.kernel_plugin_collection import (
logger: logging.Logger = logging.getLogger(__name__)
class Plan(KernelFunctionBase):
class Plan:
_state: ContextVariables = PrivateAttr()
_steps: List["Plan"] = PrivateAttr()
_function: KernelFunctionBase = PrivateAttr()
_function: KernelFunction = PrivateAttr()
_parameters: ContextVariables = PrivateAttr()
_outputs: List[str] = PrivateAttr()
_has_next_step: bool = PrivateAttr()
@@ -98,7 +98,7 @@ class Plan(KernelFunctionBase):
parameters: Optional[ContextVariables] = None,
outputs: Optional[List[str]] = None,
steps: Optional[List["Plan"]] = None,
function: Optional[KernelFunctionBase] = None,
function: Optional[KernelFunction] = None,
) -> None:
super().__init__()
self._name = "" if name is None else name
@@ -122,7 +122,7 @@ class Plan(KernelFunctionBase):
return cls(description=goal, plugin_name=cls.__name__)
@classmethod
def from_function(cls, function: KernelFunctionBase) -> "Plan":
def from_function(cls, function: KernelFunction) -> "Plan":
plan = cls()
plan.set_function(function)
return plan
@@ -184,11 +184,11 @@ class Plan(KernelFunctionBase):
def set_ai_configuration(
self,
settings: PromptExecutionSettings,
) -> KernelFunctionBase:
) -> KernelFunction:
if self._function is not None:
self._function.set_ai_configuration(settings)
def set_ai_service(self, service: Callable[[], TextCompletionClientBase]) -> KernelFunctionBase:
def set_ai_service(self, service: Callable[[], TextCompletionClientBase]) -> KernelFunction:
if self._function is not None:
self._function.set_ai_service(service)
@@ -215,7 +215,7 @@ class Plan(KernelFunctionBase):
return plan
def add_steps(self, steps: Union[List["Plan"], List[KernelFunctionBase]]) -> None:
def add_steps(self, steps: Union[List["Plan"], List[KernelFunction]]) -> None:
for step in steps:
if type(step) is Plan:
self._steps.append(step)
@@ -233,7 +233,7 @@ class Plan(KernelFunctionBase):
new_step.set_function(step)
self._steps.append(new_step)
def set_function(self, function: KernelFunctionBase) -> None:
def set_function(self, function: KernelFunction) -> None:
self._function = function
self._name = function.name
self._plugin_name = function.plugin_name
@@ -25,7 +25,7 @@ from semantic_kernel.semantic_functions.semantic_function_config import (
if TYPE_CHECKING:
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
SEQUENTIAL_PLANNER_DEFAULT_DESCRIPTION = (
"Given a request or command or goal generate a step by step plan to "
@@ -47,7 +47,7 @@ class SequentialPlanner:
config: SequentialPlannerConfig
_context: "KernelContext"
_function_flow_function: "KernelFunctionBase"
_function_flow_function: "KernelFunction"
def __init__(self, kernel: Kernel, config: SequentialPlannerConfig = None, prompt: str = None):
assert isinstance(kernel, Kernel)
@@ -7,7 +7,7 @@ from xml.etree import ElementTree as ET
from semantic_kernel.kernel_exception import KernelException
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.planning.plan import Plan
from semantic_kernel.planning.planning_exception import PlanningException
@@ -23,8 +23,8 @@ class SequentialPlanParser:
@staticmethod
def get_plugin_function(
context: KernelContext,
) -> Callable[[str, str], Optional[KernelFunctionBase]]:
def function(plugin_name: str, function_name: str) -> Optional[KernelFunctionBase]:
) -> Callable[[str, str], Optional[KernelFunction]]:
def function(plugin_name: str, function_name: str) -> Optional[KernelFunction]:
try:
return context.plugins[plugin_name][function_name]
except KeyError:
@@ -38,7 +38,7 @@ class SequentialPlanParser:
def to_plan_from_xml(
xml_string: str,
goal: str,
get_plugin_function: Callable[[str, str], Optional[KernelFunctionBase]],
get_plugin_function: Callable[[str, str], Optional[KernelFunction]],
allow_missing_functions: bool = False,
):
xml_string = "<xml>" + xml_string + "</xml>"
@@ -30,7 +30,7 @@ from semantic_kernel.semantic_functions.semantic_function_config import (
)
if TYPE_CHECKING:
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
logger: logging.Logger = logging.getLogger(__name__)
@@ -65,7 +65,7 @@ def is_null_or_empty(value: str) -> bool:
class StepwisePlanner:
config: StepwisePlannerConfig
_context: "KernelContext"
_function_flow_function: "KernelFunctionBase"
_function_flow_function: "KernelFunction"
def __init__(
self,
@@ -373,7 +373,7 @@ class StepwisePlanner:
function_name: str,
prompt_template: str,
config: PromptTemplateConfig = None,
) -> "KernelFunctionBase":
) -> "KernelFunction":
template = PromptTemplate(prompt_template, kernel.prompt_template_engine, config)
function_config = SemanticFunctionConfig(config, template)
@@ -11,7 +11,7 @@ else:
from pydantic import Field, StringConstraints
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
class KernelPlugin(KernelBaseModel):
@@ -22,24 +22,22 @@ class KernelPlugin(KernelBaseModel):
name (str): The name of the plugin. The name can be upper/lower
case letters and underscores.
description (str): The description of the plugin.
functions (Dict[str, KernelFunctionBase]): The functions in the plugin,
functions (Dict[str, KernelFunction]): The functions in the plugin,
indexed by their name.
"""
name: Annotated[str, StringConstraints(pattern=r"^[A-Za-z_]+$", min_length=1)]
description: Optional[str] = Field(default=None)
functions: Optional[Dict[str, "KernelFunctionBase"]] = Field(default_factory=dict)
functions: Optional[Dict[str, KernelFunction]] = Field(default_factory=dict)
def __init__(
self, name: str, description: Optional[str] = None, functions: Optional[List[KernelFunctionBase]] = None
):
def __init__(self, name: str, description: Optional[str] = None, functions: Optional[List[KernelFunction]] = None):
"""
Initialize a new instance of the KernelPlugin class
Args:
name (str): The name of the plugin.
description (Optional[str]): The description of the plugin.
functions (List[KernelFunctionBase]): The functions in the plugin.
functions (List[KernelFunction]): The functions in the plugin.
Raises:
ValueError: If the functions list contains duplicate function names.
@@ -74,7 +72,7 @@ class KernelPlugin(KernelBaseModel):
"""
return function_name in self.functions.keys()
def __getitem__(self, name: str) -> "KernelFunctionBase":
def __getitem__(self, name: str) -> "KernelFunction":
"""Define the [] operator for the plugin
Args:
@@ -92,13 +90,13 @@ class KernelPlugin(KernelBaseModel):
@classmethod
def from_functions(
cls, functions: List["KernelFunctionBase"], plugin_name: str, description: Optional[str] = None
cls, functions: List["KernelFunction"], plugin_name: str, description: Optional[str] = None
) -> "KernelPlugin":
"""
Creates a KernelPlugin from a KernelFunctionBase instance.
Creates a KernelPlugin from a KernelFunction instance.
Args:
functions (List[KernelFunctionBase]): The functions to create the plugin from.
functions (List[KernelFunction]): The functions to create the plugin from.
plugin_name (Optional[str]): The name of the plugin. If not specified,
the name of the function will be used.
description (Optional[str]): The description of the plugin.
@@ -5,7 +5,7 @@ from typing import Any, Dict, Iterable, List, Optional, TypeVar, Union
from pydantic import Field
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.plugin_definition.functions_view import FunctionsView
from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin
@@ -76,13 +76,13 @@ class KernelPluginCollection(KernelBaseModel):
raise ValueError(f"Plugin with name {plugin.name} already exists")
self.plugins[plugin.name] = plugin
def add_plugin_from_functions(self, plugin_name: str, functions: List["KernelFunctionBase"]) -> None:
def add_plugin_from_functions(self, plugin_name: str, functions: List[KernelFunction]) -> None:
"""
Add a function to a new plugin in the collection
Args:
plugin_name (str): The name of the plugin to create.
functions (List[KernelFunctionBase]): The functions to add to the plugin.
functions (List[KernelFunction]): The functions to add to the plugin.
Raises:
ValueError: If the function or plugin_name is None or invalid.
@@ -95,12 +95,12 @@ class KernelPluginCollection(KernelBaseModel):
plugin = KernelPlugin.from_functions(plugin_name=plugin_name, functions=functions)
self.plugins[plugin_name] = plugin
def add_functions_to_plugin(self, functions: List["KernelFunctionBase"], plugin_name: str) -> None:
def add_functions_to_plugin(self, functions: List[KernelFunction], plugin_name: str) -> None:
"""
Add functions to a plugin in the collection
Args:
functions (List[KernelFunctionBase]): The function to add to the plugin.
functions (List[KernelFunction]): The function to add to the plugin.
plugin_name (str): The name of the plugin to add the function to.
Raises:
@@ -201,7 +201,7 @@ class KernelPluginCollection(KernelBaseModel):
for _, function in plugin.functions.items():
if include_semantic and function.is_semantic:
result.add_function(function.describe())
elif include_native and function.is_native:
elif include_native and not function.is_semantic:
result.add_function(function.describe())
return result
@@ -115,7 +115,12 @@ class ChatPromptTemplate(PromptTemplate, Generic[ChatMessageT]):
]:
self.add_user_message(message=self.template)
await asyncio.gather(*[message.render_message(context) for message in self.messages])
return [message.as_dict() for message in self.messages]
# Don't resend the assistant + tool_calls message as it will error
return [
message.as_dict()
for message in self.messages
if not (message.role == "assistant" and hasattr(message, "tool_calls"))
]
def dump_messages(self) -> List[Dict[str, str]]:
"""Return the messages as a list of dicts with role, content, name and function_call."""
@@ -5,7 +5,7 @@ from typing import Any, List, Optional, Tuple
import pydantic as pdt
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.plugin_definition.kernel_plugin_collection import KernelPluginCollection
from semantic_kernel.template_engine.blocks.block import Block
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
@@ -115,7 +115,7 @@ class CodeBlock(Block):
def _get_function_from_plugin_collection(
self, plugins: KernelPluginCollection, f_block: FunctionIdBlock
) -> Optional[KernelFunctionBase]:
) -> Optional[KernelFunction]:
"""
Get the function from the plugin collection
@@ -9,13 +9,13 @@ def test_can_be_instantiated():
def test_can_be_imported():
kernel = sk.Kernel()
assert kernel.import_plugin(TextPlugin(), "text_plugin")
assert kernel.plugins["text_plugin"]["trim"].is_native
assert not kernel.plugins["text_plugin"]["trim"].is_semantic
def test_can_be_imported_with_name():
kernel = sk.Kernel()
assert kernel.import_plugin(TextPlugin(), "text")
assert kernel.plugins["text"]["trim"].is_native
assert not kernel.plugins["text"]["trim"].is_semantic
def test_can_trim():
@@ -5,7 +5,7 @@ import pytest
from semantic_kernel import Kernel
from semantic_kernel.kernel_exception import KernelException
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.plugin_definition.kernel_function_decorator import kernel_function
@@ -24,7 +24,7 @@ async def test_register_valid_native_function():
registered_func = kernel.register_native_function("TestPlugin", decorated_native_function)
assert isinstance(registered_func, KernelFunctionBase)
assert isinstance(registered_func, KernelFunction)
assert kernel.plugins["TestPlugin"]["getLightStatus"] == registered_func
func_result = await registered_func.invoke("testtest")
assert func_result.result == "test"
@@ -30,17 +30,17 @@ def test_init_native_function_with_input_description():
native_function = KernelFunction.from_native_method(mock_method, "MockPlugin")
assert native_function._function == mock_method
assert native_function._parameters[0].name == "input"
assert native_function._parameters[0].description == "Mock input description"
assert native_function._parameters[0].default_value == "default_input_value"
assert native_function._parameters[0].type_ == "string"
assert native_function._parameters[0].required is False
assert native_function._parameters[1].name == "param1"
assert native_function._parameters[1].description == "Param 1 description"
assert native_function._parameters[1].default_value == "default_param1_value"
assert native_function._parameters[1].type_ == "string"
assert native_function._parameters[1].required is False
assert native_function.function == mock_method
assert native_function.parameters[0].name == "input"
assert native_function.parameters[0].description == "Mock input description"
assert native_function.parameters[0].default_value == "default_input_value"
assert native_function.parameters[0].type_ == "string"
assert native_function.parameters[0].required is False
assert native_function.parameters[1].name == "param1"
assert native_function.parameters[1].description == "Param 1 description"
assert native_function.parameters[1].default_value == "default_param1_value"
assert native_function.parameters[1].type_ == "string"
assert native_function.parameters[1].required is False
def test_init_native_function_without_input_description():
@@ -63,12 +63,12 @@ def test_init_native_function_without_input_description():
native_function = KernelFunction.from_native_method(mock_method, "MockPlugin")
assert native_function._function == mock_method
assert native_function._parameters[0].name == "param1"
assert native_function._parameters[0].description == "Param 1 description"
assert native_function._parameters[0].default_value == "default_param1_value"
assert native_function._parameters[0].type_ == "string"
assert native_function._parameters[0].required is True
assert native_function.function == mock_method
assert native_function.parameters[0].name == "param1"
assert native_function.parameters[0].description == "Param 1 description"
assert native_function.parameters[0].default_value == "default_param1_value"
assert native_function.parameters[0].type_ == "string"
assert native_function.parameters[0].required is True
def test_init_native_function_from_kernel_function_decorator():
@@ -89,12 +89,12 @@ def test_init_native_function_from_kernel_function_decorator():
native_function = KernelFunction.from_native_method(decorated_function, "MockPlugin")
assert native_function._function == decorated_function
assert native_function._parameters[0].name == "input"
assert native_function._parameters[0].description == "Test input description"
assert native_function._parameters[0].default_value == "test_default_value"
assert native_function._parameters[0].type_ == "string"
assert native_function._parameters[0].required is False
assert native_function.function == decorated_function
assert native_function.parameters[0].name == "input"
assert native_function.parameters[0].description == "Test input description"
assert native_function.parameters[0].default_value == "test_default_value"
assert native_function.parameters[0].type_ == "string"
assert native_function.parameters[0].required is False
def test_init_native_function_from_kernel_function_decorator_defaults():
@@ -110,5 +110,5 @@ def test_init_native_function_from_kernel_function_decorator_defaults():
native_function = KernelFunction.from_native_method(decorated_function, "MockPlugin")
assert native_function._function == decorated_function
assert len(native_function._parameters) == 0
assert native_function.function == decorated_function
assert len(native_function.parameters) == 0
@@ -4,10 +4,11 @@ from unittest.mock import MagicMock, Mock
import pytest
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.memory.semantic_text_memory import SemanticTextMemoryBase
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.planning import ActionPlanner
from semantic_kernel.planning.action_planner.action_planner_config import (
ActionPlannerConfig,
@@ -21,12 +22,14 @@ from semantic_kernel.plugin_definition.kernel_plugin_collection import (
)
def create_mock_function(function_view: FunctionView) -> Mock(spec=KernelFunctionBase):
mock_function = Mock(spec=KernelFunctionBase)
def create_mock_function(function_view: FunctionView):
mock_function = Mock(spec=KernelFunction)
mock_function.describe.return_value = function_view
mock_function.name = function_view.name
mock_function.plugin_name = function_view.plugin_name
mock_function.is_semantic = function_view.is_semantic
mock_function.description = function_view.description
mock_function.prompt_execution_settings = PromptExecutionSettings()
return mock_function
@@ -55,7 +58,7 @@ async def test_plan_creation():
)
kernel = Mock(spec=Kernel)
mock_function = Mock(spec=KernelFunctionBase)
mock_function = Mock(spec=KernelFunction)
memory = Mock(spec=SemanticTextMemoryBase)
plugins = KernelPluginCollection()
@@ -186,7 +189,7 @@ async def test_empty_goal_throw():
goal = ""
kernel = Mock(spec=Kernel)
mock_function = Mock(spec=KernelFunctionBase)
mock_function = Mock(spec=KernelFunction)
memory = Mock(spec=SemanticTextMemoryBase)
plugins = MagicMock(spec=KernelPluginCollection)
@@ -4,11 +4,12 @@ from unittest.mock import Mock
import pytest
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.kernel import Kernel
from semantic_kernel.memory.semantic_text_memory import SemanticTextMemoryBase
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.planning.planning_exception import PlanningException
from semantic_kernel.planning.sequential_planner.sequential_planner import (
SequentialPlanner,
@@ -22,10 +23,13 @@ from semantic_kernel.plugin_definition.kernel_plugin_collection import (
def create_mock_function(function_view: FunctionView):
mock_function = Mock(spec=KernelFunctionBase)
mock_function = Mock(spec=KernelFunction)
mock_function.describe.return_value = function_view
mock_function.name = function_view.name
mock_function.plugin_name = function_view.plugin_name
mock_function.is_semantic = function_view.is_semantic
mock_function.description = function_view.description
mock_function.prompt_execution_settings = PromptExecutionSettings()
return mock_function
@@ -77,7 +81,7 @@ async def test_it_can_create_plan(goal):
return_context.variables.update(plan_string)
mock_function_flow_function = Mock(spec=KernelFunctionBase)
mock_function_flow_function = Mock(spec=KernelFunction)
mock_function_flow_function.invoke.return_value = return_context
kernel.plugins = plugins
@@ -130,7 +134,7 @@ async def test_invalid_xml_throws():
context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins)
mock_function_flow_function = Mock(spec=KernelFunctionBase)
mock_function_flow_function = Mock(spec=KernelFunction)
mock_function_flow_function.invoke.return_value = return_context
kernel.plugins = plugins
@@ -4,8 +4,9 @@ from unittest.mock import Mock
import pytest
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.kernel import Kernel
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.planning.planning_exception import PlanningException
from semantic_kernel.planning.sequential_planner.sequential_planner_parser import (
SequentialPlanParser,
@@ -15,12 +16,14 @@ from semantic_kernel.plugin_definition.functions_view import FunctionsView
from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin
def create_mock_function(function_view: FunctionView) -> KernelFunctionBase:
mock_function = Mock(spec=KernelFunctionBase)
def create_mock_function(function_view: FunctionView) -> KernelFunction:
mock_function = Mock(spec=KernelFunction)
mock_function.describe.return_value = function_view
mock_function.name = function_view.name
mock_function.plugin_name = function_view.plugin_name
mock_function.description = function_view.description
mock_function.is_semantic = function_view.is_semantic
mock_function.prompt_execution_settings = PromptExecutionSettings()
return mock_function
@@ -97,7 +97,7 @@ def test_create_plan_with_name_and_function():
assert plan.function is test_function
assert type(plan.parameters) is ContextVariables
assert plan.is_semantic is test_function.is_semantic
assert plan.is_native is test_function.is_native
assert plan.is_native is not test_function.is_semantic
assert plan.prompt_execution_settings == test_function.prompt_execution_settings
assert plan.has_next_step is False
assert plan.next_step_index == 0
@@ -185,7 +185,7 @@ def test_add_step_to_plan():
assert plan.function is test_function1
assert type(plan.parameters) is ContextVariables
assert plan.is_semantic is test_function1.is_semantic
assert plan.is_native is test_function1.is_native
assert plan.is_native is not test_function1.is_semantic
assert plan.prompt_execution_settings == test_function1.prompt_execution_settings
assert plan.has_next_step is True
assert plan.next_step_index == 0
@@ -45,7 +45,7 @@ class TestCodeBlock:
function = KernelFunction(
delegate_type=DelegateTypes.InKernelContext,
delegate_function=invoke,
plugin_name="",
plugin_name="test",
function_name="funcName",
description="",
parameters=[],
@@ -240,7 +240,7 @@ class TestCodeBlock:
function = KernelFunction(
delegate_type=DelegateTypes.InKernelContext,
delegate_function=invoke,
plugin_name="",
plugin_name="test",
function_name="funcName",
description="",
parameters=[],
@@ -302,7 +302,7 @@ class TestCodeBlock:
function = KernelFunction(
delegate_type=DelegateTypes.InKernelContext,
delegate_function=invoke,
plugin_name="",
plugin_name="test",
function_name="funcName",
description="",
parameters=[],
@@ -354,7 +354,7 @@ class TestCodeBlock:
function = KernelFunction(
delegate_type=DelegateTypes.InKernelContext,
delegate_function=invoke,
plugin_name="",
plugin_name="test",
function_name="funcName",
description="",
parameters=[],
@@ -126,7 +126,7 @@ async def test_it_renders_code_using_input(
def my_function(cx: KernelContext) -> str:
return f"F({cx.variables.input})"
func = KernelFunction.from_native_method(my_function)
func = KernelFunction.from_native_method(my_function, "test")
assert func is not None
variables.update("INPUT-BAR")
@@ -146,7 +146,7 @@ async def test_it_renders_code_using_variables(
def my_function(cx: KernelContext) -> str:
return f"F({cx.variables.input})"
func = KernelFunction.from_native_method(my_function)
func = KernelFunction.from_native_method(my_function, "test")
assert func is not None
variables.set("myVar", "BAR")
@@ -166,7 +166,7 @@ async def test_it_renders_code_using_variables_async(
async def my_function(cx: KernelContext) -> str:
return cx.variables.input
func = KernelFunction.from_native_method(my_function)
func = KernelFunction.from_native_method(my_function, "test")
assert func is not None
variables.set("myVar", "BAR")
+3 -3
View File
@@ -5,14 +5,14 @@ from unittest.mock import AsyncMock, Mock
import pytest
from semantic_kernel import Kernel
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.plugin_definition.function_view import FunctionView
from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin
def create_mock_function(name) -> KernelFunctionBase:
def create_mock_function(name) -> KernelFunction:
function_view = FunctionView(name, "SummarizePlugin", "Summarize an input", [], True, True)
mock_function = Mock(spec=KernelFunctionBase)
mock_function = Mock(spec=KernelFunction)
mock_function.describe.return_value = function_view
mock_function.name = function_view.name
mock_function.plugin_name = function_view.plugin_name
+1 -3
View File
@@ -4,7 +4,6 @@ import pytest
import typing_extensions as te
from pydantic import Field, Json
from semantic_kernel import KernelFunctionBase
from semantic_kernel.core_plugins.conversation_summary_plugin import (
ConversationSummaryPlugin,
)
@@ -99,7 +98,7 @@ def kernel_factory() -> t.Callable[[t.Type[_Serializable]], _Serializable]:
def my_function(cx: KernelContext) -> str:
return f"F({cx.variables.input})"
return KernelFunction.from_native_method(my_function)
return KernelFunction.from_native_method(my_function, "plugin")
def create_context_variables() -> ContextVariables:
"""Return a context variables object."""
@@ -177,7 +176,6 @@ PROTOCOLS = [
BASE_CLASSES = [
SemanticTextMemoryBase,
KernelFunctionBase,
]
STATELESS_CLASSES = [