Python: restructure: Python samples into progressive 01-05 layout (#3862)

* restructure: Python samples into progressive 01-05 layout

- 01-get-started/: 6 numbered steps (hello agent → hosting)
- 02-agents/: all agent concept samples (tools, middleware, providers, etc.)
- 03-workflows/: ALL existing workflow samples preserved as-is
- 04-hosting/: azure-functions, durabletask, a2a
- 05-end-to-end/: demos, evaluation, hosted agents
- Old files moved to _to_delete/ for review
- Added AGENTS.md with structure documentation
- autogen-migration/ and semantic-kernel-migration/ preserved at root

* fix: switch to AzureOpenAI Foundry, fix CI failures

- Switch all 01-get-started samples to AzureOpenAIResponsesClient with
  Azure AI Foundry project endpoint (AZURE_AI_PROJECT_ENDPOINT +
  AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME + AzureCliCredential)
- Add _to_delete/ and 05-end-to-end/ to pyrightconfig.samples.json excludes
- Fix test paths in packages/ that referenced old getting_started/ dirs:
  durabletask conftest + streaming test, azurefunctions conftest,
  devui conftest + capture_messages + openai_sdk_integration
- Fix workflow_as_agent_human_in_the_loop.py import (sibling import)
- Update hosting READMEs and tool comment paths
- Replace root README.md with new structure overview
- Update AGENTS.md to document Azure OpenAI Foundry as default provider

* cleanup: remove _to_delete folder, copy resource files to active dirs

All files in _to_delete/ were either:
- Exact duplicates of files in the new structure (240 files)
- Same file with only comment path updates (100 files)
- One import-fix diff (workflow_as_agent_human_in_the_loop.py)
- One superseded minimal_sample.py

Resource files (sample.pdf, countries.json, employees.pdf, weather.json)
copied to 02-agents/sample_assets/ and 02-agents/resources/ since active
samples reference them.

* fix: address PR review comments, centralize resources, remove root duplicates

- Fix type annotation in 04_memory.py (string union -> proper types)
- Fix old sample paths in observability files
- Fix grammar/spelling in observability samples
- Move sample_assets/ and resources/ to shared/ folder
- Remove 8 duplicate observability files from 02-agents root
- Update resource path references in multimodal_input and provider samples

* fix: update broken links from old getting_started paths to new structure

- Update relative paths in READMEs: getting_started/ → 01-get-started/,
  02-agents/, 03-workflows/, 04-hosting/, 05-end-to-end/
- Fix absolute GitHub URLs in package READMEs
- Fix broken link in ollama package README

* fix: convert absolute GitHub URLs to relative paths for link checker

Absolute URLs to python/samples/ on main branch 404 until PR merges.
Converted to relative paths that linkspector can verify locally.

* fix: update link for handoff sample moved to orchestrations/

* fix: update chatkit-integration README path from demos/ to 05-end-to-end/

* fix: update broken links in orchestrations README to match flat directory structure
This commit is contained in:
Eduard van Valkenburg
2026-02-12 18:36:36 +01:00
committed by GitHub
Unverified
parent 69dcfe31ee
commit a2856d3b92
536 changed files with 3816 additions and 1632 deletions
+19
View File
@@ -0,0 +1,19 @@
# Auto-generated Dockerfiles from DevUI deployment
*/Dockerfile
# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
# Environment files (may contain secrets)
.env
*.env
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
+160
View File
@@ -0,0 +1,160 @@
# DevUI Samples
This folder contains sample agents and workflows designed to work with the Agent Framework DevUI - a lightweight web interface for running and testing agents interactively.
## What is DevUI?
DevUI is a sample application that provides:
- A web interface for testing agents and workflows
- OpenAI-compatible API endpoints
- Directory-based entity discovery
- In-memory entity registration
- Sample entity gallery
> **Note**: DevUI is a sample app for development and testing. For production use, build your own custom interface using the Agent Framework SDK.
## Quick Start
### Option 1: In-Memory Mode (Simplest)
Run a single sample directly. This demonstrates how to wrap agents and workflows programmatically without needing a directory structure:
```bash
cd python/samples/02-agents/devui
python in_memory_mode.py
```
This opens your browser at http://localhost:8090 with pre-configured agents and a basic workflow.
### Option 2: Directory Discovery
Launch DevUI to discover all samples in this folder:
```bash
cd python/samples/02-agents/devui
devui
```
This starts the server at http://localhost:8080 with all agents and workflows available.
## Sample Structure
Each agent/workflow follows a strict structure required by DevUI's discovery system:
```
agent_name/
├── __init__.py # Must export: agent = Agent(...)
├── agent.py # Agent implementation
└── .env.example # Example environment variables
```
## Available Samples
### Agents
| Sample | Description | Features | Required Environment Variables |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| [**weather_agent_azure/**](weather_agent_azure/) | Weather agent using Azure OpenAI with API key authentication | Azure OpenAI integration, function calling, mock weather tools | `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, `AZURE_OPENAI_ENDPOINT` |
| [**foundry_agent/**](foundry_agent/) | Weather agent using Azure AI Agent (Foundry) with Azure CLI authentication (run `az login` first) | Azure AI Agent integration, Azure CLI authentication, mock weather tools | `AZURE_AI_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_DEPLOYMENT_NAME` |
### Workflows
| Sample | Description | Features | Required Environment Variables |
| -------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| [**declarative/**](declarative/) | Declarative YAML workflow with conditional branching | YAML-based workflow definition, conditional logic, no Python code required | None - uses mock data |
| [**workflow_agents/**](workflow_agents/) | Content review workflow with agents as executors | Agents as workflow nodes, conditional routing based on structured outputs, quality-based paths (Writer -> Reviewer -> Editor/Publisher) | `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, `AZURE_OPENAI_ENDPOINT` |
| [**spam_workflow/**](spam_workflow/) | 5-step email spam detection workflow with branching logic | Sequential execution, conditional branching (spam vs. legitimate), multiple executors, mock spam detection | None - uses mock data |
| [**fanout_workflow/**](fanout_workflow/) | Advanced data processing workflow with parallel execution | Fan-out/fan-in patterns, complex state management, multi-stage processing (validation -> transformation -> quality assurance) | None - uses mock data |
### Standalone Examples
| Sample | Description | Features |
| ------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| [**in_memory_mode.py**](in_memory_mode.py) | Demonstrates programmatic entity registration without directory structure | In-memory agent and workflow registration, multiple entities served from a single file, includes basic workflow, simplest way to get started |
## Environment Variables
Each sample that requires API keys includes a `.env.example` file. To use:
1. Copy `.env.example` to `.env` in the same directory
2. Fill in your actual API keys
3. DevUI automatically loads `.env` files from entity directories
Alternatively, set environment variables globally:
```bash
export OPENAI_API_KEY="your-key-here"
export OPENAI_CHAT_MODEL_ID="gpt-4o"
```
## Using DevUI with Your Own Agents
To make your agent discoverable by DevUI:
1. Create a folder for your agent
2. Add an `__init__.py` that exports `agent` or `workflow`
3. (Optional) Add a `.env` file for environment variables
Example:
```python
# my_agent/__init__.py
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
agent = Agent(
name="MyAgent",
description="My custom agent",
client=OpenAIChatClient(),
# ... your configuration
)
```
Then run:
```bash
devui /path/to/my/agents/folder
```
## API Usage
DevUI exposes OpenAI-compatible endpoints:
```bash
curl -X POST http://localhost:8080/v1/responses \
-H "Content-Type: application/json" \
-d '{
"model": "agent-framework",
"input": "What is the weather in Seattle?",
"extra_body": {"entity_id": "agent_directory_weather-agent_<uuid>"}
}'
```
List available entities:
```bash
curl http://localhost:8080/v1/entities
```
## Learn More
- [DevUI Documentation](../../../packages/devui/README.md)
- [Agent Framework Documentation](https://docs.microsoft.com/agent-framework)
- [Sample Guidelines](../../SAMPLE_GUIDELINES.md)
## Troubleshooting
**Missing API keys**: Check your `.env` files or environment variables.
**Import errors**: Make sure you've installed the devui package:
```bash
pip install agent-framework-devui --pre
```
**Port conflicts**: DevUI uses ports 8080 (directory mode) and 8090 (in-memory mode) by default. Close other services or specify a different port:
```bash
devui --port 8888
```
@@ -0,0 +1,15 @@
# Azure OpenAI Responses API Configuration
# The Responses API supports PDF uploads, images, and other multimodal content.
# Requires api-version 2025-03-01-preview or later.
# Option 1: Use API key authentication
AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
# Option 2: Use Azure CLI authentication (run 'az login' first)
# No API key needed - just leave AZURE_OPENAI_API_KEY unset
# Required: Azure OpenAI endpoint with Responses API support
AZURE_OPENAI_ENDPOINT=https://your-resource.cognitiveservices.azure.com/
# Required: Deployment name (must support Responses API)
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=gpt-4.1-mini
@@ -0,0 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
"""Azure Responses Agent sample for DevUI."""
from .agent import agent
__all__ = ["agent"]
@@ -0,0 +1,124 @@
# Copyright (c) Microsoft. All rights reserved.
"""Sample agent using Azure OpenAI Responses API for Agent Framework DevUI.
This agent uses the Responses API which supports:
- PDF file uploads
- Image uploads
- Audio inputs
- And other multimodal content
The Chat Completions API (AzureOpenAIChatClient) does NOT support PDF uploads.
Use this agent when you need to process documents or other file types.
Required environment variables:
- AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint
- AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: Deployment name for Responses API
(falls back to AZURE_OPENAI_CHAT_DEPLOYMENT_NAME if not set)
- AZURE_OPENAI_API_KEY: Your API key (or use Azure CLI auth)
"""
import logging
import os
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.azure import AzureOpenAIResponsesClient
logger = logging.getLogger(__name__)
# Get deployment name - try responses-specific env var first, fall back to chat deployment
_deployment_name = os.environ.get(
"AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME",
os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", ""),
)
# Get endpoint - try responses-specific env var first, fall back to default
_endpoint = os.environ.get(
"AZURE_OPENAI_RESPONSES_ENDPOINT",
os.environ.get("AZURE_OPENAI_ENDPOINT", ""),
)
def analyze_content(
query: Annotated[str, "What to analyze or extract from the uploaded content"],
) -> str:
"""Analyze uploaded content based on the user's query.
This is a placeholder - the actual analysis is done by the model
when processing the uploaded files.
"""
return f"Analyzing content for: {query}"
# 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_threads.py.
@tool(approval_mode="never_require")
def summarize_document(
length: Annotated[str, "Desired summary length: 'brief', 'medium', or 'detailed'"] = "medium",
) -> str:
"""Generate a summary of the uploaded document."""
return f"Generating {length} summary of the document..."
@tool(approval_mode="never_require")
def extract_key_points(
max_points: Annotated[int, "Maximum number of key points to extract"] = 5,
) -> str:
"""Extract key points from the uploaded document."""
return f"Extracting up to {max_points} key points..."
# Agent using Azure OpenAI Responses API (supports PDF uploads!)
agent = Agent(
name="AzureResponsesAgent",
description="An agent that can analyze PDFs, images, and other documents using Azure OpenAI Responses API",
instructions="""
You are a helpful document analysis assistant. You can:
1. Analyze uploaded PDF documents and extract information
2. Summarize document contents
3. Answer questions about uploaded files
4. Extract key points and insights
When a user uploads a file, carefully analyze its contents and provide
helpful, accurate information based on what you find.
For PDFs, you can read and understand the text, tables, and structure.
For images, you can describe what you see and extract any text.
""",
client=AzureOpenAIResponsesClient(
deployment_name=_deployment_name,
endpoint=_endpoint,
api_version="2025-03-01-preview", # Required for Responses API
),
tools=[summarize_document, extract_key_points],
)
def main():
"""Launch the Azure Responses agent in DevUI."""
from agent_framework_devui import serve
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger.info("=" * 60)
logger.info("Starting Azure Responses Agent")
logger.info("=" * 60)
logger.info("")
logger.info("This agent uses the Azure OpenAI Responses API which supports:")
logger.info(" - PDF file uploads")
logger.info(" - Image uploads")
logger.info(" - Audio inputs")
logger.info("")
logger.info("Try uploading a PDF and asking questions about it!")
logger.info("")
logger.info("Required environment variables:")
logger.info(" - AZURE_OPENAI_ENDPOINT")
logger.info(" - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME")
logger.info(" - AZURE_OPENAI_API_KEY (or use Azure CLI auth)")
logger.info("")
serve(entities=[agent], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,3 @@
# Copyright (c) Microsoft. All rights reserved.
"""Declarative workflow sample for DevUI."""
@@ -0,0 +1,25 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Run the declarative workflow sample with DevUI.
Demonstrates conditional branching based on age input using YAML-defined workflow.
"""
from pathlib import Path
from agent_framework.declarative import WorkflowFactory
from agent_framework.devui import serve
factory = WorkflowFactory()
workflow_path = Path(__file__).parent / "workflow.yaml"
workflow = factory.create_workflow_from_yaml_path(workflow_path)
def main():
"""Run the declarative workflow with DevUI."""
serve(entities=[workflow], auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,64 @@
name: conditional-workflow
description: Demonstrates conditional branching based on user input
inputs:
age:
type: integer
description: The user's age in years
actions:
- kind: SetValue
id: get_age
displayName: Get user age
path: turn.age
value: =inputs.age
- kind: If
id: check_age
displayName: Check age category
condition: =turn.age < 13
then:
- kind: SetValue
path: turn.category
value: child
- kind: SendActivity
activity:
text: "Welcome, young one! Here are some fun activities for kids."
else:
- kind: If
condition: =turn.age < 20
then:
- kind: SetValue
path: turn.category
value: teenager
- kind: SendActivity
activity:
text: "Hey there! Check out these cool things for teens."
else:
- kind: If
condition: =turn.age < 65
then:
- kind: SetValue
path: turn.category
value: adult
- kind: SendActivity
activity:
text: "Welcome! Here are our professional services."
else:
- kind: SetValue
path: turn.category
value: senior
- kind: SendActivity
activity:
text: "Welcome! Enjoy our senior member benefits."
- kind: SendActivity
id: summary
displayName: Send category summary
activity:
text: '=Concat("You have been categorized as: ", turn.category)'
- kind: SetValue
id: set_output
path: workflow.outputs.category
value: =turn.category
@@ -0,0 +1,3 @@
# Copyright (c) Microsoft. All rights reserved.
"""Fanout workflow example."""
@@ -0,0 +1,703 @@
# Copyright (c) Microsoft. All rights reserved.
"""Complex Fan-In/Fan-Out Data Processing Workflow.
This workflow demonstrates a sophisticated data processing pipeline with multiple stages:
1. Data Ingestion - Simulates loading data from multiple sources
2. Data Validation - Multiple validators run in parallel to check data quality
3. Data Transformation - Fan-out to different transformation processors
4. Quality Assurance - Multiple QA checks run in parallel
5. Data Aggregation - Fan-in to combine processed results
6. Final Processing - Generate reports and complete workflow
The workflow includes realistic delays to simulate actual processing time and
shows complex fan-in/fan-out patterns with conditional processing.
"""
import asyncio
import logging
from dataclasses import dataclass
from enum import Enum
from typing import Literal
from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowContext,
handler,
)
from pydantic import BaseModel, Field
from typing_extensions import Never
class DataType(Enum):
"""Types of data being processed."""
CUSTOMER = "customer"
TRANSACTION = "transaction"
PRODUCT = "product"
ANALYTICS = "analytics"
class ValidationResult(Enum):
"""Results of data validation."""
VALID = "valid"
WARNING = "warning"
ERROR = "error"
class ProcessingRequest(BaseModel):
"""Complex input structure for data processing workflow."""
# Basic information
data_source: Literal["database", "api", "file_upload", "streaming"] = Field(
description="The source of the data to be processed", default="database"
)
data_type: Literal["customer", "transaction", "product", "analytics"] = Field(
description="Type of data being processed", default="customer"
)
processing_priority: Literal["low", "normal", "high", "critical"] = Field(
description="Processing priority level", default="normal"
)
# Processing configuration
batch_size: int = Field(description="Number of records to process in each batch", default=500, ge=100, le=10000)
quality_threshold: float = Field(
description="Minimum quality score required (0.0-1.0)", default=0.8, ge=0.0, le=1.0
)
# Validation settings
enable_schema_validation: bool = Field(description="Enable schema validation checks", default=True)
enable_security_validation: bool = Field(description="Enable security validation checks", default=True)
enable_quality_validation: bool = Field(description="Enable data quality validation checks", default=True)
# Transformation options
transformations: list[Literal["normalize", "enrich", "aggregate"]] = Field(
description="List of transformations to apply", default=["normalize", "enrich"]
)
# Optional description
description: str | None = Field(description="Optional description of the processing request", default=None)
# Test failure scenarios
force_validation_failure: bool = Field(
description="Force validation failure for testing (demo purposes)", default=False
)
force_transformation_failure: bool = Field(
description="Force transformation failure for testing (demo purposes)", default=False
)
@dataclass
class DataBatch:
"""Represents a batch of data being processed."""
batch_id: str
data_type: DataType
size: int
content: str
source: str = "unknown"
timestamp: float = 0.0
@dataclass
class ValidationReport:
"""Report from data validation."""
batch_id: str
validator_id: str
result: ValidationResult
issues_found: int
processing_time: float
details: str
@dataclass
class TransformationResult:
"""Result from data transformation."""
batch_id: str
transformer_id: str
original_size: int
processed_size: int
transformation_type: str
processing_time: float
success: bool
@dataclass
class QualityAssessment:
"""Quality assessment result."""
batch_id: str
assessor_id: str
quality_score: float
recommendations: list[str]
processing_time: float
@dataclass
class ProcessingSummary:
"""Summary of all processing stages."""
batch_id: str
total_processing_time: float
validation_reports: list[ValidationReport]
transformation_results: list[TransformationResult]
quality_assessments: list[QualityAssessment]
final_status: str
# Data Ingestion Stage
class DataIngestion(Executor):
"""Simulates ingesting data from multiple sources with delays."""
@handler
async def ingest_data(self, request: ProcessingRequest, ctx: WorkflowContext[DataBatch]) -> None:
"""Simulate data ingestion with realistic delays based on input configuration."""
# Simulate network delay based on data source
delay_map = {"database": 1.5, "api": 3.0, "file_upload": 4.0, "streaming": 1.0}
delay = delay_map.get(request.data_source, 3.0)
await asyncio.sleep(delay) # Fixed delay for demo
# Simulate data size based on priority and configuration
base_size = request.batch_size
if request.processing_priority == "critical":
size_multiplier = 1.7 # Critical priority gets the largest batches
elif request.processing_priority == "high":
size_multiplier = 1.3 # High priority gets larger batches
elif request.processing_priority == "low":
size_multiplier = 0.6 # Low priority gets smaller batches
else: # normal
size_multiplier = 1.0 # Normal priority uses base size
actual_size = int(base_size * size_multiplier)
batch = DataBatch(
batch_id=f"batch_{5555}", # Fixed batch ID for demo
data_type=DataType(request.data_type),
size=actual_size,
content=f"Processing {request.data_type} data from {request.data_source}",
source=request.data_source,
timestamp=asyncio.get_event_loop().time(),
)
# Store both batch data and original request in workflow state
ctx.set_state(f"batch_{batch.batch_id}", batch)
ctx.set_state(f"request_{batch.batch_id}", request)
await ctx.send_message(batch)
# Validation Stage (Fan-out)
class SchemaValidator(Executor):
"""Validates data schema and structure."""
@handler
async def validate_schema(self, batch: DataBatch, ctx: WorkflowContext[ValidationReport]) -> None:
"""Perform schema validation with processing delay."""
# Check if schema validation is enabled
request = ctx.get_state(f"request_{batch.batch_id}")
if not request or not request.enable_schema_validation:
return
# Simulate schema validation processing
processing_time = 2.0 # Fixed processing time
await asyncio.sleep(processing_time)
# Simulate validation results - consider force failure flag
issues = 4 if request.force_validation_failure else 2 # Fixed issue counts
result = (
ValidationResult.VALID
if issues <= 1
else (ValidationResult.WARNING if issues <= 2 else ValidationResult.ERROR)
)
report = ValidationReport(
batch_id=batch.batch_id,
validator_id=self.id,
result=result,
issues_found=issues,
processing_time=processing_time,
details=f"Schema validation found {issues} issues in {batch.data_type.value} data from {batch.source}",
)
await ctx.send_message(report)
class DataQualityValidator(Executor):
"""Validates data quality and completeness."""
@handler
async def validate_quality(self, batch: DataBatch, ctx: WorkflowContext[ValidationReport]) -> None:
"""Perform data quality validation."""
# Check if quality validation is enabled
request = ctx.get_state(f"request_{batch.batch_id}")
if not request or not request.enable_quality_validation:
return
processing_time = 2.5 # Fixed processing time
await asyncio.sleep(processing_time)
# Quality checks are stricter for higher priority data
issues = (
2 # Fixed issue count for high priority
if request.processing_priority in ["critical", "high"]
else 3 # Fixed issue count for normal priority
)
if request.force_validation_failure:
issues = max(issues, 4) # Ensure failure
result = (
ValidationResult.VALID
if issues <= 1
else (ValidationResult.WARNING if issues <= 3 else ValidationResult.ERROR)
)
report = ValidationReport(
batch_id=batch.batch_id,
validator_id=self.id,
result=result,
issues_found=issues,
processing_time=processing_time,
details=f"Quality check found {issues} data quality issues (priority: {request.processing_priority})",
)
await ctx.send_message(report)
class SecurityValidator(Executor):
"""Validates data for security and compliance issues."""
@handler
async def validate_security(self, batch: DataBatch, ctx: WorkflowContext[ValidationReport]) -> None:
"""Perform security validation."""
# Check if security validation is enabled
request = ctx.get_state(f"request_{batch.batch_id}")
if not request or not request.enable_security_validation:
return
processing_time = 3.0 # Fixed processing time
await asyncio.sleep(processing_time)
# Security is more stringent for customer/transaction data
issues = 1 if batch.data_type in [DataType.CUSTOMER, DataType.TRANSACTION] else 2
if request.force_validation_failure:
issues = max(issues, 1) # Force at least one security issue
# Security errors are more serious - less tolerance
result = ValidationResult.VALID if issues == 0 else ValidationResult.ERROR
report = ValidationReport(
batch_id=batch.batch_id,
validator_id=self.id,
result=result,
issues_found=issues,
processing_time=processing_time,
details=f"Security scan found {issues} security issues in {batch.data_type.value} data",
)
await ctx.send_message(report)
# Validation Aggregator (Fan-in)
class ValidationAggregator(Executor):
"""Aggregates validation results and decides on next steps."""
@handler
async def aggregate_validations(
self, reports: list[ValidationReport], ctx: WorkflowContext[DataBatch, str]
) -> None:
"""Aggregate all validation reports and make processing decision."""
if not reports:
return
batch_id = reports[0].batch_id
request = ctx.get_state(f"request_{batch_id}")
await asyncio.sleep(1) # Aggregation processing time
total_issues = sum(report.issues_found for report in reports)
has_errors = any(report.result == ValidationResult.ERROR for report in reports)
# Calculate quality score (0.0 to 1.0)
max_possible_issues = len(reports) * 5 # Assume max 5 issues per validator
quality_score = max(0.0, 1.0 - (total_issues / max_possible_issues))
# Decision logic: fail if errors OR quality below threshold
should_fail = has_errors or (quality_score < request.quality_threshold)
if should_fail:
failure_reason: list[str] = []
if has_errors:
failure_reason.append("validation errors detected")
if quality_score < request.quality_threshold:
failure_reason.append(
f"quality score {quality_score:.2f} below threshold {request.quality_threshold:.2f}"
)
reason = " and ".join(failure_reason)
await ctx.yield_output(
f"Batch {batch_id} failed validation: {reason}. "
f"Total issues: {total_issues}, Quality score: {quality_score:.2f}"
)
return
# Retrieve original batch from workflow state
batch_data = ctx.get_state(f"batch_{batch_id}")
if batch_data:
await ctx.send_message(batch_data)
else:
# Fallback: create a simplified batch
batch = DataBatch(
batch_id=batch_id,
data_type=DataType.ANALYTICS,
size=500,
content="Validated data ready for transformation",
)
await ctx.send_message(batch)
# Transformation Stage (Fan-out)
class DataNormalizer(Executor):
"""Normalizes and cleans data."""
@handler
async def normalize_data(self, batch: DataBatch, ctx: WorkflowContext[TransformationResult]) -> None:
"""Perform data normalization."""
request = ctx.get_state(f"request_{batch.batch_id}")
# Check if normalization is enabled
if not request or "normalize" not in request.transformations:
# Send a "skipped" result
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=batch.size,
transformation_type="normalization",
processing_time=0.1,
success=True, # Consider skipped as successful
)
await ctx.send_message(result)
return
processing_time = 4.0 # Fixed processing time
await asyncio.sleep(processing_time)
# Simulate data size change during normalization
processed_size = int(batch.size * 1.0) # No size change for demo
# Consider force failure flag
success = not request.force_transformation_failure # 75% success rate simplified to always success
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=processed_size,
transformation_type="normalization",
processing_time=processing_time,
success=success,
)
await ctx.send_message(result)
class DataEnrichment(Executor):
"""Enriches data with additional information."""
@handler
async def enrich_data(self, batch: DataBatch, ctx: WorkflowContext[TransformationResult]) -> None:
"""Perform data enrichment."""
request = ctx.get_state(f"request_{batch.batch_id}")
# Check if enrichment is enabled
if not request or "enrich" not in request.transformations:
# Send a "skipped" result
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=batch.size,
transformation_type="enrichment",
processing_time=0.1,
success=True, # Consider skipped as successful
)
await ctx.send_message(result)
return
processing_time = 5.0 # Fixed processing time
await asyncio.sleep(processing_time)
processed_size = int(batch.size * 1.3) # Enrichment increases data
# Consider force failure flag
success = not request.force_transformation_failure # 67% success rate simplified to always success
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=processed_size,
transformation_type="enrichment",
processing_time=processing_time,
success=success,
)
await ctx.send_message(result)
class DataAggregator(Executor):
"""Aggregates and summarizes data."""
@handler
async def aggregate_data(self, batch: DataBatch, ctx: WorkflowContext[TransformationResult]) -> None:
"""Perform data aggregation."""
request = ctx.get_state(f"request_{batch.batch_id}")
# Check if aggregation is enabled
if not request or "aggregate" not in request.transformations:
# Send a "skipped" result
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=batch.size,
transformation_type="aggregation",
processing_time=0.1,
success=True, # Consider skipped as successful
)
await ctx.send_message(result)
return
processing_time = 2.5 # Fixed processing time
await asyncio.sleep(processing_time)
processed_size = int(batch.size * 0.5) # Aggregation reduces data
# Consider force failure flag
success = not request.force_transformation_failure # 80% success rate simplified to always success
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=processed_size,
transformation_type="aggregation",
processing_time=processing_time,
success=success,
)
await ctx.send_message(result)
# Quality Assurance Stage (Fan-out)
class PerformanceAssessor(Executor):
"""Assesses performance characteristics of processed data."""
@handler
async def assess_performance(
self, results: list[TransformationResult], ctx: WorkflowContext[QualityAssessment]
) -> None:
"""Assess performance of transformations."""
if not results:
return
batch_id = results[0].batch_id
processing_time = 2.0 # Fixed processing time
await asyncio.sleep(processing_time)
avg_processing_time = sum(r.processing_time for r in results) / len(results)
success_rate = sum(1 for r in results if r.success) / len(results)
quality_score = (success_rate * 0.7 + (1 - min(avg_processing_time / 10, 1)) * 0.3) * 100
recommendations: list[str] = []
if success_rate < 0.8:
recommendations.append("Consider improving transformation reliability")
if avg_processing_time > 5:
recommendations.append("Optimize processing performance")
if quality_score < 70:
recommendations.append("Review overall data pipeline efficiency")
assessment = QualityAssessment(
batch_id=batch_id,
assessor_id=self.id,
quality_score=quality_score,
recommendations=recommendations,
processing_time=processing_time,
)
await ctx.send_message(assessment)
class AccuracyAssessor(Executor):
"""Assesses accuracy and correctness of processed data."""
@handler
async def assess_accuracy(
self, results: list[TransformationResult], ctx: WorkflowContext[QualityAssessment]
) -> None:
"""Assess accuracy of transformations."""
if not results:
return
batch_id = results[0].batch_id
processing_time = 3.0 # Fixed processing time
await asyncio.sleep(processing_time)
# Simulate accuracy analysis
accuracy_score = 85.0 # Fixed accuracy score
recommendations: list[str] = []
if accuracy_score < 85:
recommendations.append("Review data transformation algorithms")
if accuracy_score < 80:
recommendations.append("Implement additional validation steps")
assessment = QualityAssessment(
batch_id=batch_id,
assessor_id=self.id,
quality_score=accuracy_score,
recommendations=recommendations,
processing_time=processing_time,
)
await ctx.send_message(assessment)
# Final Processing and Completion
class FinalProcessor(Executor):
"""Final processing stage that combines all results."""
@handler
async def process_final_results(
self, assessments: list[QualityAssessment], ctx: WorkflowContext[Never, str]
) -> None:
"""Generate final processing summary and complete workflow."""
if not assessments:
await ctx.yield_output("No quality assessments received")
return
batch_id = assessments[0].batch_id
# Simulate final processing delay
await asyncio.sleep(2)
# Calculate overall metrics
avg_quality_score = sum(a.quality_score for a in assessments) / len(assessments)
total_recommendations = sum(len(a.recommendations) for a in assessments)
total_processing_time = sum(a.processing_time for a in assessments)
# Determine final status
if avg_quality_score >= 85:
final_status = "EXCELLENT"
elif avg_quality_score >= 75:
final_status = "GOOD"
elif avg_quality_score >= 65:
final_status = "ACCEPTABLE"
else:
final_status = "NEEDS_IMPROVEMENT"
completion_message = (
f"Batch {batch_id} processing completed!\n"
f"📊 Overall Quality Score: {avg_quality_score:.1f}%\n"
f"⏱️ Total Processing Time: {total_processing_time:.1f}s\n"
f"💡 Total Recommendations: {total_recommendations}\n"
f"🎖️ Final Status: {final_status}"
)
await ctx.yield_output(completion_message)
# Workflow Builder Helper
class WorkflowSetupHelper:
"""Helper class to set up the complex workflow with state management."""
@staticmethod
async def store_batch_data(batch: DataBatch, ctx: WorkflowContext) -> None:
"""Store batch data in workflow state for later retrieval."""
ctx.set_state(f"batch_{batch.batch_id}", batch)
# Create the workflow instance
def create_complex_workflow():
"""Create the complex fan-in/fan-out workflow."""
# Create all executors
data_ingestion = DataIngestion(id="data_ingestion")
# Validation stage (fan-out)
schema_validator = SchemaValidator(id="schema_validator")
quality_validator = DataQualityValidator(id="quality_validator")
security_validator = SecurityValidator(id="security_validator")
validation_aggregator = ValidationAggregator(id="validation_aggregator")
# Transformation stage (fan-out)
data_normalizer = DataNormalizer(id="data_normalizer")
data_enrichment = DataEnrichment(id="data_enrichment")
data_aggregator_exec = DataAggregator(id="data_aggregator")
# Quality assurance stage (fan-out)
performance_assessor = PerformanceAssessor(id="performance_assessor")
accuracy_assessor = AccuracyAssessor(id="accuracy_assessor")
# Final processing
final_processor = FinalProcessor(id="final_processor")
# Build the workflow with complex fan-in/fan-out patterns
return (
WorkflowBuilder(
name="Data Processing Pipeline",
description="Complex workflow with parallel validation, transformation, and quality assurance stages",
start_executor=data_ingestion,
)
# Fan-out to validation stage
.add_fan_out_edges(data_ingestion, [schema_validator, quality_validator, security_validator])
# Fan-in from validation to aggregator
.add_fan_in_edges([schema_validator, quality_validator, security_validator], validation_aggregator)
# Fan-out to transformation stage
.add_fan_out_edges(validation_aggregator, [data_normalizer, data_enrichment, data_aggregator_exec])
# Fan-in to quality assurance stage (both assessors receive all transformation results)
.add_fan_in_edges([data_normalizer, data_enrichment, data_aggregator_exec], performance_assessor)
.add_fan_in_edges([data_normalizer, data_enrichment, data_aggregator_exec], accuracy_assessor)
# Fan-in to final processor
.add_fan_in_edges([performance_assessor, accuracy_assessor], final_processor)
.build()
)
# Export the workflow for DevUI discovery
workflow = create_complex_workflow()
def main():
"""Launch the fanout workflow in DevUI."""
from agent_framework.devui import serve
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Complex Fan-In/Fan-Out Data Processing Workflow")
logger.info("Available at: http://localhost:8090")
logger.info("Entity ID: workflow_complex_workflow")
# Launch server with the workflow
serve(entities=[workflow], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,6 @@
# Azure AI Foundry Configuration
# Get your credentials from Azure AI Foundry portal
# Make sure to run 'az login' before starting devui
AZURE_AI_PROJECT_ENDPOINT=https://your-project.api.azureml.ms
FOUNDRY_MODEL_DEPLOYMENT_NAME=gpt-4o
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Weather agent sample for DevUI testing."""
from .agent import agent
__all__ = ["agent"]
@@ -0,0 +1,82 @@
# Copyright (c) Microsoft. All rights reserved.
"""Foundry-based weather agent for Agent Framework Debug UI.
This agent uses Azure AI Foundry with Azure CLI authentication.
Make sure to run 'az login' before starting devui.
"""
import os
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
from pydantic import Field
# 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_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
temperature = 22
return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C."
@tool(approval_mode="never_require")
def get_forecast(
location: Annotated[str, Field(description="The location to get the forecast for.")],
days: Annotated[int, Field(description="Number of days for forecast")] = 3,
) -> str:
"""Get weather forecast for multiple days."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
forecast: list[str] = []
for day in range(1, days + 1):
condition = conditions[day % len(conditions)]
temp = 18 + day
forecast.append(f"Day {day}: {condition}, {temp}°C")
return f"Weather forecast for {location}:\n" + "\n".join(forecast)
# Agent instance following Agent Framework conventions
agent = Agent(
name="FoundryWeatherAgent",
client=AzureAIAgentClient(
project_endpoint=os.environ.get("AZURE_AI_PROJECT_ENDPOINT"),
model_deployment_name=os.environ.get("FOUNDRY_MODEL_DEPLOYMENT_NAME"),
credential=AzureCliCredential(),
),
instructions="""
You are a weather assistant using Azure AI Foundry models. You can provide
current weather information and forecasts for any location. Always be helpful
and provide detailed weather information when asked.
""",
tools=[get_weather, get_forecast],
)
def main():
"""Launch the Foundry weather agent in DevUI."""
import logging
from agent_framework.devui import serve
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Foundry Weather Agent")
logger.info("Available at: http://localhost:8090")
logger.info("Entity ID: agent_FoundryWeatherAgent")
logger.info("Note: Make sure 'az login' has been run for authentication")
# Launch server with the agent
serve(entities=[agent], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,124 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example of using Agent Framework DevUI with in-memory entity registration.
This demonstrates the simplest way to serve agents and workflows as OpenAI-compatible API endpoints.
Includes both agents and a basic workflow to showcase different entity types.
"""
import logging
import os
from typing import Annotated
from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler, tool
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.devui import serve
from typing_extensions import Never
# 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_threads.py.
@tool(approval_mode="never_require")
# Tool functions for the agent
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, "The location to get the weather for."],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
temperature = 53
return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C."
@tool(approval_mode="never_require")
def get_time(
timezone: Annotated[str, "The timezone to get time for."] = "UTC",
) -> str:
"""Get current time for a timezone."""
from datetime import datetime
# Simplified for example
return f"Current time in {timezone}: {datetime.now().strftime('%H:%M:%S')}"
# Basic workflow executors
class UpperCase(Executor):
"""Convert text to uppercase."""
@handler
async def to_upper(self, text: str, ctx: WorkflowContext[str]) -> None:
"""Convert input to uppercase and forward to next executor."""
result = text.upper()
await ctx.send_message(result)
class AddExclamation(Executor):
"""Add exclamation mark to text."""
@handler
async def add_exclamation(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
"""Add exclamation and yield as workflow output."""
result = f"{text}!"
await ctx.yield_output(result)
def main():
"""Main function demonstrating in-memory entity registration."""
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
# Create Azure OpenAI chat client
client = AzureOpenAIChatClient(
api_key=os.environ.get("AZURE_OPENAI_API_KEY"),
azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21"),
model_id=os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "gpt-4o"),
)
# Create agents
weather_agent = Agent(
name="weather-assistant",
description="Provides weather information and time",
instructions=(
"You are a helpful weather and time assistant. Use the available tools to "
"provide accurate weather information and current time for any location."
),
client=client,
tools=[get_weather, get_time],
)
simple_agent = Agent(
name="general-assistant",
description="A simple conversational agent",
instructions="You are a helpful assistant.",
client=client,
)
# Create a basic workflow: Input -> UpperCase -> AddExclamation -> Output
upper_executor = UpperCase(id="upper_case")
exclaim_executor = AddExclamation(id="add_exclamation")
basic_workflow = (
WorkflowBuilder(
name="Text Transformer",
description="Simple 2-step workflow that converts text to uppercase and adds exclamation",
start_executor=upper_executor,
)
.add_edge(upper_executor, exclaim_executor)
.build()
)
# Collect entities for serving
entities = [weather_agent, simple_agent, basic_workflow]
logger.info("Starting DevUI on http://localhost:8090")
logger.info("Entities available:")
logger.info(" - Agents: weather-assistant, general-assistant")
logger.info(" - Workflow: basic text transformer (uppercase + exclamation)")
# Launch server with auto-generated entity IDs
serve(entities=entities, port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Spam detection workflow sample for DevUI testing."""
from .workflow import workflow
__all__ = ["workflow"]
@@ -0,0 +1,440 @@
# Copyright (c) Microsoft. All rights reserved.
"""Spam Detection Workflow Sample for DevUI.
The following sample demonstrates a comprehensive 4-step workflow with multiple executors
that process, detect spam, and handle email messages. This workflow illustrates
complex branching logic with human-in-the-loop approval and realistic processing delays.
Workflow Steps:
1. Email Preprocessor - Cleans and prepares the email
2. Spam Detector - Analyzes content and determines if the message is spam (with human approval)
3a. Spam Handler - Processes spam messages (quarantine, log, remove)
3b. Message Responder - Handles legitimate messages (validate, respond)
4. Final Processor - Completes the workflow with logging and cleanup
"""
import asyncio
import logging
from dataclasses import dataclass
from typing import Literal
from agent_framework import (
Case,
Default,
Executor,
WorkflowBuilder,
WorkflowContext,
handler,
response_handler,
)
from pydantic import BaseModel, Field
from typing_extensions import Never
# Define response model with clear user guidance
class SpamDecision(BaseModel):
"""User's decision on whether the email is spam."""
decision: Literal["spam", "not spam"] = Field(
description="Enter 'spam' to mark as spam, or 'not spam' to mark as legitimate"
)
@dataclass
class EmailContent:
"""A data class to hold the processed email content."""
original_message: str
cleaned_message: str
word_count: int
has_suspicious_patterns: bool = False
@dataclass
class SpamDetectorResponse:
"""A data class to hold the spam detection results."""
email_content: EmailContent
is_spam: bool = False
confidence_score: float = 0.0
spam_reasons: list[str] | None = None
human_reviewed: bool = False
human_decision: str | None = None
ai_original_classification: bool = False
def __post_init__(self):
"""Initialize spam_reasons list if None."""
if self.spam_reasons is None:
self.spam_reasons = []
@dataclass
class SpamApprovalRequest:
"""Human-in-the-loop approval request for spam classification."""
email_message: str
detected_as_spam: bool
confidence: float
reasons: list[str]
full_email_content: EmailContent
@dataclass
class ProcessingResult:
"""A data class to hold the final processing result."""
original_message: str
action_taken: str
processing_time: float
status: str
is_spam: bool
confidence_score: float
spam_reasons: list[str]
was_human_reviewed: bool = False
human_override: str | None = None
ai_original_decision: bool = False
class EmailRequest(BaseModel):
"""Request model for email processing."""
email: str = Field(
description="The email message to be processed.",
default="Hi there, are you interested in our new urgent offer today? Click here!",
)
class EmailPreprocessor(Executor):
"""Step 1: An executor that preprocesses and cleans email content."""
@handler
async def handle_email(self, email: EmailRequest, ctx: WorkflowContext[EmailContent]) -> None:
"""Clean and preprocess the email message."""
await asyncio.sleep(1.5) # Simulate preprocessing time
# Simulate email cleaning
cleaned = email.email.strip().lower()
word_count = len(email.email.split())
# Check for suspicious patterns
suspicious_patterns = ["urgent", "limited time", "act now", "free money"]
has_suspicious = any(pattern in cleaned for pattern in suspicious_patterns)
result = EmailContent(
original_message=email.email,
cleaned_message=cleaned,
word_count=word_count,
has_suspicious_patterns=has_suspicious,
)
await ctx.send_message(result)
class SpamDetector(Executor):
"""Step 2: An executor that analyzes content and determines if a message is spam."""
def __init__(self, spam_keywords: list[str], id: str):
"""Initialize the executor with spam keywords."""
super().__init__(id=id)
self._spam_keywords = spam_keywords
@handler
async def handle_email_content(
self, email_content: EmailContent, ctx: WorkflowContext[SpamApprovalRequest]
) -> None:
"""Analyze email content and determine if the message is spam, then request human approval."""
await asyncio.sleep(2.0) # Simulate analysis and detection time
email_text = email_content.cleaned_message
# Analyze content for risk indicators
contains_links = "http" in email_text or "www" in email_text
has_attachments = "attachment" in email_text
sentiment_score = 0.5 if email_content.has_suspicious_patterns else 0.8
# Build risk indicators
risk_indicators: list[str] = []
if email_content.has_suspicious_patterns:
risk_indicators.append("suspicious_language")
if contains_links:
risk_indicators.append("contains_links")
if has_attachments:
risk_indicators.append("has_attachments")
if email_content.word_count < 10:
risk_indicators.append("too_short")
# Check for spam keywords
keyword_matches = [kw for kw in self._spam_keywords if kw in email_text]
# Calculate spam probability
spam_score = 0.0
spam_reasons: list[str] = []
if keyword_matches:
spam_score += 0.4
spam_reasons.append(f"spam_keywords: {keyword_matches}")
if email_content.has_suspicious_patterns:
spam_score += 0.3
spam_reasons.append("suspicious_patterns")
if len(risk_indicators) >= 3:
spam_score += 0.2
spam_reasons.append("high_risk_indicators")
if sentiment_score < 0.4:
spam_score += 0.1
spam_reasons.append("negative_sentiment")
is_spam = spam_score >= 0.5
# Request human approval before proceeding using new API
approval_request = SpamApprovalRequest(
email_message=email_text[:200], # First 200 chars
detected_as_spam=is_spam,
confidence=spam_score,
reasons=spam_reasons,
full_email_content=email_content,
)
await ctx.request_info(
request_data=approval_request,
response_type=SpamDecision,
)
@response_handler
async def handle_human_response(
self, original_request: SpamApprovalRequest, response: SpamDecision, ctx: WorkflowContext[SpamDetectorResponse]
) -> None:
"""Process human approval response and continue workflow."""
print(f"[SpamDetector] handle_human_response called with response: {response}")
# Get stored detection result
ai_original = original_request.detected_as_spam
confidence_score = original_request.confidence
spam_reasons = original_request.reasons
# Parse human decision from the response model
human_decision = response.decision.strip().lower()
# Determine final classification based on human input
if human_decision in ["not spam"]:
is_spam = False
elif human_decision in ["spam"]:
is_spam = True
else:
# Default to AI decision if unclear
is_spam = ai_original
result = SpamDetectorResponse(
email_content=original_request.full_email_content,
is_spam=is_spam,
confidence_score=confidence_score,
spam_reasons=spam_reasons,
human_reviewed=True,
human_decision=response.decision,
ai_original_classification=ai_original,
)
print(
f"[SpamDetector] Sending SpamDetectorResponse: is_spam={is_spam}, confidence={confidence_score}, human_reviewed=True"
)
await ctx.send_message(result)
print("[SpamDetector] Message sent successfully")
class SpamHandler(Executor):
"""Step 3a: An executor that handles spam messages with quarantine and logging."""
@handler
async def handle_spam_detection(
self,
spam_result: SpamDetectorResponse,
ctx: WorkflowContext[ProcessingResult],
) -> None:
"""Handle spam messages by quarantining and logging."""
if not spam_result.is_spam:
raise RuntimeError("Message is not spam, cannot process with spam handler.")
await asyncio.sleep(2.2) # Simulate spam handling time
result = ProcessingResult(
original_message=spam_result.email_content.original_message,
action_taken="quarantined_and_logged",
processing_time=2.2,
status="spam_handled",
is_spam=spam_result.is_spam,
confidence_score=spam_result.confidence_score,
spam_reasons=spam_result.spam_reasons or [],
was_human_reviewed=spam_result.human_reviewed,
human_override=spam_result.human_decision,
ai_original_decision=spam_result.ai_original_classification,
)
await ctx.send_message(result)
class LegitimateMessageHandler(Executor):
"""Step 3b: An executor that handles legitimate (non-spam) messages."""
@handler
async def handle_spam_detection(
self,
spam_result: SpamDetectorResponse,
ctx: WorkflowContext[ProcessingResult],
) -> None:
"""Respond to legitimate messages."""
if spam_result.is_spam:
raise RuntimeError("Message is spam, cannot respond with message responder.")
await asyncio.sleep(2.5) # Simulate response time
result = ProcessingResult(
original_message=spam_result.email_content.original_message,
action_taken="delivered_to_inbox",
processing_time=2.5,
status="message_processed",
is_spam=spam_result.is_spam,
confidence_score=spam_result.confidence_score,
spam_reasons=spam_result.spam_reasons or [],
was_human_reviewed=spam_result.human_reviewed,
human_override=spam_result.human_decision,
ai_original_decision=spam_result.ai_original_classification,
)
await ctx.send_message(result)
class FinalProcessor(Executor):
"""Step 4: An executor that completes the workflow with final logging and cleanup."""
@handler
async def handle_processing_result(
self,
result: ProcessingResult,
ctx: WorkflowContext[Never, str],
) -> None:
"""Complete the workflow with final processing and logging."""
await asyncio.sleep(1.5) # Simulate final processing time
total_time = result.processing_time + 1.5
# Build classification status with human review info
classification = "SPAM" if result.is_spam else "LEGITIMATE"
# Add human review context
review_status = ""
if result.was_human_reviewed:
if result.ai_original_decision != result.is_spam:
review_status = " (human-overridden)"
else:
review_status = " (human-verified)"
# Build appropriate message based on classification
if result.is_spam:
# For spam messages
spam_indicators = ", ".join(result.spam_reasons) if result.spam_reasons else "none detected"
if result.was_human_reviewed:
ai_status = "SPAM" if result.ai_original_decision else "LEGITIMATE"
human_decision = result.human_override if result.human_override else "unknown"
completion_message = (
f"Email classified as {classification}{review_status}.\n"
f"AI detected: {ai_status} (confidence: {result.confidence_score:.2f})\n"
f"Human reviewer: {human_decision}\n"
f"Spam indicators: {spam_indicators}\n"
f"Action: Message quarantined for review\n"
f"Processing time: {total_time:.1f}s"
)
else:
completion_message = (
f"Email classified as {classification} (confidence: {result.confidence_score:.2f}).\n"
f"Spam indicators: {spam_indicators}\n"
f"Action: Message quarantined for review\n"
f"Processing time: {total_time:.1f}s"
)
else:
# For legitimate messages
if result.was_human_reviewed:
ai_status = "SPAM" if result.ai_original_decision else "LEGITIMATE"
human_decision = result.human_override if result.human_override else "unknown"
completion_message = (
f"Email classified as {classification}{review_status}.\n"
f"AI detected: {ai_status} (confidence: {result.confidence_score:.2f})\n"
f"Human reviewer: {human_decision}\n"
f"Action: Delivered to inbox\n"
f"Processing time: {total_time:.1f}s"
)
else:
completion_message = (
f"Email classified as {classification} (confidence: {result.confidence_score:.2f}).\n"
f"Action: Delivered to inbox\n"
f"Processing time: {total_time:.1f}s"
)
await ctx.yield_output(completion_message)
# DevUI will provide checkpoint storage automatically via the new workflow API
# No need to create checkpoint storage here anymore!
# Create the workflow instance that DevUI can discover
spam_keywords = ["spam", "advertisement", "offer", "click here", "winner", "congratulations", "urgent"]
# Create all the executors for the 4-step workflow
email_preprocessor = EmailPreprocessor(id="email_preprocessor")
spam_detector = SpamDetector(spam_keywords, id="spam_detector")
spam_handler = SpamHandler(id="spam_handler")
legitimate_message_handler = LegitimateMessageHandler(id="legitimate_message_handler")
final_processor = FinalProcessor(id="final_processor")
# Build the comprehensive 4-step workflow with branching logic and HIL support
# Note: No checkpoint_storage in constructor - DevUI will pass checkpoint_storage at runtime
workflow = (
WorkflowBuilder(
name="Email Spam Detector",
description="4-step email classification workflow with human-in-the-loop spam approval",
start_executor=email_preprocessor,
)
.add_edge(email_preprocessor, spam_detector)
# HIL handled within spam_detector via @response_handler
# Continue with branching logic after human approval
# Only route SpamDetectorResponse messages (not SpamApprovalRequest)
.add_switch_case_edge_group(
spam_detector,
[
Case(condition=lambda x: isinstance(x, SpamDetectorResponse) and x.is_spam, target=spam_handler),
Default(
target=legitimate_message_handler
), # Default handles non-spam and non-SpamDetectorResponse messages
],
)
.add_edge(spam_handler, final_processor)
.add_edge(legitimate_message_handler, final_processor)
.build()
)
# Note: Workflow metadata is determined by executors and graph structure
def main():
"""Launch the spam detection workflow in DevUI."""
from agent_framework.devui import serve
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Spam Detection Workflow")
logger.info("Available at: http://localhost:8090")
logger.info("Entity ID: workflow_spam_detection")
# Launch server with the workflow
serve(entities=[workflow], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,6 @@
# Azure OpenAI API Configuration
# Get your credentials from Azure Portal
AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=gpt-4o
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Weather agent sample for DevUI testing."""
from .agent import agent
__all__ = ["agent"]
@@ -0,0 +1,181 @@
# Copyright (c) Microsoft. All rights reserved.
"""Sample weather agent for Agent Framework Debug UI."""
import logging
import os
from collections.abc import AsyncIterable, Awaitable, Callable
from typing import Annotated
from agent_framework import (
Agent,
ChatContext,
ChatResponse,
ChatResponseUpdate,
Content,
FunctionInvocationContext,
Message,
MiddlewareTermination,
ResponseStream,
Role,
chat_middleware,
function_middleware,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_devui import register_cleanup
logger = logging.getLogger(__name__)
def cleanup_resources():
"""Cleanup function that runs when DevUI shuts down."""
logger.info("=" * 60)
logger.info(" Cleaning up resources...")
logger.info(" (In production, this would close credentials, sessions, etc.)")
logger.info("=" * 60)
@chat_middleware
async def security_filter_middleware(
context: ChatContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Chat middleware that blocks requests containing sensitive information."""
blocked_terms = ["password", "secret", "api_key", "token"]
# Check only the last message (most recent user input)
last_message = context.messages[-1] if context.messages else None
if last_message and last_message.role == Role.USER and last_message.text:
message_lower = last_message.text.lower()
for term in blocked_terms:
if term in message_lower:
error_message = (
"I cannot process requests containing sensitive information. "
"Please rephrase your question without including passwords, secrets, "
"or other sensitive data."
)
if context.stream:
# Streaming mode: wrap in ResponseStream
async def blocked_stream(msg: str = error_message) -> AsyncIterable[ChatResponseUpdate]:
yield ChatResponseUpdate(
contents=[Content.from_text(text=msg)],
role=Role.ASSISTANT,
)
response = ChatResponse(
messages=[Message(role=Role.ASSISTANT, text=error_message)]
)
context.result = ResponseStream(blocked_stream(), finalizer=lambda _, r=response: r)
else:
# Non-streaming mode: return complete response
context.result = ChatResponse(
messages=[
Message(
role=Role.ASSISTANT,
text=error_message,
)
]
)
raise MiddlewareTermination(result=context.result)
await call_next()
@function_middleware
async def atlantis_location_filter_middleware(
context: FunctionInvocationContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Function middleware that blocks weather requests for Atlantis."""
# Check if location parameter is "atlantis"
location = getattr(context.arguments, "location", None)
if location and location.lower() == "atlantis":
context.result = (
"Blocked! Hold up right there!! Tell the user that "
"'Atlantis is a special place, we must never ask about the weather there!!'"
)
raise MiddlewareTermination(result=context.result)
await call_next()
# 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_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, "The location to get the weather for."],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
temperature = 53
return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C."
@tool(approval_mode="never_require")
def get_forecast(
location: Annotated[str, "The location to get the forecast for."],
days: Annotated[int, "Number of days for forecast"] = 3,
) -> str:
"""Get weather forecast for multiple days."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
forecast: list[str] = []
for day in range(1, days + 1):
condition = conditions[0]
temp = 53
forecast.append(f"Day {day}: {condition}, {temp}°C")
return f"Weather forecast for {location}:\n" + "\n".join(forecast)
@tool(approval_mode="always_require")
def send_email(
recipient: Annotated[str, "The email address of the recipient."],
subject: Annotated[str, "The subject of the email."],
body: Annotated[str, "The body content of the email."],
) -> str:
"""Simulate sending an email."""
return f"Email sent to {recipient} with subject '{subject}'."
# Agent instance following Agent Framework conventions
agent = Agent(
name="AzureWeatherAgent",
description="A helpful agent that provides weather information and forecasts",
instructions="""
You are a weather assistant. You can provide current weather information
and forecasts for any location. Always be helpful and provide detailed
weather information when asked.
""",
client=AzureOpenAIChatClient(
api_key=os.environ.get("AZURE_OPENAI_API_KEY", ""),
),
tools=[get_weather, get_forecast, send_email],
middleware=[security_filter_middleware, atlantis_location_filter_middleware],
)
# Register cleanup hook - demonstrates resource cleanup on shutdown
register_cleanup(agent, cleanup_resources)
def main():
"""Launch the Azure weather agent in DevUI."""
import logging
from agent_framework.devui import serve
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Azure Weather Agent")
logger.info("Available at: http://localhost:8090")
logger.info("Entity ID: agent_AzureWeatherAgent")
# Launch server with the agent
serve(entities=[agent], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,7 @@
# Azure OpenAI API Configuration
# Get your credentials from Azure Portal
AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=gpt-4o
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_API_VERSION=2024-10-21
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Sequential Agents Workflow - Writer → Reviewer."""
from .workflow import workflow
__all__ = ["workflow"]
@@ -0,0 +1,170 @@
# Copyright (c) Microsoft. All rights reserved.
"""Agent Workflow - Content Review with Quality Routing.
This sample demonstrates:
- Using agents directly as executors
- Conditional routing based on structured outputs
- Quality-based workflow paths with convergence
Use case: Content creation with automated review.
Writer creates content, Reviewer evaluates quality:
- High quality (score >= 80): → Publisher → Summarizer
- Low quality (score < 80): → Editor → Publisher → Summarizer
Both paths converge at Summarizer for final report.
"""
import os
from typing import Any
from agent_framework import AgentExecutorResponse, WorkflowBuilder
from agent_framework.azure import AzureOpenAIChatClient
from pydantic import BaseModel
# Define structured output for review results
class ReviewResult(BaseModel):
"""Review evaluation with scores and feedback."""
score: int # Overall quality score (0-100)
feedback: str # Concise, actionable feedback
clarity: int # Clarity score (0-100)
completeness: int # Completeness score (0-100)
accuracy: int # Accuracy score (0-100)
structure: int # Structure score (0-100)
# Condition function: route to editor if score < 80
def needs_editing(message: Any) -> bool:
"""Check if content needs editing based on review score."""
if not isinstance(message, AgentExecutorResponse):
return False
try:
review = ReviewResult.model_validate_json(message.agent_response.text)
return review.score < 80
except Exception:
return False
# Condition function: content is approved (score >= 80)
def is_approved(message: Any) -> bool:
"""Check if content is approved (high quality)."""
if not isinstance(message, AgentExecutorResponse):
return True
try:
review = ReviewResult.model_validate_json(message.agent_response.text)
return review.score >= 80
except Exception:
return True
# Create Azure OpenAI chat client
client = AzureOpenAIChatClient(api_key=os.environ.get("AZURE_OPENAI_API_KEY", ""))
# Create Writer agent - generates content
writer = client.as_agent(
name="Writer",
instructions=(
"You are an excellent content writer. "
"Create clear, engaging content based on the user's request. "
"Focus on clarity, accuracy, and proper structure."
),
)
# Create Reviewer agent - evaluates and provides structured feedback
reviewer = client.as_agent(
name="Reviewer",
instructions=(
"You are an expert content reviewer. "
"Evaluate the writer's content based on:\n"
"1. Clarity - Is it easy to understand?\n"
"2. Completeness - Does it fully address the topic?\n"
"3. Accuracy - Is the information correct?\n"
"4. Structure - Is it well-organized?\n\n"
"Return a JSON object with:\n"
"- score: overall quality (0-100)\n"
"- feedback: concise, actionable feedback\n"
"- clarity, completeness, accuracy, structure: individual scores (0-100)"
),
default_options={"response_format": ReviewResult},
)
# Create Editor agent - improves content based on feedback
editor = client.as_agent(
name="Editor",
instructions=(
"You are a skilled editor. "
"You will receive content along with review feedback. "
"Improve the content by addressing all the issues mentioned in the feedback. "
"Maintain the original intent while enhancing clarity, completeness, accuracy, and structure."
),
)
# Create Publisher agent - formats content for publication
publisher = client.as_agent(
name="Publisher",
instructions=(
"You are a publishing agent. "
"You receive either approved content or edited content. "
"Format it for publication with proper headings and structure."
),
)
# Create Summarizer agent - creates final publication report
summarizer = client.as_agent(
name="Summarizer",
instructions=(
"You are a summarizer agent. "
"Create a final publication report that includes:\n"
"1. A brief summary of the published content\n"
"2. The workflow path taken (direct approval or edited)\n"
"3. Key highlights and takeaways\n"
"Keep it concise and professional."
),
)
# Build workflow with branching and convergence:
# Writer → Reviewer → [branches]:
# - If score >= 80: → Publisher → Summarizer (direct approval path)
# - If score < 80: → Editor → Publisher → Summarizer (improvement path)
# Both paths converge at Summarizer for final report
workflow = (
WorkflowBuilder(
name="Content Review Workflow",
description="Multi-agent content creation workflow with quality-based routing (Writer → Reviewer → Editor/Publisher)",
start_executor=writer,
)
.add_edge(writer, reviewer)
# Branch 1: High quality (>= 80) goes directly to publisher
.add_edge(reviewer, publisher, condition=is_approved)
# Branch 2: Low quality (< 80) goes to editor first, then publisher
.add_edge(reviewer, editor, condition=needs_editing)
.add_edge(editor, publisher)
# Both paths converge: Publisher → Summarizer
.add_edge(publisher, summarizer)
.build()
)
def main():
"""Launch the branching workflow in DevUI."""
import logging
from agent_framework.devui import serve
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Agent Workflow (Content Review with Quality Routing)")
logger.info("Available at: http://localhost:8093")
logger.info("\nThis workflow demonstrates:")
logger.info("- Conditional routing based on structured outputs")
logger.info("- Path 1 (score >= 80): Reviewer → Publisher → Summarizer")
logger.info("- Path 2 (score < 80): Reviewer → Editor → Publisher → Summarizer")
logger.info("- Both paths converge at Summarizer for final report")
serve(entities=[workflow], port=8093, auto_open=True)
if __name__ == "__main__":
main()