mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
### Motivation and Context <!-- 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. --> Customers are having questions about parallel function calling and whether the kernel will execute functions in parallel. ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Add a sample to show that the kernel will execute functions in parallel when the model returns parallel function calls. ### 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]>
103 lines
4.1 KiB
Python
103 lines
4.1 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
import time
|
|
from typing import Annotated
|
|
|
|
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.functions.kernel_arguments import KernelArguments
|
|
from semantic_kernel.functions.kernel_function_decorator import kernel_function
|
|
from semantic_kernel.kernel import Kernel
|
|
|
|
# This sample demonstrates how the kernel will execute functions in parallel.
|
|
# The output of this sample should look similar to the following:
|
|
#
|
|
# [2024-09-11 10:15:35.070 INFO] processing 2 tool calls in parallel.
|
|
# The employee with ID 123 is named John Doe and they are 30 years old.
|
|
# Time elapsed: 11.96s
|
|
#
|
|
# The mock plugin simulates a long-running operation to fetch the employee's name and age.
|
|
# When you run the sample, you should see the total execution time is less than the sum
|
|
# of the two function calls because the kernel executes the functions in parallel.
|
|
|
|
# This concept example shows how to handle both streaming and non-streaming responses
|
|
# To toggle the behavior, set the following flag accordingly:
|
|
stream = True
|
|
|
|
|
|
def set_up_logging():
|
|
"""Set up logging to verify the kernel execute the functions in parallel"""
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(logging.INFO)
|
|
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
handler.setLevel(logging.INFO)
|
|
handler.setFormatter(
|
|
logging.Formatter("[%(asctime)s.%(msecs)03d %(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"),
|
|
)
|
|
# Print only the logs from the chat completion client to reduce the output of the sample
|
|
handler.addFilter(lambda record: record.name == "semantic_kernel.connectors.ai.chat_completion_client_base")
|
|
|
|
root_logger.addHandler(handler)
|
|
|
|
|
|
class EmployeePlugin:
|
|
"""A mock plugin to simulate a plugin that fetches employee information"""
|
|
|
|
@kernel_function(name="get_name", description="Find the name of the employee by the id")
|
|
async def get_name(
|
|
self, id: Annotated[str, "The ID of the employee"]
|
|
) -> Annotated[str, "The name of the employee"]:
|
|
# Simulate a long-running operation
|
|
await asyncio.sleep(10)
|
|
return "John Doe"
|
|
|
|
@kernel_function(name="get_age", description="Get the age of the employee by the id")
|
|
async def get_age(self, id: Annotated[str, "The ID of the employee"]) -> Annotated[int, "The age of the employee"]:
|
|
# Simulate a long-running operation
|
|
await asyncio.sleep(10)
|
|
return 30
|
|
|
|
|
|
async def main():
|
|
kernel = Kernel()
|
|
kernel.add_service(OpenAIChatCompletion(service_id="open_ai"))
|
|
kernel.add_plugin(EmployeePlugin(), "EmployeePlugin")
|
|
|
|
# With this query, the model will call the get_name and get_age functions in parallel.
|
|
# Note that for certain queries, the model may choose to call the functions sequentially.
|
|
# For example, if the available functions are `get_email_by_id` and `get_name_by_email`,
|
|
# the model will not be able to call them in parallel because the second function depends
|
|
# on the result of the first function.
|
|
query = "What is the name and age of the employee of ID 123?"
|
|
arguments = KernelArguments(
|
|
settings=PromptExecutionSettings(
|
|
# Set the function_choice_behavior to auto to let the model
|
|
# decide which function to use, and let the kernel automatically
|
|
# execute the functions.
|
|
function_choice_behavior=FunctionChoiceBehavior.Auto(),
|
|
)
|
|
)
|
|
|
|
start = time.perf_counter()
|
|
|
|
if stream:
|
|
async for result in kernel.invoke_prompt_stream(query, arguments=arguments):
|
|
print(str(result[0]), end="")
|
|
print()
|
|
else:
|
|
result = await kernel.invoke_prompt(query, arguments=arguments)
|
|
print(result)
|
|
|
|
print(f"Time elapsed: {time.perf_counter() - start:.2f}s")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
set_up_logging()
|
|
|
|
asyncio.run(main())
|