Skip to content

Commit cee0a45

Browse files
Python: fixed middleware samples (microsoft#5026)
* fixed samples * small update to explanation * add snippet fix on root readme
1 parent 4b9856e commit cee0a45

4 files changed

Lines changed: 86 additions & 46 deletions

File tree

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,23 +94,23 @@ Create a simple Azure Responses Agent that writes a haiku about the Microsoft Ag
9494
# Use `az login` to authenticate with Azure CLI
9595
import os
9696
import asyncio
97-
from agent_framework.azure import AzureOpenAIResponsesClient
97+
from agent_framework import Agent
98+
from agent_framework.foundry import FoundryChatClient
9899
from azure.identity import AzureCliCredential
99100

100101

101102
async def main():
102-
# Initialize a chat agent with Azure OpenAI Responses
103+
# Initialize a chat agent with Microsoft Foundry
103104
# the endpoint, deployment name, and api version can be set via environment variables
104-
# or they can be passed in directly to the AzureOpenAIResponsesClient constructor
105-
agent = AzureOpenAIResponsesClient(
106-
# endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
107-
# deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
108-
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
109-
# api_key=os.environ["AZURE_OPENAI_API_KEY"], # Optional if using AzureCliCredential
110-
credential=AzureCliCredential(), # Optional, if using api_key
111-
).as_agent(
112-
name="HaikuBot",
113-
instructions="You are an upbeat assistant that writes beautifully.",
105+
# or they can be passed in directly to the FoundryChatClient constructor
106+
agent = Agent(
107+
client=FoundryChatClient(
108+
credential=AzureCliCredential(),
109+
# project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
110+
# model=os.environ["FOUNDRY_MODEL_DEPLOYMENT_NAME"],
111+
),
112+
name="HaikuBot",
113+
instructions="You are an upbeat assistant that writes beautifully.",
114114
)
115115

116116
print(await agent.run("Write a haiku about Microsoft Agent Framework."))

python/samples/02-agents/middleware/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ This folder contains focused middleware samples for `Agent`, chat clients, tools
1313
| [`exception_handling_with_middleware.py`](./exception_handling_with_middleware.py) | Shows how middleware can handle failures and recover cleanly. |
1414
| [`function_based_middleware.py`](./function_based_middleware.py) | Shows function-based agent and function middleware. |
1515
| [`middleware_termination.py`](./middleware_termination.py) | Demonstrates stopping a middleware pipeline early. |
16-
| [`override_result_with_middleware.py`](./override_result_with_middleware.py) | Shows how middleware can replace the normal result. |
17-
| [`runtime_context_delegation.py`](./runtime_context_delegation.py) | Demonstrates delegating work with runtime context data. |
16+
| [`override_result_with_middleware.py`](./override_result_with_middleware.py) | Shows how middleware can replace regular and streaming results, then post-process the final response. |
17+
| [`runtime_context_delegation.py`](./runtime_context_delegation.py) | Demonstrates delegating arguments with runtime context data. |
1818
| [`session_behavior_middleware.py`](./session_behavior_middleware.py) | Shows how middleware interacts with session-backed runs. |
1919
| [`shared_state_middleware.py`](./shared_state_middleware.py) | Demonstrates sharing mutable state across middleware invocations. |
2020
| [`usage_tracking_middleware.py`](./usage_tracking_middleware.py) | Demonstrates one chat middleware function that tracks per-call usage in non-streaming and streaming tool-loop runs. |

python/samples/02-agents/middleware/override_result_with_middleware.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ async def _override_stream() -> AsyncIterable[ChatResponseUpdate]:
8181
role="assistant",
8282
)
8383

84-
context.result = ResponseStream(_override_stream())
84+
context.result = ResponseStream(_override_stream(), finalizer=ChatResponse.from_updates)
8585
else:
8686
# For non-streaming: just replace with a new message
8787
current_text = context.result.text if isinstance(context.result, ChatResponse) else ""
@@ -99,12 +99,17 @@ async def validate_weather_middleware(context: ChatContext, call_next: Callable[
9999
return
100100

101101
if context.stream and isinstance(context.result, ResponseStream):
102+
result_stream = context.result
103+
104+
async def _validated_stream() -> AsyncIterable[ChatResponseUpdate]:
105+
async for update in result_stream:
106+
yield update
107+
yield ChatResponseUpdate(
108+
contents=[Content.from_text(text=validation_note)],
109+
role="assistant",
110+
)
102111

103-
def _append_validation_note(response: ChatResponse) -> ChatResponse:
104-
response.messages.append(Message(role="assistant", text=validation_note))
105-
return response
106-
107-
context.result.with_finalizer(_append_validation_note)
112+
context.result = ResponseStream(_validated_stream(), finalizer=ChatResponse.from_updates)
108113
elif isinstance(context.result, ChatResponse):
109114
context.result.messages.append(Message(role="assistant", text=validation_note))
110115

@@ -118,11 +123,11 @@ async def agent_cleanup_middleware(context: AgentContext, call_next: Callable[[]
118123

119124
validation_note = "Validation: weather data verified."
120125

121-
state = {"found_prefix": False}
126+
state = {"found_prefix": False, "found_validation": False}
122127

123128
def _sanitize(response: AgentResponse) -> AgentResponse:
124129
found_prefix = state["found_prefix"]
125-
found_validation = False
130+
found_validation = state["found_validation"]
126131
cleaned_messages: list[Message] = []
127132

128133
for message in response.messages:
@@ -141,12 +146,14 @@ def _sanitize(response: AgentResponse) -> AgentResponse:
141146
found_prefix = True
142147
text = text.replace("Weather Advisory:", "")
143148

144-
text = re.sub(r"\[\d+\]\s*", "", text)
149+
text = re.sub(r"\[\d+\]\s*", "", text).strip()
150+
if not text:
151+
continue
145152

146153
cleaned_messages.append(
147154
Message(
148155
role=message.role,
149-
text=text.strip(),
156+
text=text,
150157
author_name=message.author_name,
151158
message_id=message.message_id,
152159
additional_properties=message.additional_properties,
@@ -166,19 +173,30 @@ def _sanitize(response: AgentResponse) -> AgentResponse:
166173
if context.stream and isinstance(context.result, ResponseStream):
167174

168175
def _clean_update(update: AgentResponseUpdate) -> AgentResponseUpdate:
176+
cleaned_contents: list[Content] = []
177+
169178
for content in update.contents or []:
170179
if not content.text:
180+
cleaned_contents.append(content)
171181
continue
172182
text = content.text
173183
if "Weather Advisory:" in text:
174184
state["found_prefix"] = True
175185
text = text.replace("Weather Advisory:", "")
186+
if validation_note in text:
187+
state["found_validation"] = True
188+
text = text.replace(validation_note, "").strip()
189+
if not text:
190+
continue
176191
text = re.sub(r"\[\d+\]\s*", "", text)
177192
content.text = text
193+
cleaned_contents.append(content)
194+
195+
update.contents = cleaned_contents
178196
return update
179197

180198
context.result.with_transform_hook(_clean_update)
181-
context.result.with_finalizer(_sanitize)
199+
context.result.with_result_hook(_sanitize)
182200
elif isinstance(context.result, AgentResponse):
183201
context.result = _sanitize(context.result)
184202

python/samples/02-agents/middleware/runtime_context_delegation.py

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool
88
from agent_framework.foundry import FoundryChatClient
9+
from azure.identity import AzureCliCredential
910
from dotenv import load_dotenv
1011
from pydantic import Field
1112

@@ -43,6 +44,13 @@
4344
- MiddlewareTypes: Intercepts function calls to access/modify kwargs
4445
- Closure: Functions capturing variables from outer scope
4546
- kwargs Propagation: Automatic forwarding of runtime context through delegation chains
47+
48+
Environment Setup:
49+
- Configure Azure credentials (e.g., via Azure CLI)
50+
- Run `az login` to authenticate
51+
- Set FOUNDRY_PROJECT_ENDPOINT to your Azure AI Foundry project endpoint
52+
- Set FOUNDRY_MODEL to the model deployment name (for example: gpt-4o)
53+
4654
"""
4755

4856

@@ -85,7 +93,7 @@ async def inject_context_middleware(
8593
runtime_context = SessionContextContainer()
8694

8795

88-
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
96+
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production.
8997
@tool(approval_mode="never_require")
9098
async def send_email(
9199
to: Annotated[str, Field(description="Recipient email address")],
@@ -149,7 +157,7 @@ async def pattern_1_single_agent_with_closure() -> None:
149157
print("Use case: Single agent with multiple tools sharing runtime context")
150158
print()
151159

152-
client = FoundryChatClient(model="gpt-4o-mini")
160+
client = FoundryChatClient(credential=AzureCliCredential())
153161

154162
# Create agent with both tools and shared context via middleware
155163
communication_agent = Agent(
@@ -177,9 +185,11 @@ async def pattern_1_single_agent_with_closure() -> None:
177185
result1 = await communication_agent.run(
178186
user_query,
179187
# Runtime context passed as kwargs
180-
api_token="sk-test-token-xyz-789",
181-
user_id="user-12345",
182-
session_metadata={"tenant": "acme-corp", "region": "us-west"},
188+
function_invocation_kwargs={
189+
"api_token": "sk-test-token-xyz-789",
190+
"user_id": "user-12345",
191+
"session_metadata": {"tenant": "acme-corp", "region": "us-west"},
192+
},
183193
)
184194

185195
print(f"\nAgent: {result1.text}")
@@ -195,9 +205,11 @@ async def pattern_1_single_agent_with_closure() -> None:
195205
result2 = await communication_agent.run(
196206
user_query2,
197207
# Different runtime context for this request
198-
api_token="sk-prod-token-abc-456",
199-
user_id="user-67890",
200-
session_metadata={"tenant": "store-inc", "region": "eu-central"},
208+
function_invocation_kwargs={
209+
"api_token": "sk-prod-token-abc-456",
210+
"user_id": "user-67890",
211+
"session_metadata": {"tenant": "store-inc", "region": "eu-central"},
212+
},
201213
)
202214

203215
print(f"\nAgent: {result2.text}")
@@ -215,9 +227,11 @@ async def pattern_1_single_agent_with_closure() -> None:
215227

216228
result3 = await communication_agent.run(
217229
user_query3,
218-
api_token="sk-dev-token-def-123",
219-
user_id="user-11111",
220-
session_metadata={"tenant": "dev-team", "region": "us-east"},
230+
function_invocation_kwargs={
231+
"api_token": "sk-dev-token-def-123",
232+
"user_id": "user-11111",
233+
"session_metadata": {"tenant": "dev-team", "region": "us-east"},
234+
},
221235
)
222236

223237
print(f"\nAgent: {result3.text}")
@@ -234,7 +248,9 @@ async def pattern_1_single_agent_with_closure() -> None:
234248
result4 = await communication_agent.run(
235249
user_query4,
236250
# Missing api_token - tools should handle gracefully
237-
user_id="user-22222",
251+
function_invocation_kwargs={
252+
"user_id": "user-22222",
253+
},
238254
)
239255

240256
print(f"\nAgent: {result4.text}")
@@ -295,7 +311,7 @@ async def sms_kwargs_tracker(context: FunctionInvocationContext, call_next: Call
295311
print(f"[SMSAgent] Received runtime context: {list(context.kwargs.keys())}")
296312
await call_next()
297313

298-
client = FoundryChatClient(model="gpt-4o-mini")
314+
client = FoundryChatClient(credential=AzureCliCredential())
299315

300316
# Create specialized sub-agents
301317
email_agent = Agent(
@@ -341,9 +357,11 @@ async def sms_kwargs_tracker(context: FunctionInvocationContext, call_next: Call
341357
print("Test: Send email with runtime context\n")
342358
await coordinator.run(
343359
"Send an email to john@example.com with subject 'Meeting' and body 'See you at 2pm'",
344-
api_token="secret-token-abc",
345-
user_id="user-999",
346-
tenant_id="tenant-acme",
360+
function_invocation_kwargs={
361+
"api_token": "secret-token-abc",
362+
"user_id": "user-999",
363+
"tenant_id": "tenant-acme",
364+
},
347365
)
348366

349367
print(f"\n[Verification] EmailAgent received kwargs keys: {list(email_agent_kwargs.keys())}")
@@ -400,7 +418,7 @@ async def pattern_3_hierarchical_with_middleware() -> None:
400418

401419
auth_middleware = AuthContextMiddleware()
402420

403-
client = FoundryChatClient(model="gpt-4o-mini")
421+
client = FoundryChatClient(credential=AzureCliCredential())
404422

405423
# Sub-agent with validation middleware
406424
protected_agent = Agent(
@@ -428,16 +446,20 @@ async def pattern_3_hierarchical_with_middleware() -> None:
428446
print("Test 1: Valid token\n")
429447
await coordinator.run(
430448
"Execute operation: backup_database",
431-
api_token="valid-token-xyz-789",
432-
user_id="admin-123",
449+
function_invocation_kwargs={
450+
"api_token": "valid-token-xyz-789",
451+
"user_id": "admin-123",
452+
},
433453
)
434454

435455
# Test with invalid token
436456
print("\nTest 2: Invalid token\n")
437457
await coordinator.run(
438458
"Execute operation: delete_records",
439-
api_token="invalid-token-bad",
440-
user_id="user-456",
459+
function_invocation_kwargs={
460+
"api_token": "invalid-token-bad",
461+
"user_id": "user-456",
462+
},
441463
)
442464

443465
print(f"\n[Validation Summary] Validated tokens: {len(auth_middleware.validated_tokens)}")

0 commit comments

Comments
 (0)