Skip to content

Python: Support Auto Function Invocation Filter for AzureAIAgent and OpenAIAssistantAgent #11460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions python/samples/concepts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#### [Azure AI Agent](../../semantic_kernel/agents/azure_ai/azure_ai_agent.py)

- [Azure AI Agent as Kernel Function](./agents/azure_ai_agent/azure_ai_agent_as_kernel_function.py)
- [Azure AI Agent with Auto Function Invocation Filter Streaming](./agents/azure_ai_agent/azure_ai_agent_auto_func_invocation_filter_streaming.py)
- [Azure AI Agent with Auto Function Invocation Filter](./agents/azure_ai_agent/azure_ai_agent_auto_func_invocation_filter.py)
- [Azure AI Agent with Azure AI Search](./agents/azure_ai_agent/azure_ai_agent_azure_ai_search.py)
- [Azure AI Agent File Manipulation](./agents/azure_ai_agent/azure_ai_agent_file_manipulation.py)
- [Azure AI Agent Prompt Templating](./agents/azure_ai_agent/azure_ai_agent_prompt_templating.py)
Expand Down Expand Up @@ -50,6 +52,8 @@

#### [OpenAI Assistant Agent](../../semantic_kernel/agents/open_ai/open_ai_assistant_agent.py)

- [OpenAI Assistant Auto Function Invocation Filter Streaming](./agents/openai_assistant/openai_assistant_auto_func_invocation_filter_streaming.py)
- [OpenAI Assistant Auto Function Invocation Filter](./agents/openai_assistant/openai_assistant_auto_func_invocation_filter.py)
- [OpenAI Assistant Chart Maker Streaming](./agents/openai_assistant/openai_assistant_chart_maker_streaming.py)
- [OpenAI Assistant Chart Maker](./agents/openai_assistant/openai_assistant_chart_maker.py)
- [OpenAI Assistant File Manipulation Streaming](./agents/openai_assistant/openai_assistant_file_manipulation_streaming.py)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Copyright (c) Microsoft. All rights reserved.

import asyncio
from typing import Annotated

from azure.identity.aio import DefaultAzureCredential

from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import (
AutoFunctionInvocationContext,
)
from semantic_kernel.filters.filter_types import FilterTypes
from semantic_kernel.functions.function_result import FunctionResult
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel

"""
The following sample demonstrates how to create an Azure AI agent that answers
user questions. This sample demonstrates the basic steps to create an agent
and simulate a conversation with the agent.

This sample demonstrates how to create a filter that will be called for each
function call in the response. The filter can be used to modify the function
result or to terminate the function call. The filter can also be used to
log the function call or to perform any other action before or after the
function call.
"""


class MenuPlugin:
"""A sample Menu Plugin used for the concept sample."""

@kernel_function(description="Provides a list of specials from the menu.")
def get_specials(self) -> Annotated[str, "Returns the specials from the menu."]:
return """
Special Soup: Clam Chowder
Special Salad: Cobb Salad
Special Drink: Chai Tea
"""

@kernel_function(description="Provides the price of the requested menu item.")
def get_item_price(
self, menu_item: Annotated[str, "The name of the menu item."]
) -> Annotated[str, "Returns the price of the menu item."]:
return "$9.99"


# Define a kernel instance so we can attach the filter to it
kernel = Kernel()


# Define a list to store intermediate steps
intermediate_steps: list[ChatMessageContent] = []


# Define a callback function to handle intermediate step content messages
async def handle_intermediate_steps(message: ChatMessageContent) -> None:
intermediate_steps.append(message)


@kernel.filter(FilterTypes.AUTO_FUNCTION_INVOCATION)
async def auto_function_invocation_filter(context: AutoFunctionInvocationContext, next):
"""A filter that will be called for each function call in the response."""
print("\nAuto function invocation filter")
print(f"Function: {context.function.name}")

# if we don't call next, it will skip this function, and go to the next one
await next(context)
"""
Note: to simply return the unaltered function results, uncomment the `context.terminate = True` line and
comment out the lines starting with `result = context.function_result` through `context.terminate = True`.
context.terminate = True
For this sample, simply setting `context.terminate = True` will return the unaltered function result:

Auto function invocation filter
Function: get_specials
# Assistant: MenuPlugin-get_specials -
Special Soup: Clam Chowder
Special Salad: Cobb Salad
Special Drink: Chai Tea
"""
result = context.function_result
if "menu" in context.function.plugin_name.lower():
print("Altering the Menu plugin function result...\n")
context.function_result = FunctionResult(
function=result.function,
value="We are sold out, sorry!",
)
context.terminate = True


# Simulate a conversation with the agent
USER_INPUTS = ["What's the special food on the menu?", "What should I do then?"]


async def main() -> None:
ai_agent_settings = AzureAIAgentSettings.create()

async with (
DefaultAzureCredential() as creds,
AzureAIAgent.create_client(credential=creds) as client,
):
# 1. Create an agent on the Azure AI agent service
agent_definition = await client.agents.create_agent(
model=ai_agent_settings.model_deployment_name,
name="Host",
instructions="Answer the user's questions about the menu.",
)

# 2. Create a Semantic Kernel agent for the Azure AI agent
agent = AzureAIAgent(
kernel=kernel,
client=client,
definition=agent_definition,
plugins=[MenuPlugin()], # Add the plugin to the agent
)

# 3. Create a thread for the agent
# If no thread is provided, a new thread will be
# created and returned with the initial response
thread: AzureAIAgentThread = None

try:
for user_input in USER_INPUTS:
print(f"# User: {user_input}")
# 4. Invoke the agent with the specified message for response
async for response in agent.invoke(
messages=user_input, thread=thread, on_intermediate_message=handle_intermediate_steps
):
# 5. Print the response
print(f"# {response.name}: {response}")
thread = response.thread
finally:
# 6. Cleanup: Delete the thread and agent
await thread.delete() if thread else None
await client.agents.delete_agent(agent.id)

# Print the intermediate steps
print("\nIntermediate Steps:")
for msg in intermediate_steps:
if any(isinstance(item, FunctionResultContent) for item in msg.items):
for fr in msg.items:
if isinstance(fr, FunctionResultContent):
print(f"Function Result:> {fr.result} for function: {fr.name}")
elif any(isinstance(item, FunctionCallContent) for item in msg.items):
for fcc in msg.items:
if isinstance(fcc, FunctionCallContent):
print(f"Function Call:> {fcc.name} with arguments: {fcc.arguments}")
else:
print(f"{msg.role}: {msg.content}")

"""
Sample Output:

# User: What's the special food on the menu?

Auto function invocation filter
Function: get_specials
Altering the Menu plugin function result...

# Host: I'm sorry, but all the specials on the menu are currently sold out. If there's anything else you're
looking for, please let me know!
# User: What should I do then?
# Host: You might consider ordering from the regular menu items instead. If you need any recommendations or
information about specific items, such as prices or ingredients, feel free to ask!

Intermediate Steps:
Function Call:> MenuPlugin-get_specials with arguments: {}
Function Result:> We are sold out, sorry! for function: MenuPlugin-get_specials
AuthorRole.ASSISTANT: I'm sorry, but all the specials on the menu are currently sold out. If there's anything
else you're looking for, please let me know!
AuthorRole.ASSISTANT: You might consider ordering from the regular menu items instead. If you need any
recommendations or information about specific items, such as prices or ingredients, feel free to ask!
"""


if __name__ == "__main__":
asyncio.run(main())
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Copyright (c) Microsoft. All rights reserved.

import asyncio
from typing import Annotated

from azure.identity.aio import DefaultAzureCredential

from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import (
AutoFunctionInvocationContext,
)
from semantic_kernel.filters.filter_types import FilterTypes
from semantic_kernel.functions.function_result import FunctionResult
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel

"""
The following sample demonstrates how to create an Azure AI agent that answers
user questions. This sample demonstrates the basic steps to create an agent
and simulate a streaming conversation with the agent.

This sample demonstrates how to create a filter that will be called for each
function call in the response. The filter can be used to modify the function
result or to terminate the function call. The filter can also be used to
log the function call or to perform any other action before or after the
function call.
"""


class MenuPlugin:
"""A sample Menu Plugin used for the concept sample."""

@kernel_function(description="Provides a list of specials from the menu.")
def get_specials(self) -> Annotated[str, "Returns the specials from the menu."]:
return """
Special Soup: Clam Chowder
Special Salad: Cobb Salad
Special Drink: Chai Tea
"""

@kernel_function(description="Provides the price of the requested menu item.")
def get_item_price(
self, menu_item: Annotated[str, "The name of the menu item."]
) -> Annotated[str, "Returns the price of the menu item."]:
return "$9.99"


# Define a kernel instance so we can attach the filter to it
kernel = Kernel()


# Define a list to store intermediate steps
intermediate_steps: list[ChatMessageContent] = []


# Define a callback function to handle intermediate step content messages
async def handle_intermediate_steps(message: ChatMessageContent) -> None:
intermediate_steps.append(message)


@kernel.filter(FilterTypes.AUTO_FUNCTION_INVOCATION)
async def auto_function_invocation_filter(context: AutoFunctionInvocationContext, next):
"""A filter that will be called for each function call in the response."""
print("\nAuto function invocation filter")
print(f"Function: {context.function.name}")

# if we don't call next, it will skip this function, and go to the next one
await next(context)
"""
Note: to simply return the unaltered function results, uncomment the `context.terminate = True` line and
comment out the lines starting with `result = context.function_result` through `context.terminate = True`.
context.terminate = True
For this sample, simply setting `context.terminate = True` will return the unaltered function result:

Auto function invocation filter
Function: get_specials
# Assistant: MenuPlugin-get_specials -
Special Soup: Clam Chowder
Special Salad: Cobb Salad
Special Drink: Chai Tea
"""
result = context.function_result
if "menu" in context.function.plugin_name.lower():
print("Altering the Menu plugin function result...\n")
context.function_result = FunctionResult(
function=result.function,
value="We are sold out, sorry!",
)
context.terminate = True


# Simulate a conversation with the agent
USER_INPUTS = ["What's the special food on the menu?", "What should I do then?"]


async def main() -> None:
ai_agent_settings = AzureAIAgentSettings.create()

async with (
DefaultAzureCredential() as creds,
AzureAIAgent.create_client(credential=creds) as client,
):
# 1. Create an agent on the Azure AI agent service
agent_definition = await client.agents.create_agent(
model=ai_agent_settings.model_deployment_name,
name="Host",
instructions="Answer the user's questions about the menu.",
)

# 2. Create a Semantic Kernel agent for the Azure AI agent
agent = AzureAIAgent(
kernel=kernel,
client=client,
definition=agent_definition,
plugins=[MenuPlugin()], # Add the plugin to the agent
)

# 3. Create a thread for the agent
# If no thread is provided, a new thread will be
# created and returned with the initial response
thread: AzureAIAgentThread = None

try:
for user_input in USER_INPUTS:
print(f"# User: {user_input}")
# 4. Invoke the agent with the specified message for response
first_chunk = True
async for response in agent.invoke_stream(
messages=user_input, thread=thread, on_intermediate_message=handle_intermediate_steps
):
# 5. Print the response
if first_chunk:
print(f"# {response.name}: ", end="", flush=True)
first_chunk = False
print(f"{response}", end="", flush=True)
thread = response.thread
print()
finally:
# 6. Cleanup: Delete the thread and agent
await thread.delete() if thread else None
await client.agents.delete_agent(agent.id)

"""
Sample Output:

# User: What's the special food on the menu?

Auto function invocation filter
Function: get_specials
Altering the Menu plugin function result...

# Host: I'm sorry, but all the specials on the menu are currently sold out. If there's anything else you're
looking for, please let me know!
# User: What should I do then?
# Host: You might consider ordering from the regular menu items instead. If you need any recommendations or
information about specific items, such as prices or ingredients, feel free to ask!

Intermediate Steps:
Function Call:> MenuPlugin-get_specials with arguments: {}
Function Result:> We are sold out, sorry! for function: MenuPlugin-get_specials
AuthorRole.ASSISTANT: I'm sorry, but all the specials on the menu are currently sold out. If there's anything
else you're looking for, please let me know!
AuthorRole.ASSISTANT: You might consider ordering from the regular menu items instead. If you need any
recommendations or information about specific items, such as prices or ingredients, feel free to ask!
"""


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading