Skip to content

Commit 9e3cdff

Browse files
authored
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 😄
1 parent 003cc4c commit 9e3cdff

File tree

4 files changed

+147
-23
lines changed

4 files changed

+147
-23
lines changed

python/samples/concepts/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- [Azure AI Agent Prompt Templating](./agents/azure_ai_agent/azure_ai_agent_prompt_templating.py)
2525
- [Azure AI Agent Message Callback Streaming](./agents/azure_ai_agent/azure_ai_agent_message_callback_streaming.py)
2626
- [Azure AI Agent Message Callback](./agents/azure_ai_agent/azure_ai_agent_message_callback.py)
27+
- [Azure AI Agent Retrieve Messages from Thread](./agents/azure_ai_agent/azure_ai_agent_retrieve_messages_from_thread.py)
2728
- [Azure AI Agent Streaming](./agents/azure_ai_agent/azure_ai_agent_streaming.py)
2829
- [Azure AI Agent Structured Outputs](./agents/azure_ai_agent/azure_ai_agent_structured_outputs.py)
2930
- [Azure AI Agent Truncation Strategy](./agents/azure_ai_agent/azure_ai_agent_truncation_strategy.py)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import asyncio
4+
from typing import Annotated
5+
6+
from azure.identity.aio import DefaultAzureCredential
7+
8+
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
9+
from semantic_kernel.functions import kernel_function
10+
11+
"""
12+
The following sample demonstrates how to create an Azure AI agent that answers
13+
questions about a sample menu using a Semantic Kernel Plugin. After all questions
14+
are answered, it retrieves and prints the messages from the thread.
15+
"""
16+
17+
18+
# Define a sample plugin for the sample
19+
class MenuPlugin:
20+
"""A sample Menu Plugin used for the concept sample."""
21+
22+
@kernel_function(description="Provides a list of specials from the menu.")
23+
def get_specials(self) -> Annotated[str, "Returns the specials from the menu."]:
24+
return """
25+
Special Soup: Clam Chowder
26+
Special Salad: Cobb Salad
27+
Special Drink: Chai Tea
28+
"""
29+
30+
@kernel_function(description="Provides the price of the requested menu item.")
31+
def get_item_price(
32+
self, menu_item: Annotated[str, "The name of the menu item."]
33+
) -> Annotated[str, "Returns the price of the menu item."]:
34+
return "$9.99"
35+
36+
37+
# Simulate a conversation with the agent
38+
USER_INPUTS = [
39+
"Hello",
40+
"What is the special soup?",
41+
"How much does that cost?",
42+
"Thank you",
43+
]
44+
45+
46+
async def main() -> None:
47+
async with (
48+
DefaultAzureCredential() as creds,
49+
AzureAIAgent.create_client(credential=creds) as client,
50+
):
51+
# 1. Create an agent on the Azure AI agent service
52+
agent_definition = await client.agents.create_agent(
53+
model=AzureAIAgentSettings().model_deployment_name,
54+
name="Host",
55+
instructions="Answer questions about the menu.",
56+
)
57+
58+
# 2. Create a Semantic Kernel agent for the Azure AI agent
59+
agent = AzureAIAgent(
60+
client=client,
61+
definition=agent_definition,
62+
plugins=[MenuPlugin()], # Add the plugin to the agent
63+
)
64+
65+
# 3. Create a thread for the agent
66+
# If no thread is provided, a new thread will be
67+
# created and returned with the initial response
68+
thread: AzureAIAgentThread | None = None
69+
70+
try:
71+
for user_input in USER_INPUTS:
72+
print(f"# User: {user_input}")
73+
# 4. Invoke the agent for the specified thread for response
74+
async for response in agent.invoke(
75+
messages=user_input,
76+
thread=thread,
77+
):
78+
print(f"# {response.name}: {response}")
79+
thread = response.thread
80+
finally:
81+
# 5. Cleanup: Delete the thread and agent
82+
# await thread.delete() if thread else None
83+
await client.agents.delete_agent(agent.id)
84+
85+
print("*" * 50)
86+
print("# Messages in the thread (asc order):\n")
87+
async for msg in thread.get_messages(sort_order="asc"):
88+
print(f"# {msg.role} for name={msg.name}: {msg.content}")
89+
print("*" * 50)
90+
91+
await thread.delete() if thread else None
92+
93+
"""
94+
# User: Hello
95+
# Host: Hello! How can I assist you with the menu today?
96+
# User: What is the special soup?
97+
# Host: The special soup today is Clam Chowder. Would you like to know more about it or anything else
98+
on the menu?
99+
# User: How much does that cost?
100+
# Host: The Clam Chowder costs $9.99. Would you like to order it or need information on other items?
101+
# User: Thank you
102+
# Host: You're welcome! If you have any more questions or need assistance with the menu, feel free to ask.
103+
Enjoy your meal!
104+
**************************************************
105+
# Messages in the thread (asc order):
106+
107+
# AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Hello
108+
# AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Hello! How can I assist you with the menu today?
109+
# AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: What is the special soup?
110+
# AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: The special soup today is Clam Chowder. Would
111+
you like to know more about it or anything else on the menu?
112+
# AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: How much does that cost?
113+
# AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: The Clam Chowder costs $9.99. Would you like to
114+
order it or need information on other items?
115+
# AuthorRole.USER for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: Thank you
116+
# AuthorRole.ASSISTANT for name=asst_mXwZOwyJLxXGtaYKHizRH6Ip: You're welcome! If you have any more questions
117+
or need assistance with the menu, feel free to ask. Enjoy your meal!
118+
"""
119+
120+
121+
if __name__ == "__main__":
122+
asyncio.run(main())

python/semantic_kernel/agents/agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,8 +516,15 @@ async def _ensure_thread_exists_with_messages(
516516
f"{self.__class__.__name__} currently only supports agent threads of type {expected_type.__name__}."
517517
)
518518

519+
# Track the agent ID as user msg metadata, which is useful for
520+
# fetching thread messages as the agent may have been deleted.
521+
id_metadata = {
522+
"agent_id": self.id,
523+
}
524+
519525
# Notify the thread that new messages are available.
520526
for msg in normalized_messages:
527+
msg.metadata.update(id_metadata)
521528
await self._notify_thread_of_new_message(thread, msg)
522529

523530
return thread

python/semantic_kernel/agents/azure_ai/agent_thread_actions.py

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
from semantic_kernel.contents.chat_message_content import ChatMessageContent
5959
from semantic_kernel.contents.function_call_content import FunctionCallContent
6060
from semantic_kernel.contents.utils.author_role import AuthorRole
61-
from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException
61+
from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException, AgentThreadOperationException
6262
from semantic_kernel.functions import KernelArguments
6363
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
6464
from semantic_kernel.utils.feature_stage_decorator import experimental
@@ -704,29 +704,23 @@ async def get_messages(
704704
705705
Yields:
706706
An AsyncIterable of ChatMessageContent that includes the thread messages.
707-
"""
708-
agent_names: dict[str, str] = {}
709-
710-
async for message in client.agents.messages.list(
711-
thread_id=thread_id,
712-
run_id=None,
713-
limit=None,
714-
order=sort_order,
715-
before=None,
716-
):
717-
assistant_name: str | None = None
718-
719-
if message.agent_id and message.agent_id.strip() and message.agent_id not in agent_names:
720-
agent = await client.agents.get_agent(message.agent_id)
721-
if agent.name and agent.name.strip():
722-
agent_names[agent.id] = agent.name
723707
724-
assistant_name = agent_names.get(message.agent_id) or message.agent_id
725-
726-
content = generate_message_content(assistant_name, message)
727-
728-
if len(content.items) > 0:
729-
yield content
708+
Raises:
709+
AgentThreadOperationException: If the messages cannot be retrieved.
710+
"""
711+
try:
712+
async for message in client.agents.messages.list(
713+
thread_id=thread_id,
714+
run_id=None,
715+
limit=None,
716+
order=sort_order,
717+
before=None,
718+
):
719+
agent_id = (message.agent_id or message.metadata.get("agent_id") or "").strip() or "agent"
720+
yield generate_message_content(agent_id, message)
721+
except Exception as e:
722+
logger.error(f"Failed to retrieve messages for thread {thread_id}: {e}")
723+
raise AgentThreadOperationException(f"Failed to retrieve messages for thread `{thread_id}`.") from e
730724

731725
# endregion
732726

0 commit comments

Comments
 (0)