mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Python: Improve AzureAIAgent get thread msg handling. (#12535)
### Motivation and Context Currently, when fetching thread messages for an AzureAIAgent, it assumes that the agent exists, so we can pull it down and get its id. This may not always be the case - an agent could have been deleted, but the thread still exists. We need to account for this. When creating user messages that we add to the thread, there's no `name` or `id` parameter allowed. We can, however, add metadata to the message. Going forward we are adding the `agent_id` as metadata to the message, so upon retrieval later, we can attempt to get it and return the id with the user's message. This allows one to see which agent_id has responded to the user's message: ``` # Messages in the thread (asc order): # AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Hello # AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Hello! How can I assist you with the menu today? # AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: What is the special soup? # AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: The special soup today is Clam Chowder. Would you like to know more about it or anything else on the menu? # AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: How much does that cost? # AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: The Clam Chowder costs $9.99. Would you like to order it or need information on other items? # AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Thank you # AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: You're welcome! If you have any more questions or need assistance with the menu, feel free to ask. Enjoy your meal! ``` <!-- 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 - Closes #12435 <!-- 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 😄
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
- [Azure AI Agent Prompt Templating](./agents/azure_ai_agent/azure_ai_agent_prompt_templating.py)
|
||||
- [Azure AI Agent Message Callback Streaming](./agents/azure_ai_agent/azure_ai_agent_message_callback_streaming.py)
|
||||
- [Azure AI Agent Message Callback](./agents/azure_ai_agent/azure_ai_agent_message_callback.py)
|
||||
- [Azure AI Agent Retrieve Messages from Thread](./agents/azure_ai_agent/azure_ai_agent_retrieve_messages_from_thread.py)
|
||||
- [Azure AI Agent Streaming](./agents/azure_ai_agent/azure_ai_agent_streaming.py)
|
||||
- [Azure AI Agent Structured Outputs](./agents/azure_ai_agent/azure_ai_agent_structured_outputs.py)
|
||||
- [Azure AI Agent Truncation Strategy](./agents/azure_ai_agent/azure_ai_agent_truncation_strategy.py)
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# 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.functions import kernel_function
|
||||
|
||||
"""
|
||||
The following sample demonstrates how to create an Azure AI agent that answers
|
||||
questions about a sample menu using a Semantic Kernel Plugin. After all questions
|
||||
are answered, it retrieves and prints the messages from the thread.
|
||||
"""
|
||||
|
||||
|
||||
# Define a sample plugin for the sample
|
||||
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"
|
||||
|
||||
|
||||
# Simulate a conversation with the agent
|
||||
USER_INPUTS = [
|
||||
"Hello",
|
||||
"What is the special soup?",
|
||||
"How much does that cost?",
|
||||
"Thank you",
|
||||
]
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
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=AzureAIAgentSettings().model_deployment_name,
|
||||
name="Host",
|
||||
instructions="Answer questions about the menu.",
|
||||
)
|
||||
|
||||
# 2. Create a Semantic Kernel agent for the Azure AI agent
|
||||
agent = AzureAIAgent(
|
||||
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 = None
|
||||
|
||||
try:
|
||||
for user_input in USER_INPUTS:
|
||||
print(f"# User: {user_input}")
|
||||
# 4. Invoke the agent for the specified thread for response
|
||||
async for response in agent.invoke(
|
||||
messages=user_input,
|
||||
thread=thread,
|
||||
):
|
||||
print(f"# {response.name}: {response}")
|
||||
thread = response.thread
|
||||
finally:
|
||||
# 5. Cleanup: Delete the thread and agent
|
||||
# await thread.delete() if thread else None
|
||||
await client.agents.delete_agent(agent.id)
|
||||
|
||||
print("*" * 50)
|
||||
print("# Messages in the thread (asc order):\n")
|
||||
async for msg in thread.get_messages(sort_order="asc"):
|
||||
print(f"# {msg.role} for name={msg.name}: {msg.content}")
|
||||
print("*" * 50)
|
||||
|
||||
await thread.delete() if thread else None
|
||||
|
||||
"""
|
||||
# User: Hello
|
||||
# Host: Hello! How can I assist you with the menu today?
|
||||
# User: What is the special soup?
|
||||
# Host: The special soup today is Clam Chowder. Would you like to know more about it or anything else
|
||||
on the menu?
|
||||
# User: How much does that cost?
|
||||
# Host: The Clam Chowder costs $9.99. Would you like to order it or need information on other items?
|
||||
# User: Thank you
|
||||
# Host: You're welcome! If you have any more questions or need assistance with the menu, feel free to ask.
|
||||
Enjoy your meal!
|
||||
**************************************************
|
||||
# Messages in the thread (asc order):
|
||||
|
||||
# AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Hello
|
||||
# AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Hello! How can I assist you with the menu today?
|
||||
# AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: What is the special soup?
|
||||
# AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: The special soup today is Clam Chowder. Would
|
||||
you like to know more about it or anything else on the menu?
|
||||
# AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: How much does that cost?
|
||||
# AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: The Clam Chowder costs $9.99. Would you like to
|
||||
order it or need information on other items?
|
||||
# AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Thank you
|
||||
# AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: You're welcome! If you have any more questions
|
||||
or need assistance with the menu, feel free to ask. Enjoy your meal!
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -516,8 +516,15 @@ class Agent(KernelBaseModel, ABC):
|
||||
f"{self.__class__.__name__} currently only supports agent threads of type {expected_type.__name__}."
|
||||
)
|
||||
|
||||
# Track the agent ID as user msg metadata, which is useful for
|
||||
# fetching thread messages as the agent may have been deleted.
|
||||
id_metadata = {
|
||||
"agent_id": self.id,
|
||||
}
|
||||
|
||||
# Notify the thread that new messages are available.
|
||||
for msg in normalized_messages:
|
||||
msg.metadata.update(id_metadata)
|
||||
await self._notify_thread_of_new_message(thread, msg)
|
||||
|
||||
return thread
|
||||
|
||||
@@ -58,7 +58,7 @@ from semantic_kernel.connectors.ai.function_calling_utils import kernel_function
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.function_call_content import FunctionCallContent
|
||||
from semantic_kernel.contents.utils.author_role import AuthorRole
|
||||
from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException
|
||||
from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException, AgentThreadOperationException
|
||||
from semantic_kernel.functions import KernelArguments
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
@@ -704,29 +704,23 @@ class AgentThreadActions:
|
||||
|
||||
Yields:
|
||||
An AsyncIterable of ChatMessageContent that includes the thread messages.
|
||||
|
||||
Raises:
|
||||
AgentThreadOperationException: If the messages cannot be retrieved.
|
||||
"""
|
||||
agent_names: dict[str, str] = {}
|
||||
|
||||
async for message in client.agents.messages.list(
|
||||
thread_id=thread_id,
|
||||
run_id=None,
|
||||
limit=None,
|
||||
order=sort_order,
|
||||
before=None,
|
||||
):
|
||||
assistant_name: str | None = None
|
||||
|
||||
if message.agent_id and message.agent_id.strip() and message.agent_id not in agent_names:
|
||||
agent = await client.agents.get_agent(message.agent_id)
|
||||
if agent.name and agent.name.strip():
|
||||
agent_names[agent.id] = agent.name
|
||||
|
||||
assistant_name = agent_names.get(message.agent_id) or message.agent_id
|
||||
|
||||
content = generate_message_content(assistant_name, message)
|
||||
|
||||
if len(content.items) > 0:
|
||||
yield content
|
||||
try:
|
||||
async for message in client.agents.messages.list(
|
||||
thread_id=thread_id,
|
||||
run_id=None,
|
||||
limit=None,
|
||||
order=sort_order,
|
||||
before=None,
|
||||
):
|
||||
agent_id = (message.agent_id or message.metadata.get("agent_id") or "").strip() or "agent"
|
||||
yield generate_message_content(agent_id, message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve messages for thread {thread_id}: {e}")
|
||||
raise AgentThreadOperationException(f"Failed to retrieve messages for thread `{thread_id}`.") from e
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
Reference in New Issue
Block a user