mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70b44d9aa5 | ||
|
|
3d7409f2c9 | ||
|
|
1a396883ca | ||
|
|
16343368f6 | ||
|
|
adb566161f | ||
|
|
3256baa8b6 | ||
|
|
b62b1f2191 | ||
|
|
e8d0bd9051 | ||
|
|
98cd72839e | ||
|
|
184ee9d518 | ||
|
|
2f7250fe0f | ||
|
|
493891620d | ||
|
|
f3e0be9555 | ||
|
|
8b475afe17 | ||
|
|
0fcf075ea7 | ||
|
|
ff7041b990 | ||
|
|
924211a518 | ||
|
|
cd7661bbb7 | ||
|
|
0a0de4ac21 | ||
|
|
96f30bdf57 | ||
|
|
e5cddecf04 | ||
|
|
81a317714f | ||
|
|
d418794b1e | ||
|
|
f6c4e078ec | ||
|
|
474b31393d | ||
|
|
1226828ec2 |
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""Check Python test coverage against threshold for enforced modules.
|
||||
|
||||
This script parses a Cobertura XML coverage report and enforces a minimum
|
||||
coverage threshold on specific modules. Non-enforced modules are reported
|
||||
for visibility but don't block the build.
|
||||
|
||||
Usage:
|
||||
python python-check-coverage.py <coverage-xml-path> <threshold>
|
||||
|
||||
Example:
|
||||
python python-check-coverage.py python-coverage.xml 85
|
||||
"""
|
||||
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
|
||||
# =============================================================================
|
||||
# ENFORCED MODULES CONFIGURATION
|
||||
# =============================================================================
|
||||
# Add or remove modules from this set to control which packages must meet
|
||||
# the coverage threshold. Only these modules will fail the build if below
|
||||
# threshold. Other modules are reported for visibility only.
|
||||
#
|
||||
# Module paths should match the package paths as they appear in the coverage
|
||||
# report (e.g., "packages.azure-ai.agent_framework_azure_ai" for packages/azure-ai).
|
||||
# Sub-modules can be included by specifying their full path.
|
||||
# =============================================================================
|
||||
ENFORCED_MODULES: set[str] = {
|
||||
"packages.azure-ai.agent_framework_azure_ai",
|
||||
# Add more modules here as coverage improves:
|
||||
# "packages.core.agent_framework",
|
||||
# "packages.core.agent_framework._workflows",
|
||||
# "packages.anthropic.agent_framework_anthropic",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageCoverage:
|
||||
"""Coverage data for a single package."""
|
||||
|
||||
name: str
|
||||
line_rate: float
|
||||
branch_rate: float
|
||||
lines_valid: int
|
||||
lines_covered: int
|
||||
branches_valid: int
|
||||
branches_covered: int
|
||||
|
||||
@property
|
||||
def line_coverage_percent(self) -> float:
|
||||
"""Return line coverage as a percentage."""
|
||||
return self.line_rate * 100
|
||||
|
||||
@property
|
||||
def branch_coverage_percent(self) -> float:
|
||||
"""Return branch coverage as a percentage."""
|
||||
return self.branch_rate * 100
|
||||
|
||||
|
||||
def parse_coverage_xml(xml_path: str) -> tuple[dict[str, PackageCoverage], float, float]:
|
||||
"""Parse Cobertura XML and extract per-package coverage data.
|
||||
|
||||
Args:
|
||||
xml_path: Path to the Cobertura XML coverage report.
|
||||
|
||||
Returns:
|
||||
A tuple of (packages_dict, overall_line_rate, overall_branch_rate).
|
||||
"""
|
||||
tree = ET.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Get overall coverage from root element
|
||||
overall_line_rate = float(root.get("line-rate", 0))
|
||||
overall_branch_rate = float(root.get("branch-rate", 0))
|
||||
|
||||
packages: dict[str, PackageCoverage] = {}
|
||||
|
||||
for package in root.findall(".//package"):
|
||||
package_path = package.get("name", "unknown")
|
||||
|
||||
line_rate = float(package.get("line-rate", 0))
|
||||
branch_rate = float(package.get("branch-rate", 0))
|
||||
|
||||
# Count lines and branches from classes within this package
|
||||
lines_valid = 0
|
||||
lines_covered = 0
|
||||
branches_valid = 0
|
||||
branches_covered = 0
|
||||
|
||||
for class_elem in package.findall(".//class"):
|
||||
for line in class_elem.findall(".//line"):
|
||||
lines_valid += 1
|
||||
if int(line.get("hits", 0)) > 0:
|
||||
lines_covered += 1
|
||||
# Branch coverage from line elements
|
||||
if line.get("branch") == "true":
|
||||
condition_coverage = line.get("condition-coverage", "")
|
||||
if condition_coverage:
|
||||
# Parse "X% (covered/total)" format
|
||||
try:
|
||||
coverage_parts = condition_coverage.split("(")[1].rstrip(")").split("/")
|
||||
branches_covered += int(coverage_parts[0])
|
||||
branches_valid += int(coverage_parts[1])
|
||||
except (IndexError, ValueError):
|
||||
# Ignore malformed condition-coverage strings; treat this line as having no branch data.
|
||||
pass
|
||||
|
||||
# Use full package path as the key (no aggregation)
|
||||
packages[package_path] = PackageCoverage(
|
||||
name=package_path,
|
||||
line_rate=line_rate if lines_valid == 0 else lines_covered / lines_valid,
|
||||
branch_rate=branch_rate if branches_valid == 0 else branches_covered / branches_valid,
|
||||
lines_valid=lines_valid,
|
||||
lines_covered=lines_covered,
|
||||
branches_valid=branches_valid,
|
||||
branches_covered=branches_covered,
|
||||
)
|
||||
|
||||
return packages, overall_line_rate, overall_branch_rate
|
||||
|
||||
|
||||
def format_coverage_value(coverage: float, threshold: float, is_enforced: bool) -> str:
|
||||
"""Format a coverage value with optional pass/fail indicator.
|
||||
|
||||
Args:
|
||||
coverage: Coverage percentage (0-100).
|
||||
threshold: Minimum required coverage percentage.
|
||||
is_enforced: Whether this module is enforced.
|
||||
|
||||
Returns:
|
||||
Formatted string like "85.5%" or "85.5% ✅" or "75.0% ❌".
|
||||
"""
|
||||
formatted = f"{coverage:.1f}%"
|
||||
if is_enforced:
|
||||
icon = "✅" if coverage >= threshold else "❌"
|
||||
formatted = f"{formatted} {icon}"
|
||||
return formatted
|
||||
|
||||
|
||||
def print_coverage_table(
|
||||
packages: dict[str, PackageCoverage],
|
||||
threshold: float,
|
||||
overall_line_rate: float,
|
||||
overall_branch_rate: float,
|
||||
) -> None:
|
||||
"""Print a formatted coverage summary table.
|
||||
|
||||
Args:
|
||||
packages: Dictionary of package name to coverage data.
|
||||
threshold: Minimum required coverage percentage.
|
||||
overall_line_rate: Overall line coverage rate (0-1).
|
||||
overall_branch_rate: Overall branch coverage rate (0-1).
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("PYTHON TEST COVERAGE REPORT")
|
||||
print("=" * 80)
|
||||
|
||||
# Overall coverage
|
||||
print(f"\nOverall Line Coverage: {overall_line_rate * 100:.1f}%")
|
||||
print(f"Overall Branch Coverage: {overall_branch_rate * 100:.1f}%")
|
||||
print(f"Threshold: {threshold}%")
|
||||
|
||||
# Package table
|
||||
print("\n" + "-" * 110)
|
||||
print(f"{'Package':<80} {'Lines':<15} {'Line Cov':<15}")
|
||||
print("-" * 110)
|
||||
|
||||
# Sort: enforced modules first, then alphabetically
|
||||
sorted_packages = sorted(
|
||||
packages.values(),
|
||||
key=lambda p: (p.name not in ENFORCED_MODULES, p.name),
|
||||
)
|
||||
|
||||
for pkg in sorted_packages:
|
||||
is_enforced = pkg.name in ENFORCED_MODULES
|
||||
enforced_marker = "[ENFORCED] " if is_enforced else ""
|
||||
line_cov = format_coverage_value(pkg.line_coverage_percent, threshold, is_enforced)
|
||||
lines_info = f"{pkg.lines_covered}/{pkg.lines_valid}"
|
||||
package_label = f"{enforced_marker}{pkg.name}"
|
||||
|
||||
print(f"{package_label:<80} {lines_info:<15} {line_cov:<15}")
|
||||
|
||||
print("-" * 110)
|
||||
|
||||
|
||||
def check_coverage(xml_path: str, threshold: float) -> bool:
|
||||
"""Check if all enforced modules meet the coverage threshold.
|
||||
|
||||
Args:
|
||||
xml_path: Path to the Cobertura XML coverage report.
|
||||
threshold: Minimum required coverage percentage.
|
||||
|
||||
Returns:
|
||||
True if all enforced modules pass, False otherwise.
|
||||
"""
|
||||
packages, overall_line_rate, overall_branch_rate = parse_coverage_xml(xml_path)
|
||||
|
||||
print_coverage_table(packages, threshold, overall_line_rate, overall_branch_rate)
|
||||
|
||||
# Check enforced modules
|
||||
failed_modules: list[str] = []
|
||||
missing_modules: list[str] = []
|
||||
|
||||
for module_name in ENFORCED_MODULES:
|
||||
if module_name not in packages:
|
||||
missing_modules.append(module_name)
|
||||
continue
|
||||
|
||||
pkg = packages[module_name]
|
||||
if pkg.line_coverage_percent < threshold:
|
||||
failed_modules.append(f"{module_name} ({pkg.line_coverage_percent:.1f}%)")
|
||||
|
||||
# Report results
|
||||
if missing_modules:
|
||||
print(f"\n❌ FAILED: Enforced modules not found in coverage report: {', '.join(missing_modules)}")
|
||||
return False
|
||||
|
||||
if failed_modules:
|
||||
print(f"\n❌ FAILED: The following enforced modules are below {threshold}% coverage threshold:")
|
||||
for module in failed_modules:
|
||||
print(f" - {module}")
|
||||
print("\nTo fix: Add more tests to improve coverage for the failing modules.")
|
||||
return False
|
||||
|
||||
if ENFORCED_MODULES:
|
||||
found_enforced = [m for m in ENFORCED_MODULES if m in packages]
|
||||
if found_enforced:
|
||||
print(f"\n✅ PASSED: All enforced modules meet the {threshold}% coverage threshold.")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point.
|
||||
|
||||
Returns:
|
||||
Exit code: 0 for success, 1 for failure.
|
||||
"""
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <coverage-xml-path> <threshold>")
|
||||
print(f"Example: {sys.argv[0]} python-coverage.xml 85")
|
||||
return 1
|
||||
|
||||
xml_path = sys.argv[1]
|
||||
try:
|
||||
threshold = float(sys.argv[2])
|
||||
except ValueError:
|
||||
print(f"Error: Invalid threshold value: {sys.argv[2]}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
success = check_coverage(xml_path, threshold)
|
||||
return 0 if success else 1
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Coverage file not found: {xml_path}")
|
||||
return 1
|
||||
except ET.ParseError as e:
|
||||
print(f"Error: Failed to parse coverage XML: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
# Coverage threshold percentage for enforced modules
|
||||
COVERAGE_THRESHOLD: 85
|
||||
|
||||
jobs:
|
||||
python-tests-coverage:
|
||||
@@ -37,6 +39,8 @@ jobs:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run all tests with coverage report
|
||||
run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
|
||||
- name: Check coverage threshold
|
||||
run: python ${{ github.workspace }}/.github/workflows/python-check-coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }}
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
|
||||
@@ -108,7 +108,7 @@ async def main():
|
||||
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
|
||||
# api_key=os.environ["AZURE_OPENAI_API_KEY"], # Optional if using AzureCliCredential
|
||||
credential=AzureCliCredential(), # Optional, if using api_key
|
||||
).create_agent(
|
||||
).as_agent(
|
||||
name="HaikuBot",
|
||||
instructions="You are an upbeat assistant that writes beautifully.",
|
||||
)
|
||||
@@ -131,7 +131,7 @@ using OpenAI;
|
||||
// Replace the <apikey> with your OpenAI API key.
|
||||
var agent = new OpenAIClient("<apikey>")
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
@@ -150,7 +150,7 @@ var agent = new OpenAIClient(
|
||||
new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"),
|
||||
new OpenAIClientOptions() { Endpoint = new Uri("https://<resource>.openai.azure.com/openai/v1") })
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="12.0.1" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.3.0" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.4.1" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
|
||||
@@ -42,7 +42,7 @@
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.2" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
@@ -113,14 +113,14 @@
|
||||
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.1.2.3" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
|
||||
<!-- Durable Task -->
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.19.1" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.19.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.19.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.19.0" />
|
||||
<!-- Azure Functions -->
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.11.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.13.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
|
||||
|
||||
@@ -23,29 +23,43 @@
|
||||
<Project Path="samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AzureFunctions/">
|
||||
<File Path="samples/AzureFunctions/.editorconfig" />
|
||||
<File Path="samples/AzureFunctions/README.md" />
|
||||
<Project Path="samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
|
||||
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
|
||||
<Folder Name="/Samples/Durable/" />
|
||||
<Folder Name="/Samples/Durable/Agents/" />
|
||||
<Folder Name="/Samples/Durable/Agents/AzureFunctions/">
|
||||
<File Path="samples/Durable/Agents/AzureFunctions/.editorconfig" />
|
||||
<File Path="samples/Durable/Agents/AzureFunctions/README.md" />
|
||||
<Project Path="samples/Durable/Agents/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/Durable/Agents/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/Durable/Agents/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/Durable/Agents/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
|
||||
<Project Path="samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/DurableAgents/">
|
||||
<File Path="samples/DurableAgents/ConsoleApps/README.md" />
|
||||
<Folder Name="/Samples/Durable/Agents/ConsoleApps/">
|
||||
<File Path="samples/Durable/Agents/ConsoleApps/README.md" />
|
||||
<Project Path="samples/Durable/Agents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/Durable/Agents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/Durable/Agents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/Durable/Agents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/Durable/Agents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/Durable/Agents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/Durable/Agents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/DurableAgents/ConsoleApps/">
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
|
||||
<Folder Name="/Samples/Durable/Workflows/">
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/03_ConditionalEdges/03_ConditionalEdges.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/04_WorkflowAndAgents/04_WorkflowAndAgents.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/07_SubWorkflows/07_SubWorkflows.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/08_WorkflowLoop/08_WorkflowLoop.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/Durable/Workflows/AzureFunctions/">
|
||||
<Project Path="samples/Durable/Workflow/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -36,7 +36,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -36,7 +36,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -36,7 +36,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -37,7 +37,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -36,7 +36,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
@@ -36,7 +36,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+1
-1
@@ -7,4 +7,4 @@
|
||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
||||
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -41,7 +41,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>SingleAgent</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SequentialWorkflow;
|
||||
|
||||
/// <summary>
|
||||
/// Looks up an order by its ID and return an Order object.
|
||||
/// </summary>
|
||||
internal sealed class OrderLookup() : Executor<string, Order>("OrderLookup")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(
|
||||
string message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Activity] OrderLookup: Starting lookup for order '{message}'");
|
||||
Console.ResetColor();
|
||||
|
||||
// Simulate database lookup with delay
|
||||
await Task.Delay(TimeSpan.FromMicroseconds(100), cancellationToken);
|
||||
|
||||
Order order = new(
|
||||
Id: message,
|
||||
OrderDate: DateTime.UtcNow.AddDays(-1),
|
||||
IsCancelled: false,
|
||||
Customer: new Customer(Name: "Jerry", Email: "jerry@example.com"));
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($"│ [Activity] OrderLookup: Found order '{message}' for customer '{order.Customer.Name}'");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels an order.
|
||||
/// </summary>
|
||||
internal sealed class OrderCancel() : Executor<Order, Order>("OrderCancel")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(
|
||||
Order message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Activity] OrderCancel: Starting cancellation for order '{message.Id}'");
|
||||
Console.ResetColor();
|
||||
|
||||
// Simulate a slow cancellation process (e.g., calling external payment system)
|
||||
for (int i = 1; i <= 3; i++)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine("│ [Activity] OrderCancel: Processing...");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
Order cancelledOrder = message with { IsCancelled = true };
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"│ [Activity] OrderCancel: ✓ Order '{cancelledOrder.Id}' has been cancelled");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return cancelledOrder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a cancellation confirmation email to the customer.
|
||||
/// </summary>
|
||||
internal sealed class SendEmail() : Executor<Order, string>("SendEmail")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(
|
||||
Order message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Activity] SendEmail: Sending email to '{message.Customer.Email}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
string result = $"Cancellation email sent for order {message.Id} to {message.Customer.Email}.";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("│ [Activity] SendEmail: ✓ Email sent successfully!");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record Order(string Id, DateTime OrderDate, bool IsCancelled, Customer Customer);
|
||||
|
||||
internal sealed record Customer(string Name, string Email);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a batch cancellation request with multiple order IDs and a reason.
|
||||
/// This demonstrates using a complex typed object as workflow input.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1812 // Instantiated via JSON deserialization at runtime
|
||||
internal sealed record BatchCancelRequest(string[] OrderIds, string Reason, bool NotifyCustomers);
|
||||
#pragma warning restore CA1812
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of processing a batch cancellation.
|
||||
/// </summary>
|
||||
internal sealed record BatchCancelResult(int TotalOrders, int CancelledCount, string Reason);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a status report for an order.
|
||||
/// </summary>
|
||||
internal sealed class StatusReport() : Executor<Order, string>("StatusReport")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(
|
||||
Order message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Activity] StatusReport: Generating report for order '{message.Id}'");
|
||||
Console.ResetColor();
|
||||
|
||||
string status = message.IsCancelled ? "Cancelled" : "Active";
|
||||
string result = $"Order {message.Id} for {message.Customer.Name}: Status={status}, Date={message.OrderDate:yyyy-MM-dd}";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"│ [Activity] StatusReport: ✓ {result}");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a batch cancellation request. Accepts a complex <see cref="BatchCancelRequest"/> object
|
||||
/// as input, demonstrating how workflows can receive structured JSON input.
|
||||
/// </summary>
|
||||
internal sealed class BatchCancelProcessor() : Executor<BatchCancelRequest, BatchCancelResult>("BatchCancelProcessor")
|
||||
{
|
||||
public override async ValueTask<BatchCancelResult> HandleAsync(
|
||||
BatchCancelRequest message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Activity] BatchCancelProcessor: Processing {message.OrderIds.Length} orders");
|
||||
Console.WriteLine($"│ [Activity] BatchCancelProcessor: Reason: {message.Reason}");
|
||||
Console.WriteLine($"│ [Activity] BatchCancelProcessor: Notify customers: {message.NotifyCustomers}");
|
||||
Console.ResetColor();
|
||||
|
||||
// Simulate processing each order
|
||||
int cancelledCount = 0;
|
||||
foreach (string orderId in message.OrderIds)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
|
||||
cancelledCount++;
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($"│ [Activity] BatchCancelProcessor: ✓ Cancelled order '{orderId}'");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
BatchCancelResult result = new(message.OrderIds.Length, cancelledCount, message.Reason);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"│ [Activity] BatchCancelProcessor: ✓ Batch complete: {cancelledCount}/{message.OrderIds.Length} cancelled");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary of the batch cancellation.
|
||||
/// </summary>
|
||||
internal sealed class BatchCancelSummary() : Executor<BatchCancelResult, string>("BatchCancelSummary")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(
|
||||
BatchCancelResult message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine("│ [Activity] BatchCancelSummary: Generating summary");
|
||||
Console.ResetColor();
|
||||
|
||||
string result = $"Batch cancellation complete: {message.CancelledCount}/{message.TotalOrders} orders cancelled. Reason: {message.Reason}";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine($"│ [Activity] BatchCancelSummary: ✓ {result}");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates three workflows that share executors.
|
||||
// The CancelOrder workflow cancels an order and notifies the customer.
|
||||
// The OrderStatus workflow looks up an order and generates a status report.
|
||||
// The BatchCancelOrders workflow accepts a complex JSON input to cancel multiple orders.
|
||||
// Both CancelOrder and OrderStatus reuse the same OrderLookup executor, demonstrating executor sharing.
|
||||
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using SequentialWorkflow;
|
||||
|
||||
// Define executors for all workflows
|
||||
OrderLookup orderLookup = new();
|
||||
OrderCancel orderCancel = new();
|
||||
SendEmail sendEmail = new();
|
||||
StatusReport statusReport = new();
|
||||
BatchCancelProcessor batchCancelProcessor = new();
|
||||
BatchCancelSummary batchCancelSummary = new();
|
||||
|
||||
// Build the CancelOrder workflow: OrderLookup -> OrderCancel -> SendEmail
|
||||
Workflow cancelOrder = new WorkflowBuilder(orderLookup)
|
||||
.WithName("CancelOrder")
|
||||
.WithDescription("Cancel an order and notify the customer")
|
||||
.AddEdge(orderLookup, orderCancel)
|
||||
.AddEdge(orderCancel, sendEmail)
|
||||
.Build();
|
||||
|
||||
// Build the OrderStatus workflow: OrderLookup -> StatusReport
|
||||
// This workflow shares the OrderLookup executor with the CancelOrder workflow.
|
||||
Workflow orderStatus = new WorkflowBuilder(orderLookup)
|
||||
.WithName("OrderStatus")
|
||||
.WithDescription("Look up an order and generate a status report")
|
||||
.AddEdge(orderLookup, statusReport)
|
||||
.Build();
|
||||
|
||||
// Build the BatchCancelOrders workflow: BatchCancelProcessor -> BatchCancelSummary
|
||||
// This workflow demonstrates using a complex JSON object as the workflow input.
|
||||
Workflow batchCancelOrders = new WorkflowBuilder(batchCancelProcessor)
|
||||
.WithName("BatchCancelOrders")
|
||||
.WithDescription("Cancel multiple orders in a batch using a complex JSON input")
|
||||
.AddEdge(batchCancelProcessor, batchCancelSummary)
|
||||
.Build();
|
||||
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableWorkflows(workflows => workflows.AddWorkflows(cancelOrder, orderStatus, batchCancelOrders))
|
||||
.Build();
|
||||
app.Run();
|
||||
@@ -0,0 +1,100 @@
|
||||
# Sequential Workflow Sample
|
||||
|
||||
This sample demonstrates how to use the Microsoft Agent Framework to create an Azure Functions app that hosts durable workflows with sequential executor chains. It showcases two workflows that share a common executor, demonstrating executor reuse across workflows.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Defining workflows with sequential executor chains using `WorkflowBuilder`
|
||||
- Sharing executors across multiple workflows (the `OrderLookup` executor is used by both workflows)
|
||||
- Registering workflows with the Function app using `ConfigureDurableWorkflows`
|
||||
- Durable orchestration ensuring workflows survive process restarts and failures
|
||||
- Starting workflows via HTTP requests
|
||||
- Viewing workflow execution history and status in the Durable Task Scheduler (DTS) dashboard
|
||||
|
||||
## Workflows
|
||||
|
||||
This sample defines two workflows:
|
||||
|
||||
1. **CancelOrder**: `OrderLookup` → `OrderCancel` → `SendEmail` — Looks up an order, cancels it, and sends a confirmation email.
|
||||
2. **OrderStatus**: `OrderLookup` → `StatusReport` — Looks up an order and generates a status report.
|
||||
|
||||
Both workflows share the `OrderLookup` executor, which is registered only once by the framework.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending HTTP requests to the workflow endpoints.
|
||||
|
||||
You can use the `demo.http` file to trigger the workflows, or a command line tool like `curl` as shown below:
|
||||
|
||||
### Cancel an Order
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/workflows/CancelOrder/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "12345"
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/workflows/CancelOrder/run `
|
||||
-ContentType text/plain `
|
||||
-Body "12345"
|
||||
```
|
||||
|
||||
The response will confirm the workflow orchestration has started:
|
||||
|
||||
```text
|
||||
Workflow orchestration started for CancelOrder. Orchestration runId: abc123def456
|
||||
```
|
||||
|
||||
> **Tip:** You can provide a custom run ID by appending a `runId` query parameter:
|
||||
>
|
||||
> ```bash
|
||||
> curl -X POST "http://localhost:7071/api/workflows/CancelOrder/run?runId=my-order-123" \
|
||||
> -H "Content-Type: text/plain" \
|
||||
> -d "12345"
|
||||
> ```
|
||||
>
|
||||
> If not provided, a unique run ID is auto-generated.
|
||||
|
||||
In the function app logs, you will see the sequential execution of each executor:
|
||||
|
||||
```text
|
||||
│ [Activity] OrderLookup: Starting lookup for order '12345'
|
||||
│ [Activity] OrderLookup: Found order '12345' for customer 'Jerry'
|
||||
│ [Activity] OrderCancel: Starting cancellation for order '12345'
|
||||
│ [Activity] OrderCancel: ✓ Order '12345' has been cancelled
|
||||
│ [Activity] SendEmail: Sending email to 'jerry@example.com'...
|
||||
│ [Activity] SendEmail: ✓ Email sent successfully!
|
||||
```
|
||||
|
||||
### Get Order Status
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/workflows/OrderStatus/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "12345"
|
||||
```
|
||||
|
||||
The `OrderStatus` workflow reuses the same `OrderLookup` executor and then generates a status report:
|
||||
|
||||
```text
|
||||
│ [Activity] OrderLookup: Starting lookup for order '12345'
|
||||
│ [Activity] OrderLookup: Found order '12345' for customer 'Jerry'
|
||||
│ [Activity] StatusReport: Generating report for order '12345'
|
||||
│ [Activity] StatusReport: ✓ Order 12345 for Jerry: Status=Active, Date=2025-01-01
|
||||
```
|
||||
|
||||
### Viewing Workflows in the DTS Dashboard
|
||||
|
||||
After running a workflow, you can navigate to the Durable Task Scheduler (DTS) dashboard to visualize the completed orchestration, inspect inputs/outputs for each step, and view execution history.
|
||||
|
||||
If you are using the DTS emulator, the dashboard is available at `http://localhost:8082`.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Cancel an order
|
||||
POST {{authority}}/api/workflows/CancelOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
12345
|
||||
|
||||
### Cancel an order with a custom run ID
|
||||
POST {{authority}}/api/workflows/CancelOrder/run?runId=my-custom-id-123
|
||||
Content-Type: text/plain
|
||||
|
||||
99999
|
||||
|
||||
### Get order status (shares OrderLookup executor with CancelOrder)
|
||||
POST {{authority}}/api/workflows/OrderStatus/run
|
||||
Content-Type: text/plain
|
||||
|
||||
12345
|
||||
|
||||
### Batch cancel orders with a complex JSON input
|
||||
POST {{authority}}/api/workflows/BatchCancelOrders/run
|
||||
Content-Type: application/json
|
||||
|
||||
{"orderIds": ["1001", "1002", "1003"], "reason": "Customer requested cancellation", "notifyCustomers": true}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user