Skip to content

Commit fb0e418

Browse files
authored
Python: Fix for AzureAIAgent streaming func call invocations. Other fixes/improvements for tool call content types, and logging. (#12335)
### Motivation and Context The AzureAIAgent streaming function call invocation is broken when handling more than one required function call from the service. There is nested logic to handle passing the tool calls back to the service because it requires one to pass in a handler. In this PR, we're now controlling everything from the main loop and are passing back the handler as the event stream for further processing. This does require some logic to determine if we have an initial async context manager (based on the first call to create the run stream) or if we're now using the handler from submitting tool calls. We can now see that when we have multiple functions involved, we are able to get the results as we expect: ``` Sample Output: # User: 'What is the price of the special drink and then special food item added together?' Function Call:> MenuPlugin-get_specials with arguments: {} Function Result:> Special Soup: Clam Chowder Special Salad: Cobb Salad Special Drink: Chai Tea for function: MenuPlugin-get_specials Function Call:> MenuPlugin-get_item_price with arguments: {"menu_item": "Chai Tea"} Function Call:> MenuPlugin-get_item_price with arguments: {"menu_item": "Clam Chowder"} Function Result:> $9.99 for function: MenuPlugin-get_item_price Function Result:> $9.99 for function: MenuPlugin-get_item_price Function Call:> MathPlugin-Add with arguments: {"input":9.99,"amount":9.99} Function Result:> 19.98 for function: MathPlugin-Add # AuthorRole.ASSISTANT: The price of the special drink, Chai Tea, is $9.99 and the price of the special food item, Clam Chowder, is $9.99. Added together, the total price is $19.98. ``` <!-- 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 Fix handling multiple function calls for the streaming AzureAIAgent. - Fixes a missing `quote` from the url citation for streaming annotation content - Shows the correct way to retrieve Bing Grounding tool calls in the two concept samples. - Adds handling for tool content content for OpenAPI plugins. - Adds logging for streaming agent invocation tool calls. - Relaxes the handling of the `FunctionResultContent` id attribute to be `str | None` similar to that of `FunctionCallContent`. The type hints for `id` have always been `str | None`, but the actual type hint on the attribute has been `str`. We don't always get a `FunctionResultContent` id from the Azure Agent Service during streaming invocations - message addition via the overide `__add__` method still work properly. - Move the AzureAIAgent pkg dependencies to be installed by default, instead of requiring the `azure` extra. - Closes #12312 - Closes #12328 - Closes #12331 - Closes #12324 <!-- 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 4f0bf16 commit fb0e418

File tree

16 files changed

+3701
-3493
lines changed

16 files changed

+3701
-3493
lines changed

python/pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ classifiers = [
2323
"Typing :: Typed",
2424
]
2525
dependencies = [
26+
# azure agents
27+
"azure-ai-projects >= 1.0.0b11",
28+
"azure-ai-agents >= 1.0.0",
2629
"aiohttp ~= 3.8",
2730
"cloudevents ~=1.0",
2831
"pydantic >=2.0,!=2.10.0,!=2.10.1,!=2.10.2,!=2.10.3,<2.12",
@@ -67,10 +70,7 @@ autogen = [
6770
aws = [
6871
"boto3>=1.36.4,<1.39.0",
6972
]
70-
# This is temporary to get the PR reviewed, and will be replaced when their new version is public.
7173
azure = [
72-
"azure-ai-projects >= 1.0.0b11",
73-
"azure-ai-agents >= 1.0.0",
7474
"azure-ai-inference >= 1.0.0b6",
7575
"azure-core-tracing-opentelemetry >= 1.0.0b11",
7676
"azure-search-documents >= 11.6.0b4",
@@ -132,7 +132,7 @@ qdrant = [
132132
"qdrant-client ~= 1.9"
133133
]
134134
redis = [
135-
"redis[hiredis] >= 5,< 7",
135+
"redis[hiredis] >= 5,< 6",
136136
"types-redis ~= 4.6.0.20240425",
137137
"redisvl ~= 0.4"
138138
]

python/samples/concepts/agents/azure_ai_agent/azure_ai_agent_bing_grounding.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
from azure.identity.aio import DefaultAzureCredential
77

88
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
9-
from semantic_kernel.contents import AnnotationContent
9+
from semantic_kernel.contents import (
10+
AnnotationContent,
11+
ChatMessageContent,
12+
FunctionCallContent,
13+
FunctionResultContent,
14+
)
1015

1116
"""
1217
The following sample demonstrates how to create an Azure AI agent that
@@ -20,13 +25,23 @@
2025
TASK = "Which team won the 2025 NCAA basketball championship?"
2126

2227

28+
async def handle_intermediate_steps(message: ChatMessageContent) -> None:
29+
for item in message.items or []:
30+
if isinstance(item, FunctionResultContent):
31+
print(f"Function Result:> {item.result} for function: {item.name}")
32+
elif isinstance(item, FunctionCallContent):
33+
print(f"Function Call:> {item.name} with arguments: {item.arguments}")
34+
else:
35+
print(f"{item}")
36+
37+
2338
async def main() -> None:
2439
async with (
2540
DefaultAzureCredential() as creds,
2641
AzureAIAgent.create_client(credential=creds) as client,
2742
):
2843
# 1. Enter your Bing Grounding Connection Name
29-
bing_connection = await client.connections.get(connection_name="<your-bing-grounding-connection-name>")
44+
bing_connection = await client.connections.get(name="<your-bing-grounding-connection-name>")
3045
conn_id = bing_connection.id
3146

3247
# 2. Initialize agent bing tool and add the connection id
@@ -54,7 +69,9 @@ async def main() -> None:
5469
try:
5570
print(f"# User: '{TASK}'")
5671
# 6. Invoke the agent for the specified thread for response
57-
async for response in agent.invoke(messages=TASK, thread=thread):
72+
async for response in agent.invoke(
73+
messages=TASK, thread=thread, on_intermediate_message=handle_intermediate_steps
74+
):
5875
print(f"# {response.name}: {response}")
5976
thread = response.thread
6077

@@ -75,10 +92,17 @@ async def main() -> None:
7592
Sample Output:
7693
7794
# User: 'Which team won the 2025 NCAA basketball championship?'
78-
# BingGroundingAgent: The Florida Gators won the 2025 NCAA basketball championship, defeating the Houston Cougars 65-63 in the final to secure their third national title【3:5†source】【3:6†source】【3:9†source】.
79-
Annotation :> https://www.usatoday.com/story/sports/ncaab/2025/04/07/houston-florida-live-updates-national-championship-score/82982004007/, source=【3:5†source】, with start_index=147 and end_index=159
80-
Annotation :> https://bleacherreport.com/articles/25182096-winners-and-losers-2025-mens-ncaa-tournament, source=【3:6†source】, with start_index=159 and end_index=171
81-
Annotation :> https://wtop.com/ncaa-basketball/2025/04/ncaa-basketball-champions/, source=【3:9†source】, with start_index=171 and end_index=183
95+
Function Call:> bing_grounding with arguments:
96+
{
97+
'requesturl': 'https://api.bing.microsoft.com/v7.0/search?q=search(query:2025 NCAA basketball championship winner)',
98+
'response_metadata': "{'market': 'en-US', 'num_docs_retrieved': 5, 'num_docs_actually_used': 5}"
99+
}
100+
# BingGroundingAgent: The team that won the 2025 NCAA men's basketball championship was the Florida Gators. They defeated the Houston Cougars with a final score of 65-63.
101+
The championship game took place in San Antonio, Texas, and the Florida team was coached by Todd Golden. This victory made Florida the national champion for the 2024-25
102+
NCAA Division I men's basketball season【3:0†source】【3:1†source】【3:2†source】.
103+
Annotation :> https://en.wikipedia.org/wiki/2025_NCAA_Division_I_men%27s_basketball_championship_game, source=【3:0†source】, with start_index=357 and end_index=369
104+
Annotation :> https://www.ncaa.com/history/basketball-men/d1, source=【3:1†source】, with start_index=369 and end_index=381
105+
Annotation :> https://sports.yahoo.com/article/won-march-madness-2025-ncaa-100551421.html, source=【3:2†source】, with start_index=381 and end_index=393
82106
""" # noqa: E501
83107

84108

python/samples/concepts/agents/azure_ai_agent/azure_ai_agent_bing_grounding_streaming_with_message_callback.py

Lines changed: 17 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Copyright (c) Microsoft. All rights reserved.
22

33
import asyncio
4-
from functools import reduce
54

65
from azure.ai.agents.models import BingGroundingTool
76
from azure.identity.aio import DefaultAzureCredential
@@ -10,8 +9,8 @@
109
from semantic_kernel.contents import (
1110
ChatMessageContent,
1211
FunctionCallContent,
12+
FunctionResultContent,
1313
StreamingAnnotationContent,
14-
StreamingChatMessageContent,
1514
)
1615

1716
"""
@@ -32,7 +31,13 @@
3231

3332

3433
async def handle_streaming_intermediate_steps(message: ChatMessageContent) -> None:
35-
intermediate_steps.append(message)
34+
for item in message.items or []:
35+
if isinstance(item, FunctionResultContent):
36+
print(f"Function Result:> {item.result} for function: {item.name}")
37+
elif isinstance(item, FunctionCallContent):
38+
print(f"Function Call:> {item.name} with arguments: {item.arguments}")
39+
else:
40+
print(f"{item}")
3641

3742

3843
async def main() -> None:
@@ -41,8 +46,7 @@ async def main() -> None:
4146
AzureAIAgent.create_client(credential=creds) as client,
4247
):
4348
# 1. Enter your Bing Grounding Connection Name
44-
# <your-bing-grounding-connection-name>
45-
bing_connection = await client.connections.get(connection_name="skbinggrounding")
49+
bing_connection = await client.connections.get(name="<your-bing-grounding-connection-name>")
4650
conn_id = bing_connection.id
4751

4852
# 2. Initialize agent bing tool and add the connection id
@@ -94,41 +98,18 @@ async def main() -> None:
9498
await thread.delete() if thread else None
9599
await client.agents.delete_agent(agent.id)
96100

97-
print("====================================================")
98-
print("\nResponse complete:\n")
99-
# Combine the intermediate `StreamingChatMessageContent` chunks into a single message
100-
filtered_steps = [step for step in intermediate_steps if isinstance(step, StreamingChatMessageContent)]
101-
streaming_full_completion: StreamingChatMessageContent = reduce(lambda x, y: x + y, filtered_steps)
102-
# Grab the other messages that are not `StreamingChatMessageContent`
103-
other_steps = [s for s in intermediate_steps if not isinstance(s, StreamingChatMessageContent)]
104-
final_msgs = [streaming_full_completion] + other_steps
105-
for msg in final_msgs:
106-
if any(isinstance(item, FunctionCallContent) for item in msg.items):
107-
for item in msg.items:
108-
if isinstance(item, FunctionCallContent):
109-
# Note: the AI Projects SDK is not returning a `requesturl` for streaming events
110-
# The issue was raised with the AI Projects team
111-
print(f"Function call: {item.function_name} with arguments: {item.arguments}")
112-
113-
print(f"{msg.content}")
114-
115101
"""
116102
Sample Output:
117103
118104
# User: 'Which team won the 2025 NCAA basketball championship?'
119-
# BingGroundingAgent: The Florida Gators won the 2025 NCAA men's basketball championship, defeating the Houston Cougars 65-63. It marked Florida's third national title and their first since back-to-back wins in 2006-2007【5:0†source】
120-
Annotation :> https://www.usatoday.com/story/sports/ncaab/2025/04/07/houston-florida-live-updates-national-championship-score/82982004007/, source=Florida vs Houston final score: Gators win 2025 NCAA championship, with start_index=198 and end_index=210
121-
【5:5†source】
122-
Annotation :> https://www.nbcsports.com/mens-college-basketball/live/florida-vs-houston-live-score-updates-game-news-stats-highlights-for-2025-ncaa-march-madness-mens-national-championship, source=Houston vs. Florida RECAP: Highlights, stats, box score, results as ..., with start_index=210 and end_index=222
123-
.
124-
====================================================
125-
126-
Response complete:
127-
128-
Function call: bing_grounding with arguments: None
129-
Function call: bing_grounding with arguments: None
130-
131-
The Florida Gators won the 2025 NCAA men's basketball championship, defeating the Houston Cougars 65-63. It marked Florida's third national title and their first since back-to-back wins in 2006-2007【5:0†source】【5:5†source】.
105+
Function Call:> bing_grounding with arguments: {'requesturl': 'https://api.bing.microsoft.com/v7.0/search?q=search(query: 2025 NCAA basketball championship winner)'}
106+
Function Call:> bing_grounding with arguments: {'response_metadata': "{'market': 'en-US', 'num_docs_retrieved': 5, 'num_docs_actually_used': 5}"}
107+
# BingGroundingAgent: The Florida Gators won the 2025 NCAA men's basketball championship. They defeated the Houston Cougars with a close score of 65-63 in the championship game held in San Antonio, Texas. This victory marked their third national title. Florida overcame a 12-point deficit during the game to claim the championship【3:0†source】
108+
Annotation :> https://en.wikipedia.org/wiki/2025_NCAA_Division_I_men%27s_basketball_championship_game, source=None, with start_index=308 and end_index=320
109+
【3:1†source】
110+
Annotation :> https://www.ncaa.com/history/basketball-men/d1, source=None, with start_index=320 and end_index=332
111+
【3:2†source】
112+
Annotation :> https://sports.yahoo.com/article/florida-gators-win-2025-ncaa-034021303.html, source=None, with start_index=332 and end_index=344.
132113
""" # noqa: E501
133114

134115

python/samples/concepts/agents/azure_ai_agent/azure_ai_agent_message_callback_streaming.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# Copyright (c) Microsoft. All rights reserved.
22

33
import asyncio
4+
import logging
45
from typing import Annotated
56

67
from azure.identity.aio import DefaultAzureCredential
78

89
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
9-
from semantic_kernel.contents import FunctionCallContent, FunctionResultContent
10-
from semantic_kernel.contents.chat_message_content import ChatMessageContent
10+
from semantic_kernel.contents import ChatMessageContent, FunctionCallContent, FunctionResultContent
11+
from semantic_kernel.core_plugins import MathPlugin
1112
from semantic_kernel.functions import kernel_function
1213

1314
"""
@@ -22,6 +23,8 @@
2223
while assistant replies stream back incrementally through the main response loop.
2324
"""
2425

26+
logging.basicConfig(level=logging.DEBUG)
27+
2528

2629
# Define a sample plugin for the sample
2730
class MenuPlugin:
@@ -67,14 +70,14 @@ async def main() -> None:
6770
agent_definition = await client.agents.create_agent(
6871
model=ai_agent_settings.model_deployment_name,
6972
name="Host",
70-
instructions="Answer questions about the menu.",
73+
instructions="Answer questions from the user using your provided functions. You must invoke multiple functions to answer the user's questions. ", # noqa: E501
7174
)
7275

7376
# Create the AzureAI Agent
7477
agent = AzureAIAgent(
7578
client=client,
7679
definition=agent_definition,
77-
plugins=[MenuPlugin()], # add the sample plugin to the agent
80+
plugins=[MenuPlugin(), MathPlugin()],
7881
)
7982

8083
# Create a thread for the agent
@@ -83,10 +86,7 @@ async def main() -> None:
8386
thread: AzureAIAgentThread = None
8487

8588
user_inputs = [
86-
"Hello",
87-
"What is the special soup?",
88-
"How much does that cost?",
89-
"Thank you",
89+
"What is the price of the special drink and the special food item added together?",
9090
]
9191

9292
try:
@@ -112,23 +112,21 @@ async def main() -> None:
112112
"""
113113
Sample Output:
114114
115-
# User: 'Hello'
116-
# AuthorRole.ASSISTANT: Hello! How can I assist you today?
117-
# User: 'What is the special soup?'
115+
# User: 'What is the price of the special drink and then special food item added together?'
118116
Function Call:> MenuPlugin-get_specials with arguments: {}
119117
Function Result:>
120118
Special Soup: Clam Chowder
121119
Special Salad: Cobb Salad
122120
Special Drink: Chai Tea
123121
for function: MenuPlugin-get_specials
124-
# AuthorRole.ASSISTANT: The special soup is Clam Chowder. Would you like to know more about it or anything
125-
else from the menu?
126-
# User: 'How much does that cost?'
127-
Function Call:> MenuPlugin-get_item_price with arguments: {"menu_item":"Clam Chowder"}
122+
Function Call:> MenuPlugin-get_item_price with arguments: {"menu_item": "Chai Tea"}
123+
Function Call:> MenuPlugin-get_item_price with arguments: {"menu_item": "Clam Chowder"}
124+
Function Result:> $9.99 for function: MenuPlugin-get_item_price
128125
Function Result:> $9.99 for function: MenuPlugin-get_item_price
129-
# AuthorRole.ASSISTANT: The Clam Chowder costs $9.99. Would you like to order it?
130-
# User: 'Thank you'
131-
# AuthorRole.ASSISTANT: You're welcome! Let me know if you need anything else. Enjoy your day! 😊
126+
Function Call:> MathPlugin-Add with arguments: {"input":9.99,"amount":9.99}
127+
Function Result:> 19.98 for function: MathPlugin-Add
128+
# AuthorRole.ASSISTANT: The price of the special drink, Chai Tea, is $9.99 and the price of the special food
129+
item, Clam Chowder, is $9.99. Added together, the total price is $19.98.
132130
"""
133131

134132

python/samples/getting_started_with_agents/azure_ai_agent/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ To set up the required resources, follow the "Quickstart: Create a new agent" gu
77
You will need to install the optional Semantic Kernel `azure` dependencies if you haven't already via:
88

99
```bash
10-
pip install semantic-kernel[azure]
10+
pip install semantic-kernel
1111
```
1212

1313
Before running an Azure AI Agent, modify your .env file to include:

python/samples/getting_started_with_agents/azure_ai_agent/step6_azure_ai_agent_openapi.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from azure.identity.aio import DefaultAzureCredential
99

1010
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
11-
from semantic_kernel.contents import AuthorRole
11+
from semantic_kernel.contents import ChatMessageContent, FunctionCallContent, FunctionResultContent
1212

1313
"""
1414
The following sample demonstrates how to create a simple, Azure AI agent that
@@ -23,6 +23,16 @@
2323
]
2424

2525

26+
async def handle_streaming_intermediate_steps(message: ChatMessageContent) -> None:
27+
for item in message.items or []:
28+
if isinstance(item, FunctionResultContent):
29+
print(f"Function Result:> {item.result} for function: {item.name}")
30+
elif isinstance(item, FunctionCallContent):
31+
print(f"Function Call:> {item.name} with arguments: {item.arguments}")
32+
else:
33+
print(f"{item}")
34+
35+
2636
async def main() -> None:
2737
async with (
2838
DefaultAzureCredential() as creds,
@@ -76,12 +86,11 @@ async def main() -> None:
7686
print(f"# User: '{user_input}'")
7787
# 7. Invoke the agent for the specified thread for response
7888
async for response in agent.invoke(messages=user_input, thread=thread):
79-
if response.role != AuthorRole.TOOL:
80-
print(f"# Agent: {response}")
89+
print(f"# Agent: {response}")
8190
thread = response.thread
8291
finally:
8392
# 8. Cleanup: Delete the thread and agent
84-
await client.agents.threads.delete(thread.id)
93+
await client.agents.threads.delete(thread.id) if thread else None
8594
await client.agents.delete_agent(agent.id)
8695

8796
"""

python/samples/getting_started_with_agents/azure_ai_agent/step7_azure_ai_agent_retrieval.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from azure.identity.aio import DefaultAzureCredential
66

77
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentThread
8+
from semantic_kernel.contents import ChatMessageContent, FunctionCallContent, FunctionResultContent
89

910
"""
1011
The following sample demonstrates how to use an already existing
@@ -16,10 +17,20 @@
1617

1718
# Simulate a conversation with the agent
1819
USER_INPUTS = [
19-
"Why is the sky blue?",
20+
"Using the provided doc, tell me about the evolution of RAG.",
2021
]
2122

2223

24+
async def handle_streaming_intermediate_steps(message: ChatMessageContent) -> None:
25+
for item in message.items or []:
26+
if isinstance(item, FunctionResultContent):
27+
print(f"Function Result:> {item.result} for function: {item.name}")
28+
elif isinstance(item, FunctionCallContent):
29+
print(f"Function Call:> {item.name} with arguments: {item.arguments}")
30+
else:
31+
print(f"{item}")
32+
33+
2334
async def main() -> None:
2435
async with (
2536
DefaultAzureCredential() as creds,
@@ -29,7 +40,7 @@ async def main() -> None:
2940
# Replace the "your-agent-id" with the actual agent ID
3041
# you want to use.
3142
agent_definition = await client.agents.get_agent(
32-
agent_id="your-agent-id",
43+
agent_id="<your-agent-id>",
3344
)
3445

3546
# 2. Create a Semantic Kernel agent for the Azure AI agent
@@ -47,8 +58,15 @@ async def main() -> None:
4758
for user_input in USER_INPUTS:
4859
print(f"# User: '{user_input}'")
4960
# 4. Invoke the agent for the specified thread for response
50-
response = await agent.get_response(messages=user_input, thread=thread)
51-
print(f"# {response.name}: {response}")
61+
async for response in agent.invoke_stream(
62+
messages=user_input,
63+
thread=thread,
64+
on_intermediate_message=handle_streaming_intermediate_steps,
65+
):
66+
# Print the agent's response
67+
print(f"{response}", end="", flush=True)
68+
# Update the thread for subsequent messages
69+
thread = response.thread
5270
finally:
5371
# 5. Cleanup: Delete the thread and agent
5472
await thread.delete() if thread else None

0 commit comments

Comments
 (0)