mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
348ac764e6 | ||
|
|
e8243b7d11 | ||
|
|
4b0f724e62 | ||
|
+20 |
edb367a2b9 | ||
|
|
4bffe1ebc8 | ||
|
|
406a8560c6 | ||
|
|
c1830c20c9 | ||
|
|
ef60f38084 | ||
|
|
c8bb3d1835 | ||
|
|
caf3c4b92d | ||
|
|
863a632ccf | ||
|
|
8d8c94b312 | ||
|
|
180f82373b | ||
|
|
d59bd20765 | ||
|
|
1d7292fba6 | ||
|
|
cd9073aa11 | ||
|
|
5fd2a0c287 | ||
|
|
b565b25b04 | ||
|
|
24298cd89e | ||
|
|
ad2ebfc0c8 | ||
|
|
a39e6561fd | ||
|
|
69dd532cd4 | ||
|
|
cf7c9fce40 | ||
|
|
04b662543c | ||
|
|
32bd884bfd | ||
|
|
93ab43d788 | ||
|
|
177b0c95be | ||
|
|
297d9d7fb3 | ||
|
|
d3827e8c11 | ||
|
|
448aff536a | ||
|
|
12fc19b360 | ||
|
|
7a45929807 | ||
|
|
c0c12df851 | ||
|
|
6cc0e2a0d8 | ||
|
|
01f3a3d881 | ||
|
|
f41e103ee2 | ||
|
|
866b4198bf | ||
|
|
42da3cb6a4 | ||
|
|
e7224b5efb | ||
|
|
548e0f028e | ||
|
|
45dc0ff073 | ||
|
|
0e7183dbd8 |
@@ -8,11 +8,11 @@ name: dotnet-build-and-test
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ["main", "feature*"]
|
||||
merge_group:
|
||||
branches: ["main"]
|
||||
branches: ["main", "feature*"]
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ["main", "feature*"]
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # Run at midnight UTC daily
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
- name: Pytest coverage comment
|
||||
id: coverageComment
|
||||
uses: MishaKav/pytest-coverage-comment@v1.1.57
|
||||
uses: MishaKav/pytest-coverage-comment@v1.1.59
|
||||
with:
|
||||
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
|
||||
@@ -7,33 +7,33 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Aspire -->
|
||||
<AspireAppHostSdkVersion>9.5.2</AspireAppHostSdkVersion>
|
||||
<AspireAppHostSdkVersion>13.0.0</AspireAppHostSdkVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.5.1-preview.1.25502.11" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.9.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.435" />
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.7" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||
<!-- System.* -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.7.0" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.10" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.8.0" />
|
||||
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="System.Net.Http.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<!-- OpenTelemetry -->
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
|
||||
@@ -44,39 +44,39 @@
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
|
||||
<!-- Microsoft.AspNetCore.* -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.10.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.10.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.10.0-preview.1.25513.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.2-preview.1.25552.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.10.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="10.0.0-preview.1.25559.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.0-preview.1.25559.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
|
||||
<!-- Semantic Kernel -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.66.0" />
|
||||
<!-- Vector Stores -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.67.0-preview" />
|
||||
<!-- Semantic Kernel -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.67.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.67.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
|
||||
<!-- A2A -->
|
||||
@@ -86,7 +86,7 @@
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
<!-- Inference SDKs -->
|
||||
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.2" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.6" />
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
|
||||
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.6.0" />
|
||||
@@ -101,11 +101,11 @@
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="Moq" Version="[4.18.4]" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.66.0-beta" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.67.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.67.0-beta" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
@@ -115,7 +115,7 @@
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<!-- Toolset -->
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -135,7 +135,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="[4.14.0]" />
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="[4.14.1]" />
|
||||
<PackageReference Include="Roslynator.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AGUIClientServer/">
|
||||
<Project Path="samples/AGUIClientServer/AGUIClient/AGUIClient.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
<Project Path="samples/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
@@ -47,8 +48,7 @@
|
||||
<File Path="samples/GettingStarted/Agents/README.md" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step03.1_UsingFunctionTools/Agent_Step03.1_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Agent_Step03.2_UsingFunctionTools_FromOpenAPI.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
|
||||
@@ -58,20 +58,23 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step13_Memory/Agent_Step13_Memory.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step13_BackgroundResponsesWithToolsAndPersistence/Agent_Step13_BackgroundResponsesWithToolsAndPersistence.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step14_Middleware/Agent_Step14_Middleware.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step15_Plugins/Agent_Step15_Plugins.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Agent_Step16_ChatReduction.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Agent_Step17_BackgroundResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Agent_Step18_TextSearchRag.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Agent_Step19_Mem0Provider.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step20_BackgroundResponsesWithToolsAndPersistence/Agent_Step20_BackgroundResponsesWithToolsAndPersistence.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step21_ChatHistoryMemoryProvider/Agent_Step21_ChatHistoryMemoryProvider.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/DevUI/">
|
||||
<File Path="samples/GettingStarted/DevUI/README.md" />
|
||||
<Project Path="samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithMemory/">
|
||||
<File Path="samples/GettingStarted/AgentWithMemory/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/AgentWithMemory_Step02_MemoryUsingMem0.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/AgentWithMemory_Step03_CustomMemory.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" />
|
||||
@@ -80,7 +83,8 @@
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithRAG/">
|
||||
<File Path="samples/GettingStarted/AgentWithRAG/README.md" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/AgentWithRAG_Step02_ExternalDataSourceRAG.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/ModelContextProtocol/">
|
||||
<File Path="samples/GettingStarted/ModelContextProtocol/README.md" />
|
||||
@@ -155,10 +159,10 @@
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/07_MixedWorkflowAgentsAndExecutors.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/Catalog/">
|
||||
<Project Path="samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj" />
|
||||
<Project Path="samples/Catalog/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
|
||||
<Project Path="samples/Catalog/DeepResearchAgent/DeepResearchAgent.csproj" />
|
||||
<Folder Name="/Samples/HostedAgents/">
|
||||
<Project Path="samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj" />
|
||||
<Project Path="samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
|
||||
<Project Path="samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/">
|
||||
<File Path=".editorconfig" />
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251107.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251107.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251107.1</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251110.2</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251110.2</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251110.2</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>b9c3f1e1-2fb4-5g29-0e52-53e2b7g9gf21</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
[JsonSerializable(typeof(WeatherInfo))]
|
||||
[JsonSerializable(typeof(Recipe))]
|
||||
[JsonSerializable(typeof(Ingredient))]
|
||||
[JsonSerializable(typeof(RecipeResponse))]
|
||||
internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ChatClient = OpenAI.Chat.ChatClient;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
internal static class ChatClientAgentFactory
|
||||
{
|
||||
private static AzureOpenAIClient? s_azureOpenAIClient;
|
||||
private static string? s_deploymentName;
|
||||
|
||||
public static void Initialize(IConfiguration configuration)
|
||||
{
|
||||
string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
s_azureOpenAIClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential());
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateAgenticChat()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "AgenticChat",
|
||||
description: "A simple chat agent using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateBackendToolRendering()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "BackendToolRenderer",
|
||||
description: "An agent that can render backend tools using Azure OpenAI",
|
||||
tools: [AIFunctionFactory.Create(
|
||||
GetWeather,
|
||||
name: "get_weather",
|
||||
description: "Get the weather for a given location.",
|
||||
AGUIDojoServerSerializerContext.Default.Options)]);
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateHumanInTheLoop()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "HumanInTheLoopAgent",
|
||||
description: "An agent that involves human feedback in its decision-making process using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateToolBasedGenerativeUI()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "ToolBasedGenerativeUIAgent",
|
||||
description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static ChatClientAgent CreateAgenticUI()
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
return chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "AgenticUIAgent",
|
||||
description: "An agent that generates agentic user interfaces using Azure OpenAI");
|
||||
}
|
||||
|
||||
public static AIAgent CreateSharedState(JsonSerializerOptions options)
|
||||
{
|
||||
ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
|
||||
|
||||
var baseAgent = chatClient.AsIChatClient().CreateAIAgent(
|
||||
name: "SharedStateAgent",
|
||||
description: "An agent that demonstrates shared state patterns using Azure OpenAI");
|
||||
|
||||
return new SharedStateAgent(baseAgent, options);
|
||||
}
|
||||
|
||||
[Description("Get the weather for a given location.")]
|
||||
private static WeatherInfo GetWeather([Description("The location to get the weather for.")] string location) => new()
|
||||
{
|
||||
Temperature = 20,
|
||||
Conditions = "sunny",
|
||||
Humidity = 50,
|
||||
WindSpeed = 10,
|
||||
FeelsLike = 25
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
internal sealed class Ingredient
|
||||
{
|
||||
[JsonPropertyName("icon")]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public string Amount { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AGUIDojoServer;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.AspNetCore.HttpLogging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddHttpLogging(logging =>
|
||||
{
|
||||
logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody
|
||||
| HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody;
|
||||
logging.RequestBodyLogLimit = int.MaxValue;
|
||||
logging.ResponseBodyLogLimit = int.MaxValue;
|
||||
});
|
||||
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIDojoServerSerializerContext.Default));
|
||||
builder.Services.AddAGUI();
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
app.UseHttpLogging();
|
||||
|
||||
// Initialize the factory
|
||||
ChatClientAgentFactory.Initialize(app.Configuration);
|
||||
|
||||
// Map the AG-UI agent endpoints for different scenarios
|
||||
app.MapAGUI("/agentic_chat", ChatClientAgentFactory.CreateAgenticChat());
|
||||
|
||||
app.MapAGUI("/backend_tool_rendering", ChatClientAgentFactory.CreateBackendToolRendering());
|
||||
|
||||
app.MapAGUI("/human_in_the_loop", ChatClientAgentFactory.CreateHumanInTheLoop());
|
||||
|
||||
app.MapAGUI("/tool_based_generative_ui", ChatClientAgentFactory.CreateToolBasedGenerativeUI());
|
||||
|
||||
app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI());
|
||||
|
||||
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
|
||||
app.MapAGUI("/shared_state", ChatClientAgentFactory.CreateSharedState(jsonOptions.Value.SerializerOptions));
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
public partial class Program { }
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"AGUIDojoServer": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:5018"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
internal sealed class Recipe
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("skill_level")]
|
||||
public string SkillLevel { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("cooking_time")]
|
||||
public string CookingTime { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("special_preferences")]
|
||||
public List<string> SpecialPreferences { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("ingredients")]
|
||||
public List<Ingredient> Ingredients { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("instructions")]
|
||||
public List<string> Instructions { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
#pragma warning disable CA1812 // Used for the JsonSchema response format
|
||||
internal sealed class RecipeResponse
|
||||
#pragma warning restore CA1812
|
||||
{
|
||||
[JsonPropertyName("recipe")]
|
||||
public Recipe Recipe { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateSharedState")]
|
||||
internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
|
||||
!properties.TryGetValue("ag_ui_state", out JsonElement state))
|
||||
{
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
var firstRunOptions = new ChatClientAgentRunOptions
|
||||
{
|
||||
ChatOptions = chatRunOptions.ChatOptions.Clone(),
|
||||
AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
|
||||
ContinuationToken = chatRunOptions.ContinuationToken,
|
||||
ChatClientFactory = chatRunOptions.ChatClientFactory,
|
||||
};
|
||||
|
||||
// Configure JSON schema response format for structured state output
|
||||
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<RecipeResponse>(
|
||||
schemaName: "RecipeResponse",
|
||||
schemaDescription: "A response containing a recipe with title, skill level, cooking time, preferences, ingredients, and instructions");
|
||||
|
||||
ChatMessage stateUpdateMessage = new(
|
||||
ChatRole.System,
|
||||
[
|
||||
new TextContent("Here is the current state in JSON format:"),
|
||||
new TextContent(state.GetRawText()),
|
||||
new TextContent("The new state is:")
|
||||
]);
|
||||
|
||||
var firstRunMessages = messages.Append(stateUpdateMessage);
|
||||
|
||||
var allUpdates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
allUpdates.Add(update);
|
||||
|
||||
// Yield all non-text updates (tool calls, etc.)
|
||||
bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
|
||||
if (hasNonTextContent)
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
var response = allUpdates.ToAgentRunResponse();
|
||||
|
||||
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateSnapshot,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
|
||||
yield return new AgentRunResponseUpdate
|
||||
{
|
||||
Contents = [new DataContent(stateBytes, "application/json")]
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var secondRunMessages = messages.Concat(response.Messages).Append(
|
||||
new ChatMessage(
|
||||
ChatRole.System,
|
||||
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
|
||||
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoServer;
|
||||
|
||||
internal sealed class WeatherInfo
|
||||
{
|
||||
[JsonPropertyName("temperature")]
|
||||
public int Temperature { get; init; }
|
||||
|
||||
[JsonPropertyName("conditions")]
|
||||
public string Conditions { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("humidity")]
|
||||
public int Humidity { get; init; }
|
||||
|
||||
[JsonPropertyName("wind_speed")]
|
||||
public int WindSpeed { get; init; }
|
||||
|
||||
[JsonPropertyName("feelsLike")]
|
||||
public int FeelsLike { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -107,8 +107,8 @@ app.UseSwaggerUI(options => options.SwaggerEndpoint("/openapi/v1.json", "Agents
|
||||
app.UseExceptionHandler();
|
||||
|
||||
// attach a2a with simple message communication
|
||||
app.MapA2A(agentName: "pirate", path: "/a2a/pirate");
|
||||
app.MapA2A(agentName: "knights-and-knaves", path: "/a2a/knights-and-knaves", agentCard: new()
|
||||
app.MapA2A(pirateAgentBuilder, path: "/a2a/pirate");
|
||||
app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard: new()
|
||||
{
|
||||
Name = "Knights and Knaves",
|
||||
Description = "An agent that helps you solve the knights and knaves puzzle.",
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</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.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,23 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</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.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -142,11 +142,11 @@ You:
|
||||
Besides the Aspire Dashboard and the Application Insights native UI, you can also use Grafana to visualize the telemetry data in Application Insights. There are two tailored dashboards for you to get started quickly:
|
||||
|
||||
### Agent Overview dashboard
|
||||
Grafana Dashboard Gallery link: <https://aka.ms/amg/dash/af-agent>
|
||||
Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-agent>
|
||||

|
||||
|
||||
### Workflow Overview dashboard
|
||||
Grafana Dashboard Gallery link: <https://aka.ms/amg/dash/af-workflow>
|
||||
Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-workflow>
|
||||

|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Agent Framework Retrieval Augmented Generation (RAG)
|
||||
|
||||
These samples show how to create an agent with the Agent Framework that uses Memory to remember previous conversations or facts from previous conversations.
|
||||
|
||||
|Sample|Description|
|
||||
|---|---|
|
||||
|[Chat History memory](./AgentWithMemory_Step01_ChatHistoryMemory/)|This sample demonstrates how to enable an agent to remember messages from previous conversations.|
|
||||
|[Memory with MemoryStore](./AgentWithMemory_Step02_MemoryUsingMem0/)|This sample demonstrates how to create and run an agent that uses the Mem0 service to extract and retrieve individual memories.|
|
||||
|[Custom Memory Implementation](./AgentWithMemory_Step03_CustomMemory/)|This sample demonstrates how to create a custom memory component and attach it to an agent.|
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to use Qdrant to add retrieval augmented generation (RAG) capabilities to an AI agent.
|
||||
// This sample shows how to use Qdrant with a custom schema to add retrieval augmented generation (RAG) capabilities to an AI agent.
|
||||
// While the sample is using Qdrant, it can easily be replaced with any other vector store that implements the Microsoft.Extensions.VectorData abstractions.
|
||||
// The TextSearchProvider runs a search against the vector store before each model invocation and injects the results into the model context.
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to use TextSearchProvider to add retrieval augmented generation (RAG)
|
||||
// capabilities to an AI agent. The provider runs a search against an external knowledge base
|
||||
// capabilities to an AI agent. This shows a mock implementation of a search function,
|
||||
// which can be replaced with any custom search logic to query any external knowledge base.
|
||||
// The provider invokes the custom search function
|
||||
// before each model invocation and injects the results into the model context.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
@@ -5,4 +5,5 @@ These samples show how to create an agent with the Agent Framework that uses Ret
|
||||
|Sample|Description|
|
||||
|---|---|
|
||||
|[Basic Text RAG](./AgentWithRAG_Step01_BasicTextRAG/)|This sample demonstrates how to create and run a basic agent with simple text Retrieval Augmented Generation (RAG).|
|
||||
|[RAG with external Vector Store and custom schema](./AgentWithRAG_Step02_ExternalDataSourceRAG/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with an external vector store. It also uses a custom schema for the documents stored in the vector store.|
|
||||
|[RAG with Vector Store and custom schema](./AgentWithRAG_Step02_CustomVectorStoreRAG/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with a vector store. It also uses a custom schema for the documents stored in the vector store.|
|
||||
|[RAG with custom RAG data source](./AgentWithRAG_Step03_CustomRAGDataSource/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with a custom RAG data source.|
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Plugins.OpenApi" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="OpenAPISpec.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
-354
@@ -1,354 +0,0 @@
|
||||
{
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "Github Versions API",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.github.com"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"basic-error": {
|
||||
"title": "Basic Error",
|
||||
"description": "Basic Error",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"documentation_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"title": "Label",
|
||||
"description": "Color-coded labels help you categorize and filter your issues (just like labels in Gmail).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique identifier for the label.",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"example": 208045946
|
||||
},
|
||||
"node_id": {
|
||||
"type": "string",
|
||||
"example": "MDU6TGFiZWwyMDgwNDU5NDY="
|
||||
},
|
||||
"url": {
|
||||
"description": "URL for the label",
|
||||
"example": "https://api.github.com/repositories/42/labels/bug",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the label.",
|
||||
"example": "bug",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Optional description of the label, such as its purpose.",
|
||||
"type": "string",
|
||||
"example": "Something isn't working",
|
||||
"nullable": true
|
||||
},
|
||||
"color": {
|
||||
"description": "6-character hex code, without the leading #, identifying the color",
|
||||
"example": "FFFFFF",
|
||||
"type": "string"
|
||||
},
|
||||
"default": {
|
||||
"description": "Whether this label comes by default in a new repository.",
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"node_id",
|
||||
"url",
|
||||
"name",
|
||||
"description",
|
||||
"color",
|
||||
"default"
|
||||
]
|
||||
},
|
||||
"tag": {
|
||||
"title": "Tag",
|
||||
"description": "Tag",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "v0.1"
|
||||
},
|
||||
"commit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sha": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sha",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"zipball_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"example": "https://github.com/octocat/Hello-World/zipball/v0.1"
|
||||
},
|
||||
"tarball_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"example": "https://github.com/octocat/Hello-World/tarball/v0.1"
|
||||
},
|
||||
"node_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"node_id",
|
||||
"commit",
|
||||
"zipball_url",
|
||||
"tarball_url"
|
||||
]
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"label-items": {
|
||||
"value": [
|
||||
{
|
||||
"id": 208045946,
|
||||
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
|
||||
"url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
|
||||
"name": "bug",
|
||||
"description": "Something isn't working",
|
||||
"color": "f29513",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": 208045947,
|
||||
"node_id": "MDU6TGFiZWwyMDgwNDU5NDc=",
|
||||
"url": "https://api.github.com/repos/octocat/Hello-World/labels/enhancement",
|
||||
"name": "enhancement",
|
||||
"description": "New feature or request",
|
||||
"color": "a2eeef",
|
||||
"default": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"tag-items": {
|
||||
"value": [
|
||||
{
|
||||
"name": "v0.1",
|
||||
"commit": {
|
||||
"sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc",
|
||||
"url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc"
|
||||
},
|
||||
"zipball_url": "https://github.com/octocat/Hello-World/zipball/v0.1",
|
||||
"tarball_url": "https://github.com/octocat/Hello-World/tarball/v0.1",
|
||||
"node_id": "MDQ6VXNlcjE="
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"owner": {
|
||||
"name": "owner",
|
||||
"description": "The account owner of the repository. The name is not case sensitive.",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"repo": {
|
||||
"name": "repo",
|
||||
"description": "The name of the repository without the `.git` extension. The name is not case sensitive.",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"per-page": {
|
||||
"name": "per_page",
|
||||
"description": "The number of results per page (max 100). For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"name": "page",
|
||||
"description": "The page number of the results to fetch. For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"not_found": {
|
||||
"description": "Resource not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/basic-error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"link": {
|
||||
"example": "<https://api.github.com/resource?page=2>; rel=\"next\", <https://api.github.com/resource?page=5>; rel=\"last\"",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/repos/{owner}/{repo}/tags": {
|
||||
"get": {
|
||||
"summary": "List repository tags",
|
||||
"description": "",
|
||||
"tags": [
|
||||
"repos"
|
||||
],
|
||||
"operationId": "repos/list-tags",
|
||||
"externalDocs": {
|
||||
"description": "API method documentation",
|
||||
"url": "https://docs.github.com/rest/repos/repos#list-repository-tags"
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/owner"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/repo"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/per-page"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/page"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/tag"
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"default": {
|
||||
"$ref": "#/components/examples/tag-items"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"Link": {
|
||||
"$ref": "#/components/headers/link"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-github": {
|
||||
"githubCloudOnly": false,
|
||||
"enabledForGitHubApps": true,
|
||||
"category": "repos",
|
||||
"subcategory": "repos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/labels": {
|
||||
"get": {
|
||||
"summary": "List labels for a repository",
|
||||
"description": "Lists all labels for a repository.",
|
||||
"tags": [
|
||||
"issues"
|
||||
],
|
||||
"operationId": "issues/list-labels-for-repo",
|
||||
"externalDocs": {
|
||||
"description": "API method documentation",
|
||||
"url": "https://docs.github.com/rest/issues/labels#list-labels-for-a-repository"
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/owner"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/repo"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/per-page"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/page"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/label"
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"default": {
|
||||
"$ref": "#/components/examples/label-items"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"Link": {
|
||||
"$ref": "#/components/headers/link"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/not_found"
|
||||
}
|
||||
},
|
||||
"x-github": {
|
||||
"githubCloudOnly": false,
|
||||
"enabledForGitHubApps": true,
|
||||
"category": "issues",
|
||||
"subcategory": "labels"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use a ChatClientAgent with function tools provided via an OpenAPI spec.
|
||||
// It uses functionality from Semantic Kernel to parse the OpenAPI spec and create function tools to use with the Agent Framework Agent.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Plugins.OpenApi;
|
||||
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";
|
||||
|
||||
// Load the OpenAPI Spec from a file.
|
||||
KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("github", "OpenAPISpec.json");
|
||||
|
||||
// Convert the Semantic Kernel plugin to Agent Framework function tools.
|
||||
// This requires a dummy Kernel instance, since KernelFunctions cannot execute without one.
|
||||
Kernel kernel = new();
|
||||
List<AITool> tools = plugin.Select(x => x.WithKernel(kernel)).Cast<AITool>().ToList();
|
||||
|
||||
// Create the chat client and agent, and provide the OpenAPI function tools to the agent.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are a helpful assistant", tools: tools);
|
||||
|
||||
// Run the agent with the OpenAPI function tools.
|
||||
Console.WriteLine(await agent.RunAsync("Please list the names, colors and descriptions of all the labels available in the microsoft/agent-framework repository on github."));
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -28,8 +28,8 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|---|---|
|
||||
|[Running a simple agent](./Agent_Step01_Running/)|This sample demonstrates how to create and run a basic agent with instructions|
|
||||
|[Multi-turn conversation with a simple agent](./Agent_Step02_MultiturnConversation/)|This sample demonstrates how to implement a multi-turn conversation with a simple agent|
|
||||
|[Using function tools with a simple agent](./Agent_Step03.1_UsingFunctionTools/)|This sample demonstrates how to use function tools with a simple agent|
|
||||
|[Using OpenAPI function tools with a simple agent](./Agent_Step03.2_UsingFunctionTools_FromOpenAPI/)|This sample demonstrates how to create function tools from an OpenAPI spec and use them with a simple agent|
|
||||
|[Using function tools with a simple agent](./Agent_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with a simple agent|
|
||||
|[Using OpenAPI function tools with a simple agent](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/AgentFrameworkMigration/AzureOpenAI/Step04_ToolCall_WithOpenAPI)|This sample demonstrates how to create function tools from an OpenAPI spec and use them with a simple agent (note that this sample is in the Semantic Kernel repository)|
|
||||
|[Using function tools with approvals](./Agent_Step04_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution|
|
||||
|[Structured output with a simple agent](./Agent_Step05_StructuredOutput/)|This sample demonstrates how to use structured output with a simple agent|
|
||||
|[Persisted conversations with a simple agent](./Agent_Step06_PersistedConversations/)|This sample demonstrates how to persist conversations and reload them later. This is useful for cases where an agent is hosted in a stateless service|
|
||||
@@ -39,14 +39,12 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|[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|
|
||||
|[Exposing a simple agent as a function tool](./Agent_Step12_AsFunctionTool/)|This sample demonstrates how to expose an agent as a function tool|
|
||||
|[Using memory with an agent](./Agent_Step13_Memory/)|This sample demonstrates how to create a simple memory component and use it with an agent|
|
||||
|[Background responses with tools and persistence](./Agent_Step13_BackgroundResponsesWithToolsAndPersistence/)|This sample demonstrates advanced background response scenarios including function calling during background operations and state persistence|
|
||||
|[Using middleware with an agent](./Agent_Step14_Middleware/)|This sample demonstrates how to use middleware with an agent|
|
||||
|[Using plugins with an agent](./Agent_Step15_Plugins/)|This sample demonstrates how to use plugins with an agent|
|
||||
|[Reducing chat history size](./Agent_Step16_ChatReduction/)|This sample demonstrates how to reduce the chat history to constrain its size, where chat history is maintained locally|
|
||||
|[Background responses](./Agent_Step17_BackgroundResponses/)|This sample demonstrates how to use background responses for long-running operations with polling and resumption support|
|
||||
|[Adding RAG with text search](./Agent_Step18_TextSearchRag/)|This sample demonstrates how to enrich agent responses with retrieval augmented generation using the text search provider|
|
||||
|[Using Mem0-backed memory](./Agent_Step19_Mem0Provider/)|This sample demonstrates how to use the Mem0Provider to persist and recall memories across conversations|
|
||||
|[Background responses with tools and persistence](./Agent_Step20_BackgroundResponsesWithToolsAndPersistence/)|This sample demonstrates advanced background response scenarios including function calling during background operations and state persistence|
|
||||
|[Deep research with an agent](./Agent_Step18_DeepResearch/)|This sample demonstrates how to use the Deep Research Tool to perform comprehensive research on complex topics|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
// This sample demonstrates basic usage of the DevUI in an ASP.NET Core application with AI agents.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
@@ -18,10 +19,11 @@ namespace DevUI_Step01_BasicUsage;
|
||||
/// <remarks>
|
||||
/// This sample shows how to:
|
||||
/// 1. Set up Azure OpenAI as the chat client
|
||||
/// 2. Register agents and workflows using the hosting packages
|
||||
/// 3. Map the DevUI endpoint which automatically configures the middleware
|
||||
/// 4. Map the dynamic OpenAI Responses API for Python DevUI compatibility
|
||||
/// 5. Access the DevUI in a web browser
|
||||
/// 2. Create function tools for agents to use
|
||||
/// 3. Register agents and workflows using the hosting packages with tools
|
||||
/// 4. Map the DevUI endpoint which automatically configures the middleware
|
||||
/// 5. Map the dynamic OpenAI Responses API for Python DevUI compatibility
|
||||
/// 6. Access the DevUI in a web browser
|
||||
///
|
||||
/// The DevUI provides an interactive web interface for testing and debugging AI agents.
|
||||
/// DevUI assets are served from embedded resources within the assembly.
|
||||
@@ -50,10 +52,30 @@ internal static class Program
|
||||
|
||||
builder.Services.AddChatClient(chatClient);
|
||||
|
||||
// Register sample agents
|
||||
builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.");
|
||||
// Define some example tools
|
||||
[Description("Get the weather for a given location.")]
|
||||
static string GetWeather([Description("The location to get the weather for.")] string location)
|
||||
=> $"The weather in {location} is cloudy with a high of 15°C.";
|
||||
|
||||
[Description("Calculate the sum of two numbers.")]
|
||||
static double Add([Description("The first number.")] double a, [Description("The second number.")] double b)
|
||||
=> a + b;
|
||||
|
||||
[Description("Get the current time.")]
|
||||
static string GetCurrentTime()
|
||||
=> DateTime.Now.ToString("HH:mm:ss");
|
||||
|
||||
// Register sample agents with tools
|
||||
builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.")
|
||||
.WithAITools(
|
||||
AIFunctionFactory.Create(GetWeather, name: "get_weather"),
|
||||
AIFunctionFactory.Create(GetCurrentTime, name: "get_current_time")
|
||||
);
|
||||
|
||||
builder.AddAIAgent("poet", "You are a creative poet. Respond to all requests with beautiful poetry.");
|
||||
builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.");
|
||||
|
||||
builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.")
|
||||
.WithAITool(AIFunctionFactory.Create(Add, name: "add"));
|
||||
|
||||
// Register sample workflows
|
||||
var assistantBuilder = builder.AddAIAgent("workflow-assistant", "You are a helpful assistant in a workflow.");
|
||||
|
||||
@@ -9,6 +9,8 @@ of the agent framework.
|
||||
|---|---|
|
||||
|[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|
|
||||
|[Agents With Retrieval Augmented Generation (RAG)](./AgentWithRAG/README.md)|Adding Retrieval Augmented Generation (RAG) capabilities to your agents.|
|
||||
|[Agents With Memory](./AgentWithMemory/README.md)|Adding Memory capabilities to your agents.|
|
||||
|[A2A](./A2A/README.md)|Getting started with A2A (Agent-to-Agent) specific features|
|
||||
|[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|
|
||||
|
||||
+10
-25
@@ -16,7 +16,7 @@ internal static class WorkflowFactory
|
||||
internal static Workflow BuildWorkflow(IChatClient chatClient)
|
||||
{
|
||||
// Create executors
|
||||
var startExecutor = new ConcurrentStartExecutor();
|
||||
var startExecutor = new ChatForwardingExecutor("Start");
|
||||
var aggregationExecutor = new ConcurrentAggregationExecutor();
|
||||
AIAgent frenchAgent = GetLanguageAgent("French", chatClient);
|
||||
AIAgent englishAgent = GetLanguageAgent("English", chatClient);
|
||||
@@ -38,33 +38,11 @@ internal static class WorkflowFactory
|
||||
private static ChatClientAgent GetLanguageAgent(string targetLanguage, IChatClient chatClient) =>
|
||||
new(chatClient, instructions: $"You're a helpful assistant who always responds in {targetLanguage}.", name: $"{targetLanguage}Agent");
|
||||
|
||||
/// <summary>
|
||||
/// Executor that starts the concurrent processing by sending messages to the agents.
|
||||
/// </summary>
|
||||
private sealed class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
|
||||
{
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
|
||||
{
|
||||
return routeBuilder
|
||||
.AddHandler<List<ChatMessage>>(this.RouteMessages)
|
||||
.AddHandler<TurnToken>(this.RouteTurnTokenAsync);
|
||||
}
|
||||
|
||||
private ValueTask RouteMessages(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return context.SendMessageAsync(messages, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return context.SendMessageAsync(token, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that aggregates the results from the concurrent agents.
|
||||
/// </summary>
|
||||
private sealed class ConcurrentAggregationExecutor() : Executor<List<ChatMessage>>("ConcurrentAggregationExecutor")
|
||||
private sealed class ConcurrentAggregationExecutor() :
|
||||
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor"), IResettableExecutor
|
||||
{
|
||||
private readonly List<ChatMessage> _messages = [];
|
||||
|
||||
@@ -85,5 +63,12 @@ internal static class WorkflowFactory
|
||||
await context.YieldOutputAsync(formattedMessages, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask ResetAsync()
|
||||
{
|
||||
this._messages.Clear();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ internal sealed class Program
|
||||
Console.WriteLine(code);
|
||||
}
|
||||
|
||||
private const string DefaultWorkflow = "HelloWorld.yaml";
|
||||
private const string DefaultWorkflow = "Marketing.yaml";
|
||||
|
||||
private string WorkflowFile { get; }
|
||||
|
||||
|
||||
@@ -92,11 +92,11 @@ The repository has example workflows available in the root [`/workflow-samples`]
|
||||
2. Run the demo referencing a sample workflow by name:
|
||||
|
||||
```sh
|
||||
dotnet run HelloWorld
|
||||
dotnet run Marketing
|
||||
```
|
||||
|
||||
3. Run the demo with a path to any workflow file:
|
||||
|
||||
```sh
|
||||
dotnet run c:/myworkflows/HelloWorld.yaml
|
||||
dotnet run c:/myworkflows/Marketing.yaml
|
||||
```
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!--
|
||||
Disable central package management for this project.
|
||||
This project requires explicit package references with versions specified inline rather than
|
||||
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
|
||||
be created from this project, and the Docker build process only has access to this folder
|
||||
and cannot access parent folders where Directory.Packages.props resides.
|
||||
-->
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Remove analyzer PackageReference items inherited from Directory.Packages.props.
|
||||
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
|
||||
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
|
||||
and the PackageReference items are always inherited through MSBuild imports regardless of the
|
||||
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
|
||||
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
|
||||
<PackageReference Remove="xunit.analyzers" />
|
||||
<PackageReference Remove="Moq.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251110.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.2-preview.1.25552.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Add analyzers with compatible versions -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
# Build the application
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy files from the current directory on the host to the working directory in the container
|
||||
COPY . .
|
||||
|
||||
RUN dotnet restore
|
||||
RUN dotnet build -c Release --no-restore
|
||||
RUN dotnet publish -c Release --no-build -o /app
|
||||
|
||||
# Run the application
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything needed to run the app from the "build" stage.
|
||||
COPY --from=build /app .
|
||||
|
||||
EXPOSE 8088
|
||||
ENTRYPOINT ["dotnet", "AgentWithHostedMCP.dll"]
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with OpenAI Responses as the backend, that uses a Hosted MCP Tool.
|
||||
// In this case the OpenAI responses service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework.
|
||||
// The sample demonstrates how to use MCP tools with auto approval by setting ApprovalMode to NeverRequire.
|
||||
|
||||
using Azure.AI.AgentServer.AgentFramework.Extensions;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
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 MCP tool that can be called without approval.
|
||||
AITool mcpTool = new HostedMcpServerTool(serverName: "microsoft_learn", serverAddress: "https://learn.microsoft.com/api/mcp")
|
||||
{
|
||||
AllowedTools = ["microsoft_docs_search"],
|
||||
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire
|
||||
};
|
||||
|
||||
// Create an agent with the MCP tool using Azure OpenAI Responses.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
name: "MicrosoftLearnAgent",
|
||||
tools: [mcpTool]);
|
||||
|
||||
await agent.RunAIAgentAsync();
|
||||
@@ -0,0 +1,43 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
This sample demonstrates how to use a Hosted Model Context Protocol (MCP) server with an AI agent.
|
||||
The agent connects to the Microsoft Learn MCP server to search documentation and answer questions using official Microsoft content.
|
||||
|
||||
Key features:
|
||||
- Configuring MCP tools with automatic approval (no user confirmation required)
|
||||
- Filtering available tools from an MCP server
|
||||
- Using Azure OpenAI Responses with MCP tools
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running this sample, ensure you have:
|
||||
|
||||
1. An Azure OpenAI endpoint configured
|
||||
2. A deployment of a chat model (e.g., gpt-4o-mini)
|
||||
3. Azure CLI installed and authenticated
|
||||
|
||||
**Note**: This sample uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
# Replace with your Azure OpenAI endpoint
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-openai-resource.openai.azure.com/"
|
||||
|
||||
# Optional, defaults to gpt-4o-mini
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The sample connects to the Microsoft Learn MCP server and uses its documentation search capabilities:
|
||||
|
||||
1. The agent is configured with a HostedMcpServerTool pointing to `https://learn.microsoft.com/api/mcp`
|
||||
2. Only the `microsoft_docs_search` tool is enabled from the available MCP tools
|
||||
3. Approval mode is set to `NeverRequire`, allowing automatic tool execution
|
||||
4. When you ask questions, Azure OpenAI Responses automatically invokes the MCP tool to search documentation
|
||||
5. The agent returns answers based on the Microsoft Learn content
|
||||
|
||||
In this configuration, the OpenAI Responses service manages tool invocation directly - the Agent Framework does not handle MCP tool calls.
|
||||
@@ -0,0 +1,29 @@
|
||||
name: AgentWithHostedMCP
|
||||
displayName: "Microsoft Learn Response Agent with MCP"
|
||||
description: >
|
||||
An AI agent that uses Azure OpenAI Responses with a Hosted Model Context Protocol (MCP) server.
|
||||
The agent answers questions by searching Microsoft Learn documentation using MCP tools.
|
||||
This demonstrates how MCP tools can be integrated with Azure OpenAI Responses where the service
|
||||
itself handles tool invocation.
|
||||
metadata:
|
||||
authors:
|
||||
- Microsoft Agent Framework Team
|
||||
tags:
|
||||
- Microsoft Agent Framework
|
||||
- Model Context Protocol
|
||||
- MCP
|
||||
template:
|
||||
kind: hosted
|
||||
name: AgentWithHostedMCP
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v1
|
||||
environment_variables:
|
||||
- name: AZURE_OPENAI_ENDPOINT
|
||||
value: ${AZURE_OPENAI_ENDPOINT}
|
||||
- name: AZURE_OPENAI_DEPLOYMENT_NAME
|
||||
value: gpt-4o-mini
|
||||
resources:
|
||||
- name: "gpt-4o-mini"
|
||||
kind: model
|
||||
id: gpt-4o-mini
|
||||
@@ -0,0 +1,30 @@
|
||||
@host = http://localhost:8088
|
||||
@endpoint = {{host}}/responses
|
||||
|
||||
### Health Check
|
||||
GET {{host}}/readiness
|
||||
|
||||
### Simple string input - Ask about MCP Tools
|
||||
POST {{endpoint}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"input": "Please summarize the Azure AI Agent documentation related to MCP Tool calling?"
|
||||
}
|
||||
|
||||
### Explicit input - Ask about Agent Framework
|
||||
POST {{endpoint}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "What is the Microsoft Agent Framework?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!--
|
||||
Disable central package management for this project.
|
||||
This project requires explicit package references with versions specified inline rather than
|
||||
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
|
||||
be created from this project, and the Docker build process only has access to this folder
|
||||
and cannot access parent folders where Directory.Packages.props resides.
|
||||
-->
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Remove analyzer PackageReference items inherited from Directory.Packages.props.
|
||||
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
|
||||
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
|
||||
and the PackageReference items are always inherited through MSBuild imports regardless of the
|
||||
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
|
||||
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
|
||||
<PackageReference Remove="xunit.analyzers" />
|
||||
<PackageReference Remove="Moq.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251110.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.2-preview.1.25552.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Add analyzers with compatible versions -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
# Build the application
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy files from the current directory on the host to the working directory in the container
|
||||
COPY . .
|
||||
|
||||
RUN dotnet restore
|
||||
RUN dotnet build -c Release --no-restore
|
||||
RUN dotnet publish -c Release --no-build -o /app
|
||||
|
||||
# Run the application
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything needed to run the app from the "build" stage.
|
||||
COPY --from=build /app .
|
||||
|
||||
EXPOSE 8088
|
||||
ENTRYPOINT ["dotnet", "AgentWithTextSearchRag.dll"]
|
||||
+3
-13
@@ -4,8 +4,7 @@
|
||||
// capabilities to an AI agent. The provider runs a search against an external knowledge base
|
||||
// before each model invocation and injects the results into the model context.
|
||||
|
||||
// Also see the AgentWithRAG folder for more advanced RAG scenarios.
|
||||
|
||||
using Azure.AI.AgentServer.AgentFramework.Extensions;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
@@ -25,7 +24,7 @@ TextSearchProviderOptions textSearchOptions = new()
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
@@ -33,16 +32,7 @@ AIAgent agent = new AzureOpenAIClient(
|
||||
AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
|
||||
});
|
||||
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
Console.WriteLine(">> Asking about returns\n");
|
||||
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
|
||||
|
||||
Console.WriteLine("\n>> Asking about shipping\n");
|
||||
Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
|
||||
|
||||
Console.WriteLine("\n>> Asking about product care\n");
|
||||
Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
|
||||
await agent.RunAIAgentAsync();
|
||||
|
||||
static Task<IEnumerable<TextSearchProvider.TextSearchResult>> MockSearchAsync(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -0,0 +1,30 @@
|
||||
name: AgentWithTextSearchRag
|
||||
displayName: "Text Search RAG Agent"
|
||||
description: >
|
||||
An AI agent that uses TextSearchProvider for retrieval augmented generation (RAG) capabilities.
|
||||
The agent runs searches against an external knowledge base before each model invocation and
|
||||
injects the results into the model context. It can answer questions about Contoso Outdoors
|
||||
policies and products, including return policies, refunds, shipping options, and product care
|
||||
instructions such as tent maintenance.
|
||||
metadata:
|
||||
authors:
|
||||
- Microsoft Agent Framework Team
|
||||
tags:
|
||||
- Microsoft Agent Framework
|
||||
- Retrieval-Augmented Generation
|
||||
- RAG
|
||||
template:
|
||||
kind: hosted
|
||||
name: AgentWithTextSearchRag
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v1
|
||||
environment_variables:
|
||||
- name: AZURE_OPENAI_ENDPOINT
|
||||
value: ${AZURE_OPENAI_ENDPOINT}
|
||||
- name: AZURE_OPENAI_DEPLOYMENT_NAME
|
||||
value: gpt-4o-mini
|
||||
resources:
|
||||
- name: "gpt-4o-mini"
|
||||
kind: model
|
||||
id: gpt-4o-mini
|
||||
@@ -0,0 +1,30 @@
|
||||
@host = http://localhost:8088
|
||||
@endpoint = {{host}}/responses
|
||||
|
||||
### Health Check
|
||||
GET {{host}}/readiness
|
||||
|
||||
### Simple string input
|
||||
POST {{endpoint}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"input": "Hi! I need help understanding the return policy."
|
||||
}
|
||||
|
||||
### Explicit input
|
||||
POST {{endpoint}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "How long does standard shipping usually take?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!--
|
||||
Disable central package management for this project.
|
||||
This project requires explicit package references with versions specified inline rather than
|
||||
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
|
||||
be created from this project, and the Docker build process only has access to this folder
|
||||
and cannot access parent folders where Directory.Packages.props resides.
|
||||
-->
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Remove analyzer PackageReference items inherited from Directory.Packages.props.
|
||||
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
|
||||
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
|
||||
and the PackageReference items are always inherited through MSBuild imports regardless of the
|
||||
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
|
||||
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
|
||||
<PackageReference Remove="xunit.analyzers" />
|
||||
<PackageReference Remove="Moq.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251110.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.2-preview.1.25552.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Add analyzers with compatible versions -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.12.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
# Build the application
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy files from the current directory on the host to the working directory in the container
|
||||
COPY . .
|
||||
|
||||
RUN dotnet restore
|
||||
RUN dotnet build -c Release --no-restore
|
||||
RUN dotnet publish -c Release --no-build -o /app
|
||||
|
||||
# Run the application
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything needed to run the app from the "build" stage.
|
||||
COPY --from=build /app .
|
||||
|
||||
EXPOSE 8088
|
||||
ENTRYPOINT ["dotnet", "AgentsInWorkflows.dll"]
|
||||
+7
-18
@@ -4,6 +4,7 @@
|
||||
// Three translation agents are connected sequentially to create a translation chain:
|
||||
// English → French → Spanish → English, showing how agents can be composed as workflow executors.
|
||||
|
||||
using Azure.AI.AgentServer.AgentFramework.Extensions;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
@@ -14,7 +15,7 @@ using Microsoft.Extensions.AI;
|
||||
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";
|
||||
|
||||
IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsIChatClient();
|
||||
|
||||
@@ -23,26 +24,14 @@ AIAgent frenchAgent = GetTranslationAgent("French", chatClient);
|
||||
AIAgent spanishAgent = GetTranslationAgent("Spanish", chatClient);
|
||||
AIAgent englishAgent = GetTranslationAgent("English", chatClient);
|
||||
|
||||
// Build the workflow by adding executors and connecting them
|
||||
Workflow workflow = new WorkflowBuilder(frenchAgent)
|
||||
// Build the workflow and turn it into an agent
|
||||
AIAgent agent = new WorkflowBuilder(frenchAgent)
|
||||
.AddEdge(frenchAgent, spanishAgent)
|
||||
.AddEdge(spanishAgent, englishAgent)
|
||||
.Build();
|
||||
.Build()
|
||||
.AsAgent();
|
||||
|
||||
// Execute the workflow
|
||||
await using 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())
|
||||
{
|
||||
if (evt is AgentRunUpdateEvent executorComplete)
|
||||
{
|
||||
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
}
|
||||
}
|
||||
await agent.RunAIAgentAsync();
|
||||
|
||||
static ChatClientAgent GetTranslationAgent(string targetLanguage, IChatClient chatClient) =>
|
||||
new(chatClient, $"You are a translation assistant that translates the provided text to {targetLanguage}.");
|
||||
@@ -0,0 +1,27 @@
|
||||
name: AgentsInWorkflows
|
||||
displayName: "Translation Chain Workflow Agent"
|
||||
description: >
|
||||
A workflow agent that performs sequential translation through multiple languages.
|
||||
The agent translates text from English to French, then to Spanish, and finally back
|
||||
to English, leveraging AI-powered translation capabilities in a pipeline workflow.
|
||||
metadata:
|
||||
authors:
|
||||
- Microsoft Agent Framework Team
|
||||
tags:
|
||||
- Microsoft Agent Framework
|
||||
- Workflows
|
||||
template:
|
||||
kind: hosted
|
||||
name: AgentsInWorkflows
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v1
|
||||
environment_variables:
|
||||
- name: AZURE_OPENAI_ENDPOINT
|
||||
value: ${AZURE_OPENAI_ENDPOINT}
|
||||
- name: AZURE_OPENAI_DEPLOYMENT_NAME
|
||||
value: gpt-4o-mini
|
||||
resources:
|
||||
- name: "gpt-4o-mini"
|
||||
kind: model
|
||||
id: gpt-4o-mini
|
||||
@@ -0,0 +1,30 @@
|
||||
@host = http://localhost:8088
|
||||
@endpoint = {{host}}/responses
|
||||
|
||||
### Health Check
|
||||
GET {{host}}/readiness
|
||||
|
||||
### Simple string input
|
||||
POST {{endpoint}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"input": "Hello, how are you today?"
|
||||
}
|
||||
|
||||
### Explicit input
|
||||
POST {{endpoint}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Hello, how are you today?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -152,6 +153,8 @@ public sealed class AGUIChatClient : DelegatingChatClient
|
||||
|
||||
private sealed class AGUIChatClientHandler : IChatClient
|
||||
{
|
||||
private static readonly MediaTypeHeaderValue s_json = new("application/json");
|
||||
|
||||
private readonly AGUIHttpService _httpService;
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
private readonly ILogger _logger;
|
||||
@@ -199,6 +202,9 @@ public sealed class AGUIChatClient : DelegatingChatClient
|
||||
var threadId = ExtractTemporaryThreadId(messagesList) ??
|
||||
ExtractThreadIdFromOptions(options) ?? $"thread_{Guid.NewGuid():N}";
|
||||
|
||||
// Extract state from the last message if it contains DataContent with application/json
|
||||
JsonElement state = this.ExtractAndRemoveStateFromMessages(messagesList);
|
||||
|
||||
// Create the input for the AGUI service
|
||||
var input = new RunAgentInput
|
||||
{
|
||||
@@ -207,6 +213,7 @@ public sealed class AGUIChatClient : DelegatingChatClient
|
||||
ThreadId = threadId,
|
||||
RunId = runId,
|
||||
Messages = messagesList.AsAGUIMessages(this._jsonSerializerOptions),
|
||||
State = state,
|
||||
};
|
||||
|
||||
// Add tools if provided
|
||||
@@ -300,6 +307,51 @@ public sealed class AGUIChatClient : DelegatingChatClient
|
||||
return threadId;
|
||||
}
|
||||
|
||||
// Extract state from the last message's DataContent with application/json media type
|
||||
// and remove that message from the list
|
||||
private JsonElement ExtractAndRemoveStateFromMessages(List<ChatMessage> messagesList)
|
||||
{
|
||||
if (messagesList.Count == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// Check the last message for state DataContent
|
||||
ChatMessage lastMessage = messagesList[messagesList.Count - 1];
|
||||
for (int i = 0; i < lastMessage.Contents.Count; i++)
|
||||
{
|
||||
if (lastMessage.Contents[i] is DataContent dataContent &&
|
||||
MediaTypeHeaderValue.TryParse(dataContent.MediaType, out var mediaType) &&
|
||||
mediaType.Equals(s_json))
|
||||
{
|
||||
// Deserialize the state JSON directly from UTF-8 bytes
|
||||
try
|
||||
{
|
||||
JsonElement stateElement = (JsonElement)JsonSerializer.Deserialize(
|
||||
dataContent.Data.Span,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))!;
|
||||
|
||||
// Remove the DataContent from the message contents
|
||||
lastMessage.Contents.RemoveAt(i);
|
||||
|
||||
// If no contents remain, remove the entire message
|
||||
if (lastMessage.Contents.Count == 0)
|
||||
{
|
||||
messagesList.RemoveAt(messagesList.Count - 1);
|
||||
}
|
||||
|
||||
return stateElement;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to deserialize state JSON from DataContent: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No resources to dispose
|
||||
@@ -316,7 +368,7 @@ public sealed class AGUIChatClient : DelegatingChatClient
|
||||
}
|
||||
}
|
||||
|
||||
private class ServerFunctionCallContent(FunctionCallContent functionCall) : AIContent
|
||||
private sealed class ServerFunctionCallContent(FunctionCallContent functionCall) : AIContent
|
||||
{
|
||||
public FunctionCallContent FunctionCallContent { get; } = functionCall;
|
||||
}
|
||||
|
||||
@@ -27,4 +27,8 @@ internal static class AGUIEventTypes
|
||||
public const string ToolCallEnd = "TOOL_CALL_END";
|
||||
|
||||
public const string ToolCallResult = "TOOL_CALL_RESULT";
|
||||
|
||||
public const string StateSnapshot = "STATE_SNAPSHOT";
|
||||
|
||||
public const string StateDelta = "STATE_DELTA";
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace Microsoft.Agents.AI.AGUI;
|
||||
[JsonSerializable(typeof(ToolCallArgsEvent))]
|
||||
[JsonSerializable(typeof(ToolCallEndEvent))]
|
||||
[JsonSerializable(typeof(ToolCallResultEvent))]
|
||||
[JsonSerializable(typeof(StateSnapshotEvent))]
|
||||
[JsonSerializable(typeof(StateDeltaEvent))]
|
||||
[JsonSerializable(typeof(IDictionary<string, object?>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object?>))]
|
||||
[JsonSerializable(typeof(IDictionary<string, System.Text.Json.JsonElement?>))]
|
||||
@@ -57,6 +59,6 @@ namespace Microsoft.Agents.AI.AGUI;
|
||||
[JsonSerializable(typeof(float))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(decimal))]
|
||||
internal partial class AGUIJsonSerializerContext : JsonSerializerContext
|
||||
internal sealed partial class AGUIJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ internal sealed class BaseEventJsonConverter : JsonConverter<BaseEvent>
|
||||
AGUIEventTypes.ToolCallArgs => jsonElement.Deserialize(options.GetTypeInfo(typeof(ToolCallArgsEvent))) as ToolCallArgsEvent,
|
||||
AGUIEventTypes.ToolCallEnd => jsonElement.Deserialize(options.GetTypeInfo(typeof(ToolCallEndEvent))) as ToolCallEndEvent,
|
||||
AGUIEventTypes.ToolCallResult => jsonElement.Deserialize(options.GetTypeInfo(typeof(ToolCallResultEvent))) as ToolCallResultEvent,
|
||||
AGUIEventTypes.StateSnapshot => jsonElement.Deserialize(options.GetTypeInfo(typeof(StateSnapshotEvent))) as StateSnapshotEvent,
|
||||
_ => throw new JsonException($"Unknown BaseEvent type discriminator: '{discriminator}'")
|
||||
};
|
||||
|
||||
@@ -95,8 +96,14 @@ internal sealed class BaseEventJsonConverter : JsonConverter<BaseEvent>
|
||||
case ToolCallResultEvent toolCallResult:
|
||||
JsonSerializer.Serialize(writer, toolCallResult, options.GetTypeInfo(typeof(ToolCallResultEvent)));
|
||||
break;
|
||||
case StateSnapshotEvent stateSnapshot:
|
||||
JsonSerializer.Serialize(writer, stateSnapshot, options.GetTypeInfo(typeof(StateSnapshotEvent)));
|
||||
break;
|
||||
case StateDeltaEvent stateDelta:
|
||||
JsonSerializer.Serialize(writer, stateDelta, options.GetTypeInfo(typeof(StateDeltaEvent)));
|
||||
break;
|
||||
default:
|
||||
throw new JsonException($"Unknown BaseEvent type: {value.GetType().Name}");
|
||||
throw new InvalidOperationException($"Unknown event type: {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -18,6 +19,9 @@ namespace Microsoft.Agents.AI.AGUI.Shared;
|
||||
|
||||
internal static class ChatResponseUpdateAGUIExtensions
|
||||
{
|
||||
private static readonly MediaTypeHeaderValue? s_jsonPatchMediaType = new("application/json-patch+json");
|
||||
private static readonly MediaTypeHeaderValue? s_json = new("application/json");
|
||||
|
||||
public static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAsync(
|
||||
this IAsyncEnumerable<BaseEvent> events,
|
||||
JsonSerializerOptions jsonSerializerOptions,
|
||||
@@ -70,11 +74,73 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
case ToolCallResultEvent toolCallResult:
|
||||
yield return toolCallAccumulator.EmitToolCallResult(toolCallResult, jsonSerializerOptions);
|
||||
break;
|
||||
|
||||
// State snapshot events
|
||||
case StateSnapshotEvent stateSnapshot:
|
||||
if (stateSnapshot.Snapshot.HasValue)
|
||||
{
|
||||
yield return CreateStateSnapshotUpdate(stateSnapshot, conversationId, responseId, jsonSerializerOptions);
|
||||
}
|
||||
break;
|
||||
case StateDeltaEvent stateDelta:
|
||||
if (stateDelta.Delta.HasValue)
|
||||
{
|
||||
yield return CreateStateDeltaUpdate(stateDelta, conversationId, responseId, jsonSerializerOptions);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TextMessageBuilder()
|
||||
private static ChatResponseUpdate CreateStateSnapshotUpdate(
|
||||
StateSnapshotEvent stateSnapshot,
|
||||
string? conversationId,
|
||||
string? responseId,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
// Serialize JsonElement directly to UTF-8 bytes using AOT-safe overload
|
||||
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateSnapshot.Snapshot!.Value,
|
||||
jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
|
||||
DataContent dataContent = new(jsonBytes, "application/json");
|
||||
|
||||
return new ChatResponseUpdate(ChatRole.Assistant, [dataContent])
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
ResponseId = responseId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary
|
||||
{
|
||||
["is_state_snapshot"] = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ChatResponseUpdate CreateStateDeltaUpdate(
|
||||
StateDeltaEvent stateDelta,
|
||||
string? conversationId,
|
||||
string? responseId,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
// Serialize JsonElement directly to UTF-8 bytes using AOT-safe overload
|
||||
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateDelta.Delta!.Value,
|
||||
jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
|
||||
DataContent dataContent = new(jsonBytes, "application/json-patch+json");
|
||||
|
||||
return new ChatResponseUpdate(ChatRole.Assistant, [dataContent])
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
ResponseId = responseId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary
|
||||
{
|
||||
["is_state_delta"] = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TextMessageBuilder()
|
||||
{
|
||||
private ChatRole _currentRole;
|
||||
private string? _currentMessageId;
|
||||
@@ -154,7 +220,7 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
};
|
||||
}
|
||||
|
||||
private class ToolCallBuilder
|
||||
private sealed class ToolCallBuilder
|
||||
{
|
||||
private string? _conversationId;
|
||||
private string? _responseId;
|
||||
@@ -348,6 +414,55 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
Role = AGUIRoles.Tool
|
||||
};
|
||||
}
|
||||
else if (content is DataContent dataContent)
|
||||
{
|
||||
if (MediaTypeHeaderValue.TryParse(dataContent.MediaType, out var mediaType) && mediaType.Equals(s_json))
|
||||
{
|
||||
// State snapshot event
|
||||
yield return new StateSnapshotEvent
|
||||
{
|
||||
#if NET472 || NETSTANDARD2_0
|
||||
Snapshot = (JsonElement?)JsonSerializer.Deserialize(
|
||||
dataContent.Data.ToArray(),
|
||||
jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))
|
||||
#else
|
||||
Snapshot = (JsonElement?)JsonSerializer.Deserialize(
|
||||
dataContent.Data.Span,
|
||||
jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))
|
||||
#endif
|
||||
};
|
||||
}
|
||||
else if (mediaType is { } && mediaType.Equals(s_jsonPatchMediaType))
|
||||
{
|
||||
// State snapshot patch event must be a valid JSON patch,
|
||||
// but its not up to us to validate that here.
|
||||
yield return new StateDeltaEvent
|
||||
{
|
||||
#if NET472 || NETSTANDARD2_0
|
||||
Delta = (JsonElement?)JsonSerializer.Deserialize(
|
||||
dataContent.Data.ToArray(),
|
||||
jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))
|
||||
#else
|
||||
Delta = (JsonElement?)JsonSerializer.Deserialize(
|
||||
dataContent.Data.Span,
|
||||
jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))
|
||||
#endif
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Text content event
|
||||
yield return new TextMessageContentEvent
|
||||
{
|
||||
MessageId = chatResponse.MessageId!,
|
||||
#if NET472 || NETSTANDARD2_0
|
||||
Delta = Encoding.UTF8.GetString(dataContent.Data.ToArray())
|
||||
#else
|
||||
Delta = Encoding.UTF8.GetString(dataContent.Data.Span)
|
||||
#endif
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
#if ASPNETCORE
|
||||
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
|
||||
#else
|
||||
namespace Microsoft.Agents.AI.AGUI.Shared;
|
||||
#endif
|
||||
|
||||
internal sealed class StateDeltaEvent : BaseEvent
|
||||
{
|
||||
public StateDeltaEvent()
|
||||
{
|
||||
this.Type = AGUIEventTypes.StateDelta;
|
||||
}
|
||||
|
||||
[JsonPropertyName("delta")]
|
||||
public JsonElement? Delta { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
#if ASPNETCORE
|
||||
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
|
||||
#else
|
||||
namespace Microsoft.Agents.AI.AGUI.Shared;
|
||||
#endif
|
||||
|
||||
internal sealed class StateSnapshotEvent : BaseEvent
|
||||
{
|
||||
public StateSnapshotEvent()
|
||||
{
|
||||
this.Type = AGUIEventTypes.StateSnapshot;
|
||||
}
|
||||
|
||||
[JsonPropertyName("snapshot")]
|
||||
public JsonElement? Snapshot { get; set; }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -32,6 +33,7 @@ public class AgentRunOptions
|
||||
_ = Throw.IfNull(options);
|
||||
this.ContinuationToken = options.ContinuationToken;
|
||||
this.AllowBackgroundResponses = options.AllowBackgroundResponses;
|
||||
this.AdditionalProperties = options.AdditionalProperties?.Clone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,4 +76,18 @@ public class AgentRunOptions
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool? AllowBackgroundResponses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional properties associated with these options.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// An <see cref="AdditionalPropertiesDictionary"/> containing custom properties,
|
||||
/// or <see langword="null"/> if no additional properties are present.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Additional properties provide a way to include custom metadata or provider-specific
|
||||
/// information that doesn't fit into the standard options schema. This is useful for
|
||||
/// preserving implementation-specific details or extending the options with custom data.
|
||||
/// </remarks>
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public static class DevUIExtensions
|
||||
{
|
||||
var group = endpoints.MapGroup("");
|
||||
group.MapDevUI(pattern: "/devui");
|
||||
group.MapMeta();
|
||||
group.MapEntities();
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,16 @@ namespace Microsoft.Agents.AI.DevUI.Entities;
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonSerializable(typeof(EntityInfo))]
|
||||
[JsonSerializable(typeof(DiscoveryResponse))]
|
||||
[JsonSerializable(typeof(MetaResponse))]
|
||||
[JsonSerializable(typeof(EnvVarRequirement))]
|
||||
[JsonSerializable(typeof(List<EntityInfo>))]
|
||||
[JsonSerializable(typeof(List<JsonElement>))]
|
||||
[JsonSerializable(typeof(List<Dictionary<string, JsonElement>>))]
|
||||
[JsonSerializable(typeof(List<Dictionary<string, string>>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, Dictionary<string, string>>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(int))]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed partial class EntitiesJsonContext : JsonSerializerContext;
|
||||
|
||||
@@ -36,16 +36,16 @@ internal sealed record EntityInfo(
|
||||
string Name,
|
||||
|
||||
[property: JsonPropertyName("description")]
|
||||
string? Description = null,
|
||||
string? Description,
|
||||
|
||||
[property: JsonPropertyName("framework")]
|
||||
string Framework = "dotnet",
|
||||
string Framework,
|
||||
|
||||
[property: JsonPropertyName("tools")]
|
||||
List<string>? Tools = null,
|
||||
List<string> Tools,
|
||||
|
||||
[property: JsonPropertyName("metadata")]
|
||||
Dictionary<string, JsonElement>? Metadata = null
|
||||
Dictionary<string, JsonElement> Metadata
|
||||
)
|
||||
{
|
||||
[JsonPropertyName("source")]
|
||||
@@ -54,6 +54,32 @@ internal sealed record EntityInfo(
|
||||
[JsonPropertyName("original_url")]
|
||||
public string? OriginalUrl { get; init; }
|
||||
|
||||
// Deployment support
|
||||
[JsonPropertyName("deployment_supported")]
|
||||
public bool DeploymentSupported { get; init; }
|
||||
|
||||
[JsonPropertyName("deployment_reason")]
|
||||
public string? DeploymentReason { get; init; }
|
||||
|
||||
// Agent-specific fields
|
||||
[JsonPropertyName("instructions")]
|
||||
public string? Instructions { get; init; }
|
||||
|
||||
[JsonPropertyName("model_id")]
|
||||
public string? ModelId { get; init; }
|
||||
|
||||
[JsonPropertyName("chat_client_type")]
|
||||
public string? ChatClientType { get; init; }
|
||||
|
||||
[JsonPropertyName("context_providers")]
|
||||
public List<string>? ContextProviders { get; init; }
|
||||
|
||||
[JsonPropertyName("middleware")]
|
||||
public List<string>? Middleware { get; init; }
|
||||
|
||||
[JsonPropertyName("module_path")]
|
||||
public string? ModulePath { get; init; }
|
||||
|
||||
// Workflow-specific fields
|
||||
[JsonPropertyName("required_env_vars")]
|
||||
public List<EnvVarRequirement>? RequiredEnvVars { get; init; }
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Server metadata response for the /meta endpoint.
|
||||
/// Provides information about the DevUI server configuration, capabilities, and requirements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This response is used by the frontend to:
|
||||
/// - Determine the UI mode (developer vs user interface)
|
||||
/// - Check server capabilities (tracing, OpenAI proxy support)
|
||||
/// - Verify authentication requirements
|
||||
/// - Display framework and version information
|
||||
/// </remarks>
|
||||
internal sealed record MetaResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the UI interface mode.
|
||||
/// "developer" shows debug tools and advanced features, "user" shows a simplified interface.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ui_mode")]
|
||||
public string UiMode { get; init; } = "developer";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DevUI version string.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; init; } = "0.1.0";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backend framework identifier.
|
||||
/// Always "agent_framework" for Agent Framework implementations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("framework")]
|
||||
public string Framework { get; init; } = "agent_framework";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backend runtime/language.
|
||||
/// "dotnet" for .NET implementations, "python" for Python implementations.
|
||||
/// Used by frontend for deployment guides and feature availability.
|
||||
/// </summary>
|
||||
[JsonPropertyName("runtime")]
|
||||
public string Runtime { get; init; } = "dotnet";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server capabilities dictionary.
|
||||
/// Key-value pairs indicating which optional features are enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Standard capability keys:
|
||||
/// - "tracing": Whether trace events are emitted for debugging
|
||||
/// - "openai_proxy": Whether the server can proxy requests to OpenAI
|
||||
/// </remarks>
|
||||
[JsonPropertyName("capabilities")]
|
||||
public Dictionary<string, bool> Capabilities { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Bearer token authentication is required for API access.
|
||||
/// When true, clients must include "Authorization: Bearer {token}" header in requests.
|
||||
/// </summary>
|
||||
[JsonPropertyName("auth_required")]
|
||||
public bool AuthRequired { get; init; }
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
@@ -17,31 +19,37 @@ internal static class WorkflowSerializationExtensions
|
||||
/// Converts a workflow to a dictionary representation compatible with DevUI frontend.
|
||||
/// This matches the Python workflow.to_dict() format expected by the UI.
|
||||
/// </summary>
|
||||
public static Dictionary<string, object> ToDevUIDict(this Workflow workflow)
|
||||
/// <param name="workflow">The workflow to convert.</param>
|
||||
/// <returns>A dictionary with string keys and JsonElement values containing the workflow data.</returns>
|
||||
public static Dictionary<string, JsonElement> ToDevUIDict(this Workflow workflow)
|
||||
{
|
||||
var result = new Dictionary<string, object>
|
||||
var result = new Dictionary<string, JsonElement>
|
||||
{
|
||||
["id"] = workflow.Name ?? Guid.NewGuid().ToString(),
|
||||
["start_executor_id"] = workflow.StartExecutorId,
|
||||
["max_iterations"] = MaxIterationsDefault
|
||||
["id"] = Serialize(workflow.Name ?? Guid.NewGuid().ToString(), EntitiesJsonContext.Default.String),
|
||||
["start_executor_id"] = Serialize(workflow.StartExecutorId, EntitiesJsonContext.Default.String),
|
||||
["max_iterations"] = Serialize(MaxIterationsDefault, EntitiesJsonContext.Default.Int32)
|
||||
};
|
||||
|
||||
// Add optional fields
|
||||
if (!string.IsNullOrEmpty(workflow.Name))
|
||||
{
|
||||
result["name"] = workflow.Name;
|
||||
result["name"] = Serialize(workflow.Name, EntitiesJsonContext.Default.String);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(workflow.Description))
|
||||
{
|
||||
result["description"] = workflow.Description;
|
||||
result["description"] = Serialize(workflow.Description, EntitiesJsonContext.Default.String);
|
||||
}
|
||||
|
||||
// Convert executors to Python-compatible format
|
||||
result["executors"] = ConvertExecutorsToDict(workflow);
|
||||
result["executors"] = Serialize(
|
||||
ConvertExecutorsToDict(workflow),
|
||||
EntitiesJsonContext.Default.DictionaryStringDictionaryStringString);
|
||||
|
||||
// Convert edges to edge_groups format
|
||||
result["edge_groups"] = ConvertEdgesToEdgeGroups(workflow);
|
||||
result["edge_groups"] = Serialize(
|
||||
ConvertEdgesToEdgeGroups(workflow),
|
||||
EntitiesJsonContext.Default.ListDictionaryStringJsonElement);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -49,9 +57,9 @@ internal static class WorkflowSerializationExtensions
|
||||
/// <summary>
|
||||
/// Converts workflow executors to a dictionary format compatible with Python
|
||||
/// </summary>
|
||||
private static Dictionary<string, object> ConvertExecutorsToDict(Workflow workflow)
|
||||
private static Dictionary<string, Dictionary<string, string>> ConvertExecutorsToDict(Workflow workflow)
|
||||
{
|
||||
var executors = new Dictionary<string, object>();
|
||||
var executors = new Dictionary<string, Dictionary<string, string>>();
|
||||
|
||||
// Extract executor IDs from edges and start executor
|
||||
// (Registrations is internal, so we infer executors from the graph structure)
|
||||
@@ -73,7 +81,7 @@ internal static class WorkflowSerializationExtensions
|
||||
// Create executor entries (we can't access internal Registrations for type info)
|
||||
foreach (var executorId in executorIds)
|
||||
{
|
||||
executors[executorId] = new Dictionary<string, object>
|
||||
executors[executorId] = new Dictionary<string, string>
|
||||
{
|
||||
["id"] = executorId,
|
||||
["type"] = "Executor"
|
||||
@@ -86,9 +94,9 @@ internal static class WorkflowSerializationExtensions
|
||||
/// <summary>
|
||||
/// Converts workflow edges to edge_groups format expected by the UI
|
||||
/// </summary>
|
||||
private static List<object> ConvertEdgesToEdgeGroups(Workflow workflow)
|
||||
private static List<Dictionary<string, JsonElement>> ConvertEdgesToEdgeGroups(Workflow workflow)
|
||||
{
|
||||
var edgeGroups = new List<object>();
|
||||
var edgeGroups = new List<Dictionary<string, JsonElement>>();
|
||||
var edgeGroupId = 0;
|
||||
|
||||
// Get edges using the public ReflectEdges method
|
||||
@@ -101,13 +109,13 @@ internal static class WorkflowSerializationExtensions
|
||||
if (edgeInfo is DirectEdgeInfo directEdge)
|
||||
{
|
||||
// Single edge group for direct edges
|
||||
var edges = new List<object>();
|
||||
var edges = new List<Dictionary<string, string>>();
|
||||
|
||||
foreach (var source in directEdge.Connection.SourceIds)
|
||||
{
|
||||
foreach (var sink in directEdge.Connection.SinkIds)
|
||||
{
|
||||
var edge = new Dictionary<string, object>
|
||||
var edge = new Dictionary<string, string>
|
||||
{
|
||||
["source_id"] = source,
|
||||
["target_id"] = sink
|
||||
@@ -123,23 +131,25 @@ internal static class WorkflowSerializationExtensions
|
||||
}
|
||||
}
|
||||
|
||||
edgeGroups.Add(new Dictionary<string, object>
|
||||
var edgeGroup = new Dictionary<string, JsonElement>
|
||||
{
|
||||
["id"] = $"edge_group_{edgeGroupId++}",
|
||||
["type"] = "SingleEdgeGroup",
|
||||
["edges"] = edges
|
||||
});
|
||||
["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
|
||||
["type"] = Serialize("SingleEdgeGroup", EntitiesJsonContext.Default.String),
|
||||
["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
|
||||
};
|
||||
|
||||
edgeGroups.Add(edgeGroup);
|
||||
}
|
||||
else if (edgeInfo is FanOutEdgeInfo fanOutEdge)
|
||||
{
|
||||
// FanOut edge group
|
||||
var edges = new List<object>();
|
||||
var edges = new List<Dictionary<string, string>>();
|
||||
|
||||
foreach (var source in fanOutEdge.Connection.SourceIds)
|
||||
{
|
||||
foreach (var sink in fanOutEdge.Connection.SinkIds)
|
||||
{
|
||||
edges.Add(new Dictionary<string, object>
|
||||
edges.Add(new Dictionary<string, string>
|
||||
{
|
||||
["source_id"] = source,
|
||||
["target_id"] = sink
|
||||
@@ -147,16 +157,16 @@ internal static class WorkflowSerializationExtensions
|
||||
}
|
||||
}
|
||||
|
||||
var fanOutGroup = new Dictionary<string, object>
|
||||
var fanOutGroup = new Dictionary<string, JsonElement>
|
||||
{
|
||||
["id"] = $"edge_group_{edgeGroupId++}",
|
||||
["type"] = "FanOutEdgeGroup",
|
||||
["edges"] = edges
|
||||
["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
|
||||
["type"] = Serialize("FanOutEdgeGroup", EntitiesJsonContext.Default.String),
|
||||
["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
|
||||
};
|
||||
|
||||
if (fanOutEdge.HasAssigner)
|
||||
{
|
||||
fanOutGroup["selection_func_name"] = "selector";
|
||||
fanOutGroup["selection_func_name"] = Serialize("selector", EntitiesJsonContext.Default.String);
|
||||
}
|
||||
|
||||
edgeGroups.Add(fanOutGroup);
|
||||
@@ -164,13 +174,13 @@ internal static class WorkflowSerializationExtensions
|
||||
else if (edgeInfo is FanInEdgeInfo fanInEdge)
|
||||
{
|
||||
// FanIn edge group
|
||||
var edges = new List<object>();
|
||||
var edges = new List<Dictionary<string, string>>();
|
||||
|
||||
foreach (var source in fanInEdge.Connection.SourceIds)
|
||||
{
|
||||
foreach (var sink in fanInEdge.Connection.SinkIds)
|
||||
{
|
||||
edges.Add(new Dictionary<string, object>
|
||||
edges.Add(new Dictionary<string, string>
|
||||
{
|
||||
["source_id"] = source,
|
||||
["target_id"] = sink
|
||||
@@ -178,16 +188,20 @@ internal static class WorkflowSerializationExtensions
|
||||
}
|
||||
}
|
||||
|
||||
edgeGroups.Add(new Dictionary<string, object>
|
||||
var edgeGroup = new Dictionary<string, JsonElement>
|
||||
{
|
||||
["id"] = $"edge_group_{edgeGroupId++}",
|
||||
["type"] = "FanInEdgeGroup",
|
||||
["edges"] = edges
|
||||
});
|
||||
["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
|
||||
["type"] = Serialize("FanInEdgeGroup", EntitiesJsonContext.Default.String),
|
||||
["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
|
||||
};
|
||||
|
||||
edgeGroups.Add(edgeGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edgeGroups;
|
||||
}
|
||||
|
||||
private static JsonElement Serialize<T>(T value, JsonTypeInfo<T> typeInfo) => JsonSerializer.SerializeToElement(value, typeInfo);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.Agents.AI.DevUI.Entities;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI;
|
||||
|
||||
@@ -54,81 +57,21 @@ internal static class EntitiesApiExtensions
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = new List<EntityInfo>();
|
||||
var entities = new Dictionary<string, EntityInfo>();
|
||||
|
||||
// Discover agents from the agent catalog
|
||||
if (agentCatalog is not null)
|
||||
// Discover agents
|
||||
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (agent.GetType().Name == "WorkflowHostAgent")
|
||||
{
|
||||
// HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows,
|
||||
// and workflows are handled below.
|
||||
continue;
|
||||
}
|
||||
|
||||
entities.Add(new EntityInfo(
|
||||
Id: agent.Name ?? agent.Id,
|
||||
Type: "agent",
|
||||
Name: agent.Name ?? agent.Id,
|
||||
Description: agent.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: null,
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory"
|
||||
});
|
||||
}
|
||||
entities[agentInfo.Id] = agentInfo;
|
||||
}
|
||||
|
||||
// Discover workflows from the workflow catalog
|
||||
if (workflowCatalog is not null)
|
||||
// Discover workflows
|
||||
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Extract executor IDs from the workflow structure
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default input schema (string type)
|
||||
var defaultInputSchema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
|
||||
entities.Add(new EntityInfo(
|
||||
Id: workflow.Name ?? workflow.StartExecutorId,
|
||||
Type: "workflow",
|
||||
Name: workflow.Name ?? workflow.StartExecutorId,
|
||||
Description: workflow.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: [.. executorIds],
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
|
||||
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
|
||||
InputTypeName = "string",
|
||||
StartExecutorId = workflow.StartExecutorId
|
||||
});
|
||||
}
|
||||
entities[workflowInfo.Id] = workflowInfo;
|
||||
}
|
||||
|
||||
return Results.Json(new DiscoveryResponse(entities), EntitiesJsonContext.Default.DiscoveryResponse);
|
||||
return Results.Json(new DiscoveryResponse([.. entities.Values.OrderBy(e => e.Id)]), EntitiesJsonContext.Default.DiscoveryResponse);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -141,93 +84,26 @@ internal static class EntitiesApiExtensions
|
||||
|
||||
private static async Task<IResult> GetEntityInfoAsync(
|
||||
string entityId,
|
||||
string? type,
|
||||
AgentCatalog? agentCatalog,
|
||||
WorkflowCatalog? workflowCatalog,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to find the entity among discovered agents
|
||||
if (agentCatalog is not null)
|
||||
if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (agent.GetType().Name == "WorkflowHostAgent")
|
||||
{
|
||||
// HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows,
|
||||
// and workflows are handled below.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(agent.Name, entityId, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(agent.Id, entityId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var entityInfo = new EntityInfo(
|
||||
Id: agent.Name ?? agent.Id,
|
||||
Type: "agent",
|
||||
Name: agent.Name ?? agent.Id,
|
||||
Description: agent.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: null,
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory"
|
||||
};
|
||||
|
||||
return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find the entity among discovered workflows
|
||||
if (workflowCatalog is not null)
|
||||
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
||||
if (string.Equals(workflowId, entityId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract executor IDs from the workflow structure
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default input schema (string type)
|
||||
var defaultInputSchema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
|
||||
var entityInfo = new EntityInfo(
|
||||
Id: workflowId,
|
||||
Type: "workflow",
|
||||
Name: workflow.Name ?? workflow.StartExecutorId,
|
||||
Description: workflow.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: [.. executorIds],
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
|
||||
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
|
||||
InputTypeName = "Input",
|
||||
StartExecutorId = workflow.StartExecutorId
|
||||
};
|
||||
|
||||
return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,4 +117,191 @@ internal static class EntitiesApiExtensions
|
||||
title: "Error getting entity info");
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<EntityInfo> DiscoverAgentsAsync(
|
||||
AgentCatalog? agentCatalog,
|
||||
string? entityIdFilter,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (agentCatalog is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// If filtering by entity ID, skip non-matching agents
|
||||
if (entityIdFilter is not null &&
|
||||
!string.Equals(agent.Name, entityIdFilter, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(agent.Id, entityIdFilter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return CreateAgentEntityInfo(agent);
|
||||
|
||||
// If we found the entity we're looking for, we're done
|
||||
if (entityIdFilter is not null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<EntityInfo> DiscoverWorkflowsAsync(
|
||||
WorkflowCatalog? workflowCatalog,
|
||||
string? entityIdFilter,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (workflowCatalog is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
||||
|
||||
// If filtering by entity ID, skip non-matching workflows
|
||||
if (entityIdFilter is not null && !string.Equals(workflowId, entityIdFilter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return CreateWorkflowEntityInfo(workflow);
|
||||
|
||||
// If we found the entity we're looking for, we're done
|
||||
if (entityIdFilter is not null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityInfo CreateAgentEntityInfo(AIAgent agent)
|
||||
{
|
||||
var entityId = agent.Name ?? agent.Id;
|
||||
|
||||
// Extract tools and other metadata using GetService
|
||||
List<string> tools = [];
|
||||
var metadata = new Dictionary<string, JsonElement>();
|
||||
|
||||
// Try to get ChatOptions from the agent which may contain tools
|
||||
if (agent.GetService<ChatOptions>() is { Tools: { Count: > 0 } agentTools })
|
||||
{
|
||||
tools = agentTools
|
||||
.Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
|
||||
.Select(tool => tool.Name!)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Extract agent-specific fields (top-level properties for compatibility with Python)
|
||||
string? instructions = null;
|
||||
string? modelId = null;
|
||||
string? chatClientType = null;
|
||||
|
||||
// Get instructions from ChatClientAgent
|
||||
if (agent is ChatClientAgent chatAgent && !string.IsNullOrWhiteSpace(chatAgent.Instructions))
|
||||
{
|
||||
instructions = chatAgent.Instructions;
|
||||
}
|
||||
|
||||
// Get IChatClient to extract metadata
|
||||
IChatClient? chatClient = agent.GetService<IChatClient>();
|
||||
if (chatClient != null)
|
||||
{
|
||||
// Get chat client type
|
||||
chatClientType = chatClient.GetType().Name;
|
||||
|
||||
// Get model ID from ChatClientMetadata
|
||||
if (chatClient.GetService<ChatClientMetadata>() is { } chatClientMetadata)
|
||||
{
|
||||
modelId = chatClientMetadata.DefaultModelId;
|
||||
|
||||
// Add additional metadata for compatibility
|
||||
if (!string.IsNullOrWhiteSpace(chatClientMetadata.ProviderName))
|
||||
{
|
||||
metadata["chat_client_provider"] = JsonSerializer.SerializeToElement(chatClientMetadata.ProviderName, EntitiesJsonContext.Default.String);
|
||||
}
|
||||
|
||||
if (chatClientMetadata.ProviderUri is not null)
|
||||
{
|
||||
metadata["provider_uri"] = JsonSerializer.SerializeToElement(chatClientMetadata.ProviderUri.ToString(), EntitiesJsonContext.Default.String);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add provider name from AIAgentMetadata if available
|
||||
if (agent.GetService<AIAgentMetadata>() is { } agentMetadata && !string.IsNullOrWhiteSpace(agentMetadata.ProviderName))
|
||||
{
|
||||
metadata["provider_name"] = JsonSerializer.SerializeToElement(agentMetadata.ProviderName, EntitiesJsonContext.Default.String);
|
||||
}
|
||||
|
||||
// Add agent type information to metadata (in addition to chat_client_type)
|
||||
var agentTypeName = agent.GetType().Name;
|
||||
metadata["agent_type"] = JsonSerializer.SerializeToElement(agentTypeName, EntitiesJsonContext.Default.String);
|
||||
|
||||
return new EntityInfo(
|
||||
Id: entityId,
|
||||
Type: "agent",
|
||||
Name: agent.DisplayName,
|
||||
Description: agent.Description,
|
||||
Framework: "agent_framework",
|
||||
Tools: tools,
|
||||
Metadata: metadata
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
Instructions = instructions,
|
||||
ModelId = modelId,
|
||||
ChatClientType = chatClientType,
|
||||
Executors = [], // Agents have empty executors list (workflows use this field)
|
||||
};
|
||||
}
|
||||
|
||||
private static EntityInfo CreateWorkflowEntityInfo(Workflow workflow)
|
||||
{
|
||||
// Extract executor IDs from the workflow structure
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default input schema (string type)
|
||||
var defaultInputSchema = new Dictionary<string, string>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
|
||||
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
||||
return new EntityInfo(
|
||||
Id: workflowId,
|
||||
Type: "workflow",
|
||||
Name: workflowId,
|
||||
Description: workflow.Description,
|
||||
Framework: "agent_framework",
|
||||
Tools: [],
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
Executors = [.. executorIds], // Workflows use Executors instead of Tools
|
||||
WorkflowDump = JsonSerializer.SerializeToElement(
|
||||
workflow.ToDevUIDict(),
|
||||
EntitiesJsonContext.Default.DictionaryStringJsonElement),
|
||||
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema, EntitiesJsonContext.Default.DictionaryStringString),
|
||||
InputTypeName = "string",
|
||||
StartExecutorId = workflow.StartExecutorId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.DevUI.Entities;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for mapping the server metadata endpoint to an <see cref="IEndpointRouteBuilder"/>.
|
||||
/// </summary>
|
||||
internal static class MetaApiExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps the HTTP API endpoint for retrieving server metadata.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <returns>The <see cref="IEndpointConventionBuilder"/> for method chaining.</returns>
|
||||
/// <remarks>
|
||||
/// This extension method registers the following endpoint:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>GET /meta - Retrieve server metadata including UI mode, version, capabilities, and auth requirements</description></item>
|
||||
/// </list>
|
||||
/// The endpoint is compatible with the Python DevUI frontend and provides essential
|
||||
/// configuration information needed for proper frontend initialization.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapMeta(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
return endpoints.MapGet("/meta", GetMeta)
|
||||
.WithName("GetMeta")
|
||||
.WithSummary("Get server metadata and configuration")
|
||||
.WithDescription("Returns server metadata including UI mode, version, framework identifier, capabilities, and authentication requirements. Used by the frontend for initialization and feature detection.")
|
||||
.Produces<MetaResponse>(StatusCodes.Status200OK, contentType: "application/json");
|
||||
}
|
||||
|
||||
private static IResult GetMeta()
|
||||
{
|
||||
// TODO: Consider making these configurable via IOptions<DevUIOptions>
|
||||
// For now, using sensible defaults that match Python DevUI behavior
|
||||
|
||||
var meta = new MetaResponse
|
||||
{
|
||||
UiMode = "developer", // Could be made configurable to support "user" mode
|
||||
Version = "0.1.0", // TODO: Extract from assembly version attribute
|
||||
Framework = "agent_framework",
|
||||
Runtime = "dotnet", // .NET runtime for deployment guides
|
||||
Capabilities = new Dictionary<string, bool>
|
||||
{
|
||||
// Tracing capability - will be enabled when trace event support is added
|
||||
["tracing"] = false,
|
||||
|
||||
// OpenAI proxy capability - not currently supported in .NET DevUI
|
||||
["openai_proxy"] = false,
|
||||
|
||||
// Deployment capability - not currently supported in .NET DevUI
|
||||
["deployment"] = false
|
||||
},
|
||||
AuthRequired = false // Could be made configurable based on authentication middleware
|
||||
};
|
||||
|
||||
return Results.Json(meta, EntitiesJsonContext.Default.MetaResponse);
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,16 @@ public static class AIAgentExtensions
|
||||
{
|
||||
// A2A SDK assigns the url on its own
|
||||
// we can help user if they did not set Url explicitly.
|
||||
agentCard.Url ??= context;
|
||||
if (string.IsNullOrEmpty(agentCard.Url))
|
||||
{
|
||||
var agentCardUrl = context.TrimEnd('/');
|
||||
if (!context.EndsWith("/v1/card", StringComparison.Ordinal))
|
||||
{
|
||||
agentCardUrl += "/v1/card";
|
||||
}
|
||||
|
||||
agentCard.Url = agentCardUrl;
|
||||
}
|
||||
|
||||
return Task.FromResult(agentCard);
|
||||
};
|
||||
|
||||
+15
-10
@@ -44,22 +44,27 @@ public static class AGUIEndpointRouteBuilderExtensions
|
||||
var jsonSerializerOptions = jsonOptions.Value.SerializerOptions;
|
||||
|
||||
var messages = input.Messages.AsChatMessages(jsonSerializerOptions);
|
||||
var agent = aiAgent;
|
||||
var clientTools = input.Tools?.AsAITools().ToList();
|
||||
|
||||
ChatClientAgentRunOptions? runOptions = null;
|
||||
List<AITool>? clientTools = input.Tools?.AsAITools().ToList();
|
||||
if (clientTools?.Count > 0)
|
||||
// Create run options with AG-UI context in AdditionalProperties
|
||||
var runOptions = new ChatClientAgentRunOptions
|
||||
{
|
||||
runOptions = new ChatClientAgentRunOptions
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
ChatOptions = new ChatOptions
|
||||
Tools = clientTools,
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary
|
||||
{
|
||||
Tools = clientTools
|
||||
["ag_ui_state"] = input.State,
|
||||
["ag_ui_context"] = input.Context?.Select(c => new KeyValuePair<string, string>(c.Description, c.Value)).ToArray(),
|
||||
["ag_ui_forwarded_properties"] = input.ForwardedProperties,
|
||||
["ag_ui_thread_id"] = input.ThreadId,
|
||||
["ag_ui_run_id"] = input.RunId
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var events = agent.RunStreamingAsync(
|
||||
// Run the agent and convert to AG-UI events
|
||||
var events = aiAgent.RunStreamingAsync(
|
||||
messages,
|
||||
options: runOptions,
|
||||
cancellationToken: cancellationToken)
|
||||
|
||||
@@ -18,7 +18,7 @@ internal abstract record Tool
|
||||
/// <summary>
|
||||
/// The type of the tool.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
[JsonIgnore]
|
||||
public abstract string Type { get; }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ internal sealed record FunctionTool : Tool
|
||||
/// <summary>
|
||||
/// The type of the tool. Always "function".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
[JsonIgnore]
|
||||
public override string Type => "function";
|
||||
|
||||
/// <summary>
|
||||
@@ -88,7 +88,7 @@ internal sealed record CustomTool : Tool
|
||||
/// <summary>
|
||||
/// The type of the tool. Always "custom".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
[JsonIgnore]
|
||||
public override string Type => "custom";
|
||||
|
||||
/// <summary>
|
||||
@@ -160,5 +160,5 @@ internal sealed record CustomToolFormat
|
||||
/// Additional format properties (schema definition).
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object?>? AdditionalProperties { get; init; }
|
||||
public Dictionary<string, object?>? AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ internal static class OpenAIHostingJsonUtilities
|
||||
[JsonSerializable(typeof(MCPApprovalRequestItemResource))]
|
||||
[JsonSerializable(typeof(MCPApprovalResponseItemResource))]
|
||||
[JsonSerializable(typeof(MCPCallItemResource))]
|
||||
[JsonSerializable(typeof(ExecutorActionItemResource))]
|
||||
[JsonSerializable(typeof(List<ItemResource>))]
|
||||
// ItemParam types
|
||||
[JsonSerializable(typeof(ItemParam))]
|
||||
|
||||
+89
-1
@@ -45,6 +45,9 @@ internal static class AgentRunResponseUpdateExtensions
|
||||
var updateEnumerator = updates.GetAsyncEnumerator(cancellationToken);
|
||||
await using var _ = updateEnumerator.ConfigureAwait(false);
|
||||
|
||||
// Track active item IDs by executor ID to pair invoked/completed/failed events
|
||||
Dictionary<string, string> executorItemIds = [];
|
||||
|
||||
AgentRunResponseUpdate? previousUpdate = null;
|
||||
StreamingEventGenerator? generator = null;
|
||||
while (await updateEnumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
@@ -55,7 +58,92 @@ internal static class AgentRunResponseUpdateExtensions
|
||||
// Special-case for agent framework workflow events.
|
||||
if (update.RawRepresentation is WorkflowEvent workflowEvent)
|
||||
{
|
||||
yield return CreateWorkflowEventResponse(workflowEvent, seq.Increment(), outputIndex);
|
||||
// Convert executor events to standard OpenAI output_item events
|
||||
if (workflowEvent is ExecutorInvokedEvent invokedEvent)
|
||||
{
|
||||
var itemId = IdGenerator.NewId(prefix: "item");
|
||||
// Store the item ID for this executor so we can reuse it for completion/failure
|
||||
executorItemIds[invokedEvent.ExecutorId] = itemId;
|
||||
|
||||
var item = new ExecutorActionItemResource
|
||||
{
|
||||
Id = itemId,
|
||||
ExecutorId = invokedEvent.ExecutorId,
|
||||
Status = "in_progress",
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
yield return new StreamingOutputItemAdded
|
||||
{
|
||||
SequenceNumber = seq.Increment(),
|
||||
OutputIndex = outputIndex,
|
||||
Item = item
|
||||
};
|
||||
}
|
||||
else if (workflowEvent is ExecutorCompletedEvent completedEvent)
|
||||
{
|
||||
// Reuse the item ID from the invoked event, or generate a new one if not found
|
||||
var itemId = executorItemIds.TryGetValue(completedEvent.ExecutorId, out var existingId)
|
||||
? existingId
|
||||
: IdGenerator.NewId(prefix: "item");
|
||||
|
||||
// Remove from tracking as this executor run is now complete
|
||||
executorItemIds.Remove(completedEvent.ExecutorId);
|
||||
JsonElement? resultData = null;
|
||||
if (completedEvent.Data != null && JsonSerializer.IsReflectionEnabledByDefault)
|
||||
{
|
||||
resultData = JsonSerializer.SerializeToElement(
|
||||
completedEvent.Data,
|
||||
OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
|
||||
}
|
||||
|
||||
var item = new ExecutorActionItemResource
|
||||
{
|
||||
Id = itemId,
|
||||
ExecutorId = completedEvent.ExecutorId,
|
||||
Status = "completed",
|
||||
Result = resultData,
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
yield return new StreamingOutputItemDone
|
||||
{
|
||||
SequenceNumber = seq.Increment(),
|
||||
OutputIndex = outputIndex,
|
||||
Item = item
|
||||
};
|
||||
}
|
||||
else if (workflowEvent is ExecutorFailedEvent failedEvent)
|
||||
{
|
||||
// Reuse the item ID from the invoked event, or generate a new one if not found
|
||||
var itemId = executorItemIds.TryGetValue(failedEvent.ExecutorId, out var existingId)
|
||||
? existingId
|
||||
: IdGenerator.NewId(prefix: "item");
|
||||
|
||||
// Remove from tracking as this executor run has now failed
|
||||
executorItemIds.Remove(failedEvent.ExecutorId);
|
||||
|
||||
var item = new ExecutorActionItemResource
|
||||
{
|
||||
Id = itemId,
|
||||
ExecutorId = failedEvent.ExecutorId,
|
||||
Status = "failed",
|
||||
Error = failedEvent.Data?.ToString(),
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
yield return new StreamingOutputItemDone
|
||||
{
|
||||
SequenceNumber = seq.Increment(),
|
||||
OutputIndex = outputIndex,
|
||||
Item = item
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// For other workflow events (not executor-specific), keep the old format as fallback
|
||||
yield return CreateWorkflowEventResponse(workflowEvent, seq.Increment(), outputIndex);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -45,6 +45,7 @@ internal sealed class ItemResourceConverter : JsonConverter<ItemResource>
|
||||
MCPApprovalRequestItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPApprovalRequestItemResource),
|
||||
MCPApprovalResponseItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPApprovalResponseItemResource),
|
||||
MCPCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPCallItemResource),
|
||||
ExecutorActionItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.ExecutorActionItemResource),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
@@ -106,6 +107,9 @@ internal sealed class ItemResourceConverter : JsonConverter<ItemResource>
|
||||
case MCPCallItemResource mcpCall:
|
||||
JsonSerializer.Serialize(writer, mcpCall, OpenAIHostingJsonContext.Default.MCPCallItemResource);
|
||||
break;
|
||||
case ExecutorActionItemResource executorAction:
|
||||
JsonSerializer.Serialize(writer, executorAction, OpenAIHostingJsonContext.Default.ExecutorActionItemResource);
|
||||
break;
|
||||
default:
|
||||
throw new JsonException($"Unknown item type: {value.GetType().Name}");
|
||||
}
|
||||
|
||||
@@ -888,3 +888,47 @@ internal sealed class MCPCallItemResource : ItemResource
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An executor action item resource for workflow execution visualization.
|
||||
/// </summary>
|
||||
internal sealed class ExecutorActionItemResource : ItemResource
|
||||
{
|
||||
/// <summary>
|
||||
/// The constant item type identifier for executor action items.
|
||||
/// </summary>
|
||||
public const string ItemType = "executor_action";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Type => ItemType;
|
||||
|
||||
/// <summary>
|
||||
/// The executor identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("executor_id")]
|
||||
public required string ExecutorId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The execution status: "in_progress", "completed", "failed", or "cancelled".
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The executor result data (for completed status).
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public JsonElement? Result { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The error message (for failed status).
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The creation timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public long CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
+1
-1
@@ -565,7 +565,7 @@ internal sealed class StreamingWorkflowEventComplete : StreamingResponseEvent
|
||||
/// <summary>
|
||||
/// The constant event type identifier for workflow event events.
|
||||
/// </summary>
|
||||
public const string EventType = "response.workflow_event.complete";
|
||||
public const string EventType = "response.workflow_event.completed";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration options for <see cref="ChatForwardingExecutor"/>.
|
||||
/// </summary>
|
||||
public class ChatForwardingExecutorOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the chat role to use when converting string messages to <see cref="ChatMessage"/> instances.
|
||||
/// If set, the executor will accept string messages and convert them to chat messages with this role.
|
||||
/// </summary>
|
||||
public ChatRole? StringMessageChatRole { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A ChatProtocol executor that forwards all messages it receives. Useful for splitting inputs into parallel
|
||||
/// processing paths.
|
||||
/// </summary>
|
||||
/// <remarks>This executor is designed to be cross-run shareable and can be reset to its initial state. It handles
|
||||
/// multiple chat-related types, enabling flexible message forwarding scenarios. Thread safety and reusability are
|
||||
/// ensured by its design.</remarks>
|
||||
/// <param name="id">The unique identifier for the executor instance. Used to distinguish this executor within the system.</param>
|
||||
/// <param name="options">Optional configuration settings for the executor. If null, default options are used.</param>
|
||||
public sealed class ChatForwardingExecutor(string id, ChatForwardingExecutorOptions? options = null) : Executor(id, declareCrossRunShareable: true), IResettableExecutor
|
||||
{
|
||||
private readonly ChatRole? _stringMessageChatRole = options?.StringMessageChatRole;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
|
||||
{
|
||||
if (this._stringMessageChatRole.HasValue)
|
||||
{
|
||||
routeBuilder = routeBuilder.AddHandler<string>(
|
||||
(message, context) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message)));
|
||||
}
|
||||
|
||||
return routeBuilder.AddHandler<ChatMessage>(ForwardMessageAsync)
|
||||
.AddHandler<IEnumerable<ChatMessage>>(ForwardMessagesAsync)
|
||||
.AddHandler<ChatMessage[]>(ForwardMessagesAsync)
|
||||
.AddHandler<List<ChatMessage>>(ForwardMessagesAsync)
|
||||
.AddHandler<TurnToken>(ForwardTurnTokenAsync);
|
||||
}
|
||||
|
||||
private static ValueTask ForwardMessageAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
// Note that this can be used to split a turn into multiple parallel turns taken, which will cause streaming ChatMessages
|
||||
// to overlap.
|
||||
private static ValueTask ForwardTurnTokenAsync(TurnToken message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
// TODO: This is not ideal, but until we have a way of guaranteeing correct routing of interfaces across serialization
|
||||
// boundaries, we need to do type unification. It behaves better when used as a handler in ChatProtocolExecutor because
|
||||
// it is a strictly contravariant use, whereas this forces invariance on the type because it is directly forwarded.
|
||||
private static ValueTask ForwardMessagesAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(messages is List<ChatMessage> messageList ? messageList : messages.ToList(), cancellationToken);
|
||||
|
||||
private static ValueTask ForwardMessagesAsync(ChatMessage[] messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(messages, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask ResetAsync() => default;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Specialized;
|
||||
|
||||
/// <summary>Executor that forwards all messages.</summary>
|
||||
internal sealed class ChatForwardingExecutor(string id) : Executor(id, declareCrossRunShareable: true), IResettableExecutor
|
||||
{
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder
|
||||
.AddHandler<string>((message, context, cancellationToken) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken: cancellationToken))
|
||||
.AddHandler<ChatMessage>((message, context, cancellationToken) => context.SendMessageAsync(message, cancellationToken: cancellationToken))
|
||||
.AddHandler<List<ChatMessage>>((messages, context, cancellationToken) => context.SendMessageAsync(messages, cancellationToken: cancellationToken))
|
||||
.AddHandler<TurnToken>((turnToken, context, cancellationToken) => context.SendMessageAsync(turnToken, cancellationToken: cancellationToken));
|
||||
|
||||
public ValueTask ResetAsync() => default;
|
||||
}
|
||||
@@ -281,6 +281,8 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
base.GetService(serviceType, serviceKey) ??
|
||||
(serviceType == typeof(AIAgentMetadata) ? this._agentMetadata
|
||||
: serviceType == typeof(IChatClient) ? this.ChatClient
|
||||
: serviceType == typeof(ChatOptions) ? this._agentOptions?.ChatOptions
|
||||
: serviceType == typeof(ChatClientAgentOptions) ? this._agentOptions
|
||||
: this.ChatClient.GetService(serviceType, serviceKey));
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -1282,6 +1282,312 @@ public sealed class AGUIAgentTests
|
||||
// AG-UI requirement: full history on every turn (which happens when ConversationId is null for FunctionInvokingChatClient)
|
||||
Assert.True(captureHandler.RequestWasMade);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_ExtractsStateFromDataContent_AndRemovesStateMessageAsync()
|
||||
{
|
||||
// Arrange
|
||||
var stateData = new { counter = 42, status = "active" };
|
||||
string stateJson = JsonSerializer.Serialize(stateData);
|
||||
byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson);
|
||||
var dataContent = new DataContent(stateBytes, "application/json");
|
||||
|
||||
var captureHandler = new StateCapturingTestDelegatingHandler();
|
||||
captureHandler.AddResponse(
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new TextMessageStartEvent { MessageId = "msg1", Role = AGUIRoles.Assistant },
|
||||
new TextMessageContentEvent { MessageId = "msg1", Delta = "Response" },
|
||||
new TextMessageEndEvent { MessageId = "msg1" },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
]);
|
||||
using HttpClient httpClient = new(captureHandler);
|
||||
|
||||
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.System, [dataContent])
|
||||
];
|
||||
|
||||
// Act
|
||||
await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
// Just consume the stream
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.True(captureHandler.RequestWasMade);
|
||||
Assert.NotNull(captureHandler.CapturedState);
|
||||
Assert.Equal(42, captureHandler.CapturedState.Value.GetProperty("counter").GetInt32());
|
||||
Assert.Equal("active", captureHandler.CapturedState.Value.GetProperty("status").GetString());
|
||||
|
||||
// Verify state message was removed - only user message should be in the request
|
||||
Assert.Equal(1, captureHandler.CapturedMessageCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_WithNoStateDataContent_SendsEmptyStateAsync()
|
||||
{
|
||||
// Arrange
|
||||
var captureHandler = new StateCapturingTestDelegatingHandler();
|
||||
captureHandler.AddResponse(
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new TextMessageStartEvent { MessageId = "msg1", Role = AGUIRoles.Assistant },
|
||||
new TextMessageContentEvent { MessageId = "msg1", Delta = "Response" },
|
||||
new TextMessageEndEvent { MessageId = "msg1" },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
]);
|
||||
using HttpClient httpClient = new(captureHandler);
|
||||
|
||||
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
|
||||
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
|
||||
|
||||
// Act
|
||||
await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
// Just consume the stream
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.True(captureHandler.RequestWasMade);
|
||||
Assert.Null(captureHandler.CapturedState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_WithMalformedStateJson_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] invalidJson = System.Text.Encoding.UTF8.GetBytes("{invalid json");
|
||||
var dataContent = new DataContent(invalidJson, "application/json");
|
||||
|
||||
using HttpClient httpClient = this.CreateMockHttpClient([]);
|
||||
|
||||
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.System, [dataContent])
|
||||
];
|
||||
|
||||
// Act & Assert
|
||||
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
// Just consume the stream
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Contains("Failed to deserialize state JSON", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_WithEmptyStateObject_SendsEmptyObjectAsync()
|
||||
{
|
||||
// Arrange
|
||||
var emptyState = new { };
|
||||
string stateJson = JsonSerializer.Serialize(emptyState);
|
||||
byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson);
|
||||
var dataContent = new DataContent(stateBytes, "application/json");
|
||||
|
||||
var captureHandler = new StateCapturingTestDelegatingHandler();
|
||||
captureHandler.AddResponse(
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
]);
|
||||
using HttpClient httpClient = new(captureHandler);
|
||||
|
||||
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.System, [dataContent])
|
||||
];
|
||||
|
||||
// Act
|
||||
await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
// Just consume the stream
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.True(captureHandler.RequestWasMade);
|
||||
Assert.NotNull(captureHandler.CapturedState);
|
||||
Assert.Equal(JsonValueKind.Object, captureHandler.CapturedState.Value.ValueKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_OnlyProcessesDataContentFromLastMessage_IgnoresEarlierOnesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var oldState = new { counter = 10 };
|
||||
string oldStateJson = JsonSerializer.Serialize(oldState);
|
||||
byte[] oldStateBytes = System.Text.Encoding.UTF8.GetBytes(oldStateJson);
|
||||
var oldDataContent = new DataContent(oldStateBytes, "application/json");
|
||||
|
||||
var newState = new { counter = 20 };
|
||||
string newStateJson = JsonSerializer.Serialize(newState);
|
||||
byte[] newStateBytes = System.Text.Encoding.UTF8.GetBytes(newStateJson);
|
||||
var newDataContent = new DataContent(newStateBytes, "application/json");
|
||||
|
||||
var captureHandler = new StateCapturingTestDelegatingHandler();
|
||||
captureHandler.AddResponse(
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
]);
|
||||
using HttpClient httpClient = new(captureHandler);
|
||||
|
||||
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First message"),
|
||||
new ChatMessage(ChatRole.System, [oldDataContent]),
|
||||
new ChatMessage(ChatRole.User, "Second message"),
|
||||
new ChatMessage(ChatRole.System, [newDataContent])
|
||||
];
|
||||
|
||||
// Act
|
||||
await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
// Just consume the stream
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.True(captureHandler.RequestWasMade);
|
||||
Assert.NotNull(captureHandler.CapturedState);
|
||||
// Should use the new state from the last message
|
||||
Assert.Equal(20, captureHandler.CapturedState.Value.GetProperty("counter").GetInt32());
|
||||
|
||||
// Should have removed only the last state message
|
||||
Assert.Equal(3, captureHandler.CapturedMessageCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_WithNonJsonMediaType_IgnoresDataContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
byte[] imageData = System.Text.Encoding.UTF8.GetBytes("fake image data");
|
||||
var dataContent = new DataContent(imageData, "image/png");
|
||||
|
||||
var captureHandler = new StateCapturingTestDelegatingHandler();
|
||||
captureHandler.AddResponse(
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
]);
|
||||
using HttpClient httpClient = new(captureHandler);
|
||||
|
||||
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, [new TextContent("Hello"), dataContent])
|
||||
];
|
||||
|
||||
// Act
|
||||
await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
// Just consume the stream
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.True(captureHandler.RequestWasMade);
|
||||
Assert.Null(captureHandler.CapturedState);
|
||||
// Message should not be removed since it's not state
|
||||
Assert.Equal(1, captureHandler.CapturedMessageCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_RoundTripState_PreservesJsonStructureAsync()
|
||||
{
|
||||
// Arrange - Server returns state snapshot
|
||||
var returnedState = new { counter = 100, nested = new { value = "test" } };
|
||||
JsonElement stateSnapshot = JsonSerializer.SerializeToElement(returnedState);
|
||||
|
||||
var captureHandler = new StateCapturingTestDelegatingHandler();
|
||||
captureHandler.AddResponse(
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateSnapshotEvent { Snapshot = stateSnapshot },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
]);
|
||||
captureHandler.AddResponse(
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run2" },
|
||||
new TextMessageStartEvent { MessageId = "msg1", Role = AGUIRoles.Assistant },
|
||||
new TextMessageContentEvent { MessageId = "msg1", Delta = "Done" },
|
||||
new TextMessageEndEvent { MessageId = "msg1" },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run2" }
|
||||
]);
|
||||
using HttpClient httpClient = new(captureHandler);
|
||||
|
||||
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
|
||||
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
|
||||
|
||||
// Act - First turn: receive state
|
||||
DataContent? receivedStateContent = null;
|
||||
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
if (update.Contents.Any(c => c is DataContent dc && dc.MediaType == "application/json"))
|
||||
{
|
||||
receivedStateContent = (DataContent)update.Contents.First(c => c is DataContent);
|
||||
}
|
||||
}
|
||||
|
||||
// Second turn: send the received state back
|
||||
Assert.NotNull(receivedStateContent);
|
||||
messages.Add(new ChatMessage(ChatRole.System, [receivedStateContent]));
|
||||
await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
// Just consume the stream
|
||||
}
|
||||
|
||||
// Assert - Verify the round-tripped state
|
||||
Assert.NotNull(captureHandler.CapturedState);
|
||||
Assert.Equal(100, captureHandler.CapturedState.Value.GetProperty("counter").GetInt32());
|
||||
Assert.Equal("test", captureHandler.CapturedState.Value.GetProperty("nested").GetProperty("value").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_ReceivesStateSnapshot_AsDataContentWithAdditionalPropertiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var state = new { sessionId = "abc123", step = 5 };
|
||||
JsonElement stateSnapshot = JsonSerializer.SerializeToElement(state);
|
||||
|
||||
using HttpClient httpClient = this.CreateMockHttpClient(
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateSnapshotEvent { Snapshot = stateSnapshot },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
]);
|
||||
|
||||
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
|
||||
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Test")];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, null))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
ChatResponseUpdate stateUpdate = updates.First(u => u.Contents.Any(c => c is DataContent));
|
||||
Assert.NotNull(stateUpdate.AdditionalProperties);
|
||||
Assert.True((bool)stateUpdate.AdditionalProperties!["is_state_snapshot"]!);
|
||||
|
||||
DataContent dataContent = (DataContent)stateUpdate.Contents[0];
|
||||
Assert.Equal("application/json", dataContent.MediaType);
|
||||
|
||||
string jsonText = System.Text.Encoding.UTF8.GetString(dataContent.Data.ToArray());
|
||||
JsonElement deserializedState = JsonSerializer.Deserialize<JsonElement>(jsonText);
|
||||
Assert.Equal("abc123", deserializedState.GetProperty("sessionId").GetString());
|
||||
Assert.Equal(5, deserializedState.GetProperty("step").GetInt32());
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TestDelegatingHandler : DelegatingHandler
|
||||
@@ -1376,3 +1682,58 @@ internal sealed class CapturingTestDelegatingHandler : DelegatingHandler
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class StateCapturingTestDelegatingHandler : DelegatingHandler
|
||||
{
|
||||
private readonly Queue<Func<HttpRequestMessage, Task<HttpResponseMessage>>> _responseFactories = new();
|
||||
|
||||
public bool RequestWasMade { get; private set; }
|
||||
public JsonElement? CapturedState { get; private set; }
|
||||
public int CapturedMessageCount { get; private set; }
|
||||
|
||||
public void AddResponse(BaseEvent[] events)
|
||||
{
|
||||
this._responseFactories.Enqueue(_ => Task.FromResult(CreateResponse(events)));
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
this.RequestWasMade = true;
|
||||
|
||||
// Capture the state and message count from the request
|
||||
#if NET472 || NETSTANDARD2_0
|
||||
string requestBody = await request.Content!.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#else
|
||||
string requestBody = await request.Content!.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
RunAgentInput? input = JsonSerializer.Deserialize(requestBody, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
if (input != null)
|
||||
{
|
||||
if (input.State.ValueKind != JsonValueKind.Undefined && input.State.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
this.CapturedState = input.State;
|
||||
}
|
||||
this.CapturedMessageCount = input.Messages.Count();
|
||||
}
|
||||
|
||||
if (this._responseFactories.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No more responses configured for StateCapturingTestDelegatingHandler.");
|
||||
}
|
||||
|
||||
var factory = this._responseFactories.Dequeue();
|
||||
return await factory(request);
|
||||
}
|
||||
|
||||
private static HttpResponseMessage CreateResponse(BaseEvent[] events)
|
||||
{
|
||||
string sseContent = string.Join("", events.Select(e =>
|
||||
$"data: {JsonSerializer.Serialize(e, AGUIJsonSerializerContext.Default.BaseEvent)}\n\n"));
|
||||
|
||||
return new HttpResponseMessage
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
Content = new StringContent(sseContent)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+408
@@ -369,4 +369,412 @@ public sealed class ChatResponseUpdateAGUIExtensionsTests
|
||||
Assert.Equal("call_2", functionCalls[1].CallId);
|
||||
Assert.Equal("Tool2", functionCalls[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_ConvertsStateSnapshotEvent_ToDataContentWithJsonAsync()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement stateSnapshot = JsonSerializer.SerializeToElement(new { counter = 42, status = "active" });
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateSnapshotEvent { Snapshot = stateSnapshot },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
ChatResponseUpdate stateUpdate = updates.First(u => u.Contents.Any(c => c is DataContent));
|
||||
Assert.Equal(ChatRole.Assistant, stateUpdate.Role);
|
||||
Assert.Equal("thread1", stateUpdate.ConversationId);
|
||||
Assert.Equal("run1", stateUpdate.ResponseId);
|
||||
|
||||
DataContent dataContent = Assert.IsType<DataContent>(stateUpdate.Contents[0]);
|
||||
Assert.Equal("application/json", dataContent.MediaType);
|
||||
|
||||
// Verify the JSON content
|
||||
string jsonText = System.Text.Encoding.UTF8.GetString(dataContent.Data.ToArray());
|
||||
JsonElement deserializedState = JsonSerializer.Deserialize<JsonElement>(jsonText);
|
||||
Assert.Equal(42, deserializedState.GetProperty("counter").GetInt32());
|
||||
Assert.Equal("active", deserializedState.GetProperty("status").GetString());
|
||||
|
||||
// Verify additional properties
|
||||
Assert.NotNull(stateUpdate.AdditionalProperties);
|
||||
Assert.True((bool)stateUpdate.AdditionalProperties["is_state_snapshot"]!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithNullStateSnapshot_DoesNotEmitUpdateAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateSnapshotEvent { Snapshot = null },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(updates, u => u.Contents.Any(c => c is DataContent));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithEmptyObjectStateSnapshot_EmitsDataContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement emptyState = JsonSerializer.SerializeToElement(new { });
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateSnapshotEvent { Snapshot = emptyState },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
ChatResponseUpdate stateUpdate = updates.First(u => u.Contents.Any(c => c is DataContent));
|
||||
DataContent dataContent = Assert.IsType<DataContent>(stateUpdate.Contents[0]);
|
||||
string jsonText = System.Text.Encoding.UTF8.GetString(dataContent.Data.ToArray());
|
||||
Assert.Equal("{}", jsonText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithComplexStateSnapshot_PreservesJsonStructureAsync()
|
||||
{
|
||||
// Arrange
|
||||
var complexState = new
|
||||
{
|
||||
user = new { name = "Alice", age = 30 },
|
||||
items = new[] { "item1", "item2", "item3" },
|
||||
metadata = new { timestamp = "2024-01-01T00:00:00Z", version = 2 }
|
||||
};
|
||||
JsonElement stateSnapshot = JsonSerializer.SerializeToElement(complexState);
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new StateSnapshotEvent { Snapshot = stateSnapshot }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
ChatResponseUpdate stateUpdate = updates.First();
|
||||
DataContent dataContent = Assert.IsType<DataContent>(stateUpdate.Contents[0]);
|
||||
string jsonText = System.Text.Encoding.UTF8.GetString(dataContent.Data.ToArray());
|
||||
JsonElement roundTrippedState = JsonSerializer.Deserialize<JsonElement>(jsonText);
|
||||
|
||||
Assert.Equal("Alice", roundTrippedState.GetProperty("user").GetProperty("name").GetString());
|
||||
Assert.Equal(30, roundTrippedState.GetProperty("user").GetProperty("age").GetInt32());
|
||||
Assert.Equal(3, roundTrippedState.GetProperty("items").GetArrayLength());
|
||||
Assert.Equal("item1", roundTrippedState.GetProperty("items")[0].GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithStateSnapshotAndTextMessages_EmitsBothAsync()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement state = JsonSerializer.SerializeToElement(new { step = 1 });
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new TextMessageStartEvent { MessageId = "msg1", Role = AGUIRoles.Assistant },
|
||||
new TextMessageContentEvent { MessageId = "msg1", Delta = "Processing..." },
|
||||
new TextMessageEndEvent { MessageId = "msg1" },
|
||||
new StateSnapshotEvent { Snapshot = state },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains(updates, u => u.Contents.Any(c => c is TextContent));
|
||||
Assert.Contains(updates, u => u.Contents.Any(c => c is DataContent));
|
||||
}
|
||||
|
||||
#region State Delta Tests
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_ConvertsStateDeltaEvent_ToDataContentWithJsonPatchAsync()
|
||||
{
|
||||
// Arrange - Create JSON Patch operations (RFC 6902)
|
||||
JsonElement stateDelta = JsonSerializer.SerializeToElement(new object[]
|
||||
{
|
||||
new { op = "replace", path = "/counter", value = 43 },
|
||||
new { op = "add", path = "/newField", value = "test" }
|
||||
});
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateDeltaEvent { Delta = stateDelta },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
ChatResponseUpdate deltaUpdate = updates.First(u => u.Contents.Any(c => c is DataContent dc && dc.MediaType == "application/json-patch+json"));
|
||||
Assert.Equal(ChatRole.Assistant, deltaUpdate.Role);
|
||||
Assert.Equal("thread1", deltaUpdate.ConversationId);
|
||||
Assert.Equal("run1", deltaUpdate.ResponseId);
|
||||
|
||||
DataContent dataContent = Assert.IsType<DataContent>(deltaUpdate.Contents[0]);
|
||||
Assert.Equal("application/json-patch+json", dataContent.MediaType);
|
||||
|
||||
// Verify the JSON Patch content
|
||||
string jsonText = System.Text.Encoding.UTF8.GetString(dataContent.Data.ToArray());
|
||||
JsonElement deserializedDelta = JsonSerializer.Deserialize<JsonElement>(jsonText);
|
||||
Assert.Equal(JsonValueKind.Array, deserializedDelta.ValueKind);
|
||||
Assert.Equal(2, deserializedDelta.GetArrayLength());
|
||||
|
||||
// Verify first operation
|
||||
JsonElement firstOp = deserializedDelta[0];
|
||||
Assert.Equal("replace", firstOp.GetProperty("op").GetString());
|
||||
Assert.Equal("/counter", firstOp.GetProperty("path").GetString());
|
||||
Assert.Equal(43, firstOp.GetProperty("value").GetInt32());
|
||||
|
||||
// Verify second operation
|
||||
JsonElement secondOp = deserializedDelta[1];
|
||||
Assert.Equal("add", secondOp.GetProperty("op").GetString());
|
||||
Assert.Equal("/newField", secondOp.GetProperty("path").GetString());
|
||||
Assert.Equal("test", secondOp.GetProperty("value").GetString());
|
||||
|
||||
// Verify additional properties
|
||||
Assert.NotNull(deltaUpdate.AdditionalProperties);
|
||||
Assert.True((bool)deltaUpdate.AdditionalProperties["is_state_delta"]!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithNullStateDelta_DoesNotEmitUpdateAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateDeltaEvent { Delta = null },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert - Only run started and finished should be present
|
||||
Assert.Equal(2, updates.Count);
|
||||
Assert.IsType<ChatResponseUpdate>(updates[0]); // Run started
|
||||
Assert.IsType<ChatResponseUpdate>(updates[1]); // Run finished
|
||||
Assert.DoesNotContain(updates, u => u.Contents.Any(c => c is DataContent));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithEmptyStateDelta_EmitsUpdateAsync()
|
||||
{
|
||||
// Arrange - Empty JSON Patch array is valid
|
||||
JsonElement emptyDelta = JsonSerializer.SerializeToElement(Array.Empty<object>());
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateDeltaEvent { Delta = emptyDelta },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains(updates, u => u.Contents.Any(c => c is DataContent dc && dc.MediaType == "application/json-patch+json"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithMultipleStateDeltaEvents_ConvertsAllAsync()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement delta1 = JsonSerializer.SerializeToElement(new[] { new { op = "replace", path = "/counter", value = 1 } });
|
||||
JsonElement delta2 = JsonSerializer.SerializeToElement(new[] { new { op = "replace", path = "/counter", value = 2 } });
|
||||
JsonElement delta3 = JsonSerializer.SerializeToElement(new[] { new { op = "replace", path = "/counter", value = 3 } });
|
||||
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateDeltaEvent { Delta = delta1 },
|
||||
new StateDeltaEvent { Delta = delta2 },
|
||||
new StateDeltaEvent { Delta = delta3 },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var deltaUpdates = updates.Where(u => u.Contents.Any(c => c is DataContent dc && dc.MediaType == "application/json-patch+json")).ToList();
|
||||
Assert.Equal(3, deltaUpdates.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_ConvertsDataContentWithJsonPatch_ToStateDeltaEventAsync()
|
||||
{
|
||||
// Arrange - Create a ChatResponseUpdate with JSON Patch DataContent
|
||||
JsonElement patchOps = JsonSerializer.SerializeToElement(new object[]
|
||||
{
|
||||
new { op = "remove", path = "/oldField" },
|
||||
new { op = "add", path = "/newField", value = "newValue" }
|
||||
});
|
||||
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(patchOps);
|
||||
DataContent dataContent = new(jsonBytes, "application/json-patch+json");
|
||||
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, [dataContent])
|
||||
{
|
||||
MessageId = "msg1"
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
StateDeltaEvent? deltaEvent = outputEvents.OfType<StateDeltaEvent>().FirstOrDefault();
|
||||
Assert.NotNull(deltaEvent);
|
||||
Assert.NotNull(deltaEvent.Delta);
|
||||
Assert.Equal(JsonValueKind.Array, deltaEvent.Delta.Value.ValueKind);
|
||||
|
||||
// Verify patch operations
|
||||
JsonElement delta = deltaEvent.Delta.Value;
|
||||
Assert.Equal(2, delta.GetArrayLength());
|
||||
Assert.Equal("remove", delta[0].GetProperty("op").GetString());
|
||||
Assert.Equal("/oldField", delta[0].GetProperty("path").GetString());
|
||||
Assert.Equal("add", delta[1].GetProperty("op").GetString());
|
||||
Assert.Equal("/newField", delta[1].GetProperty("path").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithBothSnapshotAndDelta_EmitsBothEventsAsync()
|
||||
{
|
||||
// Arrange
|
||||
JsonElement snapshot = JsonSerializer.SerializeToElement(new { counter = 0 });
|
||||
byte[] snapshotBytes = JsonSerializer.SerializeToUtf8Bytes(snapshot);
|
||||
DataContent snapshotContent = new(snapshotBytes, "application/json");
|
||||
|
||||
JsonElement delta = JsonSerializer.SerializeToElement(new[] { new { op = "replace", path = "/counter", value = 1 } });
|
||||
byte[] deltaBytes = JsonSerializer.SerializeToUtf8Bytes(delta);
|
||||
DataContent deltaContent = new(deltaBytes, "application/json-patch+json");
|
||||
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, [snapshotContent]) { MessageId = "msg1" },
|
||||
new ChatResponseUpdate(ChatRole.Assistant, [deltaContent]) { MessageId = "msg2" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains(outputEvents, e => e is StateSnapshotEvent);
|
||||
Assert.Contains(outputEvents, e => e is StateDeltaEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StateDeltaEvent_RoundTrip_PreservesJsonPatchOperationsAsync()
|
||||
{
|
||||
// Arrange - Create complex JSON Patch with various operations
|
||||
JsonElement originalDelta = JsonSerializer.SerializeToElement(new object[]
|
||||
{
|
||||
new { op = "add", path = "/user/email", value = "test@example.com" },
|
||||
new { op = "remove", path = "/user/tempData" },
|
||||
new { op = "replace", path = "/user/lastLogin", value = "2025-11-09T12:00:00Z" },
|
||||
new { op = "move", from = "/user/oldAddress", path = "/user/previousAddress" },
|
||||
new { op = "copy", from = "/user/name", path = "/user/displayName" },
|
||||
new { op = "test", path = "/user/version", value = 2 }
|
||||
});
|
||||
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new StateDeltaEvent { Delta = originalDelta },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
|
||||
// Act - Convert to ChatResponseUpdate and back to events
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
List<BaseEvent> roundTripEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
roundTripEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
StateDeltaEvent? roundTripDelta = roundTripEvents.OfType<StateDeltaEvent>().FirstOrDefault();
|
||||
Assert.NotNull(roundTripDelta);
|
||||
Assert.NotNull(roundTripDelta.Delta);
|
||||
|
||||
JsonElement delta = roundTripDelta.Delta.Value;
|
||||
Assert.Equal(6, delta.GetArrayLength());
|
||||
|
||||
// Verify each operation type
|
||||
Assert.Equal("add", delta[0].GetProperty("op").GetString());
|
||||
Assert.Equal("remove", delta[1].GetProperty("op").GetString());
|
||||
Assert.Equal("replace", delta[2].GetProperty("op").GetString());
|
||||
Assert.Equal("move", delta[3].GetProperty("op").GetString());
|
||||
Assert.Equal("copy", delta[4].GetProperty("op").GetString());
|
||||
Assert.Equal("test", delta[5].GetProperty("op").GetString());
|
||||
}
|
||||
|
||||
#endregion State Delta Tests
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ public class AgentRunOptionsTests
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
ContinuationToken = new object(),
|
||||
AllowBackgroundResponses = true
|
||||
AllowBackgroundResponses = true,
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary
|
||||
{
|
||||
["key1"] = "value1",
|
||||
["key2"] = 42
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -28,6 +33,10 @@ public class AgentRunOptionsTests
|
||||
Assert.NotNull(clone);
|
||||
Assert.Same(options.ContinuationToken, clone.ContinuationToken);
|
||||
Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses);
|
||||
Assert.NotNull(clone.AdditionalProperties);
|
||||
Assert.NotSame(options.AdditionalProperties, clone.AdditionalProperties);
|
||||
Assert.Equal("value1", clone.AdditionalProperties["key1"]);
|
||||
Assert.Equal(42, clone.AdditionalProperties["key2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -42,7 +51,12 @@ public class AgentRunOptionsTests
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
|
||||
AllowBackgroundResponses = true
|
||||
AllowBackgroundResponses = true,
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary
|
||||
{
|
||||
["key1"] = "value1",
|
||||
["key2"] = 42
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -54,5 +68,13 @@ public class AgentRunOptionsTests
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), deserialized!.ContinuationToken);
|
||||
Assert.Equal(options.AllowBackgroundResponses, deserialized.AllowBackgroundResponses);
|
||||
Assert.NotNull(deserialized.AdditionalProperties);
|
||||
Assert.Equal(2, deserialized.AdditionalProperties.Count);
|
||||
Assert.True(deserialized.AdditionalProperties.TryGetValue("key1", out object? value1));
|
||||
Assert.IsType<JsonElement>(value1);
|
||||
Assert.Equal("value1", ((JsonElement)value1!).GetString());
|
||||
Assert.True(deserialized.AdditionalProperties.TryGetValue("key2", out object? value2));
|
||||
Assert.IsType<JsonElement>(value2);
|
||||
Assert.Equal(42, ((JsonElement)value2!).GetInt32());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
|
||||
|
||||
public sealed class A2AIntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that calling the A2A card endpoint with MapA2A returns an agent card with a URL populated.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapA2A_WithAgentCard_CardEndpointReturnsCardWithUrlAsync()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.WebHost.UseTestServer();
|
||||
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("test-agent", "Test instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
var agentCard = new AgentCard
|
||||
{
|
||||
Name = "Test Agent",
|
||||
Description = "A test agent for A2A communication",
|
||||
Version = "1.0"
|
||||
};
|
||||
|
||||
// Map A2A with the agent card
|
||||
app.MapA2A(agentBuilder, "/a2a/test-agent", agentCard);
|
||||
|
||||
await app.StartAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Get the test server client
|
||||
TestServer testServer = app.Services.GetRequiredService<IServer>() as TestServer
|
||||
?? throw new InvalidOperationException("TestServer not found");
|
||||
var httpClient = testServer.CreateClient();
|
||||
|
||||
// Act - Query the agent card endpoint
|
||||
var requestUri = new Uri("/a2a/test-agent/v1/card", UriKind.Relative);
|
||||
var response = await httpClient.GetAsync(requestUri);
|
||||
|
||||
// Assert
|
||||
Assert.True(response.IsSuccessStatusCode, $"Expected successful response but got {response.StatusCode}");
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var jsonDoc = JsonDocument.Parse(content);
|
||||
var root = jsonDoc.RootElement;
|
||||
|
||||
// Verify the card has expected properties
|
||||
Assert.True(root.TryGetProperty("name", out var nameProperty));
|
||||
Assert.Equal("Test Agent", nameProperty.GetString());
|
||||
|
||||
Assert.True(root.TryGetProperty("description", out var descProperty));
|
||||
Assert.Equal("A test agent for A2A communication", descProperty.GetString());
|
||||
|
||||
// Verify the card has a URL property and it's not null/empty
|
||||
Assert.True(root.TryGetProperty("url", out var urlProperty));
|
||||
Assert.NotEqual(JsonValueKind.Null, urlProperty.ValueKind);
|
||||
|
||||
var url = urlProperty.GetString();
|
||||
Assert.NotNull(url);
|
||||
Assert.NotEmpty(url);
|
||||
Assert.StartsWith("http", url, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal($"{testServer.BaseAddress.ToString().TrimEnd('/')}/a2a/test-agent/v1/card", url);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await app.StopAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-24
@@ -1,10 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -478,25 +476,4 @@ public sealed class EndpointRouteA2ABuilderExtensionsTests
|
||||
var result = app.MapA2A(agentBuilder, "/a2a", agentCard);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
private sealed class DummyChatClient : IChatClient
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
serviceType.IsInstanceOfType(this) ? this : null;
|
||||
|
||||
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user