mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
8457533c69
commit
1b10b051fd
@@ -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(
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+16
-16
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user