Python: (samples): adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs (#3873)

* adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs

* Updates

* add comment
This commit is contained in:
Evan Mattson
2026-02-12 19:46:58 +09:00
committed by GitHub
Unverified
parent 8457533c69
commit 1b10b051fd
73 changed files with 1612 additions and 686 deletions
@@ -58,13 +58,13 @@ class TextProcessor(Executor):
) -> None:
"""Process a text string and return statistics."""
text_preview = f"'{request.text[:50]}{'...' if len(request.text) > 50 else ''}'"
print(f"πŸ” Sub-workflow processing text (Task {request.task_id}): {text_preview}")
print(f"Sub-workflow processing text (Task {request.task_id}): {text_preview}")
# Simple text processing
word_count = len(request.text.split()) if request.text.strip() else 0
char_count = len(request.text)
print(f"πŸ“Š Task {request.task_id}: {word_count} words, {char_count} characters")
print(f"Task {request.task_id}: {word_count} words, {char_count} characters")
# Create result
result = TextProcessingResult(
@@ -74,7 +74,7 @@ class TextProcessor(Executor):
char_count=char_count,
)
print(f"βœ… Sub-workflow completed task {request.task_id}")
print(f"Sub-workflow completed task {request.task_id}")
# Signal completion by yielding the result
await ctx.yield_output(result)
@@ -92,7 +92,7 @@ class TextProcessingOrchestrator(Executor):
@handler
async def start_processing(self, texts: list[str], ctx: WorkflowContext[TextProcessingRequest]) -> None:
"""Start processing multiple text strings."""
print(f"πŸ“„ Starting processing of {len(texts)} text strings")
print(f"Starting processing of {len(texts)} text strings")
print("=" * 60)
self.expected_count = len(texts)
@@ -101,7 +101,7 @@ class TextProcessingOrchestrator(Executor):
for i, text in enumerate(texts):
task_id = f"task_{i + 1}"
request = TextProcessingRequest(text=text, task_id=task_id)
print(f"πŸ“€ Dispatching {task_id} to sub-workflow")
print(f"Dispatching {task_id} to sub-workflow")
await ctx.send_message(request, target_id="text_processor_workflow")
@handler
@@ -111,12 +111,12 @@ class TextProcessingOrchestrator(Executor):
ctx: WorkflowContext[Never, list[TextProcessingResult]],
) -> None:
"""Collect results from sub-workflows."""
print(f"πŸ“₯ Collected result from {result.task_id}")
print(f"Collected result from {result.task_id}")
self.results.append(result)
# Check if all results are collected
if len(self.results) == self.expected_count:
print("\nπŸŽ‰ All tasks completed!")
print("\nAll tasks completed!")
await ctx.yield_output(self.results)
@@ -138,7 +138,7 @@ def get_result_summary(results: list[TextProcessingResult]) -> dict[str, Any]:
def create_sub_workflow() -> WorkflowExecutor:
"""Create the text processing sub-workflow."""
print("πŸš€ Setting up sub-workflow...")
print("Setting up sub-workflow...")
text_processor = TextProcessor()
processing_workflow = (
@@ -151,7 +151,7 @@ def create_sub_workflow() -> WorkflowExecutor:
async def main():
"""Main function to run the basic sub-workflow example."""
print("πŸ”§ Setting up parent workflow...")
print("Setting up parent workflow...")
# Step 1: Create the parent workflow
orchestrator = TextProcessingOrchestrator()
sub_workflow_executor = create_sub_workflow()
@@ -172,14 +172,14 @@ async def main():
" Spaces around text ",
]
print(f"\nπŸ§ͺ Testing with {len(test_texts)} text strings")
print(f"\nTesting with {len(test_texts)} text strings")
print("=" * 60)
# Step 3: Run the workflow
result = await main_workflow.run(test_texts)
# Step 4: Display results
print("\nπŸ“Š Processing Results:")
print("\nProcessing Results:")
print("=" * 60)
# Sort results by task_id for consistent display
@@ -190,19 +190,19 @@ async def main():
for result in sorted_results:
preview = result.text[:30] + "..." if len(result.text) > 30 else result.text
preview = preview.replace("\n", " ").strip() or "(empty)"
print(f"βœ… {result.task_id}: '{preview}' -> {result.word_count} words, {result.char_count} chars")
print(f"{result.task_id}: '{preview}' -> {result.word_count} words, {result.char_count} chars")
# Step 6: Display summary
summary = get_result_summary(sorted_results)
print("\nπŸ“ˆ Summary:")
print("\nSummary:")
print("=" * 60)
print(f"πŸ“„ Total texts processed: {summary['total_texts']}")
print(f"πŸ“ Total words: {summary['total_words']}")
print(f"πŸ”€ Total characters: {summary['total_characters']}")
print(f"πŸ“Š Average words per text: {summary['average_words_per_text']}")
print(f"πŸ“ Average characters per text: {summary['average_characters_per_text']}")
print(f"Total texts processed: {summary['total_texts']}")
print(f"Total words: {summary['total_words']}")
print(f"Total characters: {summary['total_characters']}")
print(f"Average words per text: {summary['average_words_per_text']}")
print(f"Average characters per text: {summary['average_characters_per_text']}")
print("\n🏁 Processing complete!")
print("\nProcessing complete!")
if __name__ == "__main__":
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from typing import Annotated, Any
from agent_framework import (
@@ -9,8 +10,9 @@ from agent_framework import (
WorkflowExecutor,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
"""
Sample: Sub-Workflow kwargs Propagation
@@ -26,7 +28,8 @@ Key Concepts:
- Useful for passing authentication tokens, configuration, or request context
Prerequisites:
- OpenAI environment variables configured
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured
"""
@@ -74,7 +77,11 @@ async def main() -> None:
print("=" * 70)
# Create chat client
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create an agent with tools that use kwargs
inner_agent = client.as_agent(
@@ -319,14 +319,14 @@ async def main() -> None:
]
# Run the workflow
print(f"πŸ§ͺ Testing with {len(test_requests)} mixed requests.")
print("πŸš€ Starting main workflow...")
print(f"Testing with {len(test_requests)} mixed requests.")
print("Starting main workflow...")
run_result = await main_workflow.run(test_requests)
# Handle request info events
request_info_events = run_result.get_request_info_events()
if request_info_events:
print(f"\nπŸ” Handling {len(request_info_events)} request info events...\n")
print(f"\nHandling {len(request_info_events)} request info events...\n")
responses: dict[str, ResourceResponse | PolicyResponse] = {}
for event in request_info_events:
@@ -73,7 +73,7 @@ def build_email_address_validation_workflow() -> Workflow:
email address to the next executor in the workflow.
"""
sanitized = email_address.strip()
print(f"βœ‚οΈ Sanitized email address: '{sanitized}'")
print(f"Sanitized email address: '{sanitized}'")
await ctx.send_message(SanitizedEmailResult(original=email_address, sanitized=sanitized, is_valid=False))
class EmailFormatValidator(Executor):
@@ -91,14 +91,14 @@ def build_email_address_validation_workflow() -> Workflow:
When the format is valid, it sends the validated email address to the next executor in the workflow.
"""
if "@" not in partial_result.sanitized or "." not in partial_result.sanitized.split("@")[-1]:
print(f"❌ Invalid email format: '{partial_result.sanitized}'")
print(f"Invalid email format: '{partial_result.sanitized}'")
await ctx.yield_output(
SanitizedEmailResult(
original=partial_result.original, sanitized=partial_result.sanitized, is_valid=False
)
)
return
print(f"βœ… Validated email format: '{partial_result.sanitized}'")
print(f"Validated email format: '{partial_result.sanitized}'")
await ctx.send_message(
SanitizedEmailResult(
original=partial_result.original, sanitized=partial_result.sanitized, is_valid=False
@@ -120,7 +120,7 @@ def build_email_address_validation_workflow() -> Workflow:
to an external system to user for validation.
"""
domain = partial_result.sanitized.split("@")[-1]
print(f"πŸ” Validating domain: '{domain}'")
print(f"Validating domain: '{domain}'")
self._pending_domains[domain] = partial_result
# Send a request to the external system via the request_info mechanism
await ctx.request_info(request_data=domain, response_type=bool)
@@ -138,14 +138,14 @@ def build_email_address_validation_workflow() -> Workflow:
raise ValueError(f"Received response for unknown domain: '{original_request}'")
partial_result = self._pending_domains.pop(original_request)
if is_valid:
print(f"βœ… Domain '{original_request}' is valid.")
print(f"Domain '{original_request}' is valid.")
await ctx.yield_output(
SanitizedEmailResult(
original=partial_result.original, sanitized=partial_result.sanitized, is_valid=True
)
)
else:
print(f"❌ Domain '{original_request}' is invalid.")
print(f"Domain '{original_request}' is invalid.")
await ctx.yield_output(
SanitizedEmailResult(
original=partial_result.original, sanitized=partial_result.sanitized, is_valid=False
@@ -201,15 +201,15 @@ class SmartEmailOrchestrator(Executor):
"""
recipient = email.recipient
if recipient in self._approved_recipients:
print(f"πŸ“§ Recipient '{recipient}' has been previously approved.")
print(f"Recipient '{recipient}' has been previously approved.")
await ctx.send_message(email)
return
if recipient in self._disapproved_recipients:
print(f"🚫 Blocking email to previously disapproved recipient: '{recipient}'")
print(f"Blocking email to previously disapproved recipient: '{recipient}'")
await ctx.yield_output(False)
return
print(f"πŸ” Validating new recipient email address: '{recipient}'")
print(f"Validating new recipient email address: '{recipient}'")
self._pending_emails[recipient] = email
await ctx.send_message(recipient)
@@ -227,7 +227,7 @@ class SmartEmailOrchestrator(Executor):
raise TypeError(f"Expected domain string, got {type(request.source_event.data)}")
domain = request.source_event.data
is_valid = domain in self._approved_domains
print(f"🌐 External domain validation for '{domain}': {'valid' if is_valid else 'invalid'}")
print(f"External domain validation for '{domain}': {'valid' if is_valid else 'invalid'}")
await ctx.send_message(request.create_response(is_valid), target_id=request.executor_id)
@handler
@@ -243,11 +243,11 @@ class SmartEmailOrchestrator(Executor):
email = self._pending_emails.pop(result.original)
email.recipient = result.sanitized # Use the sanitized email address
if result.is_valid:
print(f"βœ… Email address '{result.original}' is valid.")
print(f"Email address '{result.original}' is valid.")
self._approved_recipients.add(result.original)
await ctx.send_message(email)
else:
print(f"🚫 Email address '{result.original}' is invalid. Blocking email.")
print(f"Email address '{result.original}' is invalid. Blocking email.")
self._disapproved_recipients.add(result.original)
await ctx.yield_output(False)
@@ -258,9 +258,9 @@ class EmailDelivery(Executor):
@handler
async def handle(self, email: Email, ctx: WorkflowContext[Never, bool]) -> None:
"""Simulate sending the email and yield True as the final result."""
print(f"πŸ“€ Sending email to '{email.recipient}' with subject '{email.subject}'")
print(f"Sending email to '{email.recipient}' with subject '{email.subject}'")
await asyncio.sleep(1) # Simulate network delay
print(f"βœ… Email sent to '{email.recipient}' successfully.")
print(f"Email sent to '{email.recipient}' successfully.")
await ctx.yield_output(True)
@@ -294,10 +294,10 @@ async def main() -> None:
# Execute the workflow
for email in test_emails:
print(f"\nπŸš€ Processing email to '{email.recipient}'")
print(f"\nProcessing email to '{email.recipient}'")
async for event in workflow.run(email, stream=True):
if event.type == "output":
print(f"πŸŽ‰ Final result for '{email.recipient}': {'Delivered' if event.data else 'Blocked'}")
print(f"Final result for '{email.recipient}': {'Delivered' if event.data else 'Blocked'}")
if __name__ == "__main__":