Merge branch 'main' into copilot-agent-updates

This commit is contained in:
Dmytro Struk
2025-09-11 10:03:51 -07:00
Unverified
291 changed files with 15148 additions and 10728 deletions
+10
View File
@@ -16,3 +16,13 @@ documentation:
- any-glob-to-any-file:
- docs/**
- '**/*.md'
# Add 'workflows' label to any change within the dotnet or python workflows src or samples
workflows:
- changed-files:
- any-glob-to-any-file:
- dotnet/src/Microsoft.Agents.Workflows/**
- dotnet/src/Microsoft.Agents.Workflows.Declarative/**
- dotnet/samples/GettingStarted/Workflow/**
- python/packages/workflows/**
- python/samples/getting_started/workflow/**
+1 -1
View File
@@ -20,4 +20,4 @@ Please help reviewers and future users, providing the following information:
- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone :smile:
- [ ] **Is this a breaking change?** If yes, add "[BREAKING]" prefix to the title of the PR.
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
pull-requests: write
steps:
- uses: actions/labeler@v5
- uses: actions/labeler@v6
with:
repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}"
+24 -4
View File
@@ -17,6 +17,7 @@ env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
RUN_INTEGRATION_TESTS: "true"
RUN_SAMPLES_TESTS: ${{ vars.RUN_SAMPLES_TESTS }}
jobs:
paths-filter:
@@ -77,7 +78,13 @@ jobs:
run: |
uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
- name: Test with pytest
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml
timeout-minutes: 10
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
working-directory: ./python
- name: Test main samples
timeout-minutes: 10
if: env.RUN_SAMPLES_TESTS == 'true'
run: uv run pytest tests/samples/ -m "openai"
working-directory: ./python
- name: Move coverage file
run: |
@@ -139,11 +146,17 @@ jobs:
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml
timeout-minutes: 10
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
working-directory: ./python
- name: Test azure samples
timeout-minutes: 10
if: env.RUN_SAMPLES_TESTS == 'true'
run: uv run pytest tests/samples/ -m "azure"
working-directory: ./python
- name: Move coverage file
run: |
mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ env.PACKAGE_NAME }}.xml
mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml ./coverage_${{ env.PACKAGE_NAME }}.xml
working-directory: ./python
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
@@ -200,7 +213,13 @@ jobs:
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml
timeout-minutes: 10
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
working-directory: ./python
- name: Test foundry samples
timeout-minutes: 10
if: env.RUN_SAMPLES_TESTS == 'true'
run: uv run pytest tests/samples/ -m "foundry"
working-directory: ./python
- name: Move coverage file
run: |
@@ -220,6 +239,7 @@ jobs:
display-options: fEX
fail-on-empty: true
title: Test results
python-integration-tests-check:
if: always()
runs-on: ubuntu-latest
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Install the project
run: uv sync --all-extras --dev
- name: Run all tests with coverage report
run: uv run poe all-tests --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
+30 -3
View File
@@ -37,6 +37,7 @@ jobs:
- name: Install the project
run: |
uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
# Main package tests
- name: Set environment variables - main - win
if: ${{ matrix.os == 'windows-latest' }}
@@ -47,7 +48,7 @@ jobs:
run: |
echo "PACKAGE_NAME=main" >> $GITHUB_ENV
- name: Test with pytest - main
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
working-directory: ./python
- name: Move coverage file - main
run: |
@@ -58,6 +59,7 @@ jobs:
with:
name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }}
path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml
# Azure package tests
- name: Set environment variables - azure - win
if: ${{ matrix.os == 'windows-latest' }}
@@ -68,7 +70,7 @@ jobs:
run: |
echo "PACKAGE_NAME=azure" >> $GITHUB_ENV
- name: Test with pytest - azure
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
working-directory: ./python
- name: Move coverage file - azure
run: |
@@ -79,6 +81,7 @@ jobs:
with:
name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }}
path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml
# Foundry package tests
- name: Set environment variables - foundry - win
if: ${{ matrix.os == 'windows-latest' }}
@@ -89,7 +92,7 @@ jobs:
run: |
echo "PACKAGE_NAME=foundry" >> $GITHUB_ENV
- name: Test with pytest - foundry
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test --junitxml=coverage.xml
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
working-directory: ./python
- name: Move coverage file - foundry
run: |
@@ -100,6 +103,30 @@ jobs:
with:
name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }}
path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml
# Workflow package tests
- name: Set environment variables - workflow - win
if: ${{ matrix.os == 'windows-latest' }}
run: |
echo "PACKAGE_NAME=workflow" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Set environment variables - workflow
if: ${{ matrix.os != 'windows-latest' }}
run: |
echo "PACKAGE_NAME=workflow" >> $GITHUB_ENV
- name: Test with pytest - workflow
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
working-directory: ./python
- name: Move coverage file - workflow
run: |
mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml
working-directory: ./python
- name: Upload coverage artifact - workflow
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }}
path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml
# Surface failing tests
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@v0.7.2
+16 -6
View File
@@ -4,15 +4,25 @@
You're getting early access to Microsoft's comprehensive multi-language framework for building, orchestrating, and deploying AI agents with support for both .NET and Python implementations. This framework provides everything from simple chat agents to complex multi-agent workflows with graph-based orchestration.
**A few important notes:**
- There currently are not public pypi or nuget packages for the SDKs. In order for the code and samples to work, please clone this repo and run the code here.
- The repo is an active project so make sure to sync regularly.
### 📋 Important Setup Information
**Package Availability:** Public PyPI and NuGet packages are not yet available. You have two options:
**We want your feedback!**
**Option 1: Run samples directly from this repository (no package installation needed)**
- Clone this repository
- For .NET: Run samples with `dotnet run` from any sample directory (e.g., `dotnet/samples/GettingStarted/Agents/Agent_Step01_Running`)
- For Python: Run samples from any sample directory (e.g., [`python/samples/getting_started/minimal_sample.py`](python/samples/getting_started/minimal_sample.py)) after setting up the local dev environment following this [guide](python/DEV_SETUP.md).
**Option 2: Install packages in your own project**
- **[.NET Getting Started Guide](./user-documentation-dotnet/getting-started/README.md)** - Instructions for using nightly packages
- **[Python Package Installation Guide](./user-documentation-python/getting-started/package_installation.md)** - Install packages directly from GitHub
**Stay Updated:** This is an active project - sync your local repository regularly to get the latest updates.
### 💬 **We want your feedback!**
- For bugs, please file a [GitHub issue](https://github.com/microsoft/agent-framework/issues).
- For feedback and suggestions for the team, please fill out [this survey](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR9huAe5pW55CqgnnimXONJJUMlVMUzdCN1ZGOURXODlBSVJOSkxERVNCNS4u).
**Highlights**
### ✨ **Highlights**
- Flexible Agent Framework: build, orchestrate, and deploy AI agents and workflows
- Multi-Agent Orchestration: group chat, sequential, concurrent, and handoff patterns
- Graph-based Workflows: connect agents and deterministic functions using data flows with streaming, checkpointing, time-travel, and Human-in-the-loop.
@@ -32,7 +42,7 @@ Below are the basics for each language implementation. For more details on pytho
- [Azure Integration](./python/packages/azure): Azure OpenAI and AI Foundry integration
- [Getting Started with Workflows](./python/samples/getting_started/workflow): basic workflow creation and integration with agents
### .Net
### .NET
- [Getting Started with Agents](./dotnet/samples/GettingStarted/Agents): basic agent creation and tool usage
- [Agent Provider Samples](./dotnet/samples/GettingStarted/AgentProviders): samples showing different agent providers
- [Orchestration Samples](./dotnet/samples/GettingStarted/Orchestration): advanced multi-agent patterns
+48 -44
View File
@@ -11,35 +11,36 @@
</PropertyGroup>
<ItemGroup>
<!-- Azure.* -->
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.4.1-preview.1.25408.4" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.4.1" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="9.4.1" />
<PackageVersion Include="Aspire.Hosting.Azure.CosmosDB" Version="9.4.1" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="9.4.1" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.4.1" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.3" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.3.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.14.2" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.8.0-preview.1.25412.6" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.8.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="9.3.1" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.4.2-preview.1.25428.12" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.4.2" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="9.4.2" />
<PackageVersion Include="Aspire.Hosting.Azure.CosmosDB" Version="9.4.2" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="9.4.2" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.4.2" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.4" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.3.0-beta.2" />
<PackageVersion Include="Azure.Identity" Version="1.15.0" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.7.2" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.9.0-preview.1.25458.4" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.2" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.52.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.53.1" />
<!-- Newtonsoft (Required by CosmosClient) -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
<!-- System.* -->
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.8" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.8" />
<PackageVersion Include="System.Threading.Channels" Version="9.0.8" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.9" />
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.9" />
<PackageVersion Include="System.Threading.Channels" Version="9.0.9" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
@@ -47,38 +48,41 @@
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
<PackageVersion Include="OpenAI" Version="2.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.8.0" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.8.0-preview.1.25412.6" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.8.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Testing" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.8" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.9" />
<PackageVersion Include="OpenAI" Version="2.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.0-preview.1.25458.4" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Testing" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.9" />
<!-- Vector Stores -->
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.64.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.64.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.64.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.64.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.64.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.65.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.65.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.65.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.65.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.65.0-preview" />
<!-- Agent SDKs -->
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.1.151" />
<!-- A2A -->
<PackageVersion Include="A2A" Version="0.1.0-preview.2" />
<PackageVersion Include="A2A.AspNetCore" Version="0.1.0-preview.2" />
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.4" />
<!-- Inference SDKs -->
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.9.0" />
<PackageVersion Include="OllamaSharp" Version="5.3.6" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.74.1" />
<!-- Workflows -->
+48 -19
View File
@@ -7,6 +7,11 @@
<Folder Name="/Samples/">
<File Path="samples/README.md" />
</Folder>
<Folder Name="/Samples/A2AClientServer/">
<File Path="samples/A2AClientServer/README.md" />
<Project Path="samples/A2AClientServer/A2AClient/A2AClient.csproj" />
<Project Path="samples/A2AClientServer/A2AServer/A2AServer.csproj" />
</Folder>
<Folder Name="/Samples/AgentWebChat/">
<Project Path="samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj" />
<Project Path="samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj" />
@@ -25,6 +30,8 @@
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
@@ -41,36 +48,34 @@
<Project Path="samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithOpenAI/">
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/ModelContextProtocol/">
<Project Path="samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
<Project Path="samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Telemetry/">
<Project Path="samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/">
<File Path="samples/GettingStarted/Workflow/README.md" />
<Folder Name="/Samples/GettingStarted/Workflows/">
<File Path="samples/GettingStarted/Workflows/README.md" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/Concurrent/">
<Project Path="samples/GettingStarted/Workflow/Concurrent/Concurrent.csproj" />
<Folder Name="/Samples/GettingStarted/Workflows/Concurrent/">
<Project Path="samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/ConditionalEdges/">
<Project Path="samples/GettingStarted/Workflow/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj" />
<Project Path="samples/GettingStarted/Workflow/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj" />
<Project Path="samples/GettingStarted/Workflow/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj" />
<Folder Name="/Samples/GettingStarted/Workflows/ConditionalEdges/">
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj" />
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj" />
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/Foundational/">
<Project Path="samples/GettingStarted/Workflow/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
<Project Path="samples/GettingStarted/Workflow/Foundational/02_Streaming/02_Streaming.csproj" />
<Project Path="samples/GettingStarted/Workflow/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj" />
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/">
<Project Path="samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/SharedStates/">
<Project Path="samples/GettingStarted/Workflow/SharedStates/SharedStates.csproj" />
</Folder>
<Folder Name="/Samples/DeclarativeWorkflow/">
<Project Path="samples/DeclarativeWorkflow/DeclarativeWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/DeclarativeWorkflow/Example/">
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/Examples/">
<File Path="../workflows/DeepResearch.yaml" />
<File Path="../workflows/HelloWorld.yaml" />
<File Path="../workflows/MathChat.yaml" />
@@ -78,6 +83,30 @@
<File Path="../workflows/README.md" />
<File Path="../workflows/wttr.json" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/SharedStates/">
<Project Path="samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Loop/">
<Project Path="samples/GettingStarted/Workflows/Loop/Loop.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Agents/">
<Project Path="samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj" />
<Project Path="samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj" />
<Project Path="samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Checkpoint/">
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj" />
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj" />
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/HumanInTheLoop/">
<Project Path="samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/_Foundational/">
<Project Path="samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
<Project Path="samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj" />
<Project Path="samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj" />
</Folder>
<Folder Name="/Samples/SemanticKernelMigration/" />
<Folder Name="/Samples/SemanticKernelMigration/AzureAIFoundry/">
<Project Path="samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/AzureAIFoundry_Step01_Basics.csproj" />
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-preview.5.25277.114" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-preview.5.25277.114" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.A2A\Microsoft.Extensions.AI.Agents.A2A.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.Abstractions\Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.AI.Agents.A2A;
using Microsoft.Extensions.Logging;
using OpenAI;
namespace A2A;
internal sealed class HostClientAgent
{
internal HostClientAgent(ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory;
this._logger = loggerFactory.CreateLogger("HostClientAgent");
}
internal async Task InitializeAgentAsync(string modelId, string apiKey, string[] agentUrls)
{
try
{
this._logger.LogInformation("Initializing Agent Framework agent with model: {ModelId}", modelId);
// Connect to the remote agents via A2A
var createAgentTasks = agentUrls.Select(agentUrl => this.CreateAgentAsync(agentUrl));
var agents = await Task.WhenAll(createAgentTasks);
var tools = agents.Select(agent => (AITool)AgentAIFunctionFactory.CreateFromAgent(agent)).ToList();
// Create the agent that uses the remote agents as tools
this.Agent = new OpenAIClient(new ApiKeyCredential(apiKey))
.GetChatClient(modelId)
.CreateAIAgent(instructions: "You specialize in handling queries for users and using your tools to provide answers.", name: "HostClient", tools: tools);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Failed to initialize HostClientAgent");
throw;
}
}
/// <summary>
/// The associated <see cref="Agent"/>
/// </summary>
public AIAgent? Agent { get; private set; }
#region private
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
private async Task<AIAgent> CreateAgentAsync(string agentUri)
{
var url = new Uri(agentUri);
var httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(60)
};
var agentCardResolver = new A2ACardResolver(url, httpClient);
return await agentCardResolver.GetAIAgentAsync();
}
#endregion
}
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Reflection;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace A2A;
public static class Program
{
public static async Task<int> Main(string[] args)
{
// Create root command with options
var rootCommand = new RootCommand("A2AClient");
rootCommand.SetHandler(HandleCommandsAsync);
// Run the command
return await rootCommand.InvokeAsync(args);
}
public static async Task HandleCommandsAsync(InvocationContext context)
{
// Set up the logging
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
var logger = loggerFactory.CreateLogger("A2AClient");
// Retrieve configuration settings
IConfigurationRoot configRoot = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.Build();
var apiKey = configRoot["A2AClient:ApiKey"] ?? throw new ArgumentException("A2AClient:ApiKey must be provided");
var modelId = configRoot["A2AClient:ModelId"] ?? "gpt-4.1";
var agentUrls = configRoot["A2AClient:AgentUrls"] ?? "http://localhost:5000/;http://localhost:5001/;http://localhost:5002/";
// Create the Host agent
var hostAgent = new HostClientAgent(loggerFactory);
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
AgentThread thread = hostAgent.Agent!.GetNewThread();
try
{
while (true)
{
// Get user message
Console.Write("\nUser (:q or quit to exit): ");
string? message = Console.ReadLine();
if (string.IsNullOrWhiteSpace(message))
{
Console.WriteLine("Request cannot be empty.");
continue;
}
if (message == ":q" || message == "quit")
{
break;
}
var agentResponse = await hostAgent.Agent!.RunAsync(message, thread);
foreach (var chatMessage in agentResponse.Messages)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"\nAgent: {chatMessage.Text}");
Console.ResetColor();
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while running the A2AClient");
return;
}
}
}
@@ -0,0 +1,26 @@
# A2A Client Sample
Show how to create an A2A Client with a command line interface which invokes agents using the A2A protocol.
## Run the Sample
To run the sample, follow these steps:
1. Run the A2A client:
```bash
cd A2AClient
dotnet run
```
2. Enter your request e.g. "Show me all invoices for Contoso?"
## Set Environment Variables
The agent urls are provided as a ` ` delimited list of strings
```powershell
cd dotnet/samples/A2AClientServer/A2AClient
$env:OPENAI_MODEL_ID="gpt-4o-mini"
$env:OPENAI_API_KEY="<Your OPENAI api key>"
$env:AGENT_URLS="http://localhost:5000/policy;http://localhost:5000/invoice;http://localhost:5000/logistics"
```
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A.AspNetCore" />
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-preview.5.25277.114" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-preview.5.25277.114" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.A2A\Microsoft.Extensions.AI.Agents.A2A.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,82 @@
### Each A2A agent is available at a different host address
@hostInvoice = http://localhost:5000
@hostPolicy = http://localhost:5001
@hostLogistics = http://localhost:5002
### Query agent card for the invoice agent
GET {{hostInvoice}}/.well-known/agent.json
### Send a message to the invoice agent
POST {{hostInvoice}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"id": "12345",
"message": {
"role": "user",
"messageId": "msg_1",
"parts": [
{
"kind": "text",
"text": "Show me all invoices for Contoso?"
}
]
}
}
}
### Query agent card for the policy agent
GET {{hostPolicy}}/.well-known/agent.json
### Send a message to the policy agent
POST {{hostPolicy}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"id": "12345",
"message": {
"role": "user",
"messageId": "msg_1",
"parts": [
{
"kind": "text",
"text": "What is the policy for short shipments?"
}
]
}
}
}
### Query agent card for the logistics agent
GET {{hostLogistics}}/.well-known/agent.json
### Send a message to the logistics agent
POST {{hostLogistics}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"id": "12345",
"message": {
"role": "user",
"messageId": "msg_1",
"parts": [
{
"kind": "text",
"text": "What is the status for SHPMT-SAP-001?"
}
]
}
}
}
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft. All rights reserved.
using A2A;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.AI.Agents.A2A;
using OpenAI;
namespace A2AServer;
internal static class HostAgentFactory
{
internal static async Task<A2AHostAgent> CreateFoundryHostAgentAsync(string agentType, string modelId, string endpoint, string assistantId, IList<AITool>? tools = null)
{
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
PersistentAgent persistentAgent = await persistentAgentsClient.Administration.GetAgentAsync(assistantId);
AIAgent agent = await persistentAgentsClient
.GetAIAgentAsync(persistentAgent.Id, chatOptions: new() { Tools = tools });
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
"INVOICE" => GetInvoiceAgentCard(),
"POLICY" => GetPolicyAgentCard(),
"LOGISTICS" => GetLogisticsAgentCard(),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
return new A2AHostAgent(agent, agentCard);
}
internal static async Task<A2AHostAgent> CreateChatCompletionHostAgentAsync(string agentType, string modelId, string apiKey, string name, string instructions, IList<AITool>? tools = null)
{
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(modelId)
.CreateAIAgent(instructions, name, tools: tools);
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
"INVOICE" => GetInvoiceAgentCard(),
"POLICY" => GetPolicyAgentCard(),
"LOGISTICS" => GetLogisticsAgentCard(),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
return new A2AHostAgent(agent, agentCard);
}
#region private
private static AgentCard GetInvoiceAgentCard()
{
var capabilities = new AgentCapabilities()
{
Streaming = false,
PushNotifications = false,
};
var invoiceQuery = new AgentSkill()
{
Id = "id_invoice_agent",
Name = "InvoiceQuery",
Description = "Handles requests relating to invoices.",
Tags = ["invoice", "semantic-kernel"],
Examples =
[
"List the latest invoices for Contoso.",
],
};
return new()
{
Name = "InvoiceAgent",
Description = "Handles requests relating to invoices.",
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [invoiceQuery],
};
}
private static AgentCard GetPolicyAgentCard()
{
var capabilities = new AgentCapabilities()
{
Streaming = false,
PushNotifications = false,
};
var invoiceQuery = new AgentSkill()
{
Id = "id_policy_agent",
Name = "PolicyAgent",
Description = "Handles requests relating to policies and customer communications.",
Tags = ["policy", "semantic-kernel"],
Examples =
[
"What is the policy for short shipments?",
],
};
return new AgentCard()
{
Name = "PolicyAgent",
Description = "Handles requests relating to policies and customer communications.",
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [invoiceQuery],
};
}
private static AgentCard GetLogisticsAgentCard()
{
var capabilities = new AgentCapabilities()
{
Streaming = false,
PushNotifications = false,
};
var logisticsQuery = new AgentSkill()
{
Id = "id_logistics_agent",
Name = "LogisticsQuery",
Description = "Handles requests relating to logistics.",
Tags = ["logistics", "semantic-kernel"],
Examples =
[
"What is the status for SHPMT-SAP-001",
],
};
return new AgentCard()
{
Name = "LogisticsAgent",
Description = "Handles requests relating to logistics.",
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [logisticsQuery],
};
}
#endregion
}
@@ -0,0 +1,176 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
namespace A2A;
/// <summary>
/// A simple invoice plugin that returns mock data.
/// </summary>
public class Product
{
public string Name { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; } // Price per unit
public Product(string name, int quantity, decimal price)
{
this.Name = name;
this.Quantity = quantity;
this.Price = price;
}
public decimal TotalPrice()
{
return this.Quantity * this.Price; // Total price for this product
}
}
public class Invoice
{
public string TransactionId { get; set; }
public string InvoiceId { get; set; }
public string CompanyName { get; set; }
public DateTime InvoiceDate { get; set; }
public List<Product> Products { get; set; } // List of products
public Invoice(string transactionId, string invoiceId, string companyName, DateTime invoiceDate, List<Product> products)
{
this.TransactionId = transactionId;
this.InvoiceId = invoiceId;
this.CompanyName = companyName;
this.InvoiceDate = invoiceDate;
this.Products = products;
}
public decimal TotalInvoicePrice()
{
return this.Products.Sum(product => product.TotalPrice()); // Total price of all products in the invoice
}
}
public class InvoiceQuery
{
private readonly List<Invoice> _invoices;
private static readonly Random s_random = new();
public InvoiceQuery()
{
// Extended mock data with quantities and prices
this._invoices =
[
new("TICKET-XYZ987", "INV789", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 150, 10.00m),
new("Hats", 200, 15.00m),
new("Glasses", 300, 5.00m)
}),
new("TICKET-XYZ111", "INV111", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 2500, 12.00m),
new("Hats", 1500, 8.00m),
new("Glasses", 200, 20.00m)
}),
new("TICKET-XYZ222", "INV222", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 1200, 14.00m),
new("Hats", 800, 7.00m),
new("Glasses", 500, 25.00m)
}),
new("TICKET-XYZ333", "INV333", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 400, 11.00m),
new("Hats", 600, 15.00m),
new("Glasses", 700, 5.00m)
}),
new("TICKET-XYZ444", "INV444", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 800, 10.00m),
new("Hats", 500, 18.00m),
new("Glasses", 300, 22.00m)
}),
new("TICKET-XYZ555", "INV555", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 1100, 9.00m),
new("Hats", 900, 12.00m),
new("Glasses", 1200, 15.00m)
}),
new("TICKET-XYZ666", "INV666", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 2500, 8.00m),
new("Hats", 1200, 10.00m),
new("Glasses", 1000, 6.00m)
}),
new("TICKET-XYZ777", "INV777", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 1900, 13.00m),
new("Hats", 1300, 16.00m),
new("Glasses", 800, 19.00m)
}),
new("TICKET-XYZ888", "INV888", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 2200, 11.00m),
new("Hats", 1700, 8.50m),
new("Glasses", 600, 21.00m)
}),
new("TICKET-XYZ999", "INV999", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 1400, 10.50m),
new("Hats", 1100, 9.00m),
new("Glasses", 950, 12.00m)
})
];
}
public static DateTime GetRandomDateWithinLastTwoMonths()
{
// Get the current date and time
DateTime endDate = DateTime.Now;
// Calculate the start date, which is two months before the current date
DateTime startDate = endDate.AddMonths(-2);
// Generate a random number of days between 0 and the total number of days in the range
int totalDays = (endDate - startDate).Days;
#pragma warning disable CA5394 // Do not use insecure randomness
int randomDays = s_random.Next(0, totalDays + 1); // +1 to include the end date
#pragma warning restore CA5394 // Do not use insecure randomness
// Return the random date
return startDate.AddDays(randomDays);
}
[Description("Retrieves invoices for the specified company and optionally within the specified time range")]
public IEnumerable<Invoice> QueryInvoices(string companyName, DateTime? startDate = null, DateTime? endDate = null)
{
var query = this._invoices.Where(i => i.CompanyName.Equals(companyName, StringComparison.OrdinalIgnoreCase));
if (startDate.HasValue)
{
query = query.Where(i => i.InvoiceDate >= startDate.Value);
}
if (endDate.HasValue)
{
query = query.Where(i => i.InvoiceDate <= endDate.Value);
}
return query.ToList();
}
[Description("Retrieves invoice using the transaction id")]
public IEnumerable<Invoice> QueryByTransactionId(string transactionId)
{
var query = this._invoices.Where(i => i.TransactionId.Equals(transactionId, StringComparison.OrdinalIgnoreCase));
return query.ToList();
}
[Description("Retrieves invoice using the invoice id")]
public IEnumerable<Invoice> QueryByInvoiceId(string invoiceId)
{
var query = this._invoices.Where(i => i.InvoiceId.Equals(invoiceId, StringComparison.OrdinalIgnoreCase));
return query.ToList();
}
}
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft. All rights reserved.
using A2A;
using A2A.AspNetCore;
using A2AServer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents.A2A;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
string agentId = string.Empty;
string agentType = string.Empty;
for (var i = 0; i < args.Length; i++)
{
if (args[i].StartsWith("--agentId", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length)
{
agentId = args[++i];
}
else if (args[i].StartsWith("--agentType", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length)
{
agentType = args[++i];
}
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
var app = builder.Build();
var httpClient = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
var logger = app.Logger;
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddUserSecrets<Program>()
.Build();
string? apiKey = configuration["OPENAI_API_KEY"];
string modelId = configuration["OPENAI_MODEL_ID"] ?? "gpt-4o-mini";
string? endpoint = configuration["FOUNDRY_PROJECT_ENDPOINT"];
var invoiceQueryPlugin = new InvoiceQuery();
IList<AITool> tools =
[
AIFunctionFactory.Create(invoiceQueryPlugin.QueryInvoices),
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByTransactionId),
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByInvoiceId)
];
A2AHostAgent? hostAgent = null;
if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentId))
{
hostAgent = agentType.ToUpperInvariant() switch
{
"INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, modelId, endpoint, agentId, tools),
"POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, modelId, endpoint, agentId),
"LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, modelId, endpoint, agentId),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
else if (!string.IsNullOrEmpty(apiKey))
{
hostAgent = agentType.ToUpperInvariant() switch
{
"INVOICE" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, modelId, apiKey, "InvoiceAgent",
"""
You specialize in handling queries related to invoices.
""", tools),
"POLICY" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, modelId, apiKey, "PolicyAgent",
"""
You specialize in handling queries related to policies and customer communications.
Always reply with exactly this text:
Policy: Short Shipment Dispute Handling Policy V2.1
Summary: "For short shipments reported by customers, first verify internal shipment records
(SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data
shows fewer items packed than invoiced, issue a credit for the missing items. Document the
resolution in SAP CRM and notify the customer via email within 2 business days, referencing the
original invoice and the credit memo number. Use the 'Formal Credit Notification' email
template."
"""),
"LOGISTICS" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, modelId, apiKey, "LogisticsAgent",
"""
You specialize in handling queries related to logistics.
Always reply with exactly:
Shipment number: SHPMT-SAP-001
Item: TSHIRT-RED-L
Quantity: 900
"""),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
else
{
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentId must be provided");
}
app.MapA2A(hostAgent!.TaskManager!, "/");
await app.RunAsync();
+234
View File
@@ -0,0 +1,234 @@
# A2A Client and Server samples
> **Warning**
> The [A2A protocol](https://google.github.io/A2A/) is still under development and changing fast.
> We will try to keep these samples updated as the protocol evolves.
These samples are built with [official A2A C# SDK](https://www.nuget.org/packages/A2A) and demonstrates:
1. Creating an A2A Server which makes an agent available via the A2A protocol.
2. Creating an A2A Client with a command line interface which invokes agents using the A2A protocol.
The demonstration has two components:
1. `A2AServer` - You will run three instances of the server to correspond to three A2A servers each providing a single Agent i.e., the Invoice, Policy and Logistics agents.
2. `A2AClient` - This represents a client application which will connect to the remote A2A servers using the A2A protocol so that it can use those agents when answering questions you will ask.
<img src="./demo-architecture.png" alt="Demo Architecture"/>
## Configuring Environment Variables
The samples can be configured to use chat completion agents or Azure AI agents.
### Configuring for use with Chat Completion Agents
Provide your OpenAI API key via an environment variable
```powershell
$env:OPENAI_API_KEY="<Your OpenAI API Key>"
```
Use the following commands to run each A2A server:
Execute the following command to build the sample:
```powershell
cd A2AServer
dotnet build
```
```bash
dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentType "invoice" --no-build
```
```bash
dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentType "policy" --no-build
```
```bash
dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentType "logistics" --no-build
```
### Configuring for use with Azure AI Agents
You must create the agents in an Azure AI Foundry project and then provide the project endpoint and agents ids. The instructions for each agent are as follows:
- Invoice Agent
```
You specialize in handling queries related to invoices.
```
- Policy Agent
```
You specialize in handling queries related to policies and customer communications.
Always reply with exactly this text:
Policy: Short Shipment Dispute Handling Policy V2.1
Summary: "For short shipments reported by customers, first verify internal shipment records
(SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data
shows fewer items packed than invoiced, issue a credit for the missing items. Document the
resolution in SAP CRM and notify the customer via email within 2 business days, referencing the
original invoice and the credit memo number. Use the 'Formal Credit Notification' email
template."
```
- Logistics Agent
```
You specialize in handling queries related to logistics.
Always reply with exactly:
Shipment number: SHPMT-SAP-001
Item: TSHIRT-RED-L
Quantity: 900"
```
```powershell
$env:FOUNDRY_PROJECT_ENDPOINT="https://ai-foundry-your-project.services.ai.azure.com/api/projects/ai-proj-ga-your-project" # Replace with your Foundry Project endpoint
```
Use the following commands to run each A2A server
```bash
dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentId "<Invoice Agent Id>" --agentType "invoice" --no-build
```
```bash
dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentId "<Policy Agent Id>" --agentType "policy" --no-build
```
```bash
dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentId "<Logistics Agent Id>" --agentType "logistics" --no-build
```
### Testing the Agents using the Rest Client
This sample contains a [.http file](https://learn.microsoft.com/aspnet/core/test/http-files?view=aspnetcore-9.0) which can be used to test the agent.
1. In Visual Studio open [./A2AServer/A2AServer.http](./A2AServer/A2AServer.http)
1. There are two sent requests for each agent, e.g., for the invoice agent:
1. Query agent card for the invoice agent
`GET {{hostInvoice}}/.well-known/agent.json`
1. Send a message to the invoice agent
```
POST {{hostInvoice}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"id": "12345",
"message": {
"role": "user",
"messageId": "msg_1",
"parts": [
{
"kind": "text",
"text": "Show me all invoices for Contoso?"
}
]
}
}
}
```
Sample output from the request to display the agent card:
<img src="./rest-client-agent-card.png" alt="Agent Card"/>
Sample output from the request to send a message to the agent via A2A protocol:
<img src="./rest-client-send-message.png" alt="Send Message"/>
### Testing the Agents using the A2A Inspector
The A2A Inspector is a web-based tool designed to help developers inspect, debug, and validate servers that implement the Google A2A (Agent-to-Agent) protocol. It provides a user-friendly interface to interact with an A2A agent, view communication, and ensure specification compliance.
For more information go [here](https://github.com/a2aproject/a2a-inspector).
Running the [inspector with Docker](https://github.com/a2aproject/a2a-inspector?tab=readme-ov-file#option-two-run-with-docker) is the easiest way to get started.
1. Navigate to the A2A Inspector in your browser: [http://127.0.0.1:8080/](http://127.0.0.1:8080/)
1. Enter the URL of the Agent you are running e.g., [http://host.docker.internal:5000](http://host.docker.internal:5000)
1. Connect to the agent and the agent card will be displayed and validated.
1. Type a message and send it to the agent using A2A protocol.
1. The response will be validated automatically and then displayed in the UI.
1. You can select the response to view the raw json.
Agent card after connecting to an agent using the A2A protocol:
<img src="./a2a-inspector-agent-card.png" alt="Agent Card"/>
Sample response after sending a message to the agent via A2A protocol:
<img src="./a2a-inspector-send-message.png" alt="Send Message"/>
Raw JSON response from an A2A agent:
<img src="./a2a-inspector-raw-json-response.png" alt="Response Raw JSON"/>
### Configuring Agents for the A2A Client
The A2A client will connect to remote agents using the A2A protocol.
By default the client will connect to the invoice, policy and logistics agents provided by the sample A2A Server.
These are available at the following URL's:
- Invoice Agent: http://localhost:5000/
- Policy Agent: http://localhost:5001/
- Logistics Agent: http://localhost:5002/
If you want to change which agents are using then set the agents url as a space delimited string as follows:
```powershell
$env:A2A_AGENT_URLS="http://localhost:5000/;http://localhost:5001/;http://localhost:5002/"
```
## Run the Sample
To run the sample, follow these steps:
1. Run the A2A server's using the commands shown earlier
2. Run the A2A client:
```bash
cd A2AClient
dotnet run
```
3. Enter your request e.g. "Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered."
4. The host client agent will call the remote agents, these calls will be displayed as console output. The final answer will use information from the remote agents. The sample below includes all three agents but in your case you may only see the policy and invoice agent.
Sample output from the A2A client:
```
A2AClient> dotnet run
info: HostClientAgent[0]
Initializing Agent Framework agent with model: gpt-4o-mini
User (:q or quit to exit): Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered.
Agent:
Agent:
Agent: The transaction details for **TICKET-XYZ987** are as follows:
- **Invoice ID:** INV789
- **Company Name:** Contoso
- **Invoice Date:** September 4, 2025
- **Products:**
- **T-Shirts:** 150 units at $10.00 each
- **Hats:** 200 units at $15.00 each
- **Glasses:** 300 units at $5.00 each
To proceed with the dispute regarding the quantity of t-shirts delivered, please specify the exact quantity issue how many t-shirts were actually received compared to the ordered amount.
### Customer Service Policy for Handling Disputes
**Short Shipment Dispute Handling Policy V2.1**
- **Summary:** For short shipments reported by customers, first verify internal shipment records and physical logistics scan data. If a discrepancy is confirmed and the logistics data shows fewer items were packed than invoiced, a credit for the missing items will be issued.
- **Follow-up Actions:** Document the resolution in the SAP CRM and notify the customer via email within 2 business days, referencing the original invoice and the credit memo number, using the 'Formal Credit Notification' email template.
Please provide me with the information regarding the specific quantity issue so I can assist you further.
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

+1 -1
View File
@@ -7,7 +7,7 @@
<IsAotCompatible>false</IsAotCompatible>
<ProjectsTargetFrameworks>net472;net9.0</ProjectsTargetFrameworks>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CA1707</NoWarn>
<NoWarn>$(NoWarn);CA1707;MEAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -8,7 +8,6 @@ using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Diagnostics;
using Microsoft.Shared.Samples;
using OpenAI;
using OpenAI.Assistants;
using OpenAI.Chat;
using OpenAI.Responses;
@@ -111,13 +110,13 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
private IChatClient GetOpenAIResponsesClient()
=> new OpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.AsNewIChatClient();
.AsIChatClient();
private IChatClient GetAzureAIAgentPersistentClient(ChatClientAgentOptions options)
=> new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential()).AsNewIChatClient(options.Id!);
private IChatClient GetOpenAIAssistantChatClient(ChatClientAgentOptions options)
=> new AssistantClient(TestConfiguration.OpenAI.ApiKey).AsNewIChatClient(options.Id!);
=> new AssistantClient(TestConfiguration.OpenAI.ApiKey).AsIChatClient(options.Id!);
#endregion
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows all the required steps to create a fully custom agent implementation.
// In this case the agent doesn't use AI at all, and simply parrots back the user input in upper case.
// You can however, build a fully custom agent that uses AI in any way you want.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using SampleApp;
AIAgent agent = new UpperCaseParrotAgent();
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
// Invoke the agent with streaming support.
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
{
Console.WriteLine(update);
}
namespace SampleApp
{
// Custom agent that parrot's the user input back in upper case.
internal sealed class UpperCaseParrotAgent : AIAgent
{
public override async Task<AgentRunResponse> RunAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
// Create a thread if the user didn't supply one.
thread ??= this.GetNewThread();
// Clone the input messages and turn them into response messages with upper case text.
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
// Notify the thread of the input and output messages.
await NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken);
await NotifyThreadOfNewMessagesAsync(thread, responseMessages, cancellationToken);
return new AgentRunResponse
{
AgentId = this.Id,
ResponseId = Guid.NewGuid().ToString(),
Messages = responseMessages
};
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Create a thread if the user didn't supply one.
thread ??= this.GetNewThread();
// Clone the input messages and turn them into response messages with upper case text.
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
// Notify the thread of the input and output messages.
await NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken);
await NotifyThreadOfNewMessagesAsync(thread, responseMessages, cancellationToken);
foreach (var message in responseMessages)
{
yield return new AgentRunResponseUpdate
{
AgentId = this.Id,
AuthorName = this.DisplayName,
Role = ChatRole.Assistant,
Contents = message.Contents,
ResponseId = Guid.NewGuid().ToString(),
MessageId = Guid.NewGuid().ToString()
};
}
}
private static IEnumerable<ChatMessage> CloneAndToUpperCase(IReadOnlyCollection<ChatMessage> messages, string agentName) => messages.Select(x =>
{
// Clone the message and update its author to be the agent.
var messageClone = x.Clone();
messageClone.Role = ChatRole.Assistant;
messageClone.MessageId = Guid.NewGuid().ToString();
messageClone.AuthorName = agentName;
// Clone and convert any text content to upper case.
messageClone.Contents = x.Contents.Select(c => c switch
{
TextContent tc => new TextContent(tc.Text.ToUpperInvariant())
{
AdditionalProperties = tc.AdditionalProperties,
Annotations = tc.Annotations,
RawRepresentation = tc.RawRepresentation
},
_ => c
}).ToList();
return messageClone;
});
}
}
@@ -0,0 +1,3 @@
# Prerequisites
This sample has no prerequisites.
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with ONNX as the backend.
// WARNING: ONNX doesn't support function calling, so any function tools passed to the agent will be ignored.
using System;
using Microsoft.Extensions.AI.Agents;
@@ -1,10 +1,18 @@
# Prerequisites
WARNING: ONNX doesn't support function calling, so any function tools passed to the agent will be ignored.
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- An ONNX model downloaded to your machine
You can download an ONNX model from hugging face, using git clone:
```powershell
git clone https://huggingface.co/microsoft/Phi-4-mini-instruct-onnx
```
Set the following environment variables:
```powershell
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OllamaSharp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with Ollama as the backend.
using System;
using Microsoft.Extensions.AI.Agents;
using OllamaSharp;
var endpoint = Environment.GetEnvironmentVariable("OLLAMA_ENDPOINT") ?? throw new InvalidOperationException("OLLAMA_ENDPOINT is not set.");
var modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set.");
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Get a chat client for Ollama and use it to construct an AIAgent.
using OllamaApiClient chatClient = new(new Uri(endpoint), modelName);
AIAgent agent = new ChatClientAgent(chatClient, JokerInstructions, JokerName);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -0,0 +1,34 @@
# Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- Docker installed and running on your machine
- An Ollama model downloaded into Ollama
To download and start Ollama on Docker using CPU, run the following command in your terminal.
```powershell
docker run -d -v "c:\temp\ollama:/root/.ollama" -p 11434:11434 --name ollama ollama/ollama
```
To download and start Ollama on Docker using GPU, run the following command in your terminal.
```powershell
docker run -d --gpus=all -v "c:\temp\ollama:/root/.ollama" -p 11434:11434 --name ollama ollama/ollama
```
After the container has started, launch a Terminal window for the docker container, e.g. if using docker desktop, choose Open in Terminal from actions.
From this terminal download the required models, e.g. here we are downloading the phi3 model.
```text
ollama pull gpt-oss
```
Set the following environment variables:
```powershell
$env:OLLAMA_ENDPOINT="http://localhost:11434"
$env:OLLAMA_MODEL_NAME="gpt-oss"
```
@@ -9,6 +9,7 @@ see the [Getting Started Steps](../GettingStartedSteps/README.md) samples.
## Prerequisites
See the README.md for each sample for the prerequisites for that sample.
## Samples
|Sample|Description|
@@ -17,6 +18,8 @@ See the README.md for each sample for the prerequisites for that sample.
|[Creating an AIAgent with AzureFoundry](./Agent_With_AzureFoundry/)|This sample demonstrates how to create an Azure Foundry agent and expose it as an AIAgent|
|[Creating an AIAgent with Azure OpenAI ChatCompletion](./Agent_With_AzureOpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using Azure OpenAI ChatCompletion as the underlying inference service|
|[Creating an AIAgent with Azure OpenAI Responses](./Agent_With_AzureOpenAIResponses/)|This sample demonstrates how to create an AIAgent using Azure OpenAI Responses as the underlying inference service|
|[Creating an AIAgent with a custom implementation](./Agent_With_CustomImplementation/)|This sample demonstrates how to create an AIAgent with a custom implementation|
|[Creating an AIAgent with Ollama](./Agent_With_Ollama/)|This sample demonstrates how to create an AIAgent using Ollama as the underlying inference service|
|[Creating an AIAgent with ONNX](./Agent_With_ONNX/)|This sample demonstrates how to create an AIAgent using ONNX as the underlying inference service|
|[Creating an AIAgent with OpenAI ChatCompletion](./Agent_With_OpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using OpenAI ChatCompletion as the underlying inference service|
|[Creating an AIAgent with OpenAI Responses](./Agent_With_OpenAIResponses/)|This sample demonstrates how to create an AIAgent using OpenAI Responses as the underlying inference service|
@@ -22,11 +22,11 @@ AIAgent agent = new OpenAIClient(apiKey)
UserChatMessage chatMessage = new("Tell me a joke about a pirate.");
// Invoke the agent and output the text result.
ChatCompletion chatCompletion = await agent.RunAsync(chatMessage);
ChatCompletion chatCompletion = await agent.RunAsync([chatMessage]);
Console.WriteLine(chatCompletion.Content.Last().Text);
// Invoke the agent with streaming support.
AsyncCollectionResult<StreamingChatCompletionUpdate> completionUpdates = agent.RunStreamingAsync(chatMessage);
AsyncCollectionResult<StreamingChatCompletionUpdate> completionUpdates = agent.RunStreamingAsync([chatMessage]);
await foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates)
{
if (completionUpdate.ContentUpdate.Count > 0)
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use Image Multi-Modality with an AI agent.
using System;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using OpenAI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o";
var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(
name: "VisionAgent",
instructions: "You are a helpful agent that can analyze images");
ChatMessage message = new(ChatRole.User, [
new TextContent("What do you see in this image?"),
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
]);
var thread = agent.GetNewThread();
await foreach (var update in agent.RunStreamingAsync(message, thread))
{
Console.WriteLine(update);
}
@@ -0,0 +1,52 @@
# Using Images with AI Agents
This sample demonstrates how to use image multi-modality with an AI agent. It shows how to create a vision-enabled agent that can analyze and describe images using Azure OpenAI.
## What this sample demonstrates
- Creating a persistent AI agent with vision capabilities
- Sending both text and image content to an agent in a single message
- Using `UriContent` to Uri referenced images
- Processing multimodal input (text + image) with an AI agent
## Key features
- **Vision Agent**: Creates an agent specifically instructed to analyze images
- **Multimodal Input**: Combines text questions with image uri in a single message
- **Azure OpenAI Integration**: Uses AzureOpenAI LLM agents
## Prerequisites
Before running this sample, ensure you have:
1. An Azure OpenAI project set up
2. A compatible model deployment (e.g., gpt-4o)
3. Azure CLI installed and authenticated
## Environment Variables
Set the following environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI endpoint
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o" # Replace with your model deployment name (optional, defaults to gpt-4o)
```
## Run the sample
Navigate to the sample directory and run:
```powershell
cd Agent_Step11_UsingImages
dotnet run
```
## Expected behavior
The sample will:
1. Create a vision-enabled agent named "VisionAgent"
2. Send a message containing both text ("What do you see in this image?") and a Uri image of a green walk
3. The agent will analyze the image and provide a description
4. Clean up resources by deleting the thread and agent
@@ -36,6 +36,7 @@ Before you begin, ensure you have the following prerequisites:
|[Telemetry with a simple agent](./Agent_Step08_Telemetry/)|This sample demonstrates how to add telemetry to a simple agent|
|[Dependency injection with a simple agent](./Agent_Step09_DependencyInjection/)|This sample demonstrates how to add and resolve an agent with a dependency injection container|
|[Exposing a simple agent as MCP tool](./Agent_Step10_AsMcpTool/)|This sample demonstrates how to expose an agent as an MCP tool|
|[Using images with a simple agent](./Agent_Step11_UsingImages/)|This sample demonstrates how to use image multi-modality with an AI agent|
## Running the samples from the console
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-preview.4.25258.110" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with tools from an MCP Server.
using System;
using System.Linq;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using ModelContextProtocol.Client;
using OpenAI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Create an MCPClient for the GitHub server
await using var mcpClient = await McpClientFactory.CreateAsync(new StdioClientTransport(new()
{
Name = "MCPServer",
Command = "npx",
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-github"],
}));
// Retrieve the list of tools available on the GitHub server
var mcpTools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You answer questions related to GitHub repositories only.", tools: [.. mcpTools.Cast<AITool>()]);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Summarize the last four commits to the microsoft/semantic-kernel repository?"));
@@ -0,0 +1,31 @@
# Model Context Protocol Sample
This example demonstrates how to use tools from a Model Context Protocol server with Agent Framework.
MCP is an open protocol that standardizes how applications provide context to LLMs.
For information on Model Context Protocol (MCP) please refer to the [documentation](https://modelcontextprotocol.io/introduction).
The sample shows:
1. How to connect to an MCP Server
1. Retrieve the list of tools the MCP Server makes available
1. Convert the MCP tools to `AIFunction`'s so they can be added to an agent
1. Invoke the tools from an agent using function calling
## Configuring Environment Variables
Set the following environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
## Setup and Running
Run the ModelContextProtocolPluginAuth sample
```bash
dotnet run
```
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-preview.4.25258.110" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with tools from an MCP Server that requires authentication.
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Client;
using OpenAI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// We can customize a shared HttpClient with a custom handler if desired
using var sharedHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1)
};
using var httpClient = new HttpClient(sharedHandler);
var consoleLoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
});
// Create SSE client transport for the MCP server
var serverUrl = "http://localhost:7071/";
var transport = new SseClientTransport(new()
{
Endpoint = new Uri(serverUrl),
Name = "Secure Weather Client",
OAuth = new()
{
ClientName = "ProtectedMcpClient",
RedirectUri = new Uri("http://localhost:1179/callback"),
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
}
}, httpClient, consoleLoggerFactory);
// Create an MCPClient for the protected MCP server
await using var mcpClient = await McpClientFactory.CreateAsync(transport, loggerFactory: consoleLoggerFactory);
// Retrieve the list of tools available on the GitHub server
var mcpTools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You answer questions related to the weather.", tools: [.. mcpTools.Select(mcpTool => (AITool)mcpTool)]);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Get current weather alerts for New York?"));
// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser.
// This implementation demonstrates how SDK consumers can provide their own authorization flow.
static async Task<string?> HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken)
{
Console.WriteLine("Starting OAuth authorization flow...");
Console.WriteLine($"Opening browser to: {authorizationUrl}");
var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority);
if (!listenerPrefix.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
{
listenerPrefix += "/";
}
using var listener = new HttpListener();
listener.Prefixes.Add(listenerPrefix);
try
{
listener.Start();
Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}");
OpenBrowser(authorizationUrl);
var context = await listener.GetContextAsync();
var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty);
var code = query["code"];
var error = query["error"];
string responseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>";
byte[] buffer = Encoding.UTF8.GetBytes(responseHtml);
context.Response.ContentLength64 = buffer.Length;
context.Response.ContentType = "text/html";
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
context.Response.Close();
if (!string.IsNullOrEmpty(error))
{
Console.WriteLine($"Auth error: {error}");
return null;
}
if (string.IsNullOrEmpty(code))
{
Console.WriteLine("No authorization code received");
return null;
}
Console.WriteLine("Authorization code received successfully.");
return code;
}
catch (Exception ex)
{
Console.WriteLine($"Error getting auth code: {ex.Message}");
return null;
}
finally
{
if (listener.IsListening)
{
listener.Stop();
}
}
}
// Opens the specified URL in the default browser.
static void OpenBrowser(Uri url)
{
try
{
var psi = new ProcessStartInfo
{
FileName = url.ToString(),
UseShellExecute = true
};
Process.Start(psi);
}
catch (Exception ex)
{
Console.WriteLine($"Error opening browser. {ex.Message}");
Console.WriteLine($"Please manually open this URL: {url}");
}
}
@@ -0,0 +1,125 @@
# Model Context Protocol Sample
This example demonstrates how to use tools from a protected Model Context Protocol server with Agent Framework.
MCP is an open protocol that standardizes how applications provide context to LLMs.
For information on Model Context Protocol (MCP) please refer to the [documentation](https://modelcontextprotocol.io/introduction).
The sample shows:
1. How to connect to a protected MCP Server using OAuth 2.0 authentication
1. How to implement a custom OAuth authorization flow with browser-based authentication
1. Retrieve the list of tools the MCP Server makes available
1. Convert the MCP tools to `AIFunction`'s so they can be added to an agent
1. Invoke the tools from an agent using function calling
## Installing Prerequisites
- A self-signed certificate to enable HTTPS use in development, see [dotnet dev-certs](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-dev-certs)
- .NET 9.0 or later
- A running TestOAuthServer (for OAuth authentication), see [Start the Test OAuth Server](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpClient#step-1-start-the-test-oauth-server)
- A running ProtectedMCPServer (for MCP services), see [Start the Protected MCP Server](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpClient#step-2-start-the-protected-mcp-server)
## Configuring Environment Variables
Set the following environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
## Setup and Running
### Step 1: Start the Test OAuth Server
First, you need to start the TestOAuthServer which provides OAuth authentication:
```bash
cd <MCP CSHARP-SDK>\tests\ModelContextProtocol.TestOAuthServer
dotnet run --framework net9.0
```
The OAuth server will start at `https://localhost:7029`
### Step 2: Start the Protected MCP Server
Next, start the ProtectedMCPServer which provides the weather tools:
```bash
cd <MCP CSHARP-SDK>\samples\ProtectedMCPServer
dotnet run
```
The protected server will start at `http://localhost:7071`
### Step 3: Run the ModelContextProtocolPluginAuth sample
Finally, run this client:
```bash
dotnet run
```
## What Happens
1. The client attempts to connect to the protected MCP server at `http://localhost:7071`
2. The server responds with OAuth metadata indicating authentication is required
3. The client initiates OAuth 2.0 authorization code flow:
- Opens a browser to the authorization URL at the OAuth server
- Starts a local HTTP listener on `http://localhost:1179/callback` to receive the authorization code
- Exchanges the authorization code for an access token
4. The client uses the access token to authenticate with the MCP server
5. The client lists available tools and calls the `GetAlerts` tool for New York state
The following diagram outlines an example OAuth flow:
```mermaid
sequenceDiagram
participant Client as Client
participant Server as MCP Server (Resource Server)
participant AuthServer as Authorization Server
Client->>Server: MCP request without access token
Server-->>Client: HTTP 401 Unauthorized with WWW-Authenticate header
Note over Client: Analyze and delegate tasks
Client->>Server: GET /.well-known/oauth-protected-resource
Server-->>Client: Resource metadata with authorization server URL
Note over Client: Validate RS metadata, build AS metadata URL
Client->>AuthServer: GET /.well-known/oauth-authorization-server
AuthServer-->>Client: Authorization server metadata
Note over Client,AuthServer: OAuth 2.0 authorization flow happens here
Client->>AuthServer: Token request
AuthServer-->>Client: Access token
Client->>Server: MCP request with access token
Server-->>Client: MCP response
Note over Client,Server: MCP communication continues with valid token
```
## OAuth Configuration
The client is configured with:
- **Client ID**: `demo-client`
- **Client Secret**: `demo-secret`
- **Redirect URI**: `http://localhost:1179/callback`
- **OAuth Server**: `https://localhost:7029`
- **Protected Resource**: `http://localhost:7071`
## Available Tools
Once authenticated, the client can access weather tools including:
- **GetAlerts**: Get weather alerts for a US state
- **GetForecast**: Get weather forecast for a location (latitude/longitude)
## Troubleshooting
- Ensure the ASP.NET Core dev certificate is trusted.
```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```
- Ensure all three services are running in the correct order
- Check that ports 7029, 7071, and 1179 are available
- If the browser doesn't open automatically, copy the authorization URL from the console and open it manually
- Make sure to allow the OAuth server's self-signed certificate in your browser
@@ -0,0 +1,64 @@
# Getting started with Model Content Protocol
The getting started with Model Content Protocol samples demonstrate how to use MCP Server tools from an agent.
## Getting started with agents prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 9.0 SDK or later
- Azure OpenAI service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource.
**Note**: These samples use Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
**Note**: These samples use Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource and have the `Cognitive Services OpenAI Contributor` role. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
## Samples
|Sample|Description|
|---|---|
|[Agent with MCP server tools](./Agent_MCP_Server/)|This sample demonstrates how to use MCP server tools with a simple agent|
|[Agent with MCP server tools and authorization](./Agent_MCP_Server_Auth/)|This sample demonstrates how to use MCP Server tools from a protected MCP server with a simple agent|
## Running the samples from the console
To run the samples, navigate to the desired sample directory, e.g.
```powershell
cd Agents_Step01_Running
```
Set the following environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
```
If the variables are not set, you will be prompted for the values when running the samples.
Execute the following command to build the sample:
```powershell
dotnet build
```
Execute the following command to run the sample:
```powershell
dotnet run --no-build
```
Or just build and run in one step:
```powershell
dotnet run
```
## Running the samples from Visual Studio
Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`.
You will be prompted for any required environment variables if they are not already set.
+4 -1
View File
@@ -7,6 +7,9 @@ of the agent framework.
|Sample|Description|
|---|---|
|[Agents](./Agents/README.md)|Getting started with agents|
|[Agents](./Agents/README.md)|Step by step instructions for getting started with agents|
|[Agent Providers](./AgentProviders/README.md)|Getting started with creating agents using various providers|
|[Agent Open Telemetry](./AgentOpenTelemetry/README.md)|Getting started with OpenTelemetry for agents|
|[Agent With OpenAI exchange types](./AgentWithOpenAI/README.md)|Using OpenAI exchange types with agents|
|[Workflow](./Workflow/README.md)|Getting started with Workflow|
|[Model Context Protocol](./ModelContextProtocol/README.md)|Getting started with Model Context Protocol|
@@ -1,41 +0,0 @@
# Workflow Getting Started Samples
The getting started with workflow samples demonstrate the fundamental concepts and functionalities of workflows in Agent Framework.
## Samples Overview
### Foundational Concepts - Start Here
Please begin with the [Foundational](./Foundational) samples in order. These three samples introduce the core concepts of executors, edges, agents in workflows, streaming, and workflow construction.
| Sample | Concepts |
|--------|----------|
| [Executors and Edges](./Foundational/01_ExecutorsAndEdges) | Minimal workflow with basic executors and edges |
| [Streaming](./Foundational/02_Streaming) | Extends workflows with event streaming |
| [Agents](./Foundational/03_AgentsInWorkflows) | Use agents in workflows |
Once completed, please proceed to other samples listed below.
> Note that you don't need to follow a strict order after the foundational samples. However, some samples build upon concepts from previous ones, so it's beneficial to be aware of the dependencies.
### Concurrent Execution
| Sample | Concepts |
|--------|----------|
| [Fan-Out and Fan-In](./Concurrent) | Introduces parallel processing with fan-out and fan-in patterns |
### Workflow Shared States
| Sample | Concepts |
|--------|----------|
| [Shared States](./SharedStates) | Demonstrates shared states between executors for data sharing and coordination |
### Conditional Edges
| Sample | Concepts |
|--------|----------|
| [Edge Conditions](./ConditionalEdges/01_EdgeCondition) | Introduces conditional edges for dynamic routing based on executor outputs |
| [Switch-Case Routing](./ConditionalEdges/02_SwitchCase) | Extends conditional edges with switch-case routing for multiple paths |
| [Multi-Selection Routing](./ConditionalEdges/03_MultiSelection) | Demonstrates multi-selection routing where one executor can trigger multiple downstream executors |
> These 3 samples build upon each other. It's recommended to explore them in sequence to fully grasp the concepts.
@@ -0,0 +1,246 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
namespace WorkflowCustomAgentExecutorsSample;
/// <summary>
/// This sample demonstrates how to create custom executors for AI agents.
/// This is useful when you want more control over the agent's behaviors in a workflow.
///
/// In this example, we create two custom executors:
/// 1. SloganWriterExecutor: An AI agent that generates slogans based on a given task.
/// 2. FeedbackExecutor: An AI agent that provides feedback on the generated slogans.
/// (These two executors manage the agent instances and their conversation threads.)
///
/// The workflow alternates between these two executors until the slogan meets a certain
/// quality threshold or a maximum number of attempts is reached.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// - An Azure OpenAI chat completion deployment that supports structured outputs must be configured.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Set up the Azure OpenAI client
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
// Create the executors
var sloganWriter = new SloganWriterExecutor(chatClient);
var feedbackProvider = new FeedbackExecutor(chatClient);
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(sloganWriter);
builder.AddEdge(sloganWriter, feedbackProvider);
builder.AddEdge(feedbackProvider, sloganWriter);
var workflow = builder.Build<string>();
// Execute the workflow
var ask = "Create a slogan for a new electric SUV that is affordable and fun to drive.";
StreamingRun run = await InProcessExecution.StreamAsync(workflow, ask);
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is SloganGeneratedEvent || evt is FeedbackEvent)
{
// Custom events to allow us to monitor the progress of the workflow.
Console.WriteLine($"{evt}");
}
if (evt is WorkflowCompletedEvent completedEvent)
{
Console.WriteLine($"{completedEvent}");
}
}
}
}
/// <summary>
/// A class representing the output of the slogan writer agent.
/// </summary>
public sealed class SloganResult
{
[JsonPropertyName("task")]
public required string Task { get; set; }
[JsonPropertyName("slogan")]
public required string Slogan { get; set; }
}
/// <summary>
/// A class representing the output of the feedback agent.
/// </summary>
public sealed class FeedbackResult
{
[JsonPropertyName("comments")]
public string Comments { get; set; } = string.Empty;
[JsonPropertyName("rating")]
public int Rating { get; set; }
[JsonPropertyName("actions")]
public string Actions { get; set; } = string.Empty;
}
/// <summary>
/// A custom event to indicate that a slogan has been generated.
/// </summary>
internal sealed class SloganGeneratedEvent(SloganResult sloganResult) : WorkflowEvent(sloganResult)
{
public override string ToString() => $"Slogan: {sloganResult.Slogan}";
}
/// <summary>
/// A custom executor that uses an AI agent to generate slogans based on a given task.
/// Note that this executor has two message handlers:
/// 1. HandleAsync(string message): Handles the initial task to create a slogan.
/// 2. HandleAsync(Feedback message): Handles feedback to improve the slogan.
/// </summary>
internal sealed class SloganWriterExecutor
: ReflectingExecutor<SloganWriterExecutor>,
IMessageHandler<string, SloganResult>,
IMessageHandler<FeedbackResult, SloganResult>
{
private const string Instruction = """
You are a professional slogan writer. You will be given a task to create a slogan.
""";
private readonly AIAgent _agent;
private readonly AgentThread _thread;
/// <summary>
/// Initializes a new instance of the <see cref="SloganWriterExecutor"/> class.
/// </summary>
/// <param name="chatClient">The chat client to use for the AI agent.</param>
public SloganWriterExecutor(IChatClient chatClient)
{
var agentOptions = new ChatClientAgentOptions(instructions: Instruction)
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(SloganResult))
)
}
};
this._agent = new ChatClientAgent(chatClient, agentOptions);
this._thread = this._agent.GetNewThread();
}
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context)
{
var result = await this._agent.RunAsync(message, this._thread);
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
return sloganResult;
}
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context)
{
var feedbackMessage = $"""
Here is the feedback on your previous slogan:
Comments: {message.Comments}
Rating: {message.Rating}
Suggested Actions: {message.Actions}
Please use this feedback to improve your slogan.
""";
var result = await this._agent.RunAsync(feedbackMessage, this._thread);
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
return sloganResult;
}
}
/// <summary>
/// A custom event to indicate that feedback has been provided.
/// </summary>
internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEvent(feedbackResult)
{
private readonly JsonSerializerOptions _options = new() { WriteIndented = true };
public override string ToString() => $"Feedback:\n{JsonSerializer.Serialize(feedbackResult, this._options)}";
}
/// <summary>
/// A custom executor that uses an AI agent to provide feedback on a slogan.
/// </summary>
internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, IMessageHandler<SloganResult>
{
private const string Instruction = """
You are a professional editor. You will be given a slogan and the task it is meant to accomplish.
""";
private readonly AIAgent _agent;
private readonly AgentThread _thread;
public int MinimumRating { get; init; } = 8;
public int MaxAttempts { get; init; } = 3;
private int _attempts = 0;
/// <summary>
/// Initializes a new instance of the <see cref="FeedbackExecutor"/> class.
/// </summary>
/// <param name="chatClient">The chat client to use for the AI agent.</param>
public FeedbackExecutor(IChatClient chatClient)
{
var agentOptions = new ChatClientAgentOptions(instructions: Instruction)
{
ChatOptions = new()
{
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
schema: AIJsonUtilities.CreateJsonSchema(typeof(FeedbackResult))
)
}
};
this._agent = new ChatClientAgent(chatClient, agentOptions);
this._thread = this._agent.GetNewThread();
}
public async ValueTask HandleAsync(SloganResult message, IWorkflowContext context)
{
var sloganMessage = $"""
Here is a slogan for the task '{message.Task}':
Slogan: {message.Slogan}
Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
""";
var response = await this._agent.RunAsync(sloganMessage, this._thread);
var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
await context.AddEventAsync(new FeedbackEvent(feedback));
if (feedback.Rating >= this.MinimumRating)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"The following slogan was accepted:\n\n{message.Slogan}"));
return;
}
if (this._attempts >= this.MaxAttempts)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}"));
return;
}
await context.SendMessageAsync(feedback);
this._attempts++;
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
namespace WorkflowFoundryAgentSample;
/// <summary>
/// This sample shows how to use Azure Foundry Agents within a workflow.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// - An Azure Foundry project endpoint and model id.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Set up the Azure OpenAI client
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_MODEL_ID") ?? "gpt-4o-mini";
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
// Create agents
AIAgent frenchAgent = await GetTranslationAgentAsync("French", persistentAgentsClient, model);
AIAgent spanishAgent = await GetTranslationAgentAsync("Spanish", persistentAgentsClient, model);
AIAgent englishAgent = await GetTranslationAgentAsync("English", persistentAgentsClient, model);
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(frenchAgent);
builder.AddEdge(frenchAgent, spanishAgent);
builder.AddEdge(spanishAgent, englishAgent);
var workflow = builder.Build<ChatMessage>();
// Execute the workflow
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
// Must send the turn token to trigger the agents.
// The agents are wrapped as executors. When they receive messages,
// they will cache the messages and only start processing when they receive a TurnToken.
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is AgentRunUpdateEvent executorComplete)
{
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
}
}
// Cleanup the agents created for the sample.
await persistentAgentsClient.Administration.DeleteAgentAsync(frenchAgent.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(spanishAgent.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(englishAgent.Id);
}
/// <summary>
/// Creates a translation agent for the specified target language.
/// </summary>
/// <param name="targetLanguage">The target language for translation</param>
/// <param name="persistentAgentsClient">The PersistentAgentsClient to create the agent</param>
/// <param name="model">The model to use for the agent</param>
/// <returns>A ChatClientAgent configured for the specified language</returns>
private static async Task<ChatClientAgent> GetTranslationAgentAsync(
string targetLanguage,
PersistentAgentsClient persistentAgentsClient,
string model)
{
string instructions = $"You are a translation assistant that translates the provided text to {targetLanguage}.";
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
model: model,
name: $"{targetLanguage} Translator",
instructions: instructions);
return await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
}
}
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
namespace WorkflowAsAnAgentsSample;
/// <summary>
/// This sample introduces the concepts workflows as agents, where a workflow can be
/// treated as an <see cref="AIAgent"/>. This allows you to interact with a workflow
/// as if it were a single agent.
///
/// In this example, we create a workflow that uses two language agents to process
/// input concurrently, one that responds in French and another that responds in English.
///
/// You will interact with the workflow in an interactive loop, sending messages and receiving
/// streaming responses from the workflow as if it were an agent who responds in both languages.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// - This sample uses concurrent processing.
/// - An Azure OpenAI endpoint and deployment name.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Set up the Azure OpenAI client
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
// Create the workflow and turn it into an agent
var workflow = WorkflowHelper.GetWorkflow(chatClient);
var agent = workflow.AsAgent("workflow-agent", "Workflow Agent");
var thread = agent.GetNewThread();
// Start an interactive loop to interact with the workflow as if it were an agent
while (true)
{
Console.WriteLine();
Console.Write("User (or 'exit' to quit): ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
await ProcessInputAsync(agent, thread, input);
}
// Helper method to process user input and display streaming responses. To display
// multiple interleaved responses correctly, we buffer updates by message ID and
// re-render all messages on each update.
static async Task ProcessInputAsync(AIAgent agent, AgentThread thread, string input)
{
Dictionary<string, List<AgentRunResponseUpdate>> buffer = [];
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread).ConfigureAwait(false))
{
if (update.MessageId == null)
{
// skip updates that don't have a message ID
continue;
}
Console.Clear();
if (!buffer.TryGetValue(update.MessageId, out List<AgentRunResponseUpdate>? value))
{
value = [];
buffer[update.MessageId] = value;
}
value.Add(update);
foreach (var (messageId, segments) in buffer)
{
string combinedText = string.Concat(segments);
Console.WriteLine($"{segments[0].AuthorName}: {combinedText}");
Console.WriteLine();
}
}
}
}
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
namespace WorkflowAsAnAgentsSample;
internal static class WorkflowHelper
{
/// <summary>
/// Creates a workflow that uses two language agents to process input concurrently.
/// </summary>
/// <param name="chatClient">The chat client to use for the agents</param>
/// <returns>A workflow that processes input using two language agents</returns>
internal static Workflow<List<ChatMessage>> GetWorkflow(IChatClient chatClient)
{
// Create executors
var startExecutor = new ConcurrentStartExecutor();
var aggregationExecutor = new ConcurrentAggregationExecutor();
AIAgent frenchAgent = GetLanguageAgent("French", chatClient);
AIAgent englishAgent = GetLanguageAgent("English", chatClient);
// Build the workflow by adding executors and connecting them
WorkflowBuilder builder = new(startExecutor);
builder.AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent]);
builder.AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent]);
return builder.Build<List<ChatMessage>>();
}
/// <summary>
/// Creates a language agent for the specified target language.
/// </summary>
/// <param name="targetLanguage">The target language for translation</param>
/// <param name="chatClient">The chat client to use for the agent</param>
/// <returns>A ChatClientAgent configured for the specified language</returns>
private static ChatClientAgent GetLanguageAgent(string targetLanguage, IChatClient chatClient)
{
string instructions = $"You're a helpful assistant who always responds in {targetLanguage}.";
return new ChatClientAgent(chatClient, instructions, name: $"{targetLanguage}Agent");
}
/// <summary>
/// Executor that starts the concurrent processing by sending messages to the agents.
/// </summary>
private sealed class ConcurrentStartExecutor() :
ReflectingExecutor<ConcurrentStartExecutor>("ConcurrentStartExecutor"),
IMessageHandler<List<ChatMessage>>
{
/// <summary>
/// Starts the concurrent processing by sending messages to the agents.
/// </summary>
/// <param name="message">The user message to process</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
public async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context)
{
// Broadcast the message to all connected agents. Receiving agents will queue
// the message but will not start processing until they receive a turn token.
await context.SendMessageAsync(message);
// Broadcast the turn token to kick off the agents.
await context.SendMessageAsync(new TurnToken(emitEvents: true));
}
}
/// <summary>
/// Executor that aggregates the results from the concurrent agents.
/// </summary>
private sealed class ConcurrentAggregationExecutor() :
ReflectingExecutor<ConcurrentAggregationExecutor>("ConcurrentAggregationExecutor"),
IMessageHandler<ChatMessage>
{
private readonly List<ChatMessage> _messages = [];
/// <summary>
/// Handles incoming messages from the agents and aggregates their responses.
/// </summary>
/// <param name="message">The message from the agent</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
{
this._messages.Add(message);
if (this._messages.Count == 2)
{
var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.Text}"));
await context.AddEventAsync(new WorkflowCompletedEvent(formattedMessages));
}
}
}
}
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
namespace WorkflowCheckpointAndRehydrateSample;
/// <summary>
/// This sample introduces the concepts of check points and shows how to save and restore
/// the state of a workflow using checkpoints.
/// This sample demonstrates checkpoints, which allow you to save and restore a workflow's state.
/// Key concepts:
/// - Super Steps: A workflow executes in stages called "super steps". Each super step runs
/// one or more executors and completes when all those executors finish their work.
/// - Checkpoints: The system automatically saves the workflow's state at the end of each
/// super step. You can use these checkpoints to resume the workflow from any saved point.
/// - Rehydration: You can rehydrate a new workflow instance from a saved checkpoint, allowing
/// you to continue execution from that point.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Create the workflow
var workflow = WorkflowHelper.GetWorkflow();
// Create checkpoint manager
var checkpointManager = new CheckpointManager();
var checkpoints = new List<CheckpointInfo>();
// Execute the workflow and save checkpoints
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
.ConfigureAwait(false);
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is ExecutorCompletedEvent executorCompletedEvt)
{
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
}
if (evt is SuperStepCompletedEvent superStepCompletedEvt)
{
// Checkpoints are automatically created at the end of each super step when a
// checkpoint manager is provided. You can store the checkpoint info for later use.
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
if (checkpoint != null)
{
checkpoints.Add(checkpoint);
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
}
}
if (evt is WorkflowCompletedEvent workflowCompletedEvt)
{
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
}
}
if (checkpoints.Count == 0)
{
throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
}
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
// Rehydrate a new workflow instance from a saved checkpoint and continue execution
var newWorkflow = WorkflowHelper.GetWorkflow();
var checkpointIndex = 5;
Console.WriteLine($"\n\nHydrating a new workflow instance from the {checkpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
Checkpointed<StreamingRun> newCheckpointedRun = await InProcessExecution
.StreamAsync(newWorkflow, NumberSignal.Init, checkpointManager)
.ConfigureAwait(false);
await newCheckpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
await foreach (WorkflowEvent evt in newCheckpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is ExecutorCompletedEvent executorCompletedEvt)
{
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
}
if (evt is WorkflowCompletedEvent workflowCompletedEvt)
{
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
}
}
}
}
@@ -0,0 +1,165 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
namespace WorkflowCheckpointAndRehydrateSample;
internal static class WorkflowHelper
{
/// <summary>
/// Get a workflow that plays a number guessing game with checkpointing support.
/// The workflow consists of two executors that are connected in a feedback loop:
/// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
/// The workflow continues until the correct number is guessed.
/// </summary>
internal static Workflow<NumberSignal> GetWorkflow()
{
// Create the executors
GuessNumberExecutor guessNumberExecutor = new(1, 100);
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(guessNumberExecutor)
.AddEdge(guessNumberExecutor, judgeExecutor)
.AddEdge(judgeExecutor, guessNumberExecutor)
.Build<NumberSignal>();
return workflow;
}
}
/// <summary>
/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
/// </summary>
internal enum NumberSignal
{
Init,
Above,
Below,
}
/// <summary>
/// Executor that makes a guess based on the current bounds.
/// </summary>
internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExecutor>("Guess"), IMessageHandler<NumberSignal>
{
/// <summary>
/// The lower bound of the guessing range.
/// </summary>
public int LowerBound { get; private set; }
/// <summary>
/// The upper bound of the guessing range.
/// </summary>
public int UpperBound { get; private set; }
private const string StateKey = "GuessNumberExecutorState";
/// <summary>
/// Initializes a new instance of the <see cref="GuessNumberExecutor"/> class.
/// </summary>
/// <param name="lowerBound">The initial lower bound of the guessing range.</param>
/// <param name="upperBound">The initial upper bound of the guessing range.</param>
public GuessNumberExecutor(int lowerBound, int upperBound) : this()
{
this.LowerBound = lowerBound;
this.UpperBound = upperBound;
}
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
{
switch (message)
{
case NumberSignal.Init:
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
case NumberSignal.Above:
this.UpperBound = this.NextGuess - 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
case NumberSignal.Below:
this.LowerBound = this.NextGuess + 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
}
}
/// <summary>
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
}
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
}
}
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
private const string StateKey = "JudgeExecutorState";
/// <summary>
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
/// </summary>
/// <param name="targetNumber">The number to be guessed.</param>
public JudgeExecutor(int targetNumber) : this()
{
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
{
this._tries++;
if (message == this._targetNumber)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
.ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
}
}
/// <summary>
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, this._tries);
}
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
namespace WorkflowCheckpointAndResumeSample;
/// <summary>
/// This sample introduces the concepts of check points and shows how to save and restore
/// the state of a workflow using checkpoints.
/// This sample demonstrates checkpoints, which allow you to save and restore a workflow's state.
/// Key concepts:
/// - Super Steps: A workflow executes in stages called "super steps". Each super step runs
/// one or more executors and completes when all those executors finish their work.
/// - Checkpoints: The system automatically saves the workflow's state at the end of each
/// super step. You can use these checkpoints to resume the workflow from any saved point.
/// - Resume: If needed, you can restore a checkpoint and continue execution from that state.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Create the workflow
var workflow = WorkflowHelper.GetWorkflow();
// Create checkpoint manager
var checkpointManager = new CheckpointManager();
var checkpoints = new List<CheckpointInfo>();
// Execute the workflow and save checkpoints
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
.ConfigureAwait(false);
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is ExecutorCompletedEvent executorCompletedEvt)
{
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
}
if (evt is SuperStepCompletedEvent superStepCompletedEvt)
{
// Checkpoints are automatically created at the end of each super step when a
// checkpoint manager is provided. You can store the checkpoint info for later use.
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
if (checkpoint != null)
{
checkpoints.Add(checkpoint);
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
}
}
if (evt is WorkflowCompletedEvent workflowCompletedEvt)
{
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
}
}
if (checkpoints.Count == 0)
{
throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
}
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
// Restoring from a checkpoint and resuming execution
var checkpointIndex = 5;
Console.WriteLine($"\n\nRestoring from the {checkpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
// Note that we are restoring the state directly to the same run instance.
await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is ExecutorCompletedEvent executorCompletedEvt)
{
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
}
if (evt is WorkflowCompletedEvent workflowCompletedEvt)
{
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
}
}
}
}
@@ -0,0 +1,165 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
namespace WorkflowCheckpointAndResumeSample;
internal static class WorkflowHelper
{
/// <summary>
/// Get a workflow that plays a number guessing game with checkpointing support.
/// The workflow consists of two executors that are connected in a feedback loop:
/// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
/// The workflow continues until the correct number is guessed.
/// </summary>
internal static Workflow<NumberSignal> GetWorkflow()
{
// Create the executors
GuessNumberExecutor guessNumberExecutor = new(1, 100);
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(guessNumberExecutor)
.AddEdge(guessNumberExecutor, judgeExecutor)
.AddEdge(judgeExecutor, guessNumberExecutor)
.Build<NumberSignal>();
return workflow;
}
}
/// <summary>
/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
/// </summary>
internal enum NumberSignal
{
Init,
Above,
Below,
}
/// <summary>
/// Executor that makes a guess based on the current bounds.
/// </summary>
internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExecutor>("Guess"), IMessageHandler<NumberSignal>
{
/// <summary>
/// The lower bound of the guessing range.
/// </summary>
public int LowerBound { get; private set; }
/// <summary>
/// The upper bound of the guessing range.
/// </summary>
public int UpperBound { get; private set; }
private const string StateKey = "GuessNumberExecutorState";
/// <summary>
/// Initializes a new instance of the <see cref="GuessNumberExecutor"/> class.
/// </summary>
/// <param name="lowerBound">The initial lower bound of the guessing range.</param>
/// <param name="upperBound">The initial upper bound of the guessing range.</param>
public GuessNumberExecutor(int lowerBound, int upperBound) : this()
{
this.LowerBound = lowerBound;
this.UpperBound = upperBound;
}
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
{
switch (message)
{
case NumberSignal.Init:
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
case NumberSignal.Above:
this.UpperBound = this.NextGuess - 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
case NumberSignal.Below:
this.LowerBound = this.NextGuess + 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
}
}
/// <summary>
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
}
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
}
}
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
private const string StateKey = "JudgeExecutorState";
/// <summary>
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
/// </summary>
/// <param name="targetNumber">The number to be guessed.</param>
public JudgeExecutor(int targetNumber) : this()
{
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
{
this._tries++;
if (message == this._targetNumber)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
.ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
}
}
/// <summary>
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, this._tries);
}
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
}
}
@@ -0,0 +1,138 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
namespace WorkflowCheckpointWithHumanInTheLoopSample;
/// <summary>
/// This sample demonstrates how to create a workflow with human-in-the-loop interaction and
/// checkpointing support. The workflow plays a number guessing game where the user provides
/// guesses based on feedback from the workflow. The workflow state is checkpointed at the end
/// of each super step, allowing it to be restored and resumed later.
/// Each InputPort request and response cycle takes two super steps:
/// 1. The InputPort sends a RequestInfoEvent to request input from the external world.
/// 2. The external world sends a response back to the InputPort.
/// Thus, two checkpoints are created for each human-in-the-loop interaction.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// - This sample builds upon the HumanInTheLoopBasic sample. It's recommended to go through that
/// sample first to understand the basics of human-in-the-loop workflows.
/// - This sample also builds upon the CheckpointAndResume sample. It's recommended to
/// go through that sample first to understand the basics of checkpointing and resuming workflows.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Create the workflow
var workflow = WorkflowHelper.GetWorkflow();
// Create checkpoint manager
var checkpointManager = new CheckpointManager();
var checkpoints = new List<CheckpointInfo>();
// Execute the workflow and save checkpoints
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
.StreamAsync(workflow, new SignalWithNumber(NumberSignal.Init), checkpointManager)
.ConfigureAwait(false);
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
{
switch (evt)
{
case RequestInfoEvent requestInputEvt:
// Handle `RequestInfoEvent` from the workflow
ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
await checkpointedRun.Run.SendResponseAsync(response).ConfigureAwait(false);
break;
case ExecutorCompletedEvent executorCompletedEvt:
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
break;
case SuperStepCompletedEvent superStepCompletedEvt:
// Checkpoints are automatically created at the end of each super step when a
// checkpoint manager is provided. You can store the checkpoint info for later use.
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
if (checkpoint != null)
{
checkpoints.Add(checkpoint);
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
}
break;
case WorkflowCompletedEvent workflowCompletedEvt:
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
break;
}
}
if (checkpoints.Count == 0)
{
throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
}
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
// Restoring from a checkpoint and resuming execution
var checkpointIndex = 1;
Console.WriteLine($"\n\nRestoring from the {checkpointIndex + 1}th checkpoint.");
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
// Note that we are restoring the state directly to the same run instance.
await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
{
switch (evt)
{
case RequestInfoEvent requestInputEvt:
// Handle `RequestInfoEvent` from the workflow
ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
await checkpointedRun.Run.SendResponseAsync(response).ConfigureAwait(false);
break;
case ExecutorCompletedEvent executorCompletedEvt:
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
break;
case WorkflowCompletedEvent workflowCompletedEvt:
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
break;
}
}
}
private static ExternalResponse HandleExternalRequest(ExternalRequest request)
{
if (request.Port.Request == typeof(SignalWithNumber))
{
var signal = (SignalWithNumber)request.Data;
switch (signal.Signal)
{
case NumberSignal.Init:
int initialGuess = ReadIntegerFromConsole("Please provide your initial guess: ");
return request.CreateResponse<int>(initialGuess);
case NumberSignal.Above:
int lowerGuess = ReadIntegerFromConsole($"You previously guessed {signal.Number} too large. Please provide a new guess: ");
return request.CreateResponse<int>(lowerGuess);
case NumberSignal.Below:
int higherGuess = ReadIntegerFromConsole($"You previously guessed {signal.Number} too small. Please provide a new guess: ");
return request.CreateResponse<int>(higherGuess);
}
}
throw new NotSupportedException($"Request {request.Port.Request} is not supported");
}
private static int ReadIntegerFromConsole(string prompt)
{
while (true)
{
Console.Write(prompt);
string? input = Console.ReadLine();
if (int.TryParse(input, out int value))
{
return value;
}
Console.WriteLine("Invalid input. Please enter a valid integer.");
}
}
}
@@ -0,0 +1,110 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
namespace WorkflowCheckpointWithHumanInTheLoopSample;
internal static class WorkflowHelper
{
/// <summary>
/// Get a workflow that plays a number guessing game with human-in-the-loop interaction.
/// An input port allows the external world to provide inputs to the workflow upon requests.
/// </summary>
internal static Workflow<SignalWithNumber> GetWorkflow()
{
// Create the executors
InputPort numberInputPort = InputPort.Create<SignalWithNumber, int>("GuessNumber");
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(numberInputPort)
.AddEdge(numberInputPort, judgeExecutor)
.AddEdge(judgeExecutor, numberInputPort)
.Build<SignalWithNumber>();
return workflow;
}
}
/// <summary>
/// Signals indicating if the guess was too high, too low, or an initial guess.
/// </summary>
internal enum NumberSignal
{
Init,
Above,
Below,
}
/// <summary>
/// Signals used for communication between guesses and the JudgeExecutor.
/// </summary>
internal sealed class SignalWithNumber
{
public NumberSignal Signal { get; }
public int? Number { get; }
public SignalWithNumber(NumberSignal signal, int? number = null)
{
this.Signal = signal;
this.Number = number;
}
}
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
private const string StateKey = "JudgeExecutorState";
/// <summary>
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
/// </summary>
/// <param name="targetNumber">The number to be guessed.</param>
public JudgeExecutor(int targetNumber) : this()
{
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
{
this._tries++;
if (message == this._targetNumber)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
.ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Below, message)).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Above, message)).ConfigureAwait(false);
}
}
/// <summary>
/// Checkpoint the current state of the executor.
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
return context.QueueStateUpdateAsync(StateKey, this._tries);
}
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
{
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
}
}
@@ -85,7 +85,7 @@ internal sealed class ConcurrentStartExecutor() :
/// </summary>
/// <param name="message">The user message to process</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <returns></returns>
/// <returns>A task representing the asynchronous operation</returns>
public async ValueTask HandleAsync(string message, IWorkflowContext context)
{
// Broadcast the message to all connected agents. Receiving agents will queue
@@ -110,7 +110,7 @@ internal sealed class ConcurrentAggregationExecutor() :
/// </summary>
/// <param name="message">The message from the agent</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <returns></returns>
/// <returns>A task representing the asynchronous operation</returns>
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
{
this._messages.Add(message);
@@ -26,8 +26,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.Workflows.Declarative\Microsoft.Agents.Workflows.Declarative.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.Workflows.Declarative\Microsoft.Agents.Workflows.Declarative.csproj" />
</ItemGroup>
</Project>
@@ -17,7 +17,7 @@ using Microsoft.Extensions.Configuration;
namespace Demo.DeclarativeWorkflow;
/// <summary>
/// HOW TO: Create a workflow from a declartive (yaml based) definition.
/// HOW TO: Create a workflow from a declarative (yaml based) definition.
/// </summary>
/// <remarks>
/// <b>Configuration</b>
@@ -92,13 +92,21 @@ internal sealed class Program
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is ExecutorInvokeEvent executorInvoked)
if (evt is ExecutorInvokedEvent executorInvoked)
{
Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}");
Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
}
else if (evt is ExecutorCompleteEvent executorComplete)
else if (evt is ExecutorCompletedEvent executorCompleted)
{
Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
}
if (evt is DeclarativeActionInvokeEvent actionInvoked)
{
Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]");
}
else if (evt is DeclarativeActionCompleteEvent actionComplete)
{
Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]");
}
else if (evt is ExecutorFailureEvent executorFailure)
{
@@ -21,10 +21,10 @@ $env:FOUNDRY_PROJECT_ENDPOINT="https://..."
To set your secrets with .NET Secret Manager:
1. From the root of the respository, navigate the console to the project folder:
1. From the root of the repository, navigate the console to the project folder:
```
cd dotnet/demos/DeclarativeWorkflow
cd dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow
```
2. Examine existing secret definitions:
@@ -49,16 +49,16 @@ To set your secrets with .NET Secret Manager:
Use [_Azure CLI_](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) to authorize access to your Azure Foundry Project:
```
az login
az account get-access-token
```
```
az login
az account get-access-token
```
#### Agents
The sample workflows rely on agents defined in your Azure Foundry Project.
To create agents, run the [`Create.ps1`](../../../workflows/) script.
To create agents, run the [`Create.ps1`](../../../../../workflows/) script.
This will create the agents used in the sample workflows in your Azure Foundry Project and format a script you can copy and use to configure your environment.
> Note: `Create.ps1` relies upon the `FOUNDRY_PROJECT_ENDPOINT` setting.
@@ -66,13 +66,12 @@ This will create the agents used in the sample workflows in your Azure Foundry P
## Execution
Run the demo from the console by specifying a path to a declarative (YAML) workflow file.
The repository has example workflows available in the root [`/workflows`](../../../workflows) folder.
The repository has example workflows available in the root [`/workflows`](../../../../../workflows) folder.
1. From the root of the respository, navigate the console to the project folder:
1. From the root of the repository, navigate the console to the project folder:
```sh
cd dotnet/demos/DeclarativeWorkflow
cd dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow
```
2. Run the demo referencing a sample workflow by name:
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
namespace WorkflowHumanInTheLoopBasicSample;
/// <summary>
/// This sample introduces the concept of InputPort and ExternalRequest to enable
/// human-in-the-loop interaction scenarios.
/// An input port can be used as if it were an executor in the workflow graph. Upon receiving
/// a message, the input port generates an RequestInfoEvent that gets emitted to the external world.
/// The external world can then respond to the request by sending an ExternalResponse back to
/// the workflow.
/// The sample implements a simple number guessing game where the external user tries to guess
/// a pre-defined target number. The workflow consists of a single JudgeExecutor that judges
/// the user's guesses and provides feedback.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Create the workflow
var workflow = WorkflowHelper.GetWorkflow();
// Execute the workflow
StreamingRun handle = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(false))
{
switch (evt)
{
case RequestInfoEvent requestInputEvt:
// Handle `RequestInfoEvent` from the workflow
ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
await handle.SendResponseAsync(response).ConfigureAwait(false);
break;
case WorkflowCompletedEvent workflowCompleteEvt:
// The workflow has completed successfully
Console.WriteLine($"Workflow completed with result: {workflowCompleteEvt.Data}");
return;
}
}
}
private static ExternalResponse HandleExternalRequest(ExternalRequest request)
{
if (request.Port.Request == typeof(NumberSignal))
{
var signal = (NumberSignal)request.Data;
switch (signal)
{
case NumberSignal.Init:
int initialGuess = ReadIntegerFromConsole("Please provide your initial guess: ");
return request.CreateResponse<int>(initialGuess);
case NumberSignal.Above:
int lowerGuess = ReadIntegerFromConsole("You previously guessed too large. Please provide a new guess: ");
return request.CreateResponse<int>(lowerGuess);
case NumberSignal.Below:
int higherGuess = ReadIntegerFromConsole("You previously guessed too small. Please provide a new guess: ");
return request.CreateResponse<int>(higherGuess);
}
}
throw new NotSupportedException($"Request {request.Port.Request} is not supported");
}
private static int ReadIntegerFromConsole(string prompt)
{
while (true)
{
Console.Write(prompt);
string? input = Console.ReadLine();
if (int.TryParse(input, out int value))
{
return value;
}
Console.WriteLine("Invalid input. Please enter a valid integer.");
}
}
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
namespace WorkflowHumanInTheLoopBasicSample;
internal static class WorkflowHelper
{
/// <summary>
/// Get a workflow that plays a number guessing game with human-in-the-loop interaction.
/// An input port allows the external world to provide inputs to the workflow upon requests.
/// </summary>
internal static Workflow<NumberSignal> GetWorkflow()
{
// Create the executors
InputPort numberInputPort = InputPort.Create<NumberSignal, int>("GuessNumber");
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(numberInputPort)
.AddEdge(numberInputPort, judgeExecutor)
.AddEdge(judgeExecutor, numberInputPort)
.Build<NumberSignal>();
return workflow;
}
}
/// <summary>
/// Signals used for communication between guesses and the JudgeExecutor.
/// </summary>
internal enum NumberSignal
{
Init,
Above,
Below,
}
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
/// <summary>
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
/// </summary>
/// <param name="targetNumber">The number to be guessed.</param>
public JudgeExecutor(int targetNumber) : this()
{
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
{
this._tries++;
if (message == this._targetNumber)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
.ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Reflection;
namespace WorkflowLoopSample;
/// <summary>
/// This sample demonstrates a simple number guessing game using a workflow with looping behavior.
///
/// The workflow consists of two executors that are connected in a feedback loop:
/// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
/// The workflow continues until the correct number is guessed.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Create the executors
GuessNumberExecutor guessNumberExecutor = new(1, 100);
JudgeExecutor judgeExecutor = new(42);
// Build the workflow by connecting executors in a loop
var workflow = new WorkflowBuilder(guessNumberExecutor)
.AddEdge(guessNumberExecutor, judgeExecutor)
.AddEdge(judgeExecutor, guessNumberExecutor)
.Build<NumberSignal>();
// Execute the workflow
StreamingRun run = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is WorkflowCompletedEvent workflowCompleteEvt)
{
Console.WriteLine($"Result: {workflowCompleteEvt}");
}
}
}
}
/// <summary>
/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
/// </summary>
internal enum NumberSignal
{
Init,
Above,
Below,
}
/// <summary>
/// Executor that makes a guess based on the current bounds.
/// </summary>
internal sealed class GuessNumberExecutor : ReflectingExecutor<GuessNumberExecutor>, IMessageHandler<NumberSignal>
{
/// <summary>
/// The lower bound of the guessing range.
/// </summary>
public int LowerBound { get; private set; }
/// <summary>
/// The upper bound of the guessing range.
/// </summary>
public int UpperBound { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="GuessNumberExecutor"/> class.
/// </summary>
/// <param name="lowerBound">The initial lower bound of the guessing range.</param>
/// <param name="upperBound">The initial upper bound of the guessing range.</param>
public GuessNumberExecutor(int lowerBound, int upperBound)
{
this.LowerBound = lowerBound;
this.UpperBound = upperBound;
}
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
{
switch (message)
{
case NumberSignal.Init:
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
case NumberSignal.Above:
this.UpperBound = this.NextGuess - 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
case NumberSignal.Below:
this.LowerBound = this.NextGuess + 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
break;
}
}
}
/// <summary>
/// Executor that judges the guess and provides feedback.
/// </summary>
internal sealed class JudgeExecutor : ReflectingExecutor<JudgeExecutor>, IMessageHandler<int>
{
private readonly int _targetNumber;
private int _tries = 0;
/// <summary>
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
/// </summary>
/// <param name="targetNumber">The number to be guessed.</param>
public JudgeExecutor(int targetNumber)
{
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
{
this._tries++;
if (message == this._targetNumber)
{
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
.ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,77 @@
# Workflow Getting Started Samples
The getting started with workflow samples demonstrate the fundamental concepts and functionalities of workflows in Agent Framework.
## Samples Overview
### Foundational Concepts - Start Here
Please begin with the [Foundational](./_Foundational) samples in order. These three samples introduce the core concepts of executors, edges, agents in workflows, streaming, and workflow construction.
> The folder name starts with an underscore (`_Foundational`) to ensure it appears first in the explorer view.
| Sample | Concepts |
|--------|----------|
| [Executors and Edges](./_Foundational/01_ExecutorsAndEdges) | Minimal workflow with basic executors and edges |
| [Streaming](./_Foundational/02_Streaming) | Extends workflows with event streaming |
| [Agents](./_Foundational/03_AgentsInWorkflows) | Use agents in workflows |
Once completed, please proceed to other samples listed below.
> Note that you don't need to follow a strict order after the foundational samples. However, some samples build upon concepts from previous ones, so it's beneficial to be aware of the dependencies.
### Agents
| Sample | Concepts |
|--------|----------|
| [Foundry Agents in Workflows](./Agents/FoundryAgent) | Demonstrates using Azure Foundry Agents within a workflow |
| [Custom Agent Executors](./Agents/CustomAgentExecutors) | Shows how to create a custom agent executor for more complex scenarios |
| [Workflow as an Agent](./Agents/WorkflowAsAgent) | Illustrates how to encapsulate a workflow as an agent |
### Concurrent Execution
| Sample | Concepts |
|--------|----------|
| [Fan-Out and Fan-In](./Concurrent) | Introduces parallel processing with fan-out and fan-in patterns |
### Loop
| Sample | Concepts |
|--------|----------|
| [Looping](./Loop) | Shows how to create a loop within a workflow |
### Workflow Shared States
| Sample | Concepts |
|--------|----------|
| [Shared States](./SharedStates) | Demonstrates shared states between executors for data sharing and coordination |
### Conditional Edges
| Sample | Concepts |
|--------|----------|
| [Edge Conditions](./ConditionalEdges/01_EdgeCondition) | Introduces conditional edges for dynamic routing based on executor outputs |
| [Switch-Case Routing](./ConditionalEdges/02_SwitchCase) | Extends conditional edges with switch-case routing for multiple paths |
| [Multi-Selection Routing](./ConditionalEdges/03_MultiSelection) | Demonstrates multi-selection routing where one executor can trigger multiple downstream executors |
> These 3 samples build upon each other. It's recommended to explore them in sequence to fully grasp the concepts.
### Declarative Workflows
| Sample | Concepts |
|--------|----------|
| [DeclarativeWorkflow](./DeclarativeWorkflow) | Demonstrates execution of declartive workflows. |
### Checkpointing
| Sample | Concepts |
|--------|----------|
| [Checkpoint and Resume](./Checkpoint/CheckpointAndResume) | Introduces checkpoints for saving and restoring workflow state for time travel purposes |
| [Checkpoint and Rehydrate](./Checkpoint/CheckpointAndRehydrate) | Demonstrates hydrating a new workflow instance from a saved checkpoint |
| [Checkpoint with Human-in-the-Loop](./Checkpoint/CheckpointWithHumanInTheLoop) | Combines checkpointing with human-in-the-loop interactions |
### Human-in-the-Loop
| Sample | Concepts |
|--------|----------|
| [Basic Human-in-the-Loop](./HumanInTheLoop/HumanIntheLoopBasic) | Introduces human-in-the-loop interaction using input ports and external requests |
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -35,7 +35,7 @@ public static class Program
Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
foreach (WorkflowEvent evt in run.NewEvents)
{
if (evt is ExecutorCompleteEvent executorComplete)
if (evt is ExecutorCompletedEvent executorComplete)
{
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -34,9 +34,9 @@ public static class Program
StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Hello, World!");
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is ExecutorCompleteEvent executorComplete)
if (evt is ExecutorCompletedEvent executorCompleted)
{
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
Console.WriteLine($"{executorCompleted.ExecutorId}: {executorCompleted.Data}");
}
}
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More