mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a84ad42f6d | ||
|
|
ded17b178c | ||
|
|
55dc3ce734 | ||
|
|
9d8e5ca4f5 | ||
|
|
af787569b3 | ||
|
|
3db2004e49 | ||
|
|
efdabd56dc | ||
|
|
371a869e44 | ||
|
|
e532ced950 | ||
|
|
5d8dd4ea4b | ||
|
|
4c4e1d9b87 | ||
|
|
1d301af7d2 | ||
|
|
8fbda1de22 | ||
|
|
ef86fb51d5 | ||
|
|
d5c07f2623 | ||
|
|
ae989b92e7 | ||
|
|
3242d8a4c4 | ||
|
|
e1e6e3d35e | ||
|
|
08697f8037 | ||
|
|
b0f5fa541c | ||
|
|
e3290a2d22 | ||
|
|
200488cb08 | ||
|
|
bd4fc64b4d | ||
|
|
b2e77067e9 | ||
|
|
08541ee5a9 | ||
|
|
dc4bafbc1e | ||
|
|
de6d0267f2 | ||
|
|
0099a6e2fa | ||
|
|
793403f3db | ||
|
|
9fdd7429a8 | ||
|
|
abc9b60ec9 | ||
|
|
6bc0dc5911 | ||
|
|
cf91819625 | ||
|
|
578416a379 | ||
|
|
c82c0133fc | ||
|
|
950673ba47 |
@@ -1,15 +1,19 @@
|
||||
{
|
||||
"name": "Python 3",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:3.14-bookworm",
|
||||
"features": {
|
||||
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {}
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:3": {},
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
|
||||
"ghcr.io/devcontainers/features/copilot-cli:1": {}
|
||||
},
|
||||
"postCreateCommand": "bash ./devsetup.sh",
|
||||
"workspaceFolder": "/workspaces/agent-framework/python/",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"GitHub.copilot",
|
||||
"GitHub.vscode-github-actions",
|
||||
"ms-python.python",
|
||||
"ms-windows-ai-studio.windows-ai-studio",
|
||||
"littlefoxteam.vscode-python-test-adapter"
|
||||
|
||||
@@ -8,7 +8,7 @@ ignorePatterns:
|
||||
- pattern: "./blob"
|
||||
- pattern: "./issues"
|
||||
- pattern: "./discussions"
|
||||
- pattern: "./pulls"
|
||||
- pattern: "./pull"
|
||||
- pattern: "https:\/\/platform.openai.com"
|
||||
- pattern: "http:\/\/localhost"
|
||||
- pattern: "http:\/\/127.0.0.1"
|
||||
|
||||
@@ -137,6 +137,7 @@ jobs:
|
||||
working-directory: ${{ env.DEVFLOW_PATH }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DEVFLOW_TOKEN: ${{ secrets.DEVFLOW_TOKEN }}
|
||||
SK_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
|
||||
AGENT_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
|
||||
ISSUE_REPO: ${{ needs.team_check.outputs.repo }}
|
||||
|
||||
+4
-4
@@ -173,11 +173,11 @@ new SampleDefinition
|
||||
```csharp
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_GenerateCode",
|
||||
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
|
||||
Name = "Workflow_Visualization",
|
||||
ProjectPath = "samples/03-workflows/Visualization",
|
||||
IsDeterministic = true,
|
||||
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
|
||||
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
|
||||
MustContain = ["Generating workflow visualization...", "Mermaid string:", "DiGraph string:"],
|
||||
ExpectedOutputDescription = ["The output should show workflow visualization in Mermaid and DiGraph formats."],
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
@@ -117,17 +117,18 @@
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
|
||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills/Agent_Step06_McpBasedSkills.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/Harness/">
|
||||
<File Path="samples/02-agents/Harness/README.md" />
|
||||
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/Harness_Shared_Console/Harness_Shared_Console.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/Harness_Shared_Console_OpenAI/Harness_Shared_Console_OpenAI.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/Harness_Step03_DataProcessing/Harness_Step03_DataProcessing.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
|
||||
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
||||
@@ -212,6 +213,7 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
|
||||
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
|
||||
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj" />
|
||||
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
|
||||
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj" />
|
||||
<Project Path="samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj" />
|
||||
@@ -240,11 +242,10 @@
|
||||
<Project Path="samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/GenerateCode/GenerateCode.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InputArguments/InputArguments.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeHttpRequest/InvokeHttpRequest.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj" />
|
||||
<Project Path="samples/03-workflows/Declarative/Marketing/Marketing.csproj" />
|
||||
@@ -281,6 +282,7 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Orchestration/">
|
||||
<Project Path="samples/03-workflows/Orchestration/Handoff/Handoff.csproj" />
|
||||
<Project Path="samples/03-workflows/Orchestration/Magentic/Magentic.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/03-workflows/Observability/">
|
||||
<Project Path="samples/03-workflows/Observability/ApplicationInsights/ApplicationInsights.csproj" />
|
||||
@@ -357,6 +359,9 @@
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/DurableAgents/" />
|
||||
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
|
||||
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
|
||||
@@ -592,8 +597,8 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Harness/Microsoft.Agents.AI.Harness.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Harness/Microsoft.Agents.AI.Harness.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
@@ -601,6 +606,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Mcp/Microsoft.Agents.AI.Mcp.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
||||
@@ -618,8 +624,8 @@
|
||||
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Foundry.Hosting.IntegrationTests.TestContainer/Foundry.Hosting.IntegrationTests.TestContainer.csproj" />
|
||||
<Project Path="tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
||||
@@ -644,8 +650,8 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Harness.UnitTests/Microsoft.Agents.AI.Harness.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||
@@ -654,6 +660,7 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Mcp.UnitTests/Microsoft.Agents.AI.Mcp.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Mem0.UnitTests/Microsoft.Agents.AI.Mem0.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
|
||||
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
|
||||
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
|
||||
"src\\Microsoft.Agents.AI.Tools.Shell\\Microsoft.Agents.AI.Tools.Shell.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
|
||||
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
<ItemGroup Condition="'$(InjectSharedIntegrationTestAzureCredentialsCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTestsAzureCredentials\*.cs" LinkBase="Shared\IntegrationTestsAzureCredentials" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedBuildTestCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\CodeTests\*.cs" LinkBase="Shared\CodeTests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedWorkflowsExecution)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Execution\*.cs" LinkBase="Shared\Workflows" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -363,6 +363,25 @@ internal static class AgentsSamples
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_Step06_McpBasedSkills",
|
||||
ProjectPath = "samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
"Discovering MCP-based skills",
|
||||
"Agent:",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show the agent converting 26.2 miles to kilometers and 75 kilograms to pounds.",
|
||||
"The response should contain approximate numeric values for both conversions.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
// ── AgentWithMemory ─────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
@@ -1151,6 +1170,25 @@ internal static class AgentsSamples
|
||||
SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_MCP_LongRunningTask_Client",
|
||||
ProjectPath = "samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client",
|
||||
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
"=== Transparent long-running MCP task (RunAsync) ===",
|
||||
"=== Transparent long-running MCP task (RunStreamingAsync) ===",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show an agent analyzing a dataset named 'sales-2025-q1' and producing a summary mentioning rows, revenue, anomalies, or outliers.",
|
||||
"The output should contain both a non-streaming response (after RunAsync) and a streaming response (after RunStreamingAsync) for the same analysis question.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "AGUI_Step01_GettingStarted_Client",
|
||||
|
||||
@@ -439,15 +439,6 @@ internal static class WorkflowSamples
|
||||
ExpectedOutputDescription = ["The output should show a workflow calling function tools (e.g. a menu plugin) to answer a question about restaurant specials."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_GenerateCode",
|
||||
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
|
||||
IsDeterministic = true,
|
||||
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
|
||||
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Workflow_Declarative_HostedWorkflow",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.6.2</VersionPrefix>
|
||||
<VersionPrefix>1.7.0</VersionPrefix>
|
||||
<RCNumber>1</RCNumber>
|
||||
<DateSuffix>260521</DateSuffix>
|
||||
<DateSuffix>260526</DateSuffix>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||
<GitTag>1.6.2</GitTag>
|
||||
<GitTag>1.7.0</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -24,6 +24,7 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
||||
var skillsProvider = new AgentSkillsProvider(
|
||||
Path.Combine(AppContext.BaseDirectory, "skills"),
|
||||
SubprocessScriptRunner.RunAsync);
|
||||
|
||||
// --- Agent Setup ---
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
|
||||
@@ -51,7 +51,7 @@ Console.WriteLine($"Agent: {response.Text}");
|
||||
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
|
||||
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
|
||||
/// are automatically discovered as skill scripts. Alternatively,
|
||||
/// <see cref="AgentSkill.Resources"/> and <see cref="AgentSkill.Scripts"/> can be overridden.
|
||||
/// <see cref="AgentClassSkill{TSelf}.Resources"/> and <see cref="AgentClassSkill{TSelf}.Scripts"/> can be overridden.
|
||||
/// </remarks>
|
||||
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
|
||||
{
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001;MCPEXP001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to discover Agent Skills served over MCP.
|
||||
//
|
||||
// When launched with "--server", this executable runs a small MCP stdio server
|
||||
// that exposes a unit-converter skill via the SEP-2640 convention:
|
||||
// - skill://index.json — discovery document listing all skills
|
||||
// - skill://unit-converter/SKILL.md — the skill instructions
|
||||
//
|
||||
// In default (client) mode the sample launches itself as a child process,
|
||||
// connects via StdioClientTransport, and uses AgentSkillsProviderBuilder
|
||||
// to discover and inject the skill into a ChatClientAgent.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenAI.Responses;
|
||||
|
||||
if (args.Length > 0 && args[0] == "--server")
|
||||
{
|
||||
await RunMcpServerAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Configuration ---
|
||||
string openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// --- MCP client + skill discovery ---
|
||||
// Launch this same assembly as a stdio MCP server in a child process.
|
||||
var thisAssemblyPath = typeof(Program).Assembly.Location;
|
||||
Console.WriteLine("Discovering MCP-based skills");
|
||||
|
||||
await using McpClient client = await McpClient.CreateAsync(
|
||||
new StdioClientTransport(new()
|
||||
{
|
||||
Name = "skills-server",
|
||||
Command = "dotnet",
|
||||
Arguments = [thisAssemblyPath, "--server"],
|
||||
}));
|
||||
|
||||
var skillsProvider = new AgentSkillsProviderBuilder()
|
||||
.UseMcpSkills(client)
|
||||
.Build();
|
||||
|
||||
// --- Agent ---
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(openAiEndpoint), new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "SkillsAgent",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful assistant. Use available skills to answer the user.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider],
|
||||
},
|
||||
model: deploymentName);
|
||||
|
||||
// --- Run ---
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
|
||||
|
||||
Console.WriteLine($"Agent: {response.Text}");
|
||||
|
||||
// --- Server mode (launched as a child process via --server) ---------------------------------
|
||||
static async Task RunMcpServerAsync()
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder();
|
||||
|
||||
// Critical for stdio transport: any provider that writes to stdout will corrupt the
|
||||
// JSON-RPC channel. Clear all providers; the MCP SDK routes its own diagnostics
|
||||
// appropriately.
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
|
||||
|
||||
builder.Services.AddMcpServer(o => o.ServerInfo = new() { Name = "SkillsServer", Version = "1.0.0" })
|
||||
.WithStdioServerTransport()
|
||||
.WithResources<SkillResources>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812 // Discovered by MCP SDK via [McpServerResourceType] attribute
|
||||
[McpServerResourceType]
|
||||
internal sealed class SkillResources
|
||||
#pragma warning restore CA1812
|
||||
{
|
||||
private const string IndexJson = """
|
||||
{
|
||||
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
||||
"skills": [
|
||||
{
|
||||
"name": "unit-converter",
|
||||
"type": "skill-md",
|
||||
"description": "Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.",
|
||||
"url": "skill://unit-converter/SKILL.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SkillMd = """
|
||||
---
|
||||
name: unit-converter
|
||||
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
When the user requests a unit conversion, use these factors:
|
||||
|
||||
| From | To | Factor |
|
||||
|-------------|-------------|----------|
|
||||
| miles | kilometers | 1.60934 |
|
||||
| kilometers | miles | 0.621371 |
|
||||
| pounds | kilograms | 0.453592 |
|
||||
| kilograms | pounds | 2.20462 |
|
||||
|
||||
Formula: result = value Ă— factor
|
||||
""";
|
||||
|
||||
[McpServerResource(UriTemplate = "skill://index.json", Name = "Skill Index", MimeType = "application/json")]
|
||||
[Description("SEP-2640 skill discovery index")]
|
||||
public static string GetIndex() => IndexJson;
|
||||
|
||||
[McpServerResource(UriTemplate = "skill://unit-converter/SKILL.md", Name = "Unit Converter Skill", MimeType = "text/markdown")]
|
||||
[Description("Unit converter skill instructions")]
|
||||
public static string GetSkillMd() => SkillMd;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# MCP-Based Agent Skills Sample
|
||||
|
||||
This sample demonstrates how to discover **Agent Skills served over MCP** with a `ChatClientAgent`.
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
- Hosting a small MCP server (in this same executable, launched with `--server`) that
|
||||
exposes skill resources following the SEP-2640 convention.
|
||||
- Connecting an `McpClient` to the embedded server via stdio transport.
|
||||
- Building an `AgentSkillsProvider` via `UseMcpSkills(client)`, which reads
|
||||
`skill://index.json` (SEP-2640 canonical discovery) and constructs skills from the
|
||||
index entries.
|
||||
- The progressive disclosure pattern across MCP: advertise → load → read resources, exactly
|
||||
as for filesystem-backed skills.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 10.0 SDK
|
||||
- Azure OpenAI endpoint with a deployed model
|
||||
|
||||
### Setup
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini"
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
@@ -9,6 +9,7 @@ Samples demonstrating Agent Skills capabilities. Each sample shows a different w
|
||||
| [Agent_Step03_ClassBasedSkills](Agent_Step03_ClassBasedSkills/) | Define skills as C# classes using `AgentClassSkill`. |
|
||||
| [Agent_Step04_MixedSkills](Agent_Step04_MixedSkills/) | **(Advanced)** Combine file-based, code-defined, and class-based skills using `AgentSkillsProviderBuilder`. |
|
||||
| [Agent_Step05_SkillsWithDI](Agent_Step05_SkillsWithDI/) | Use Dependency Injection with both code-defined (`AgentInlineSkill`) and class-based (`AgentClassSkill`) skills. |
|
||||
| [Agent_Step06_McpBasedSkills](Agent_Step06_McpBasedSkills/) | Discover skills served over the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) via `AgentMcpSkillsSource`. Spins up an in-process MCP server that exposes skills as resources (`skill://...`) and connects an `McpClient` to it. |
|
||||
|
||||
## Key Concepts
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
||||
foreach (string line in props.Title.Split('\n'))
|
||||
{
|
||||
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
||||
Console.Write(AnsiEscapes.EraseEntireLine);
|
||||
Console.Write(line);
|
||||
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,6 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
||||
for (int i = 0; i < totalItems; i++)
|
||||
{
|
||||
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
||||
Console.Write(AnsiEscapes.EraseEntireLine);
|
||||
|
||||
bool isSelected = i == props.SelectedIndex;
|
||||
bool isCustomTextOption = props.CustomTextPlaceholder != null && i == props.Items.Count;
|
||||
@@ -72,6 +71,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
||||
}
|
||||
|
||||
Console.Write(props.Items[i]);
|
||||
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
@@ -101,6 +101,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
||||
}
|
||||
|
||||
Console.Write(props.CustomText);
|
||||
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
@@ -121,6 +122,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
||||
|
||||
Console.Write(" ");
|
||||
Console.Write(props.CustomTextPlaceholder);
|
||||
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
||||
Console.Write(AnsiEscapes.ResetAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,19 @@ public record TextScrollPanelProps : ConsoleReactiveProps
|
||||
/// <summary>
|
||||
/// State for <see cref="TextScrollPanel"/>.
|
||||
/// </summary>
|
||||
/// <param name="RenderedCount">The number of items already rendered.</param>
|
||||
public record TextScrollPanelState(int RenderedCount = 0) : ConsoleReactiveState;
|
||||
public record TextScrollPanelState : ConsoleReactiveState;
|
||||
|
||||
/// <summary>
|
||||
/// A component that renders pre-rendered string items within a scroll area.
|
||||
/// All items are considered finalized — only new items since the last render are output.
|
||||
/// Use <see cref="Reset"/> to force a full re-render.
|
||||
/// The last rendered item is considered dynamic and will be re-rendered on each call.
|
||||
/// All prior items are considered finalized and are not re-rendered.
|
||||
/// Use <see cref="Invalidate"/> to force a full re-render.
|
||||
/// </summary>
|
||||
public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, TextScrollPanelState>
|
||||
{
|
||||
private int _renderedCount;
|
||||
private int _lastItemOffsetFromBottom;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextScrollPanel"/> class.
|
||||
/// </summary>
|
||||
@@ -35,12 +38,12 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
|
||||
this.State = new TextScrollPanelState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the panel so all items will be re-rendered on the next Render call.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
/// <inheritdoc />
|
||||
public override void Invalidate()
|
||||
{
|
||||
this.State = new TextScrollPanelState();
|
||||
this._renderedCount = 0;
|
||||
this._lastItemOffsetFromBottom = 0;
|
||||
base.Invalidate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -51,16 +54,59 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
|
||||
return;
|
||||
}
|
||||
|
||||
// Move cursor to the bottom of the scroll area
|
||||
Console.Write(AnsiEscapes.MoveCursor(props.Y + props.Height - 1, props.X));
|
||||
int bottomRow = props.Y + props.Height - 1;
|
||||
|
||||
// Output only new items since last rendered
|
||||
for (int i = state.RenderedCount; i < props.Items.Count; i++)
|
||||
// Determine the first item to render. If we previously rendered items,
|
||||
// re-render the last one (it may have changed/grown) from its stored position.
|
||||
int startIndex = this._renderedCount > 0 ? this._renderedCount - 1 : 0;
|
||||
|
||||
if (this._renderedCount > 0 && this._lastItemOffsetFromBottom > 0)
|
||||
{
|
||||
// Reposition cursor to where the last rendered item began
|
||||
Console.Write(AnsiEscapes.MoveCursor(bottomRow - this._lastItemOffsetFromBottom, props.X));
|
||||
}
|
||||
else
|
||||
{
|
||||
// First render — position at the bottom of the scroll area
|
||||
Console.Write(AnsiEscapes.MoveCursor(bottomRow, props.X));
|
||||
}
|
||||
|
||||
// Render from startIndex onwards
|
||||
for (int i = startIndex; i < props.Items.Count; i++)
|
||||
{
|
||||
Console.Write(props.Items[i]);
|
||||
}
|
||||
|
||||
// Update state to track what we've rendered
|
||||
this.State = new TextScrollPanelState(props.Items.Count);
|
||||
// Calculate the offset from bottom for the start of the new last item
|
||||
int lastItemLines = CountLines(props.Items[^1]);
|
||||
this._lastItemOffsetFromBottom = lastItemLines > 0 ? lastItemLines - 1 : 0;
|
||||
|
||||
// Update rendered count
|
||||
this._renderedCount = props.Items.Count;
|
||||
}
|
||||
|
||||
private static int CountLines(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 1;
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
if (text[i] == '\n')
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// If text ends with a newline, don't count the trailing empty line
|
||||
if (text[text.Length - 1] == '\n')
|
||||
{
|
||||
count--;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Harness.ConsoleReactiveFramework;
|
||||
|
||||
/// <summary>
|
||||
/// Caches the result of a mapping function and only recomputes when the input changes.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of the input value.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of the mapped output value.</typeparam>
|
||||
public class ConsoleReactiveMemo<TInput, TOutput>
|
||||
{
|
||||
private TInput? _previousInput;
|
||||
private TOutput? _cachedOutput;
|
||||
private bool _hasValue;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached output if <paramref name="input"/> equals the previously stored input;
|
||||
/// otherwise invokes <paramref name="mapper"/> to compute and cache a new output.
|
||||
/// </summary>
|
||||
/// <param name="input">The current input value.</param>
|
||||
/// <param name="mapper">A function that maps the input to an output value.</param>
|
||||
/// <returns>The cached or newly computed output.</returns>
|
||||
public TOutput Map(TInput input, Func<TInput, TOutput> mapper)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(mapper);
|
||||
|
||||
if (!this._hasValue || !EqualityComparer<TInput>.Default.Equals(input, this._previousInput))
|
||||
{
|
||||
this._previousInput = input;
|
||||
this._cachedOutput = mapper(input);
|
||||
this._hasValue = true;
|
||||
}
|
||||
|
||||
return this._cachedOutput!;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
||||
private readonly ListSelection _listSelection = new();
|
||||
private readonly TextInput _textInput = new();
|
||||
private readonly TextScrollPanel _textScrollPanel = new();
|
||||
private readonly TextPanel _textPanel = new();
|
||||
private readonly TextPanel _queuedPanel = new();
|
||||
private readonly AgentStatus _agentStatus = new();
|
||||
private readonly AgentModeAndHelp _modeAndHelp = new();
|
||||
@@ -341,16 +340,6 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the text panel height for the last scroll item
|
||||
IReadOnlyList<string> lastItems = state.ScrollAreaContentItems.Count > 0
|
||||
? [state.ScrollAreaContentItems[^1]]
|
||||
: [];
|
||||
int textPanelHeight = TextPanel.CalculateHeight(lastItems);
|
||||
if (textPanelHeight > 0)
|
||||
{
|
||||
textPanelHeight++; // Extra line for spacing between text panel and rule
|
||||
}
|
||||
|
||||
// Calculate queued items panel height
|
||||
int queuedPanelHeight = TextPanel.CalculateHeight(state.QueuedItems);
|
||||
|
||||
@@ -444,7 +433,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
||||
int modeAndHelpHeight = showStatusAndHelp ? AgentModeAndHelp.CalculateHeight(modeAndHelpProps) : 0;
|
||||
|
||||
int ruleHeight = TopBottomRule.CalculateHeight(ruleProps);
|
||||
int nonScrollHeight = ruleHeight + textPanelHeight + agentStatusHeight + queuedPanelHeight + modeAndHelpHeight + 1; // +1 for bottom padding
|
||||
int nonScrollHeight = ruleHeight + agentStatusHeight + queuedPanelHeight + modeAndHelpHeight + 1; // +1 for bottom padding
|
||||
int scrollBottom = Math.Max(1, state.ConsoleHeight - nonScrollHeight);
|
||||
|
||||
// If scroll region changed or a clear is needed, reset everything
|
||||
@@ -455,52 +444,36 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
||||
System.Console.Write(AnsiEscapes.ResetScrollRegion);
|
||||
System.Console.Write(AnsiEscapes.EraseEntireScreen);
|
||||
System.Console.Write(AnsiEscapes.EraseScrollbackBuffer);
|
||||
this._textScrollPanel.Reset();
|
||||
this._resizedSinceLastRender = false;
|
||||
|
||||
// Invalidate all children so they re-render even if props haven't changed
|
||||
this._rule.Invalidate();
|
||||
this._textScrollPanel.Invalidate();
|
||||
this._textPanel.Invalidate();
|
||||
this._queuedPanel.Invalidate();
|
||||
this._agentStatus.Invalidate();
|
||||
this._modeAndHelp.Invalidate();
|
||||
this._textInput.Invalidate();
|
||||
this._listSelection.Invalidate();
|
||||
|
||||
this._resizedSinceLastRender = false;
|
||||
}
|
||||
|
||||
this._scrollRegionBottom = scrollBottom;
|
||||
|
||||
System.Console.Write(AnsiEscapes.SetScrollRegion(scrollBottom));
|
||||
|
||||
// Render text scroll panel in the scroll area (all items except the last)
|
||||
IReadOnlyList<string> scrollItems = state.ScrollAreaContentItems.Count > 1
|
||||
? state.ScrollAreaContentItems.Take(state.ScrollAreaContentItems.Count - 1).ToList()
|
||||
: [];
|
||||
|
||||
// Render text scroll panel in the scroll area
|
||||
this._textScrollPanel.Props = new TextScrollPanelProps
|
||||
{
|
||||
X = 1,
|
||||
Y = 1,
|
||||
Width = state.ConsoleWidth,
|
||||
Height = scrollBottom,
|
||||
Items = scrollItems,
|
||||
Items = state.ScrollAreaContentItems,
|
||||
};
|
||||
this._textScrollPanel.Render();
|
||||
|
||||
// Render the text panel for the last (dynamic) item just below the scroll region
|
||||
this._textPanel.Props = new TextPanelProps
|
||||
{
|
||||
X = 1,
|
||||
Y = scrollBottom + 1,
|
||||
Width = state.ConsoleWidth,
|
||||
Height = textPanelHeight,
|
||||
Items = lastItems,
|
||||
};
|
||||
this._textPanel.Render();
|
||||
|
||||
// Render queued input items between text panel and agent status
|
||||
int queuedPanelY = scrollBottom + textPanelHeight + 1;
|
||||
// Render queued input items between scroll area and agent status
|
||||
int queuedPanelY = scrollBottom + 1;
|
||||
this._queuedPanel.Props = new TextPanelProps
|
||||
{
|
||||
X = 1,
|
||||
|
||||
+3
-3
@@ -5,17 +5,17 @@ using Microsoft.Extensions.AI;
|
||||
namespace Harness.Shared.Console.ToolFormatters;
|
||||
|
||||
/// <summary>
|
||||
/// Formats <c>AgentMode_*</c> tool calls, showing the target mode for Set operations.
|
||||
/// Formats <c>mode_*</c> tool calls, showing the target mode for Set operations.
|
||||
/// </summary>
|
||||
public sealed class ModeToolFormatter : ToolCallFormatter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("AgentMode_", StringComparison.Ordinal);
|
||||
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("mode_", StringComparison.Ordinal);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
||||
{
|
||||
"AgentMode_Set" => FormatStringArg(call, "mode"),
|
||||
"mode_set" => FormatStringArg(call, "mode"),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
|
||||
+5
-5
@@ -7,20 +7,20 @@ using Microsoft.Extensions.AI;
|
||||
namespace Harness.Shared.Console.ToolFormatters;
|
||||
|
||||
/// <summary>
|
||||
/// Formats <c>TodoList_*</c> tool calls with tree-view output for added items
|
||||
/// Formats <c>todos_*</c> tool calls with tree-view output for added items
|
||||
/// and structured output for complete/remove operations.
|
||||
/// </summary>
|
||||
public sealed class TodoToolFormatter : ToolCallFormatter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("TodoList_", StringComparison.Ordinal);
|
||||
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("todos_", StringComparison.Ordinal);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
||||
{
|
||||
"TodoList_Add" => FormatAddTodos(call),
|
||||
"TodoList_Complete" => FormatCompleteTodos(call),
|
||||
"TodoList_Remove" => FormatIdList(call, "ids", "Remove"),
|
||||
"todos_add" => FormatAddTodos(call),
|
||||
"todos_complete" => FormatCompleteTodos(call),
|
||||
"todos_remove" => FormatIdList(call, "ids", "Remove"),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAI001;MEAI001;MCPEXP001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates the Microsoft Agent Framework's MCP long-running task support.
|
||||
//
|
||||
// A small MCP server (hosted in this same executable when launched with "--server") exposes
|
||||
// a single task-supporting tool "AnalyzeDataset" that simulates ~15 seconds of work. The
|
||||
// client (default mode) connects to it over stdio via Microsoft.Agents.AI.Mcp's
|
||||
// McpClientTaskExtensions.ListAgentToolsWithTaskSupportAsync, hands the wrapped tools to a
|
||||
// ChatClientAgent, and exercises both invocation styles:
|
||||
// * RunAsync — blocks until the agent's final response is ready.
|
||||
// * RunStreamingAsync — yields response updates as the model produces them; the model
|
||||
// still waits for the tool's terminal result before it can begin
|
||||
// producing the final answer, so the perceived "pause" reflects
|
||||
// tool execution time, not stream-channel latency.
|
||||
//
|
||||
// In both cases the wrapper transparently:
|
||||
// 1. Calls tools/call with task augmentation (CallToolAsTaskAsync)
|
||||
// 2. Polls tasks/get until terminal (PollTaskUntilCompleteAsync)
|
||||
// 3. Fetches tasks/result and returns the final result to the function-calling loop
|
||||
//
|
||||
// No application-level loop or continuation tokens are required in either mode.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Mcp;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenAI.Chat;
|
||||
|
||||
if (args.Length > 0 && args[0] == "--server")
|
||||
{
|
||||
await RunMcpServerAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
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-5.4-mini";
|
||||
|
||||
// Launch this same assembly as a stdio MCP server in a child process.
|
||||
var thisAssemblyPath = typeof(Program).Assembly.Location;
|
||||
await using var mcpClient = await McpClient.CreateAsync(new StdioClientTransport(new()
|
||||
{
|
||||
Name = "DatasetAnalyzer",
|
||||
Command = "dotnet",
|
||||
Arguments = [thisAssemblyPath, "--server"],
|
||||
}));
|
||||
|
||||
// Wrap each MCP tool with task-aware behavior. The wrapper inspects the server's
|
||||
// execution.taskSupport on each tool and, when it is Required, drives the task lifecycle
|
||||
// transparently within the agent's tool loop. Tools that don't require task semantics are
|
||||
// returned as-is and invoked inline.
|
||||
var taskOptions = new McpTaskOptions
|
||||
{
|
||||
DefaultTimeToLive = TimeSpan.FromMinutes(5),
|
||||
};
|
||||
var mcpTools = await mcpClient.ListAgentToolsWithTaskSupportAsync(taskOptions);
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(
|
||||
instructions: "You answer data-analysis questions by invoking the available tools. Always invoke a tool when one matches the request.",
|
||||
tools: [.. mcpTools.Cast<AITool>()]);
|
||||
|
||||
const string Prompt = "Analyze the dataset named 'sales-2025-q1' and summarize the findings.";
|
||||
|
||||
Console.WriteLine("=== Transparent long-running MCP task (RunAsync) ===");
|
||||
Console.WriteLine("Asking the agent to analyze a dataset; the tool takes ~15s to complete.");
|
||||
Console.WriteLine("RunAsync blocks while the wrapper polls the task to completion.");
|
||||
Console.WriteLine();
|
||||
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var response = await agent.RunAsync(Prompt);
|
||||
stopwatch.Stop();
|
||||
|
||||
Console.WriteLine($"Agent response (after {stopwatch.Elapsed.TotalSeconds:F1}s):");
|
||||
Console.WriteLine(response.Text);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== Transparent long-running MCP task (RunStreamingAsync) ===");
|
||||
Console.WriteLine("Same request via the streaming API. Updates only begin to arrive after the");
|
||||
Console.WriteLine("tool's task reaches the Completed state, since the model needs the tool result");
|
||||
Console.WriteLine("before it can produce its final answer.");
|
||||
Console.WriteLine();
|
||||
|
||||
stopwatch.Restart();
|
||||
await foreach (var update in agent.RunStreamingAsync(Prompt))
|
||||
{
|
||||
Console.Write(update.Text);
|
||||
}
|
||||
stopwatch.Stop();
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"(Streaming completed after {stopwatch.Elapsed.TotalSeconds:F1}s.)");
|
||||
|
||||
// --- Server mode (launched as a child process via --server) ---------------------------------
|
||||
static async Task RunMcpServerAsync()
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder();
|
||||
|
||||
// Critical for stdio transport: any provider that writes to stdout will corrupt the
|
||||
// JSON-RPC channel. Clear all providers; the MCP SDK routes its own diagnostics
|
||||
// appropriately.
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
|
||||
|
||||
builder.Services.AddMcpServer(o =>
|
||||
{
|
||||
o.TaskStore = new InMemoryMcpTaskStore();
|
||||
o.ServerInfo = new Implementation { Name = "DatasetAnalyzer", Version = "1.0.0" };
|
||||
})
|
||||
.WithStdioServerTransport()
|
||||
.WithTools<DatasetAnalysisTools>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812 // Discovered by MCP SDK via [McpServerToolType] attribute
|
||||
[McpServerToolType]
|
||||
internal sealed class DatasetAnalysisTools
|
||||
#pragma warning restore CA1812
|
||||
{
|
||||
[McpServerTool(Name = "AnalyzeDataset", TaskSupport = ToolTaskSupport.Required)]
|
||||
[Description("Analyze a tabular dataset and return summary statistics. This tool simulates a long-running analytic job (~15 seconds).")]
|
||||
public static async Task<string> AnalyzeDatasetAsync(
|
||||
[Description("The dataset identifier, e.g. 'sales-2025-q1'.")] string datasetName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return $"Findings for '{datasetName}': 12,403 rows; avg revenue $48,712; 3 anomalies detected in week 7; outliers concentrated in EMEA region.";
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
# Agent with MCP long-running task (transparent polling)
|
||||
|
||||
This sample demonstrates Microsoft Agent Framework's MCP long-running task support: an agent invokes an MCP tool whose execution takes too long for a single request/response cycle, and the framework polls it to completion behind the function-calling loop. From the agent's perspective the tool simply returns its result.
|
||||
|
||||
## What this sample shows
|
||||
|
||||
- Using `McpClient.ListAgentToolsWithTaskSupportAsync(...)` (in `Microsoft.Agents.AI.Mcp`) to wrap MCP tools with task-aware behavior.
|
||||
- Configuring `McpTaskOptions.DefaultTimeToLive` to bound the server-side task.
|
||||
- Hosting a small MCP server (in this same executable, launched with `--server`) that advertises `execution.taskSupport=required` on a tool that sleeps for ~15 seconds.
|
||||
- No application-level polling, continuation tokens, or `AllowBackgroundResponses` flag are required.
|
||||
|
||||
The decorator drives the lifecycle internally:
|
||||
|
||||
1. `tools/call` augmented with task metadata (`CallToolAsTaskAsync`)
|
||||
2. `tasks/get` polled until terminal (`PollTaskUntilCompleteAsync`)
|
||||
3. `tasks/result` retrieved (`GetTaskResultAsync`) and returned to the function-calling loop
|
||||
|
||||
The sample exercises both invocation styles against the same wrapper:
|
||||
|
||||
- `agent.RunAsync(...)` blocks until the tool completes (~15 seconds in this sample) and returns the final response.
|
||||
- `agent.RunStreamingAsync(...)` returns immediately and yields `AgentResponseUpdate` chunks as the model emits them; in this scenario the model only begins streaming its answer once the wrapped tool's task reaches the `Completed` state, so the perceived "pause" before tokens arrive reflects tool execution time, not stream-channel latency.
|
||||
|
||||
# Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure OpenAI service endpoint and a chat-completions deployment
|
||||
- Azure CLI installed and authenticated (`az login`)
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # optional; defaults to gpt-5.4-mini
|
||||
```
|
||||
|
||||
# Running
|
||||
|
||||
```powershell
|
||||
cd Agent_MCP_LongRunningTask_Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
You should see output similar to:
|
||||
|
||||
```
|
||||
=== Transparent long-running MCP task (RunAsync) ===
|
||||
Asking the agent to analyze a dataset; the tool takes ~15s to complete.
|
||||
RunAsync blocks while the wrapper polls the task to completion.
|
||||
|
||||
Agent response (after 15.4s):
|
||||
The 'sales-2025-q1' dataset contains 12,403 rows ...
|
||||
|
||||
=== Transparent long-running MCP task (RunStreamingAsync) ===
|
||||
Same request via the streaming API. Updates only begin to arrive after the
|
||||
tool's task reaches the Completed state, since the model needs the tool result
|
||||
before it can produce its final answer.
|
||||
|
||||
The 'sales-2025-q1' dataset contains 12,403 rows ...
|
||||
(Streaming completed after 15.7s.)
|
||||
```
|
||||
@@ -22,6 +22,7 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|[Agent with MCP server tools](./Agent_MCP_Server/)|This sample demonstrates how to use MCP server tools with a simple agent|
|
||||
|[Agent with MCP server tools and authorization](./Agent_MCP_Server_Auth/)|This sample demonstrates how to use MCP Server tools from a protected MCP server with a simple agent|
|
||||
|[Responses Agent with Hosted MCP tool](./ResponseAgent_Hosted_MCP/)|This sample demonstrates how to use the Hosted MCP tool with the Responses Service, where the service invokes any MCP tools directly|
|
||||
|[Agent with long-running MCP task (transparent polling)](./Agent_MCP_LongRunningTask_Client/)|This sample demonstrates how an agent transparently drives a long-running MCP task (SEP-2663) to completion. The wrapper polls the task internally on both `RunAsync` and `RunStreamingAsync` invocations.|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
|
||||
<NoWarn>$(NoWarn);CA1812</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
||||
|
||||
namespace Demo.DeclarativeEject;
|
||||
|
||||
/// <summary>
|
||||
/// HOW TO: Convert a workflow from a declartive (yaml based) definition to code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Usage</b>
|
||||
/// Provide the path to the workflow definition file as the first argument.
|
||||
/// All other arguments are intepreted as a queue of inputs.
|
||||
/// When no input is queued, interactive input is requested from the console.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Program program = new(args);
|
||||
program.Execute();
|
||||
}
|
||||
|
||||
private void Execute()
|
||||
{
|
||||
// Read and parse the declarative workflow.
|
||||
Notify($"WORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}");
|
||||
|
||||
Stopwatch timer = Stopwatch.StartNew();
|
||||
|
||||
// Use DeclarativeWorkflowBuilder to generate code based on a YAML file.
|
||||
string code =
|
||||
DeclarativeWorkflowBuilder.Eject(
|
||||
this.WorkflowFile,
|
||||
DeclarativeWorkflowLanguage.CSharp,
|
||||
workflowNamespace: "Demo.DeclarativeCode",
|
||||
workflowPrefix: "Sample");
|
||||
|
||||
Notify($"\nWORKFLOW: Defined {timer.Elapsed}\n");
|
||||
|
||||
Console.WriteLine(code);
|
||||
}
|
||||
|
||||
private const string DefaultWorkflow = "Marketing.yaml";
|
||||
|
||||
private string WorkflowFile { get; }
|
||||
|
||||
private Program(string[] args)
|
||||
{
|
||||
this.WorkflowFile = ParseWorkflowFile(args);
|
||||
}
|
||||
|
||||
private static string ParseWorkflowFile(string[] args)
|
||||
{
|
||||
string workflowFile = args.FirstOrDefault() ?? DefaultWorkflow;
|
||||
|
||||
if (!File.Exists(workflowFile) && !Path.IsPathFullyQualified(workflowFile))
|
||||
{
|
||||
string? repoFolder = GetRepoFolder();
|
||||
if (repoFolder is not null)
|
||||
{
|
||||
workflowFile = Path.Combine(repoFolder, "declarative-agents", "workflow-samples", workflowFile);
|
||||
workflowFile = Path.ChangeExtension(workflowFile, ".yaml");
|
||||
}
|
||||
}
|
||||
|
||||
if (!File.Exists(workflowFile))
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to locate workflow: {Path.GetFullPath(workflowFile)}.");
|
||||
}
|
||||
|
||||
return workflowFile;
|
||||
|
||||
static string? GetRepoFolder()
|
||||
{
|
||||
DirectoryInfo? current = new(Directory.GetCurrentDirectory());
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(current.FullName, ".git")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Notify(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
try
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Marketing": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "\"Marketing.yaml\""
|
||||
},
|
||||
"MathChat": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "\"MathChat.yaml\""
|
||||
},
|
||||
"Question": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "\"Question.yaml\""
|
||||
},
|
||||
"Research": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "\"DeepResearch.yaml\""
|
||||
},
|
||||
"ResponseObject": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "\"ResponseObject.yaml\""
|
||||
},
|
||||
"UserInput": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "\"UserInput.yaml\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MAAIW001;OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample ports the Python Magentic orchestration sample to .NET.
|
||||
// A Magentic workflow coordinates a researcher and a coder, streams orchestration
|
||||
// events as the plan evolves, and prints the final conversation transcript.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Specialized.Magentic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace WorkflowMagenticOrchestrationSample;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates Magentic orchestration with a researcher, a coder, and an LLM manager.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - An Azure AI Foundry project endpoint and model deployment must be configured.
|
||||
/// - Run <c>az login</c> before executing the sample.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private const string TaskPrompt =
|
||||
"I am preparing a report on the energy efficiency of different machine learning model architectures. " +
|
||||
"Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 " +
|
||||
"on standard datasets (e.g., ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). " +
|
||||
"Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 " +
|
||||
"VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model " +
|
||||
"per task type (image classification, text classification, and text generation).";
|
||||
|
||||
private static async Task Main()
|
||||
{
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIProjectClient projectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
AIAgent researcherAgent = projectClient.AsAIAgent(
|
||||
deploymentName,
|
||||
name: "ResearcherAgent",
|
||||
description: "Specialist in research and information gathering.",
|
||||
instructions: "You are a researcher. Find relevant information without doing additional computation or quantitative analysis.");
|
||||
|
||||
AIAgent coderAgent = projectClient.AsAIAgent(
|
||||
deploymentName,
|
||||
name: "CoderAgent",
|
||||
description: "A helpful assistant that writes and executes code to analyze data.",
|
||||
instructions: "You solve quantitative questions by writing and running code. Show the analysis and the computation process clearly.",
|
||||
tools: [new HostedCodeInterpreterTool()]);
|
||||
|
||||
AIAgent managerAgent = projectClient.AsAIAgent(
|
||||
deploymentName,
|
||||
name: "MagenticManager",
|
||||
description: "Orchestrator that coordinates the research and coding workflow.",
|
||||
instructions: "You coordinate the team to complete complex tasks efficiently.");
|
||||
|
||||
Workflow workflow = new MagenticWorkflowBuilder(managerAgent)
|
||||
.AddParticipants([researcherAgent, coderAgent])
|
||||
.WithName("Magentic Orchestration Workflow")
|
||||
.WithDescription("Coordinates a researcher and coder to solve a complex analytical task.")
|
||||
.RequirePlanSignoff(false)
|
||||
.WithMaxRounds(10)
|
||||
.WithMaxStalls(3)
|
||||
.WithMaxResets(2)
|
||||
.Build();
|
||||
|
||||
Console.WriteLine("Building Magentic workflow...");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Task: {TaskPrompt}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Starting workflow execution...");
|
||||
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(
|
||||
workflow,
|
||||
new List<ChatMessage> { new(ChatRole.User, TaskPrompt) });
|
||||
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
|
||||
string? lastResponseId = null;
|
||||
WorkflowOutputEvent? finalOutput = null;
|
||||
|
||||
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
|
||||
{
|
||||
switch (workflowEvent)
|
||||
{
|
||||
case AgentResponseUpdateEvent updateEvent:
|
||||
WriteStreamingUpdate(updateEvent, ref lastResponseId);
|
||||
break;
|
||||
|
||||
case MagenticPlanCreatedEvent planCreated:
|
||||
WriteMagenticMessage("Initial Plan", planCreated.FullTaskLedger.Text);
|
||||
PauseIfInteractive();
|
||||
break;
|
||||
|
||||
case MagenticReplannedEvent replanned:
|
||||
WriteMagenticMessage("Replanned", replanned.FullTaskLedger.Text);
|
||||
PauseIfInteractive();
|
||||
break;
|
||||
|
||||
case MagenticProgressLedgerUpdatedEvent progressUpdated:
|
||||
WriteMagenticMessage("Progress Ledger", FormatProgressLedger(progressUpdated.ProgressLedger));
|
||||
PauseIfInteractive();
|
||||
break;
|
||||
|
||||
case WorkflowOutputEvent outputEvent when outputEvent.Is<List<ChatMessage>>():
|
||||
finalOutput = outputEvent;
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.Error.WriteLine(workflowError.Exception?.ToString() ?? "Unknown workflow error occurred.");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case ExecutorFailedEvent executorFailed:
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.Error.WriteLine($"Executor '{executorFailed.ExecutorId}' failed with {(executorFailed.Data is null ? "unknown error" : $"exception {executorFailed.Data}")}.");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalOutput?.As<List<ChatMessage>>() is { } transcript)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(new string('=', 80));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Final Conversation Transcript:");
|
||||
Console.WriteLine();
|
||||
|
||||
foreach (ChatMessage message in transcript)
|
||||
{
|
||||
Console.WriteLine($"{message.AuthorName ?? message.Role.ToString()}: {message.Text}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteStreamingUpdate(AgentResponseUpdateEvent updateEvent, ref string? lastResponseId)
|
||||
{
|
||||
string responseId = updateEvent.Update.ResponseId ?? updateEvent.Update.MessageId ?? updateEvent.ExecutorId;
|
||||
if (!string.Equals(responseId, lastResponseId, StringComparison.Ordinal))
|
||||
{
|
||||
if (lastResponseId is not null)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
Console.Write($"- {updateEvent.ExecutorId}: ");
|
||||
lastResponseId = responseId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(updateEvent.Update.Text))
|
||||
{
|
||||
Console.Write(updateEvent.Update.Text);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteMagenticMessage(string title, string? content)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"[Magentic {title}]");
|
||||
Console.WriteLine(content);
|
||||
}
|
||||
|
||||
private static string FormatProgressLedger(MagenticProgressLedger ledger) =>
|
||||
string.Join(Environment.NewLine,
|
||||
$"Request satisfied: {ledger.IsRequestSatisfied}",
|
||||
$"In loop: {ledger.IsInLoop}",
|
||||
$"Making progress: {ledger.IsProgressBeingMade}",
|
||||
$"Next speaker: {ledger.NextSpeaker}",
|
||||
$"Instruction: {ledger.InstructionOrQuestion}");
|
||||
|
||||
private static void PauseIfInteractive()
|
||||
{
|
||||
if (Console.IsInputRedirected || Console.IsOutputRedirected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Write("Press Enter to continue...");
|
||||
Console.ReadLine();
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# Magentic Orchestration Sample
|
||||
|
||||
This sample showcases the Magentic Orchestration Pattern in .NET, setting up a team with three roles:
|
||||
|
||||
- **ResearcherAgent** gathers factual background information.
|
||||
- **CoderAgent** uses `HostedCodeInterpreterTool` for quantitative analysis.
|
||||
- **MagenticManager** plans the work, tracks progress, and decides who should act next.
|
||||
|
||||
## What This Sample Demonstrates
|
||||
|
||||
- Building a Magentic workflow with `MagenticWorkflowBuilder`
|
||||
- Combining standard responses-based agents with a code interpreter-enabled participant
|
||||
- Streaming orchestration events such as the initial plan, replans, and progress-ledger updates
|
||||
- Printing the final multi-agent conversation transcript
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `AZURE_AI_PROJECT_ENDPOINT` set to your Azure AI Foundry project endpoint
|
||||
- `AZURE_AI_MODEL_DEPLOYMENT_NAME` set to your model deployment name (defaults to `gpt-5.4-mini`)
|
||||
- `az login` completed before running the sample
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
The sample prints:
|
||||
|
||||
1. The original task prompt
|
||||
2. Streamed updates from the participating agents
|
||||
3. Magentic plan and progress-ledger events as the workflow coordinates the team
|
||||
4. The final conversation transcript returned by the workflow
|
||||
|
||||
## Related Samples
|
||||
|
||||
- [Handoff Orchestration](../Handoff) - another multi-agent orchestration pattern in .NET workflows
|
||||
- [Python Magentic workflow sample](../../../../../python/samples/03-workflows/orchestrations/magentic.py) - the source scenario that this sample ports
|
||||
@@ -62,3 +62,4 @@ Once completed, please proceed to the other samples listed below.
|
||||
| Sample | Concepts |
|
||||
|--------|----------|
|
||||
| [Handoff Orchestration](./Orchestration/Handoff) | Introduces the Handoff Orchestration pattern |
|
||||
| [Magentic Orchestration](./Orchestration/Magentic) | Coordinates multiple agents with a Magentic manager, streamed plan events, and a final transcript |
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
AGENT_NAME=hosted-agent-skills
|
||||
SKILL_NAMES=support-style,escalation-policy
|
||||
# Set to true to provision sample skills to Foundry on startup (first-run convenience).
|
||||
# In production, skills are provisioned externally — leave this unset or false.
|
||||
PROVISION_SAMPLE_SKILLS=true
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
# When running outside the Foundry platform the platform-injected isolation keys are absent.
|
||||
# These two variables provide fallback values for local Docker debugging only.
|
||||
HOSTED_USER_ISOLATION_KEY=local-dev-user
|
||||
HOSTED_CHAT_ISOLATION_KEY=local-dev-chat
|
||||
@@ -0,0 +1,26 @@
|
||||
# Dockerfile for end-users consuming the Agent Framework via NuGet packages.
|
||||
#
|
||||
# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container,
|
||||
# which only succeeds when the project references its dependencies via PackageReference (see the
|
||||
# commented-out section in HostedAgentSkills.csproj). Contributors building from the
|
||||
# agent-framework repository source must use Dockerfile.contributor instead because
|
||||
# ProjectReference dependencies live outside this folder and cannot be restored from inside
|
||||
# this build context.
|
||||
#
|
||||
# Use the official .NET 10.0 ASP.NET runtime as a parent image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Final stage
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Dockerfile for contributors building from the agent-framework repository source.
|
||||
#
|
||||
# This project uses ProjectReference to the local Microsoft.Agents.AI source,
|
||||
# which means a standard multi-stage Docker build cannot resolve dependencies outside
|
||||
# this folder. Instead, pre-publish the app targeting the container runtime and copy
|
||||
# the output into the container:
|
||||
#
|
||||
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||
# docker build -f Dockerfile.contributor -t hosted-agent-skills .
|
||||
# docker run --rm -p 8088:8088 \
|
||||
# -e AGENT_NAME=hosted-agent-skills \
|
||||
# -e HOSTED_USER_ISOLATION_KEY=alice \
|
||||
# -e HOSTED_CHAT_ISOLATION_KEY=alice-chat-1 \
|
||||
# --env-file .env hosted-agent-skills
|
||||
#
|
||||
# For end-users consuming the NuGet package (not ProjectReference), use the standard
|
||||
# Dockerfile which performs a full dotnet restore + publish inside the container.
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
|
||||
WORKDIR /app
|
||||
COPY out/ .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedAgentSkills</RootNamespace>
|
||||
<AssemblyName>HostedAgentSkills</AssemblyName>
|
||||
<NoWarn>$(NoWarn);MEAI001;OPENAI001;AAIP001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
|
||||
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI" Version="1.6.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
<!-- Include the skills/ directory in the publish output so the sample can provision them -->
|
||||
<ItemGroup>
|
||||
<None Include="skills\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Hosted-AgentSkills
|
||||
//
|
||||
// Demonstrates how to host an agent that loads its behavioral guidelines from Foundry Skills at
|
||||
// startup. Skills are authored as SKILL.md files, uploaded to Foundry via the Skills REST API,
|
||||
// and downloaded by the agent on boot so guideline updates ship without code changes.
|
||||
//
|
||||
// The agent uses AgentSkillsProvider from the Agent Framework which implements the progressive
|
||||
// disclosure pattern from the Agent Skills specification (https://agentskills.io/):
|
||||
// 1. Advertise — skill names and descriptions are injected into the system prompt.
|
||||
// 2. Load — the model calls load_skill to retrieve the full SKILL.md body on demand.
|
||||
//
|
||||
// IMPORTANT: In production, skill provisioning (uploading SKILL.md files to Foundry) is an
|
||||
// external concern — it is NOT the hosted agent's responsibility. The provisioning helper below
|
||||
// is included for sample convenience only, so the sample is self-contained and runnable without
|
||||
// a separate setup step. A real deployment pipeline would provision skills separately (e.g., via
|
||||
// a CI/CD step, a CLI script, or a management portal).
|
||||
|
||||
#pragma warning disable AAIP001 // ProjectAgentSkills is experimental
|
||||
|
||||
using System.ClientModel;
|
||||
using System.IO.Compression;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Core;
|
||||
using Azure.Identity;
|
||||
using DotNetEnv;
|
||||
using Hosted_Shared_Contributor_Setup;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry.Hosting;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
// Load .env file if present (for local development)
|
||||
Env.TraversePath().Load();
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
|
||||
string skillNames = Environment.GetEnvironmentVariable("SKILL_NAMES")
|
||||
?? throw new InvalidOperationException("SKILL_NAMES is not set. Provide a comma-separated list of skill names (e.g., support-style,escalation-policy).");
|
||||
|
||||
string[] requestedSkills = skillNames.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (requestedSkills.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("SKILL_NAMES must list at least one skill name.");
|
||||
}
|
||||
|
||||
// Validate skill names to prevent path traversal.
|
||||
foreach (string name in requestedSkills)
|
||||
{
|
||||
if (name.Contains('.') || name.Contains('/') || name.Contains('\\') || Path.IsPathRooted(name))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid skill name '{name}': skill names must not contain path separators or dots.");
|
||||
}
|
||||
}
|
||||
|
||||
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
|
||||
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
|
||||
TokenCredential credential = new ChainedTokenCredential(
|
||||
new DevTemporaryTokenCredential(),
|
||||
new DefaultAzureCredential());
|
||||
|
||||
AIProjectClient projectClient = new(new Uri(endpoint), credential);
|
||||
ProjectAgentSkills skillsClient = projectClient.AgentAdministrationClient.GetAgentSkills();
|
||||
|
||||
// ── Provision skills (sample convenience only — NOT a production pattern) ─────
|
||||
// In production, skills are provisioned externally (e.g., via CI/CD or a management script).
|
||||
// This helper ensures the sample's SKILL.md files exist in Foundry so the sample is runnable
|
||||
// out of the box without a separate setup step. Set PROVISION_SAMPLE_SKILLS=true to enable.
|
||||
string sourceSkillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
|
||||
bool provisionEnabled = string.Equals(
|
||||
Environment.GetEnvironmentVariable("PROVISION_SAMPLE_SKILLS"), "true", StringComparison.OrdinalIgnoreCase);
|
||||
if (provisionEnabled && Directory.Exists(sourceSkillsDir))
|
||||
{
|
||||
await EnsureSkillsProvisionedAsync(skillsClient, sourceSkillsDir, requestedSkills);
|
||||
}
|
||||
|
||||
// ── Download skills from Foundry ─────────────────────────────────────────────
|
||||
// Pull the latest copy of each skill from Foundry into a runtime-only folder.
|
||||
// This directory is recreated on every startup so the agent always picks up
|
||||
// the latest version of each skill.
|
||||
string downloadedSkillsDir = Path.Combine(AppContext.BaseDirectory, "downloaded_skills");
|
||||
await DownloadSkillsAsync(skillsClient, requestedSkills, downloadedSkillsDir);
|
||||
|
||||
// ── Wire skills into the agent ───────────────────────────────────────────────
|
||||
// AgentSkillsProvider implements progressive disclosure: skill names and descriptions
|
||||
// are advertised in the system prompt (~100 tokens per skill), and the full SKILL.md
|
||||
// body is loaded on demand when the model calls the load_skill tool.
|
||||
AgentSkillsProvider skillsProvider = new(downloadedSkillsDir);
|
||||
|
||||
ChatClientAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-agent-skills",
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a customer-support assistant for Contoso Outdoors.",
|
||||
},
|
||||
AIContextProviders = [skillsProvider]
|
||||
});
|
||||
|
||||
// Host the agent as a Foundry Hosted Agent using the Responses API.
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddFoundryResponses(agent);
|
||||
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
|
||||
|
||||
var app = builder.Build();
|
||||
app.MapFoundryResponses();
|
||||
|
||||
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
|
||||
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
|
||||
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
|
||||
app.MapDevTemporaryLocalAgentEndpoint();
|
||||
|
||||
app.Run();
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Downloads each named skill from Foundry and extracts the ZIP archive into a
|
||||
// separate subdirectory under the target directory.
|
||||
static async Task DownloadSkillsAsync(ProjectAgentSkills skillsClient, string[] skillNames, string targetDir)
|
||||
{
|
||||
if (Directory.Exists(targetDir))
|
||||
{
|
||||
Directory.Delete(targetDir, recursive: true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetDir);
|
||||
|
||||
foreach (string name in skillNames)
|
||||
{
|
||||
Console.WriteLine($"Downloading skill '{name}' from Foundry...");
|
||||
BinaryData zipData = await skillsClient.DownloadSkillAsync(name);
|
||||
|
||||
string skillDir = Path.Combine(targetDir, name);
|
||||
Directory.CreateDirectory(skillDir);
|
||||
|
||||
using var zipStream = zipData.ToStream();
|
||||
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
|
||||
SafeExtractZip(archive, skillDir);
|
||||
|
||||
if (!File.Exists(Path.Combine(skillDir, "SKILL.md")))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Downloaded archive for '{name}' did not contain a SKILL.md at the root.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts a ZIP archive into a destination directory, rejecting entries that would
|
||||
// escape the target path (zip-slip guard).
|
||||
static void SafeExtractZip(ZipArchive archive, string destinationDir)
|
||||
{
|
||||
string destRoot = Path.GetFullPath(destinationDir);
|
||||
string destRootWithSep = Path.EndsInDirectorySeparator(destRoot)
|
||||
? destRoot
|
||||
: destRoot + Path.DirectorySeparatorChar;
|
||||
|
||||
// Use ordinal comparison on Unix (case-sensitive FS) and ordinal-ignore-case on Windows.
|
||||
var comparison = OperatingSystem.IsWindows()
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
|
||||
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||
{
|
||||
string entryPath = Path.GetFullPath(Path.Combine(destRoot, entry.FullName));
|
||||
if (!entryPath.StartsWith(destRootWithSep, comparison)
|
||||
&& !string.Equals(entryPath, destRoot, comparison))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Refusing to extract unsafe path '{entry.FullName}' outside of '{destRoot}'.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
// Directory entry — ensure it exists.
|
||||
Directory.CreateDirectory(entryPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(entryPath)!);
|
||||
entry.ExtractToFile(entryPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures each requested skill is provisioned in Foundry. For each skill name, checks whether
|
||||
// the skill exists and uploads it from the local source directory if it does not.
|
||||
//
|
||||
// This is a sample convenience helper — in production, skill provisioning is an external concern.
|
||||
static async Task EnsureSkillsProvisionedAsync(ProjectAgentSkills skillsClient, string sourceDir, string[] skillNames)
|
||||
{
|
||||
foreach (string name in skillNames)
|
||||
{
|
||||
string skillPath = Path.Combine(sourceDir, name);
|
||||
if (!Directory.Exists(skillPath) || !File.Exists(Path.Combine(skillPath, "SKILL.md")))
|
||||
{
|
||||
continue; // No local source for this skill — skip provisioning.
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await skillsClient.GetSkillAsync(name);
|
||||
Console.WriteLine($"Skill '{name}' already exists in Foundry.");
|
||||
}
|
||||
catch (ClientResultException ex) when (ex.Status == 404)
|
||||
{
|
||||
Console.WriteLine($"Provisioning skill '{name}' from {skillPath}...");
|
||||
AgentsSkill imported = await skillsClient.CreateSkillFromPackageAsync(skillPath);
|
||||
Console.WriteLine($" Imported skill '{imported.Name}' (id={imported.SkillId}, has_blob={imported.HasBlob}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that loads its behavioral guidelines from [**Foundry Skills**](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills) at startup, hosted using the **Responses protocol**. Skills are authored once as `SKILL.md` files, uploaded to your Foundry project through the Skills REST API, and downloaded by the agent on boot so updates ship without code changes.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Authoring skills
|
||||
|
||||
Each skill is a Markdown file with a YAML front matter block. This sample ships two source skills under [`skills/`](skills/):
|
||||
|
||||
| Skill | Purpose |
|
||||
|---|---|
|
||||
| [`support-style`](skills/support-style/SKILL.md) | Voice, formatting, and signature rules for Contoso Outdoors support replies. |
|
||||
| [`escalation-policy`](skills/escalation-policy/SKILL.md) | When and how to escalate a customer ticket. |
|
||||
|
||||
Each `SKILL.md` includes a unique `*-CANARY-*` token that the model is asked to echo, so you can prove the skill was loaded from Foundry (not hallucinated) by checking the response.
|
||||
|
||||
> The `name` and `description` values in the YAML front matter must be **unquoted** — quoting them causes the Skills REST API to return HTTP 500 on import.
|
||||
|
||||
### Uploading skills
|
||||
|
||||
The sample includes a convenience provisioning step that checks whether each skill exists in Foundry and uploads it if not, gated behind the `PROVISION_SAMPLE_SKILLS=true` env var. **In production, skill provisioning is an external concern** — it is NOT the hosted agent's responsibility. A real deployment pipeline would provision skills separately (e.g., via a CI/CD step, a CLI script, or a management portal).
|
||||
|
||||
The provisioning uses `ProjectAgentSkills.CreateSkillFromPackageAsync(directoryPath)` from the `Azure.AI.Projects.Agents` SDK. The method packages the `SKILL.md` file as a ZIP and uploads it to Foundry.
|
||||
|
||||
### Downloading skills at agent startup
|
||||
|
||||
[`Program.cs`](Program.cs) reads the comma-separated `SKILL_NAMES` env var and for each skill name downloads the ZIP archive from Foundry via `ProjectAgentSkills.DownloadSkillAsync(name)`, then unpacks it into a **separate runtime directory** at `downloaded_skills/<name>/` (kept distinct from the static `skills/` source folder).
|
||||
|
||||
An [`AgentSkillsProvider`](../../../../../src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs) is then built over `downloaded_skills/` and attached to the agent as a context provider. The provider follows the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern:
|
||||
|
||||
1. **Advertise** — skill names and descriptions are injected into the system prompt at session start (~100 tokens per skill).
|
||||
2. **Load** — the model calls the `load_skill` tool when it decides a skill is relevant to the user's turn, and the full `SKILL.md` body is returned.
|
||||
|
||||
This means the model only pays the token cost for a skill's full body when it actually needs it, and updating a skill in Foundry + restarting the agent is enough to pick up the change — no code redeploy required.
|
||||
|
||||
> **Note:** This sample supports instruction-only and resource-based skills. If your downloaded skills contain scripts, add a script runner when constructing the `AgentSkillsProvider`.
|
||||
|
||||
### Agent Hosting
|
||||
|
||||
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the Responses API hosting layer (`AddFoundryResponses` / `MapFoundryResponses`).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
|
||||
- Azure CLI logged in (`az login`)
|
||||
|
||||
### Required RBAC
|
||||
|
||||
Your identity (or the Managed Identity running the container in production) needs **Azure AI User** on the Foundry project scope. This single role covers both authoring skills and downloading them.
|
||||
|
||||
## Running the Agent Host
|
||||
|
||||
Set the required environment variables and run the sample with `dotnet run`:
|
||||
|
||||
```bash
|
||||
export AZURE_AI_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
|
||||
export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o"
|
||||
export SKILL_NAMES="support-style,escalation-policy"
|
||||
export PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry
|
||||
```
|
||||
|
||||
Or in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:SKILL_NAMES="support-style,escalation-policy"
|
||||
$env:PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry
|
||||
```
|
||||
|
||||
You can also place these in a `.env` file next to `Program.cs` — see [`.env.example`](.env.example).
|
||||
|
||||
On startup you should see:
|
||||
|
||||
```text
|
||||
Skill 'support-style' already exists in Foundry.
|
||||
Skill 'escalation-policy' already exists in Foundry.
|
||||
Downloading skill 'support-style' from Foundry...
|
||||
Downloading skill 'escalation-policy' from Foundry...
|
||||
```
|
||||
|
||||
The downloaded `SKILL.md` files land under `downloaded_skills/<name>/SKILL.md` next to the published output. This directory is recreated from scratch on every run, so deleting it manually is never necessary.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
> Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi, I am Alex. I just want to confirm I can return my tent within 30 days."}'
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "I want a $750 refund on Order #A-1042 right now or I am calling my lawyer."}'
|
||||
```
|
||||
|
||||
| Prompt mentions | Skill that should drive the response |
|
||||
|---|---|
|
||||
| Routine return / shipping / care question | Model loads `support-style` (canary `STYLE-CANARY-3318`) — no escalation. |
|
||||
| Injury, legal threat, press, or refund > $500 | Model loads `escalation-policy` (canary `ESC-CANARY-7742`) **and** `support-style`. |
|
||||
|
||||
Because skills are loaded on demand, the canary token in a response also proves the model actually invoked `load_skill` for the matching skill (not just saw its name in the advertised list).
|
||||
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
When deploying to Foundry, make sure `SKILL_NAMES` is set in your `azd` environment so it gets injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml):
|
||||
|
||||
```bash
|
||||
azd env set SKILL_NAMES "support-style,escalation-policy"
|
||||
```
|
||||
|
||||
The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to download skills at startup.
|
||||
|
||||
> The `skills/` source folder is **not** deployed to Foundry — only the downloaded skills are used at runtime. The provisioning step must have been run against the same Foundry project before the agent can download the skills.
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
|
||||
name: hosted-agent-skills
|
||||
displayName: "Hosted Agent Skills"
|
||||
|
||||
description: >
|
||||
An Agent Framework agent that downloads its behavioral guidelines from the Foundry
|
||||
Skills REST API at startup, demonstrating how to decouple behavioral guidelines
|
||||
(tone, escalation policy, etc.) from agent code using AgentSkillsProvider.
|
||||
|
||||
metadata:
|
||||
tags:
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Agent Framework
|
||||
- Agent Skills
|
||||
- Foundry Skills
|
||||
|
||||
template:
|
||||
name: hosted-agent-skills
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
environment_variables:
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: SKILL_NAMES
|
||||
value: "{{SKILL_NAMES}}"
|
||||
parameters:
|
||||
properties:
|
||||
- name: SKILL_NAMES
|
||||
secret: false
|
||||
description: Comma-separated list of Foundry skill names to download at startup (e.g., support-style,escalation-policy)
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
@@ -0,0 +1,14 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
kind: hosted
|
||||
name: hosted-agent-skills
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
environment_variables:
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
|
||||
- name: SKILL_NAMES
|
||||
value: ${SKILL_NAMES}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
#requires -Version 7
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Local smoke test for the Hosted-AgentSkills sample.
|
||||
.DESCRIPTION
|
||||
Publishes the sample, builds the contributor Docker image, runs the container, drives
|
||||
two conversations via curl invocations, and asserts that the agent loaded the correct
|
||||
Foundry Skill for each prompt (verified via canary tokens in the response).
|
||||
Exits non-zero on failure.
|
||||
|
||||
Prerequisites:
|
||||
- Docker
|
||||
- az login (token is fetched from the host)
|
||||
- .env populated with AZURE_AI_PROJECT_ENDPOINT and model deployment
|
||||
- Skills provisioned to Foundry (set PROVISION_SAMPLE_SKILLS=true on first run)
|
||||
.NOTES
|
||||
This script is for local Docker debugging only. The Foundry platform supplies the
|
||||
isolation keys for every inbound request in production and the dev fallback used here
|
||||
must not be enabled in production deployments.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$Port = 8088,
|
||||
[string]$ImageName = 'hosted-agent-skills-smoke',
|
||||
[string]$ContainerName = 'hosted-agent-skills-smoke'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-Location -Path $PSScriptRoot/..
|
||||
|
||||
if (-not (Test-Path .env)) {
|
||||
throw '.env not found. Copy .env.example to .env and fill in AZURE_AI_PROJECT_ENDPOINT.'
|
||||
}
|
||||
|
||||
Write-Host '==> Publishing sample for linux-musl-x64 ...'
|
||||
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw 'dotnet publish failed.' }
|
||||
|
||||
Write-Host '==> Building docker image ...'
|
||||
docker build -f Dockerfile.contributor -t $ImageName . | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw 'docker build failed.' }
|
||||
|
||||
Write-Host '==> Fetching bearer token ...'
|
||||
$bearer = az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv
|
||||
if (-not $bearer) { throw 'Failed to obtain bearer token. Run az login.' }
|
||||
|
||||
function Start-Container {
|
||||
docker rm -f $ContainerName 2>$null | Out-Null
|
||||
docker run -d --name $ContainerName -p ${Port}:8088 `
|
||||
-e AGENT_NAME=hosted-agent-skills `
|
||||
-e AZURE_BEARER_TOKEN=$bearer `
|
||||
-e HOSTED_USER_ISOLATION_KEY=smoke-user `
|
||||
-e HOSTED_CHAT_ISOLATION_KEY=smoke-chat-1 `
|
||||
--env-file .env `
|
||||
$ImageName | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) { throw "docker run failed." }
|
||||
# Wait for the server to start and download skills from Foundry.
|
||||
Write-Host ' Waiting for startup (skill download + server ready) ...'
|
||||
Start-Sleep -Seconds 15
|
||||
}
|
||||
|
||||
function Invoke-Agent([string]$Prompt, [string]$PreviousResponseId = $null) {
|
||||
$body = @{ input = $Prompt; model = 'hosted-agent-skills' }
|
||||
if ($PreviousResponseId) { $body['previous_response_id'] = $PreviousResponseId }
|
||||
$json = $body | ConvertTo-Json -Compress
|
||||
$resp = Invoke-RestMethod -Method Post -Uri "http://localhost:$Port/responses" -ContentType 'application/json' -Body $json
|
||||
return $resp
|
||||
}
|
||||
|
||||
function Get-ResponseText($response) {
|
||||
return ($response.output | ForEach-Object { $_.content | ForEach-Object { $_.text } }) -join ' '
|
||||
}
|
||||
|
||||
function Assert-Contains([string]$Haystack, [string]$Needle, [string]$Label) {
|
||||
if ($Haystack -notmatch [regex]::Escape($Needle)) {
|
||||
throw "FAILED [$Label]: expected response to contain '$Needle' but got: $Haystack"
|
||||
}
|
||||
Write-Host "PASS [$Label]: response contains '$Needle'."
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Container
|
||||
|
||||
Write-Host '==> Test 1: Routine support question -> support-style skill ...'
|
||||
$r1 = Invoke-Agent -Prompt 'Hi, I am Alex. I just want to confirm I can return my tent within 30 days.'
|
||||
$text1 = Get-ResponseText $r1
|
||||
Assert-Contains $text1 'STYLE-CANARY-3318' 'routine question: support-style canary'
|
||||
|
||||
Write-Host '==> Test 2: Escalation trigger -> escalation-policy skill ...'
|
||||
$r2 = Invoke-Agent -Prompt 'I want a $750 refund on Order #A-1042 right now or I am calling my lawyer.'
|
||||
$text2 = Get-ResponseText $r2
|
||||
Assert-Contains $text2 'ESC-CANARY-7742' 'escalation trigger: escalation-policy canary'
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '==> All smoke assertions passed.'
|
||||
}
|
||||
finally {
|
||||
docker rm -f $ContainerName 2>$null | Out-Null
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: escalation-policy
|
||||
description: When and how to escalate Contoso Outdoors customer-support tickets.
|
||||
---
|
||||
|
||||
# Contoso Outdoors Escalation Policy
|
||||
|
||||
You must follow this escalation policy on every conversation.
|
||||
|
||||
## Escalate immediately when the customer
|
||||
|
||||
- Reports an injury, allergic reaction, or other safety incident.
|
||||
- Mentions legal action, regulators, or the press.
|
||||
- Has waited more than 14 days for a refund that was already approved.
|
||||
- Requests a refund larger than $500.
|
||||
|
||||
## How to escalate
|
||||
|
||||
1. Acknowledge the issue in one sentence.
|
||||
2. Tell the customer you are escalating to a senior specialist.
|
||||
3. Provide the escalation reference `ESC-CANARY-7742` and the SLA: a senior
|
||||
specialist will reply within 1 business day.
|
||||
4. Do not promise a specific outcome (refund, replacement, compensation) on
|
||||
escalated tickets — only the senior specialist can commit to one.
|
||||
|
||||
## Do not escalate
|
||||
|
||||
- Routine returns within the standard 30-day window.
|
||||
- Shipping status questions.
|
||||
- Product care and usage questions.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: support-style
|
||||
description: Contoso Outdoors customer-support tone and formatting guidelines.
|
||||
---
|
||||
|
||||
# Contoso Outdoors Support Style
|
||||
|
||||
You are speaking on behalf of Contoso Outdoors customer support.
|
||||
|
||||
## Voice
|
||||
|
||||
- Warm, concise, and confident — never apologetic in a hand-wringing way.
|
||||
- Use the customer's name when it is known.
|
||||
- Sign every response with `— Contoso Outdoors Support`.
|
||||
|
||||
## Formatting
|
||||
|
||||
- Keep replies to 1–3 short paragraphs unless the customer asks for detail.
|
||||
- Use bullet lists only when enumerating concrete steps or options.
|
||||
- Always reference order numbers as `Order #<id>` (e.g. `Order #A-1042`).
|
||||
|
||||
## Canary
|
||||
|
||||
To prove this skill was loaded, include the token `STYLE-CANARY-3318` on a
|
||||
separate line at the bottom of every response, prefixed with `# `.
|
||||
+3
-3
@@ -27,9 +27,9 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+3
-3
@@ -26,9 +26,9 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+5
-5
@@ -32,11 +32,11 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.6.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
+4
-4
@@ -27,10 +27,10 @@
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.6.1" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
@@ -474,6 +474,7 @@ public sealed class A2AAgent : AIAgent
|
||||
ResponseId = statusUpdateEvent.TaskId,
|
||||
RawRepresentation = statusUpdateEvent,
|
||||
Role = ChatRole.Assistant,
|
||||
MessageId = statusUpdateEvent.Status.Message?.MessageId,
|
||||
FinishReason = MapTaskStateToFinishReason(statusUpdateEvent.Status.State),
|
||||
AdditionalProperties = statusUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
|
||||
Contents = statusUpdateEvent.Status.GetUserInputRequests(),
|
||||
|
||||
@@ -20,8 +20,28 @@ internal static class AGUIChatMessageExtensions
|
||||
this IEnumerable<AGUIMessage> aguiMessages,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
// Coalesce consecutive AGUIAssistantMessages that carry tool_calls into a single
|
||||
// ChatMessage. The AG-UI client (e.g. @ag-ui/client) creates a separate assistant
|
||||
// message per tool call when ToolCallStartEvent.parentMessageId is empty, but
|
||||
// OpenAI's chat-completion API requires every assistant message with tool_calls
|
||||
// to be IMMEDIATELY followed by tool responses for each of its tool_call_ids.
|
||||
// Sending two consecutive single-tool-call assistant messages before any tool
|
||||
// result triggers HTTP 400 "tool_call_ids did not have response messages".
|
||||
List<AIContent>? pendingContents = null;
|
||||
string? pendingId = null;
|
||||
|
||||
foreach (var message in aguiMessages)
|
||||
{
|
||||
bool isAssistantWithToolCalls =
|
||||
message is AGUIAssistantMessage am && am.ToolCalls is { Length: > 0 };
|
||||
|
||||
if (pendingContents is not null && !isAssistantWithToolCalls)
|
||||
{
|
||||
yield return new ChatMessage(ChatRole.Assistant, pendingContents) { MessageId = pendingId };
|
||||
pendingContents = null;
|
||||
pendingId = null;
|
||||
}
|
||||
|
||||
var role = MapChatRole(message.Role);
|
||||
|
||||
switch (message)
|
||||
@@ -84,14 +104,14 @@ internal static class AGUIChatMessageExtensions
|
||||
|
||||
case AGUIAssistantMessage assistantMessage when assistantMessage.ToolCalls is { Length: > 0 }:
|
||||
{
|
||||
var contents = new List<AIContent>();
|
||||
pendingContents ??= new List<AIContent>();
|
||||
pendingId ??= message.Id;
|
||||
|
||||
if (!string.IsNullOrEmpty(assistantMessage.Content))
|
||||
{
|
||||
contents.Add(new TextContent(assistantMessage.Content));
|
||||
pendingContents.Add(new TextContent(assistantMessage.Content));
|
||||
}
|
||||
|
||||
// Add tool calls
|
||||
foreach (var toolCall in assistantMessage.ToolCalls)
|
||||
{
|
||||
Dictionary<string, object?>? arguments = null;
|
||||
@@ -102,16 +122,12 @@ internal static class AGUIChatMessageExtensions
|
||||
jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
|
||||
}
|
||||
|
||||
contents.Add(new FunctionCallContent(
|
||||
pendingContents.Add(new FunctionCallContent(
|
||||
toolCall.Id,
|
||||
toolCall.Function.Name,
|
||||
arguments));
|
||||
}
|
||||
|
||||
yield return new ChatMessage(role, contents)
|
||||
{
|
||||
MessageId = message.Id
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -134,6 +150,12 @@ internal static class AGUIChatMessageExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining pending assistant-tool-call entry at end of stream.
|
||||
if (pendingContents is not null)
|
||||
{
|
||||
yield return new ChatMessage(ChatRole.Assistant, pendingContents) { MessageId = pendingId };
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<AGUIMessage> AsAGUIMessages(
|
||||
|
||||
@@ -448,24 +448,36 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
};
|
||||
|
||||
string? currentMessageId = null;
|
||||
string? streamingMessageId = null;
|
||||
string? textStreamingFallback = null;
|
||||
bool textInFallback = false;
|
||||
string? currentReasoningBaseId = null;
|
||||
string? currentReasoningId = null;
|
||||
string? currentReasoningMessageId = null;
|
||||
await foreach (var chatResponse in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Generate a fallback MessageId when the provider doesn't supply one.
|
||||
// This ensures all AGUI events have a valid messageId regardless of agent type.
|
||||
if (string.IsNullOrWhiteSpace(chatResponse.MessageId))
|
||||
// The text-event surface (TextMessageStart/Content/End) requires a non-empty
|
||||
// MessageId to be valid AGUI. Generate a fallback scoped to a contiguous run of
|
||||
// null/empty-MessageId chunks (one logical text message). Leave the raw
|
||||
// chatResponse.MessageId untouched so the tool-call surface below uses the raw
|
||||
// provider value — collapsing parallel tool calls under a synthetic shared parent
|
||||
// would make the FE render them as one assistant-message bubble instead of
|
||||
// distinct rows.
|
||||
string? textMessageId = chatResponse.MessageId;
|
||||
if (string.IsNullOrWhiteSpace(textMessageId))
|
||||
{
|
||||
chatResponse.MessageId = ContainsToolResult(chatResponse)
|
||||
? Guid.NewGuid().ToString("N")
|
||||
: (streamingMessageId ??= Guid.NewGuid().ToString("N"));
|
||||
textStreamingFallback ??= Guid.NewGuid().ToString("N");
|
||||
textMessageId = textStreamingFallback;
|
||||
textInFallback = true;
|
||||
}
|
||||
else if (textInFallback)
|
||||
{
|
||||
textStreamingFallback = null;
|
||||
textInFallback = false;
|
||||
}
|
||||
|
||||
if (chatResponse is { Contents.Count: > 0 } &&
|
||||
chatResponse.Contents[0] is TextContent &&
|
||||
!string.Equals(currentMessageId, chatResponse.MessageId, StringComparison.Ordinal))
|
||||
!string.Equals(currentMessageId, textMessageId, StringComparison.Ordinal))
|
||||
{
|
||||
// Close any open reasoning block before opening a text message, so AG-UI
|
||||
// events are properly bracketed. MEAI providers share one MessageId across
|
||||
@@ -498,11 +510,11 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
// Start the new message
|
||||
yield return new TextMessageStartEvent
|
||||
{
|
||||
MessageId = chatResponse.MessageId!,
|
||||
MessageId = textMessageId!,
|
||||
Role = chatResponse.Role!.Value.Value
|
||||
};
|
||||
|
||||
currentMessageId = chatResponse.MessageId;
|
||||
currentMessageId = textMessageId;
|
||||
}
|
||||
|
||||
// Emit text content if present
|
||||
@@ -577,9 +589,15 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
currentReasoningMessageId = null;
|
||||
}
|
||||
|
||||
// Each tool result is a distinct tool-role message on the AGUI wire.
|
||||
// MEAI's FunctionInvokingChatClient shares one synthetic MessageId
|
||||
// across all FunctionResultContent items, but the FE keys messages
|
||||
// by id, so emitting them with the same id collapses them in React
|
||||
// reconciliation. Derive a unique, deterministic per-result id from
|
||||
// the (LLM-assigned) call id.
|
||||
yield return new ToolCallResultEvent
|
||||
{
|
||||
MessageId = chatResponse.MessageId,
|
||||
MessageId = $"result-{functionResultContent.CallId}",
|
||||
ToolCallId = functionResultContent.CallId,
|
||||
Content = SerializeResultContent(functionResultContent, jsonSerializerOptions) ?? "",
|
||||
Role = AGUIRoles.Tool
|
||||
@@ -674,7 +692,7 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
// Text content event
|
||||
yield return new TextMessageContentEvent
|
||||
{
|
||||
MessageId = chatResponse.MessageId!,
|
||||
MessageId = textMessageId!,
|
||||
#if !NET
|
||||
Delta = Encoding.UTF8.GetString(dataContent.Data.ToArray())
|
||||
#else
|
||||
@@ -726,17 +744,4 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
_ => JsonSerializer.Serialize(functionResultContent.Result, options.GetTypeInfo(functionResultContent.Result.GetType())),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ContainsToolResult(ChatResponseUpdate chatResponse)
|
||||
{
|
||||
foreach (AIContent content in chatResponse.Contents)
|
||||
{
|
||||
if (content is FunctionResultContent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
## v1.0.0-preview.260219.1
|
||||
|
||||
- [BREAKING] Changed ChatHistory and AIContext Providers to have pipeline semantics ([#3806](https://github.com/microsoft/agent-framework/pull/3806))
|
||||
- Marked all `RunAsync<T>` overloads as `new`, added missing ones, and added support for primitives and arrays ([#3803](https://github.com/microsoft/agent-framework/pull/3803))
|
||||
- Marked all `RunAsync<T>` overloads as `new`, added missing ones, and added support for primitives and arrays #3803
|
||||
- Improve session cast error message quality and consistency ([#3973](https://github.com/microsoft/agent-framework/pull/3973))
|
||||
|
||||
## v1.0.0-preview.260212.1
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -13,7 +12,6 @@ using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Responses;
|
||||
|
||||
@@ -22,7 +20,6 @@ namespace Azure.AI.Projects;
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="AIProjectClient"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static partial class AIProjectClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
@@ -374,6 +371,7 @@ public static partial class AIProjectClientExtensions
|
||||
if (agentDefinition is DeclarativeAgentDefinition { Tools: { Count: > 0 } definitionTools })
|
||||
{
|
||||
// Check if no tools were provided while the agent definition requires in-proc tools.
|
||||
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
if (requireInvocableTools && chatOptions?.Tools is not { Count: > 0 } && definitionTools.Any(t => t is FunctionTool))
|
||||
{
|
||||
throw new ArgumentException("The agent definition in-process tools must be provided in the extension method tools parameter.");
|
||||
@@ -406,6 +404,7 @@ public static partial class AIProjectClientExtensions
|
||||
|
||||
(agentTools ??= []).Add(responseTool.AsAITool());
|
||||
}
|
||||
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
if (requireInvocableTools && missingTools is { Count: > 0 })
|
||||
{
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Foundry;
|
||||
@@ -17,7 +15,6 @@ namespace Microsoft.Agents.AI.Foundry;
|
||||
/// <c>to_prompt_agent(agent)</c> function for agents whose underlying chat client is a
|
||||
/// <see cref="FoundryChatClient"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class ChatClientAgentFoundryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using OpenAI.Responses;
|
||||
|
||||
#pragma warning disable OPENAI001
|
||||
@@ -27,7 +26,6 @@ namespace Microsoft.Agents.AI.Foundry;
|
||||
/// <c>FoundryAITool.CreateOpenApiTool(definition)</c>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class FoundryAITool
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -4,14 +4,12 @@ using System;
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Foundry;
|
||||
@@ -36,7 +34,6 @@ namespace Microsoft.Agents.AI.Foundry;
|
||||
/// <c>AsAIAgent</c> extension methods on <see cref="AIProjectClient"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public sealed class FoundryAgent : DelegatingAIAgent
|
||||
{
|
||||
/// <summary>
|
||||
@@ -261,6 +258,7 @@ public sealed class FoundryAgent : DelegatingAIAgent
|
||||
return innerAgent;
|
||||
}
|
||||
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
if (innerAgent.ChatClient.GetService<OpenAIRequestPolicies>() is { } policies)
|
||||
{
|
||||
OpenAIRequestPoliciesReflection.AddPolicyIfMissing(
|
||||
@@ -268,6 +266,7 @@ public sealed class FoundryAgent : DelegatingAIAgent
|
||||
ClientHeadersPolicy.Instance,
|
||||
PipelinePosition.PerCall);
|
||||
}
|
||||
#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
return new ClientHeadersAgent(innerAgent);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Files;
|
||||
using OpenAI.VectorStores;
|
||||
@@ -23,7 +21,6 @@ namespace Microsoft.Agents.AI.Foundry;
|
||||
/// <see cref="FoundryChatClient"/> at the agent level so callers do not need to drop down to
|
||||
/// <c>agent.GetService<FoundryChatClient>().X()</c> for common workflows.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class FoundryAgentExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -4,7 +4,6 @@ using System;
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
@@ -13,7 +12,6 @@ using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Files;
|
||||
using OpenAI.Responses;
|
||||
@@ -53,7 +51,6 @@ namespace Microsoft.Agents.AI.Foundry;
|
||||
/// behind an Agent Endpoint. It is not synonymous with the Agent Endpoint mode itself.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public sealed class FoundryChatClient : DelegatingChatClient
|
||||
{
|
||||
private readonly ChatClientMetadata _metadata;
|
||||
@@ -652,6 +649,7 @@ public sealed class FoundryChatClient : DelegatingChatClient
|
||||
/// <summary>Best-effort registration of <see cref="AgentFrameworkUserAgentPolicy"/> via the MEAI <see cref="OpenAIRequestPolicies"/> hook with at-most-once dedup per pipeline.</summary>
|
||||
private static void TryRegisterAgentFrameworkUserAgentPolicy(IChatClient? innerClient)
|
||||
{
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
if (innerClient?.GetService<OpenAIRequestPolicies>() is { } policies)
|
||||
{
|
||||
// OpenAIRequestPoliciesReflection.AddPolicyIfMissing performs a check-then-add against
|
||||
@@ -663,6 +661,7 @@ public sealed class FoundryChatClient : DelegatingChatClient
|
||||
AgentFrameworkUserAgentPolicy.Instance,
|
||||
PipelinePosition.PerCall);
|
||||
}
|
||||
#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -675,6 +674,7 @@ public sealed class FoundryChatClient : DelegatingChatClient
|
||||
/// </summary>
|
||||
private static void TryRegisterServedModelPolicy(IChatClient? innerClient)
|
||||
{
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
if (innerClient?.GetService<OpenAIRequestPolicies>() is { } policies)
|
||||
{
|
||||
OpenAIRequestPoliciesReflection.AddPolicyIfMissing(
|
||||
@@ -682,6 +682,7 @@ public sealed class FoundryChatClient : DelegatingChatClient
|
||||
ServedModelPolicy.Instance,
|
||||
PipelinePosition.PerCall);
|
||||
}
|
||||
#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
}
|
||||
|
||||
/// <summary>Default OAuth scope for the Azure AI resource. Matches the scope used by <c>Azure.AI.Extensions.OpenAI</c>'s internal authentication helper so the bearer token is accepted by the Foundry control plane.</summary>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Responses;
|
||||
|
||||
@@ -34,7 +32,6 @@ namespace Microsoft.Agents.AI.Foundry;
|
||||
/// <item><description><b>Agent Endpoint (Mode 3)</b>: throw — no local definition exists to convert.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
internal static class FoundryPromptAgentConverter
|
||||
{
|
||||
/// <summary>Performs the conversion for an agent whose chat client and chat options are supplied.</summary>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI.Foundry;
|
||||
|
||||
@@ -22,7 +21,6 @@ namespace Microsoft.Agents.AI.Foundry;
|
||||
/// <c>FoundryAITool.CreateHostedMcpToolbox(...)</c> factory overloads.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public sealed class HostedMcpToolboxAITool : HostedMcpServerTool
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
related per-agent endpoint surface). Flip back to IsReleased=true once Azure.AI.Projects
|
||||
ships a stable 2.1.0. -->
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace Azure.AI.Extensions.OpenAI;
|
||||
/// Provides extension methods for <see cref="ProjectResponsesClient"/>
|
||||
/// to simplify the creation of AI agents that work with Azure AI services.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class ProjectResponsesClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace Microsoft.Agents.AI.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods on <see cref="McpClient"/> that expose MCP server tools to a Microsoft
|
||||
/// Agent Framework agent with optional long-running task (SEP-2663) handling.
|
||||
/// </summary>
|
||||
public static class McpClientTaskExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Lists tools advertised by the connected MCP server and returns each as an
|
||||
/// <see cref="AIFunction"/>. Tools that declare <see cref="ToolTaskSupport.Required"/>
|
||||
/// are wrapped with task-aware behavior so an agent can transparently drive long-running
|
||||
/// invocations. All other tools — including those that declare
|
||||
/// <see cref="ToolTaskSupport.Optional"/> — are returned as-is, preserving inline
|
||||
/// (synchronous) invocation semantics by default.
|
||||
/// </summary>
|
||||
/// <param name="client">The connected MCP client.</param>
|
||||
/// <param name="options">
|
||||
/// Options that control the task lifecycle for task-capable tools.
|
||||
/// When <see langword="null"/>, defaults described on <see cref="McpTaskOptions"/> apply.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">Token used to cancel listing the server's tools.</param>
|
||||
/// <returns>The tools, ready to pass to <c>AsAIAgent(tools: …)</c>.</returns>
|
||||
public static async Task<IReadOnlyList<AIFunction>> ListAgentToolsWithTaskSupportAsync(
|
||||
this McpClient client,
|
||||
McpTaskOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(client);
|
||||
|
||||
McpTaskOptions effectiveOptions = options ?? new McpTaskOptions();
|
||||
|
||||
IList<McpClientTool> tools = await client.ListToolsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
AIFunction[] result = new AIFunction[tools.Count];
|
||||
for (int i = 0; i < tools.Count; i++)
|
||||
{
|
||||
ToolTaskSupport? taskSupport = tools[i].ProtocolTool.Execution?.TaskSupport;
|
||||
if (taskSupport is ToolTaskSupport.Required)
|
||||
{
|
||||
result[i] = new TaskAwareMcpClientAIFunction(client, tools[i], effectiveOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[i] = tools[i];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Agents.AI.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// Configures how an MCP client wrapper drives the
|
||||
/// <see href="https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks">MCP tasks</see>
|
||||
/// lifecycle when an underlying server tool returns a <c>CreateTaskResult</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// All members of this type are subject to change. The MCP task surface is experimental
|
||||
/// and tracks the in-flight specification.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class McpTaskOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the time-to-live the wrapper attaches to a newly created server-side task.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="null"/> the wrapper omits the <c>ttl</c> hint and lets the server
|
||||
/// pick its own value. The server's chosen TTL is always authoritative.
|
||||
/// </remarks>
|
||||
public TimeSpan? DefaultTimeToLive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the wrapper should send
|
||||
/// <c>tasks/cancel</c> when the local <see cref="System.Threading.CancellationToken"/>
|
||||
/// fires during a tool invocation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Defaults to <see langword="true"/>: a local cancellation means "the caller is giving up
|
||||
/// on this tool invocation" and the server-side task has no further consumer.
|
||||
/// </remarks>
|
||||
public bool CancelRemoteTaskOnLocalCancellation { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
<RootNamespace>Microsoft.Agents.AI.Mcp</RootNamespace>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<NoWarn>$(NoWarn);MEAI001;MAAI001;MCPEXP001</NoWarn>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Title>Microsoft Agent Framework MCP</Title>
|
||||
<Description>Provides Microsoft Agent Framework support for Model Context Protocol (MCP), including long-running task (SEP-2663) integration for MCP clients.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Disable package validation baseline until the first release -->
|
||||
<PropertyGroup>
|
||||
<PackageValidationBaselineVersion />
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Mcp.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AgentSkill"/> discovered from an MCP server exposing the Agent Skills convention.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The skill is constructed from <c>skill://index.json</c> discovery metadata only; <see cref="GetContentAsync"/>
|
||||
/// fetches the full <c>SKILL.md</c> content from the MCP server on demand via <c>resources/read</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Per SEP-2640, resources referenced inside SKILL.md are fetched on demand via the originating MCP
|
||||
/// server: <see cref="GetResourceAsync"/> resolves a relative resource name against the
|
||||
/// skill's root URI, issues a <c>resources/read</c> request, and returns an <see cref="AgentMcpSkillResource"/>
|
||||
/// with pre-fetched content.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class AgentMcpSkill : AgentSkill
|
||||
{
|
||||
private const string SkillMdSuffix = "SKILL.md";
|
||||
|
||||
private readonly McpClient _client;
|
||||
private readonly string _skillMdUri;
|
||||
private readonly string _skillRootUri;
|
||||
private string? _content;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentMcpSkill"/> class.
|
||||
/// </summary>
|
||||
/// <param name="frontmatter">The parsed frontmatter metadata for this skill.</param>
|
||||
/// <param name="skillMdUri">
|
||||
/// The full MCP resource URI of the <c>SKILL.md</c> resource (e.g. <c>skill://unit-converter/SKILL.md</c>).
|
||||
/// Used by <see cref="GetContentAsync"/> to fetch the skill content on demand. The skill's root URI
|
||||
/// (used to resolve sibling resources) is derived by stripping the trailing <c>SKILL.md</c> segment.
|
||||
/// </param>
|
||||
/// <param name="client">The MCP client used to fetch resources on demand.</param>
|
||||
public AgentMcpSkill(AgentSkillFrontmatter frontmatter, string skillMdUri, McpClient client)
|
||||
{
|
||||
this.Frontmatter = Throw.IfNull(frontmatter);
|
||||
this._skillMdUri = Throw.IfNullOrWhitespace(skillMdUri);
|
||||
this._skillRootUri = ComputeSkillRootUri(skillMdUri);
|
||||
this._client = Throw.IfNull(client);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentSkillFrontmatter Frontmatter { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Fetches the <c>SKILL.md</c> content from the MCP server via <c>resources/read</c> on the first call
|
||||
/// and caches the result.
|
||||
/// </remarks>
|
||||
public override async ValueTask<string> GetContentAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._content is not null)
|
||||
{
|
||||
return this._content;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2234 // Pass system uri objects instead of strings
|
||||
ReadResourceResult result = await this._client.ReadResourceAsync(this._skillMdUri, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
#pragma warning restore CA2234 // Pass system uri objects instead of strings
|
||||
|
||||
string text = string.Join("\n", result.Contents.OfType<TextResourceContents>().Select(c => c.Text));
|
||||
|
||||
if (text.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"The MCP server returned no text content for SKILL.md resource '{this._skillMdUri}'.");
|
||||
}
|
||||
|
||||
return this._content = text;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Resolves <paramref name="name"/> as a relative path against the skill's root URI, issues a
|
||||
/// <c>resources/read</c> request to the MCP server, and returns an <see cref="AgentMcpSkillResource"/>
|
||||
/// with the pre-fetched content. Returns <see langword="null"/> when the name is empty, the server
|
||||
/// returns no content, or the resource does not exist on the server.
|
||||
/// </remarks>
|
||||
public override async ValueTask<AgentSkillResource?> GetResourceAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string uri = this._skillRootUri + name;
|
||||
|
||||
ReadResourceResult result;
|
||||
try
|
||||
{
|
||||
#pragma warning disable CA2234 // Pass system uri objects instead of strings
|
||||
result = await this._client.ReadResourceAsync(uri, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
#pragma warning restore CA2234 // Pass system uri objects instead of strings
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AgentMcpSkillResource(name: name, result: result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips the trailing <c>SKILL.md</c> from the URI to produce the skill's root directory URI.
|
||||
/// If the URI doesn't end with <c>SKILL.md</c>, ensures it ends with a trailing slash.
|
||||
/// </summary>
|
||||
private static string ComputeSkillRootUri(string skillMdUri)
|
||||
{
|
||||
if (skillMdUri.EndsWith(SkillMdSuffix, StringComparison.Ordinal))
|
||||
{
|
||||
return skillMdUri.Substring(0, skillMdUri.Length - SkillMdSuffix.Length);
|
||||
}
|
||||
|
||||
if (skillMdUri.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
return skillMdUri;
|
||||
}
|
||||
|
||||
return skillMdUri + "/";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AgentSkillResource"/> backed by content fetched from an MCP server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The <see cref="ReadResourceResult"/> is fetched eagerly by <see cref="AgentMcpSkill.GetResourceAsync"/>
|
||||
/// at construction time; <see cref="ReadAsync"/> extracts the content from the result.
|
||||
/// </remarks>
|
||||
internal sealed class AgentMcpSkillResource : AgentSkillResource
|
||||
{
|
||||
private readonly ReadResourceResult _result;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentMcpSkillResource"/> class with a pre-fetched result.
|
||||
/// </summary>
|
||||
/// <param name="name">The resource name (e.g. a relative path or identifier).</param>
|
||||
/// <param name="result">The result returned by the MCP server's <c>resources/read</c> request.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
public AgentMcpSkillResource(string name, ReadResourceResult result, string? description = null)
|
||||
: base(Throw.IfNullOrWhitespace(name), description)
|
||||
{
|
||||
this._result = Throw.IfNull(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <returns>
|
||||
/// A <see cref="DataContent"/> when the resource contains binary content, a <see cref="string"/> when
|
||||
/// it contains text, or <see langword="null"/> when the server returned no content blocks.
|
||||
/// </returns>
|
||||
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
BlobResourceContents? blob = this._result.Contents.OfType<BlobResourceContents>().FirstOrDefault();
|
||||
if (blob is not null)
|
||||
{
|
||||
return Task.FromResult<object?>(blob.ToAIContent());
|
||||
}
|
||||
|
||||
string text = string.Join("\n", this._result.Contents.OfType<TextResourceContents>().Select(c => c.Text));
|
||||
|
||||
if (text.Length == 0)
|
||||
{
|
||||
return Task.FromResult<object?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<object?>(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AgentSkillsSource"/> that discovers Agent Skills served over the Model Context Protocol (MCP).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Discovery follows the SEP-2640 recommended approach: the source reads the well-known
|
||||
/// <c>skill://index.json</c> resource and constructs one <see cref="AgentSkill"/> per
|
||||
/// <c>skill-md</c> entry directly from the entry's <c>name</c>, <c>description</c>, and <c>url</c> fields.
|
||||
/// The referenced <c>SKILL.md</c> resource is not read during discovery; hosts fetch its body on
|
||||
/// demand via <c>resources/read</c> against the URI exposed on the resulting skill.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Only index entries of type <c>skill-md</c> are supported at the moment; entries of any other
|
||||
/// type are skipped.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <c>skill://index.json</c> is absent, unreadable, empty, or fails to parse, this source
|
||||
/// returns an empty list. Discovered skills serve their referenced resources on demand via
|
||||
/// <see cref="AgentSkill.GetResourceAsync"/>; they do not enumerate sibling files up front.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed partial class AgentMcpSkillsSource : AgentSkillsSource
|
||||
{
|
||||
/// <summary>
|
||||
/// SEP-2640 canonical discovery document URI.
|
||||
/// </summary>
|
||||
private const string IndexUri = "skill://index.json";
|
||||
|
||||
private const string SkillMdEntryType = "skill-md";
|
||||
|
||||
private readonly McpClient _client;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentMcpSkillsSource"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">An MCP client connected to a server that exposes Agent Skills resources.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory.</param>
|
||||
public AgentMcpSkillsSource(McpClient client, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
this._client = Throw.IfNull(client);
|
||||
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<AgentMcpSkillsSource>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
McpSkillIndex? index = await this.TryReadIndexAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var skills = new List<AgentSkill>();
|
||||
|
||||
foreach (var entry in index?.Skills ?? [])
|
||||
{
|
||||
if (this.TryCreateSkill(entry, out AgentMcpSkill? skill, out string skipReason))
|
||||
{
|
||||
skills.Add(skill);
|
||||
LogSkillLoaded(this._logger, skill.Frontmatter.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogIndexEntrySkipped(this._logger, entry.Name ?? "(unnamed)", skipReason);
|
||||
}
|
||||
}
|
||||
|
||||
LogSkillsLoadedTotal(this._logger, skills.Count);
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
private async Task<McpSkillIndex?> TryReadIndexAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ReadResourceResult result;
|
||||
|
||||
try
|
||||
{
|
||||
#pragma warning disable CA2234 // Pass system uri objects instead of strings
|
||||
result = await this._client.ReadResourceAsync(IndexUri, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
#pragma warning restore CA2234 // Pass system uri objects instead of strings
|
||||
}
|
||||
catch (McpException ex) when (ex is McpProtocolException pex && pex.ErrorCode == McpErrorCode.ResourceNotFound)
|
||||
{
|
||||
LogIndexAbsent(this._logger, ex.Message);
|
||||
return null;
|
||||
}
|
||||
catch (McpException ex)
|
||||
{
|
||||
LogIndexReadFailed(this._logger, ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
string? indexText = result.Contents.OfType<TextResourceContents>().FirstOrDefault()?.Text;
|
||||
if (string.IsNullOrWhiteSpace(indexText))
|
||||
{
|
||||
LogIndexEmpty(this._logger);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize(indexText, McpJsonContext.Default.McpSkillIndex);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
LogIndexParseFailed(this._logger, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryCreateSkill(
|
||||
McpSkillIndexEntry entry,
|
||||
[NotNullWhen(true)] out AgentMcpSkill? skill,
|
||||
out string skipReason)
|
||||
{
|
||||
skill = null;
|
||||
|
||||
if (!string.Equals(entry.Type, SkillMdEntryType, StringComparison.Ordinal))
|
||||
{
|
||||
skipReason = $"unsupported type '{entry.Type ?? "(none)"}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entry.Url))
|
||||
{
|
||||
skipReason = "missing required 'url' field";
|
||||
return false;
|
||||
}
|
||||
|
||||
AgentSkillFrontmatter frontmatter;
|
||||
try
|
||||
{
|
||||
frontmatter = new AgentSkillFrontmatter(entry.Name!, entry.Description!);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
skipReason = $"invalid metadata: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
|
||||
skill = new AgentMcpSkill(frontmatter, entry.Url!, this._client);
|
||||
skipReason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
[LoggerMessage(LogLevel.Information, "Loaded MCP skill: {SkillName}")]
|
||||
private static partial void LogSkillLoaded(ILogger logger, string skillName);
|
||||
|
||||
[LoggerMessage(LogLevel.Information, "Successfully loaded {Count} skills from MCP server")]
|
||||
private static partial void LogSkillsLoadedTotal(ILogger logger, int count);
|
||||
|
||||
[LoggerMessage(LogLevel.Debug, "No skill://index.json resource available on MCP server: {Reason}")]
|
||||
private static partial void LogIndexAbsent(ILogger logger, string reason);
|
||||
|
||||
[LoggerMessage(LogLevel.Warning, "Failed to read skill://index.json from MCP server.")]
|
||||
private static partial void LogIndexReadFailed(ILogger logger, Exception exception);
|
||||
|
||||
[LoggerMessage(LogLevel.Debug, "skill://index.json on MCP server returned empty/non-text contents")]
|
||||
private static partial void LogIndexEmpty(ILogger logger);
|
||||
|
||||
[LoggerMessage(LogLevel.Warning, "Failed to parse skill://index.json JSON document.")]
|
||||
private static partial void LogIndexParseFailed(ILogger logger, Exception exception);
|
||||
|
||||
[LoggerMessage(LogLevel.Debug, "Skipping skill index entry '{SkillName}': {Reason}")]
|
||||
private static partial void LogIndexEntrySkipped(ILogger logger, string skillName, string reason);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using ModelContextProtocol.Client;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// MCP-specific extension methods for <see cref="AgentSkillsProviderBuilder"/>.
|
||||
/// </summary>
|
||||
public static class AgentSkillsProviderBuilderMcpExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a skill source that discovers skills served over MCP via the supplied <paramref name="client"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to extend.</param>
|
||||
/// <param name="client">An MCP client connected to a server exposing Agent Skills resources.</param>
|
||||
/// <returns>The builder instance for chaining.</returns>
|
||||
public static AgentSkillsProviderBuilder UseMcpSkills(this AgentSkillsProviderBuilder builder, McpClient client)
|
||||
{
|
||||
_ = Throw.IfNull(builder);
|
||||
_ = Throw.IfNull(client);
|
||||
|
||||
return builder.UseSource(new AgentMcpSkillsSource(client));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON context for MCP-skills well-known DTOs.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip)]
|
||||
[JsonSerializable(typeof(McpSkillIndex))]
|
||||
[JsonSerializable(typeof(McpSkillIndexEntry))]
|
||||
internal sealed partial class McpJsonContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for the skill discovery index document served at <c>skill://index.json</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Schema reference: <see href="https://schemas.agentskills.io/discovery/0.2.0/schema.json"/>
|
||||
/// (Agent Skills Discovery v0.2.0), as bound to MCP by SEP-2640. The MCP binding differs from the
|
||||
/// base schema in two ways: the <c>url</c> field contains a full MCP resource URI, and the
|
||||
/// <c>digest</c> field is omitted (integrity is the transport's concern over an authenticated
|
||||
/// MCP connection).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// All properties are nullable so that deserialization succeeds even when the server-side index
|
||||
/// is incomplete or malformed; callers MUST validate required fields before use.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class McpSkillIndex
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the opaque schema identifier URI. Required by the base schema; clients SHOULD
|
||||
/// match this against known schema URIs (e.g.
|
||||
/// <c>https://schemas.agentskills.io/discovery/0.2.0/schema.json</c>) before processing the index.
|
||||
/// </summary>
|
||||
[JsonPropertyName("$schema")]
|
||||
public string? Schema { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the array of skill entries. Required by the schema; an empty or missing
|
||||
/// <c>skills</c> array means the index advertises no skills.
|
||||
/// </summary>
|
||||
[JsonPropertyName("skills")]
|
||||
public List<McpSkillIndexEntry>? Skills { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single entry in the skill discovery index.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Field requirements per the v0.2.0 schema and the SEP-2640 binding:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>type</c>, <c>description</c>, and <c>url</c> are REQUIRED.</description></item>
|
||||
/// <item><description><c>name</c> is REQUIRED for <c>skill-md</c> and <c>archive</c> entries; OMITTED for <c>mcp-resource-template</c>.</description></item>
|
||||
/// <item><description><c>digest</c> is part of the base schema but OMITTED under the SEP-2640 MCP binding; carried here for compatibility with non-MCP indices.</description></item>
|
||||
/// </list>
|
||||
/// All properties are nullable to keep deserialization lenient; callers validate required fields before use.
|
||||
/// </remarks>
|
||||
internal sealed class McpSkillIndexEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the skill name (1-64 chars, lowercase alphanumeric and hyphens; no leading,
|
||||
/// trailing, or consecutive hyphens). Required for <c>skill-md</c> and <c>archive</c> entries;
|
||||
/// omitted for <c>mcp-resource-template</c>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entry distribution type. Required. Schema-defined values are
|
||||
/// <c>skill-md</c> and <c>archive</c>; the SEP-2640 MCP binding additionally defines
|
||||
/// <c>mcp-resource-template</c>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the skill description (max 1024 chars per the Agent Skills specification).
|
||||
/// Required. For <c>skill-md</c> entries, SHOULD match the <c>description</c> in the skill's
|
||||
/// <c>SKILL.md</c> frontmatter.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artifact URL. Required. For <c>skill-md</c>, points at the
|
||||
/// <c>SKILL.md</c> resource. For <c>archive</c>, points at the archive file. For
|
||||
/// <c>mcp-resource-template</c>, an RFC 6570 URI template that resolves to a <c>SKILL.md</c>
|
||||
/// resource URI.
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SHA-256 digest of the artifact bytes (e.g. <c>sha256:abcd1234...</c>).
|
||||
/// Required by the base v0.2.0 schema, but OMITTED under the SEP-2640 MCP binding because
|
||||
/// integrity is the transport's concern over an authenticated MCP connection.
|
||||
/// </summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public string? Digest { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace Microsoft.Agents.AI.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AIFunction"/> wrapper around an <see cref="McpClientTool"/> that drives the
|
||||
/// <see href="https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks">MCP long-running task</see>
|
||||
/// lifecycle (SEP-2663) on behalf of the agent's tool loop.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The wrapper invokes the tool with task augmentation via
|
||||
/// <see cref="McpClient.CallToolAsTaskAsync"/>, polls to completion via
|
||||
/// <see cref="McpClient.PollTaskUntilCompleteAsync"/>, and fetches the result via
|
||||
/// <see cref="McpClient.GetTaskResultAsync"/>. The result is returned to the caller as a
|
||||
/// <see cref="JsonElement"/> containing the serialized <see cref="CallToolResult"/> — the
|
||||
/// same wire shape produced by <see cref="McpClientTool"/>.<see cref="AIFunction.InvokeAsync(AIFunctionArguments, CancellationToken)"/>
|
||||
/// so that downstream <see cref="FunctionResultContent"/> serialization is byte-identical to
|
||||
/// a non-task-augmented MCP tool call. The agent's function-calling loop is unaware that a
|
||||
/// task was used.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This wrapper is intended to be applied only to tools whose
|
||||
/// <see cref="ToolExecution.TaskSupport"/> is <see cref="ToolTaskSupport.Required"/>
|
||||
/// (selected by <see cref="McpClientTaskExtensions.ListAgentToolsWithTaskSupportAsync"/>).
|
||||
/// As a defensive fallback, if the server still rejects the task-augmented call with
|
||||
/// <see cref="McpErrorCode.MethodNotFound"/> (e.g. because tool-level capabilities changed
|
||||
/// between <c>tools/list</c> and invocation), the wrapper transparently falls back to a
|
||||
/// non-augmented call through the inner <see cref="McpClientTool"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class TaskAwareMcpClientAIFunction : AIFunction
|
||||
{
|
||||
private readonly McpClient _client;
|
||||
private readonly McpClientTool _inner;
|
||||
private readonly McpTaskOptions _options;
|
||||
|
||||
internal TaskAwareMcpClientAIFunction(McpClient client, McpClientTool inner, McpTaskOptions options)
|
||||
{
|
||||
_ = Throw.IfNull(client);
|
||||
_ = Throw.IfNull(inner);
|
||||
_ = Throw.IfNull(options);
|
||||
|
||||
this._client = client;
|
||||
this._inner = inner;
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => this._inner.Name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Description => this._inner.Description;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonElement JsonSchema => this._inner.JsonSchema;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonElement? ReturnJsonSchema => this._inner.ReturnJsonSchema;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonSerializerOptions JsonSerializerOptions => this._inner.JsonSerializerOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<object?> InvokeCoreAsync(
|
||||
AIFunctionArguments arguments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_ = Throw.IfNull(arguments);
|
||||
|
||||
McpTaskMetadata? metadata = null;
|
||||
if (this._options.DefaultTimeToLive is TimeSpan ttl)
|
||||
{
|
||||
metadata = new McpTaskMetadata { TimeToLive = ttl };
|
||||
}
|
||||
|
||||
McpTask task;
|
||||
try
|
||||
{
|
||||
task = await this._client.CallToolAsTaskAsync(
|
||||
this._inner.Name,
|
||||
arguments,
|
||||
taskMetadata: metadata,
|
||||
progress: null,
|
||||
options: null,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (McpProtocolException ex) when (ex.ErrorCode == McpErrorCode.MethodNotFound)
|
||||
{
|
||||
// Defensive fallback: the server's advertised TaskSupport indicated this tool
|
||||
// could be invoked as a task, but the server now rejects task augmentation for it
|
||||
// (e.g. capability changed between tools/list and invocation). Fall back to a
|
||||
// non-augmented call through the inner McpClientTool.
|
||||
return await this._inner.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await this.PollAndRetrieveResultAsync(task.TaskId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<JsonElement> PollAndRetrieveResultAsync(string taskId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
McpTask terminal = await this._client.PollTaskUntilCompleteAsync(taskId, options: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return terminal.Status switch
|
||||
{
|
||||
McpTaskStatus.Completed => await this._client.GetTaskResultAsync(taskId, options: null, cancellationToken).ConfigureAwait(false),
|
||||
McpTaskStatus.Cancelled => throw new OperationCanceledException(FormatTerminalStatusMessage(taskId, terminal)),
|
||||
_ => throw new InvalidOperationException(FormatTerminalStatusMessage(taskId, terminal)),// Failed (or any future non-terminal-but-unhandled status that the poll loop returns).
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException) when (this._options.CancelRemoteTaskOnLocalCancellation && cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await this.TryCancelTaskAsync(taskId).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatTerminalStatusMessage(string taskId, McpTask terminal)
|
||||
=> string.IsNullOrEmpty(terminal.StatusMessage)
|
||||
? $"MCP task '{taskId}' ended in terminal status '{terminal.Status}'."
|
||||
: $"MCP task '{taskId}' ended in terminal status '{terminal.Status}': {terminal.StatusMessage}";
|
||||
|
||||
private async Task TryCancelTaskAsync(string taskId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
_ = await this._client.CancelTaskAsync(taskId, options: null, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort cancellation; do not mask the original cancellation reason.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
|
||||
|
||||
internal abstract class ActionTemplate : CodeTemplate, IModeledAction
|
||||
{
|
||||
public string Id { get; private set; } = string.Empty;
|
||||
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
public string ParentId { get; private set; } = string.Empty;
|
||||
|
||||
public bool UseAgentProvider { get; init; }
|
||||
|
||||
protected TAction Initialize<TAction>(TAction model) where TAction : DialogAction
|
||||
{
|
||||
this.Id = model.GetId();
|
||||
this.ParentId = model.GetParentId() ?? WorkflowActionVisitor.Steps.Root();
|
||||
this.Name = this.Id.FormatType();
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
-395
@@ -1,395 +0,0 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version: 18.0.0.0
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
{
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Class to produce the template output
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
|
||||
internal partial class AddConversationMessageTemplate : ActionTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the template output
|
||||
/// </summary>
|
||||
public override string TransformText()
|
||||
{
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n/// <summary>\n/// Adds a new message to the specified agent conversation\n/// </s" +
|
||||
"ummary>\ninternal sealed class ");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Name));
|
||||
this.Write("Executor(FormulaSession session, ResponseAgentProvider agentProvider) : ActionExe" +
|
||||
"cutor(id: \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Id));
|
||||
this.Write("\", session)\n{\n // <inheritdoc />\n protected override async ValueTask<object" +
|
||||
"?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)\n " +
|
||||
" {");
|
||||
|
||||
EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true);
|
||||
this.Write("\n if (string.IsNullOrWhiteSpace(conversationId))\n {\n thr" +
|
||||
"ow new DeclarativeActionException($\"Conversation identifier must be defined: {th" +
|
||||
"is.Id}\");\n }\n ChatMessage newMessage = new(ChatRole.");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatEnum(this.Model.Role, RoleMap)));
|
||||
this.Write(", await this.GetContentAsync(context).ConfigureAwait(false)) { AdditionalProperti" +
|
||||
"es = this.GetMetadata() };\n newMessage = await agentProvider.CreateMessag" +
|
||||
"eAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false);");
|
||||
|
||||
AssignVariable(this.Message, "newMessage");
|
||||
|
||||
this.Write("\n return default;\n }\n\n private async ValueTask<IList<AIContent>> Get" +
|
||||
"ContentAsync(IWorkflowContext context)\n {\n List<AIContent> content = [" +
|
||||
"];\n ");
|
||||
|
||||
int index = 0;
|
||||
foreach (AddConversationMessageContent content in this.Model.Content)
|
||||
{
|
||||
++index;
|
||||
EvaluateMessageTemplate(content.Value, $"contentValue{index}");
|
||||
AgentMessageContentType contentType = content.Type.Value;
|
||||
if (contentType == AgentMessageContentType.ImageUrl)
|
||||
{
|
||||
this.Write("\n content.Add(UriContent(contentValue");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(index));
|
||||
this.Write(", \"image/*\"));");
|
||||
|
||||
}
|
||||
else if (contentType == AgentMessageContentType.ImageFile)
|
||||
{
|
||||
this.Write("\n content.Add(new HostedFileContent(contentValue");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(index));
|
||||
this.Write("));");
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n content.Add(new TextContent(contentValue");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(index));
|
||||
this.Write("));");
|
||||
|
||||
}
|
||||
}
|
||||
this.Write("\n return content;\n }\n\n private AdditionalPropertiesDictionary? GetMe" +
|
||||
"tadata()\n {");
|
||||
|
||||
EvaluateRecordExpression<object>(this.Model.Metadata, "metadata");
|
||||
this.Write("\n\n if (metadata is null)\n {\n return null; \n }\n" +
|
||||
"\n return new AdditionalPropertiesDictionary(metadata);\n }\n}");
|
||||
return this.GenerationEnvironment.ToString();
|
||||
}
|
||||
|
||||
void AssignVariable(PropertyPath targetVariable, string valueVariable, bool tightFormat = false)
|
||||
{
|
||||
if (targetVariable is not null)
|
||||
{
|
||||
this.Write("\n await context.QueueStateUpdateAsync(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableName(targetVariable)));
|
||||
|
||||
this.Write("\", value: ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(valueVariable));
|
||||
|
||||
this.Write(", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableScope(targetVariable)));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
if (!tightFormat)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EvaluateRecordExpression<TValue>(ObjectExpression<RecordDataValue> expression, string targetVariable)
|
||||
{
|
||||
string resultTypeName = $"Dictionary<string, {GetTypeAlias<TValue>()}?>?";
|
||||
if (expression is null)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = null;");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsLiteral)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" =\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue)));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.ReadStateAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName));
|
||||
|
||||
this.Write(">(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName));
|
||||
|
||||
this.Write("\", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName));
|
||||
|
||||
this.Write("? ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateExpressionAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName));
|
||||
|
||||
this.Write(">(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString())));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateExpressionAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName));
|
||||
|
||||
this.Write(">(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText)));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false)
|
||||
{
|
||||
string typeName = isNullable ? "string?" : "string";
|
||||
if (expression is null)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty"));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsLiteral)
|
||||
{
|
||||
if (expression.LiteralValue.Contains("\n"))
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = \n \"\"\"\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue));
|
||||
|
||||
this.Write("\n \"\"\";");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue)));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.ReadStateAsync<string>(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName));
|
||||
|
||||
this.Write("\", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<string>(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString())));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<string>(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText)));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EvaluateMessageTemplate(TemplateLine templateLine, string variableName)
|
||||
{
|
||||
if (templateLine is not null)
|
||||
{
|
||||
this.Write("\n string ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(variableName));
|
||||
|
||||
this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\"");
|
||||
|
||||
|
||||
FormatMessageTemplate(templateLine);
|
||||
this.Write("\n \"\"\");");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n string? ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(variableName));
|
||||
|
||||
this.Write(" = null;");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void FormatMessageTemplate(TemplateLine line)
|
||||
{
|
||||
foreach (string text in line.ToTemplateString().ByLine())
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(text));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
<#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #>
|
||||
<#@ output extension=".cs" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.Extensions" #>
|
||||
<#@ import namespace="Microsoft.Agents.ObjectModel" #>
|
||||
<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #>
|
||||
<#@ include file="Snippets/EvaluateRecordExpressionTemplate.tt" once="true" #>
|
||||
<#@ include file="Snippets/EvaluateStringExpressionTemplate.tt" once="true" #>
|
||||
<#@ include file="Snippets/FormatMessageTemplate.tt" once="true" #>
|
||||
/// <summary>
|
||||
/// Adds a new message to the specified agent conversation
|
||||
/// </summary>
|
||||
internal sealed class <#= this.Name #>Executor(FormulaSession session, ResponseAgentProvider agentProvider) : ActionExecutor(id: "<#= this.Id #>", session)
|
||||
{
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{<#
|
||||
EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true); #>
|
||||
if (string.IsNullOrWhiteSpace(conversationId))
|
||||
{
|
||||
throw new DeclarativeActionException($"Conversation identifier must be defined: {this.Id}");
|
||||
}
|
||||
ChatMessage newMessage = new(ChatRole.<#= FormatEnum(this.Model.Role, RoleMap) #>, await this.GetContentAsync(context).ConfigureAwait(false)) { AdditionalProperties = this.GetMetadata() };
|
||||
newMessage = await agentProvider.CreateMessageAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false);<#
|
||||
AssignVariable(this.Message, "newMessage");
|
||||
#>
|
||||
return default;
|
||||
}
|
||||
|
||||
private async ValueTask<IList<AIContent>> GetContentAsync(IWorkflowContext context)
|
||||
{
|
||||
List<AIContent> content = [];
|
||||
<#
|
||||
int index = 0;
|
||||
foreach (AddConversationMessageContent content in this.Model.Content)
|
||||
{
|
||||
++index;
|
||||
EvaluateMessageTemplate(content.Value, $"contentValue{index}");
|
||||
AgentMessageContentType contentType = content.Type.Value;
|
||||
if (contentType == AgentMessageContentType.ImageUrl)
|
||||
{#>
|
||||
content.Add(UriContent(contentValue<#= index #>, "image/*"));<#
|
||||
}
|
||||
else if (contentType == AgentMessageContentType.ImageFile)
|
||||
{#>
|
||||
content.Add(new HostedFileContent(contentValue<#= index #>));<#
|
||||
}
|
||||
else
|
||||
{#>
|
||||
content.Add(new TextContent(contentValue<#= index #>));<#
|
||||
}
|
||||
}#>
|
||||
return content;
|
||||
}
|
||||
|
||||
private AdditionalPropertiesDictionary? GetMetadata()
|
||||
{<#
|
||||
EvaluateRecordExpression<object>(this.Model.Metadata, "metadata"); #>
|
||||
|
||||
if (metadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AdditionalPropertiesDictionary(metadata);
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
|
||||
|
||||
internal partial class AddConversationMessageTemplate
|
||||
{
|
||||
public AddConversationMessageTemplate(AddConversationMessage model)
|
||||
{
|
||||
this.Model = this.Initialize(model);
|
||||
this.Message = this.Model.Message?.Path;
|
||||
this.UseAgentProvider = true;
|
||||
}
|
||||
|
||||
public AddConversationMessage Model { get; }
|
||||
|
||||
public PropertyPath? Message { get; }
|
||||
|
||||
public const string DefaultRole = nameof(ChatRole.User);
|
||||
|
||||
public static readonly FrozenDictionary<AgentMessageRoleWrapper, string> RoleMap =
|
||||
new Dictionary<AgentMessageRoleWrapper, string>()
|
||||
{
|
||||
[AgentMessageRoleWrapper.Get(AgentMessageRole.User)] = nameof(ChatRole.User),
|
||||
[AgentMessageRoleWrapper.Get(AgentMessageRole.Agent)] = nameof(ChatRole.Assistant),
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version: 18.0.0.0
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Class to produce the template output
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
|
||||
internal partial class ClearAllVariablesTemplate : ActionTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the template output
|
||||
/// </summary>
|
||||
public override string TransformText()
|
||||
{
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n/// <summary>\n/// Reset all the state for the targeted variable scope.\n/// </sum" +
|
||||
"mary>\ninternal sealed class ");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Name));
|
||||
this.Write("Executor(FormulaSession session) : ActionExecutor(id: \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Id));
|
||||
this.Write("\", session)\n{\n // <inheritdoc />\n protected override async ValueTask<object" +
|
||||
"?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)\n " +
|
||||
" {");
|
||||
|
||||
EvaluateEnumExpression<VariablesToClearWrapper, string>(this.Model.Variables, "targetScopeName", ScopeMap, isNullable: true);
|
||||
this.Write("\n await context.QueueClearScopeAsync(targetScopeName).ConfigureAwait(false" +
|
||||
");\n\n return default;\n }\n}\n");
|
||||
return this.GenerationEnvironment.ToString();
|
||||
}
|
||||
|
||||
void AssignVariable(PropertyPath targetVariable, string valueVariable, bool tightFormat = false)
|
||||
{
|
||||
if (targetVariable is not null)
|
||||
{
|
||||
this.Write("\n await context.QueueStateUpdateAsync(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableName(targetVariable)));
|
||||
|
||||
this.Write("\", value: ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(valueVariable));
|
||||
|
||||
this.Write(", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableScope(targetVariable)));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
if (!tightFormat)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EvaluateEnumExpression<TWrapper, TValue>(
|
||||
EnumExpression<TWrapper> expression,
|
||||
string targetVariable,
|
||||
IDictionary<TWrapper, string> resultMap,
|
||||
string defaultValue = null,
|
||||
bool qualifyResult = false,
|
||||
bool isNullable = false)
|
||||
where TWrapper : EnumWrapper
|
||||
{
|
||||
string resultType = $"{GetTypeAlias<TValue>()}{(isNullable ? "?" : "")}";
|
||||
if (expression is null)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue<TValue>(defaultValue)));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsLiteral)
|
||||
{
|
||||
resultMap.TryGetValue(expression.LiteralValue, out string resultValue);
|
||||
if (qualifyResult)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write(".");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultValue));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue<TValue>(resultValue)));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.ReadStateAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write(">(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName));
|
||||
|
||||
this.Write("\", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write("? ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write(">(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString())));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(resultType));
|
||||
|
||||
this.Write(">(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText)));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
<#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #>
|
||||
<#@ output extension=".cs" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ import namespace="System.Collections.Generic" #>
|
||||
<#@ import namespace="Microsoft.Agents.ObjectModel" #>
|
||||
<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #>
|
||||
<#@ include file="Snippets/EvaluateEnumExpressionTemplate.tt" once="true" #>
|
||||
/// <summary>
|
||||
/// Reset all the state for the targeted variable scope.
|
||||
/// </summary>
|
||||
internal sealed class <#= this.Name #>Executor(FormulaSession session) : ActionExecutor(id: "<#= this.Id #>", session)
|
||||
{
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{<#
|
||||
EvaluateEnumExpression<VariablesToClearWrapper, string>(this.Model.Variables, "targetScopeName", ScopeMap, isNullable: true); #>
|
||||
await context.QueueClearScopeAsync(targetScopeName).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
|
||||
|
||||
internal partial class ClearAllVariablesTemplate
|
||||
{
|
||||
public ClearAllVariablesTemplate(ClearAllVariables model)
|
||||
{
|
||||
this.Model = this.Initialize(model);
|
||||
}
|
||||
|
||||
public ClearAllVariables Model { get; }
|
||||
|
||||
public static readonly FrozenDictionary<VariablesToClearWrapper, string?> ScopeMap =
|
||||
new Dictionary<VariablesToClearWrapper, string?>()
|
||||
{
|
||||
[VariablesToClearWrapper.Get(VariablesToClear.AllGlobalVariables)] = VariableScopeNames.Global,
|
||||
[VariablesToClearWrapper.Get(VariablesToClear.ConversationScopedVariables)] = WorkflowFormulaState.DefaultScopeName,
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
|
||||
|
||||
internal abstract class CodeTemplate
|
||||
{
|
||||
private bool _endsWithNewline;
|
||||
|
||||
private string CurrentIndentField { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Create the template output
|
||||
/// </summary>
|
||||
public abstract string TransformText();
|
||||
|
||||
#region Object Model helpers
|
||||
|
||||
public static string VariableName(PropertyPath path) => Throw.IfNull(path.VariableName);
|
||||
public static string VariableScope(PropertyPath path) => Throw.IfNull(path.NamespaceAlias);
|
||||
|
||||
public static string FormatBoolValue(bool? value, bool defaultValue = false) =>
|
||||
value ?? defaultValue ? "true" : "false";
|
||||
|
||||
public static string FormatStringValue(string? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
if (value.Contains('\n') || value.Contains('\r'))
|
||||
{
|
||||
return @$"""""""{Environment.NewLine}{value}{Environment.NewLine}""""""";
|
||||
}
|
||||
|
||||
if (value.Contains('"') || value.Contains('\\'))
|
||||
{
|
||||
return @$"""""""{value}""""""";
|
||||
}
|
||||
|
||||
return @$"""{value}""";
|
||||
}
|
||||
|
||||
public static string FormatValue<TValue>(string? value)
|
||||
{
|
||||
if (typeof(TValue) == typeof(string))
|
||||
{
|
||||
return FormatStringValue(value);
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
if (typeof(TValue).IsEnum)
|
||||
{
|
||||
return $"{typeof(TValue).Name}.{value}";
|
||||
}
|
||||
|
||||
return $"{value}";
|
||||
}
|
||||
|
||||
public static string FormatDataValue(DataValue value) =>
|
||||
value switch
|
||||
{
|
||||
BlankDataValue => "null",
|
||||
BooleanDataValue booleanValue => FormatBoolValue(booleanValue.Value),
|
||||
FloatDataValue decimalValue => $"{decimalValue.Value}",
|
||||
NumberDataValue numberValue => $"{numberValue.Value}",
|
||||
DateDataValue dateValue => $"new DateTime({dateValue.Value.Ticks}, DateTimeKind.{dateValue.Value.Kind})",
|
||||
DateTimeDataValue datetimeValue => $"new DateTimeOffset({datetimeValue.Value.Ticks}, TimeSpan.FromTicks({datetimeValue.Value.Offset}))",
|
||||
TimeDataValue timeValue => $"TimeSpan.FromTicks({timeValue.Value.Ticks})",
|
||||
StringDataValue stringValue => FormatStringValue(stringValue.Value),
|
||||
OptionDataValue optionValue => @$"""{optionValue.Value}""",
|
||||
// Indenting is important here to make the generated code readable. Don't change it without testing the output.
|
||||
RecordDataValue recordValue =>
|
||||
$"""
|
||||
[
|
||||
{string.Join(",\n ", recordValue.Properties.Select(p => $"[\"{p.Key}\"] = {FormatDataValue(p.Value)}"))}
|
||||
]
|
||||
""",
|
||||
_ => throw new DeclarativeModelException($"Unable to format '{value.GetType().Name}'"),
|
||||
};
|
||||
|
||||
public static TTarget FormatEnum<TSource, TTarget>(TSource value, IDictionary<TSource, TTarget> map, TTarget? defaultValue = default)
|
||||
{
|
||||
if (map.TryGetValue(value, out TTarget? target))
|
||||
{
|
||||
return target;
|
||||
}
|
||||
|
||||
if (defaultValue is null)
|
||||
{
|
||||
throw new DeclarativeModelException($"No default value suppied for '{typeof(TTarget).Name}'");
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static string GetTypeAlias<TValue>() => GetTypeAlias(typeof(TValue));
|
||||
|
||||
public static string GetTypeAlias(Type type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
Type t when t == typeof(bool) => "bool",
|
||||
Type t when t == typeof(byte) => "byte",
|
||||
Type t when t == typeof(sbyte) => "sbyte",
|
||||
Type t when t == typeof(char) => "char",
|
||||
Type t when t == typeof(decimal) => "decimal",
|
||||
Type t when t == typeof(double) => "double",
|
||||
Type t when t == typeof(float) => "float",
|
||||
Type t when t == typeof(int) => "int",
|
||||
Type t when t == typeof(uint) => "uint",
|
||||
Type t when t == typeof(long) => "long",
|
||||
Type t when t == typeof(ulong) => "ulong",
|
||||
Type t when t == typeof(nint) => "nint",
|
||||
Type t when t == typeof(nuint) => "nuint",
|
||||
Type t when t == typeof(short) => "short",
|
||||
Type t when t == typeof(ushort) => "ushort",
|
||||
Type t when t == typeof(string) => "string",
|
||||
Type t when t == typeof(object) => "object",
|
||||
_ => type.Name
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// The string builder that generation-time code is using to assemble generated output
|
||||
/// </summary>
|
||||
public StringBuilder GenerationEnvironment
|
||||
{
|
||||
get
|
||||
{
|
||||
return field ??= new StringBuilder();
|
||||
}
|
||||
set;
|
||||
}
|
||||
/// <summary>
|
||||
/// The error collection for the generation process
|
||||
/// </summary>
|
||||
public CompilerErrorCollection Errors => field ??= [];
|
||||
|
||||
/// <summary>
|
||||
/// A list of the lengths of each indent that was added with PushIndent
|
||||
/// </summary>
|
||||
private List<int> IndentLengths { get => field ??= []; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current indent we use when adding lines to the output
|
||||
/// </summary>
|
||||
public string CurrentIndent
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.CurrentIndentField;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Current transformation session
|
||||
/// </summary>
|
||||
public virtual IDictionary<string, object>? Session { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Transform-time helpers
|
||||
|
||||
/// <summary>
|
||||
/// Write text directly into the generated output
|
||||
/// </summary>
|
||||
public void Write(string textToAppend)
|
||||
{
|
||||
if (string.IsNullOrEmpty(textToAppend))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// If we're starting off, or if the previous text ended with a newline,
|
||||
// we have to append the current indent first.
|
||||
if ((this.GenerationEnvironment.Length == 0)
|
||||
|| this._endsWithNewline)
|
||||
{
|
||||
this.GenerationEnvironment.Append(this.CurrentIndentField);
|
||||
this._endsWithNewline = false;
|
||||
}
|
||||
// Check if the current text ends with a newline
|
||||
if (textToAppend.EndsWith(Environment.NewLine, StringComparison.CurrentCulture))
|
||||
{
|
||||
this._endsWithNewline = true;
|
||||
}
|
||||
// This is an optimization. If the current indent is "", then we don't have to do any
|
||||
// of the more complex stuff further down.
|
||||
if (this.CurrentIndentField.Length == 0)
|
||||
{
|
||||
this.GenerationEnvironment.Append(textToAppend);
|
||||
return;
|
||||
}
|
||||
// Everywhere there is a newline in the text, add an indent after it
|
||||
textToAppend = textToAppend.Replace(Environment.NewLine, Environment.NewLine + this.CurrentIndentField);
|
||||
// If the text ends with a newline, then we should strip off the indent added at the very end
|
||||
// because the appropriate indent will be added when the next time Write() is called
|
||||
if (this._endsWithNewline)
|
||||
{
|
||||
this.GenerationEnvironment.Append(textToAppend, 0, textToAppend.Length - this.CurrentIndentField.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.GenerationEnvironment.Append(textToAppend);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write text directly into the generated output
|
||||
/// </summary>
|
||||
public void WriteLine(string textToAppend)
|
||||
{
|
||||
this.Write(textToAppend);
|
||||
this.GenerationEnvironment.AppendLine();
|
||||
this._endsWithNewline = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write formatted text directly into the generated output
|
||||
/// </summary>
|
||||
public void Write(string format, params object[] args)
|
||||
{
|
||||
this.Write(string.Format(CultureInfo.CurrentCulture, format, args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write formatted text directly into the generated output
|
||||
/// </summary>
|
||||
public void WriteLine(string format, params object[] args)
|
||||
{
|
||||
this.WriteLine(string.Format(CultureInfo.CurrentCulture, format, args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raise an error
|
||||
/// </summary>
|
||||
public void Error(string message)
|
||||
{
|
||||
CompilerError error = new()
|
||||
{
|
||||
ErrorText = message
|
||||
};
|
||||
this.Errors.Add(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raise a warning
|
||||
/// </summary>
|
||||
public void Warning(string message)
|
||||
{
|
||||
CompilerError error = new()
|
||||
{
|
||||
ErrorText = message,
|
||||
IsWarning = true
|
||||
};
|
||||
error.ErrorText = message;
|
||||
error.IsWarning = true;
|
||||
this.Errors.Add(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increase the indent
|
||||
/// </summary>
|
||||
public void PushIndent(string indent)
|
||||
{
|
||||
if (indent is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(indent));
|
||||
}
|
||||
this.CurrentIndentField += indent;
|
||||
this.IndentLengths.Add(indent.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the last indent that was added with PushIndent
|
||||
/// </summary>
|
||||
public string PopIndent()
|
||||
{
|
||||
string returnValue = string.Empty;
|
||||
if (this.IndentLengths.Count > 0)
|
||||
{
|
||||
int indentLength = this.IndentLengths[this.IndentLengths.Count - 1];
|
||||
this.IndentLengths.RemoveAt(this.IndentLengths.Count - 1);
|
||||
if (indentLength > 0)
|
||||
{
|
||||
returnValue = this.CurrentIndentField.Substring(this.CurrentIndentField.Length - indentLength);
|
||||
this.CurrentIndentField = this.CurrentIndentField.Remove(this.CurrentIndentField.Length - indentLength);
|
||||
}
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove any indentation
|
||||
/// </summary>
|
||||
public void ClearIndent()
|
||||
{
|
||||
this.IndentLengths.Clear();
|
||||
this.CurrentIndentField = string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ToString Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Utility class to produce culture-oriented representation of an object as a string.
|
||||
/// </summary>
|
||||
public sealed class ToStringInstanceHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// This is called from the compile/run appdomain to convert objects within an expression block to a string
|
||||
/// </summary>
|
||||
#pragma warning disable CA1822 // Required to be non-static for use in generated code
|
||||
public string ToStringWithCulture(object objectToConvert) => $"{objectToConvert}";
|
||||
#pragma warning restore CA1822
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to produce culture-oriented representation of an object as a string
|
||||
/// </summary>
|
||||
public ToStringInstanceHelper ToStringHelper { get; } = new();
|
||||
|
||||
#endregion
|
||||
}
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version: 18.0.0.0
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
{
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Class to produce the template output
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
|
||||
internal partial class ConditionGroupTemplate : ActionTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the template output
|
||||
/// </summary>
|
||||
public override string TransformText()
|
||||
{
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n/// <summary>\n/// Conditional branching similar to an if / elseif / elseif / els" +
|
||||
"e chain.\n/// </summary>\ninternal sealed class ");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Name));
|
||||
this.Write("Executor(FormulaSession session) : ActionExecutor(id: \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Id));
|
||||
this.Write("\", session)\n{\n // <inheritdoc />\n protected override async ValueTask<object" +
|
||||
"?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)\n " +
|
||||
" {");
|
||||
|
||||
for (int index = 0; index < this.Model.Conditions.Length; ++index)
|
||||
{
|
||||
ConditionItem conditionItem = this.Model.Conditions[index];
|
||||
if (conditionItem.Condition is null)
|
||||
{
|
||||
continue; // Skip if no condition is defined
|
||||
}
|
||||
|
||||
EvaluateBoolExpression(conditionItem.Condition, $"condition{index}");
|
||||
this.Write("\n if (condition");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(index));
|
||||
this.Write(")\n {\n return \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(ConditionGroupExecutor.Steps.Item(this.Model, conditionItem)));
|
||||
this.Write("\";\n }\n ");
|
||||
|
||||
}
|
||||
|
||||
this.Write("\n return \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(ConditionGroupExecutor.Steps.Else(this.Model)));
|
||||
this.Write("\";\n }\n}");
|
||||
return this.GenerationEnvironment.ToString();
|
||||
}
|
||||
|
||||
void AssignVariable(PropertyPath targetVariable, string valueVariable, bool tightFormat = false)
|
||||
{
|
||||
if (targetVariable is not null)
|
||||
{
|
||||
this.Write("\n await context.QueueStateUpdateAsync(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableName(targetVariable)));
|
||||
|
||||
this.Write("\", value: ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(valueVariable));
|
||||
|
||||
this.Write(", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableScope(targetVariable)));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
if (!tightFormat)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
this.Write("\n bool ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue)));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsLiteral)
|
||||
{
|
||||
this.Write("\n bool ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue)));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2)
|
||||
{
|
||||
this.Write("\n bool ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.ReadStateAsync<bool>(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName));
|
||||
|
||||
this.Write("\", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
this.Write("\n bool ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<bool>>(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString())));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n bool ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<bool>(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText)));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
<#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #>
|
||||
<#@ output extension=".cs" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.ObjectModel" #>
|
||||
<#@ import namespace="Microsoft.Agents.ObjectModel" #>
|
||||
<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #>
|
||||
<#@ include file="Snippets/EvaluateBoolExpressionTemplate.tt" once="true" #>
|
||||
/// <summary>
|
||||
/// Conditional branching similar to an if / elseif / elseif / else chain.
|
||||
/// </summary>
|
||||
internal sealed class <#= this.Name #>Executor(FormulaSession session) : ActionExecutor(id: "<#= this.Id #>", session)
|
||||
{
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{<#
|
||||
for (int index = 0; index < this.Model.Conditions.Length; ++index)
|
||||
{
|
||||
ConditionItem conditionItem = this.Model.Conditions[index];
|
||||
if (conditionItem.Condition is null)
|
||||
{
|
||||
continue; // Skip if no condition is defined
|
||||
}
|
||||
|
||||
EvaluateBoolExpression(conditionItem.Condition, $"condition{index}");#>
|
||||
if (condition<#= index #>)
|
||||
{
|
||||
return "<#= ConditionGroupExecutor.Steps.Item(this.Model, conditionItem)#>";
|
||||
}
|
||||
<#
|
||||
}
|
||||
#>
|
||||
return "<#= ConditionGroupExecutor.Steps.Else(this.Model)#>";
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
|
||||
|
||||
internal partial class ConditionGroupTemplate
|
||||
{
|
||||
public ConditionGroupTemplate(ConditionGroup model)
|
||||
{
|
||||
this.Model = this.Initialize(model);
|
||||
}
|
||||
|
||||
public ConditionGroup Model { get; }
|
||||
}
|
||||
-324
@@ -1,324 +0,0 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version: 18.0.0.0
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
{
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Class to produce the template output
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
|
||||
internal partial class CopyConversationMessagesTemplate : ActionTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the template output
|
||||
/// </summary>
|
||||
public override string TransformText()
|
||||
{
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n/// <summary>\n/// Copies one or more messages into the specified agent conversat" +
|
||||
"ion.\n/// </summary>\ninternal sealed class ");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Name));
|
||||
this.Write("Executor(FormulaSession session, ResponseAgentProvider agentProvider) : ActionExe" +
|
||||
"cutor(id: \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Id));
|
||||
this.Write("\", session)\n{\n // <inheritdoc />\n protected override async ValueTask<object" +
|
||||
"?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)\n " +
|
||||
" {");
|
||||
|
||||
EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true);
|
||||
this.Write("\n if (string.IsNullOrWhiteSpace(conversationId))\n {\n thr" +
|
||||
"ow new DeclarativeActionException($\"Conversation identifier must be defined: {th" +
|
||||
"is.Id}\");\n }");
|
||||
|
||||
EvaluateValueExpression<ChatMessage[]>(this.Model.Messages, "messages");
|
||||
|
||||
this.Write(@"
|
||||
if (messages is not null)
|
||||
{
|
||||
foreach (ChatMessage message in messages)
|
||||
{
|
||||
await agentProvider.CreateMessageAsync(conversationId, message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}");
|
||||
return this.GenerationEnvironment.ToString();
|
||||
}
|
||||
|
||||
void AssignVariable(PropertyPath targetVariable, string valueVariable, bool tightFormat = false)
|
||||
{
|
||||
if (targetVariable is not null)
|
||||
{
|
||||
this.Write("\n await context.QueueStateUpdateAsync(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableName(targetVariable)));
|
||||
|
||||
this.Write("\", value: ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(valueVariable));
|
||||
|
||||
this.Write(", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableScope(targetVariable)));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
if (!tightFormat)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false)
|
||||
{
|
||||
string typeName = isNullable ? "string?" : "string";
|
||||
if (expression is null)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty"));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsLiteral)
|
||||
{
|
||||
if (expression.LiteralValue.Contains("\n"))
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = \n \"\"\"\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue));
|
||||
|
||||
this.Write("\n \"\"\";");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue)));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.ReadStateAsync<string>(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName));
|
||||
|
||||
this.Write("\", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<string>(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString())));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(typeName));
|
||||
|
||||
this.Write(" ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<string>(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText)));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EvaluateValueExpression(ValueExpression expression, string targetVariable) =>
|
||||
EvaluateValueExpression<object>(expression, targetVariable);
|
||||
|
||||
void EvaluateValueExpression<TValue>(ValueExpression expression, string targetVariable)
|
||||
{
|
||||
if (expression is null)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write("? ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = null;");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsLiteral)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write("? ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue)));
|
||||
|
||||
this.Write(";");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write("? ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.ReadStateAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write(">(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName));
|
||||
|
||||
this.Write("\", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else if (expression.IsVariableReference)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write("? ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write(">(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString())));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write("? ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable));
|
||||
|
||||
this.Write(" = await context.EvaluateValueAsync<");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias<TValue>()));
|
||||
|
||||
this.Write(">(");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText)));
|
||||
|
||||
this.Write(").ConfigureAwait(false);");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
<#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #>
|
||||
<#@ output extension=".cs" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ import namespace="Microsoft.Agents.ObjectModel" #>
|
||||
<#@ import namespace="Microsoft.Extensions.AI" #>
|
||||
<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #>
|
||||
<#@ include file="Snippets/EvaluateStringExpressionTemplate.tt" once="true" #>
|
||||
<#@ include file="Snippets/EvaluateValueExpressionTemplate.tt" once="true" #>
|
||||
/// <summary>
|
||||
/// Copies one or more messages into the specified agent conversation.
|
||||
/// </summary>
|
||||
internal sealed class <#= this.Name #>Executor(FormulaSession session, ResponseAgentProvider agentProvider) : ActionExecutor(id: "<#= this.Id #>", session)
|
||||
{
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{<#
|
||||
EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true); #>
|
||||
if (string.IsNullOrWhiteSpace(conversationId))
|
||||
{
|
||||
throw new DeclarativeActionException($"Conversation identifier must be defined: {this.Id}");
|
||||
}<#
|
||||
EvaluateValueExpression<ChatMessage[]>(this.Model.Messages, "messages");
|
||||
#>
|
||||
if (messages is not null)
|
||||
{
|
||||
foreach (ChatMessage message in messages)
|
||||
{
|
||||
await agentProvider.CreateMessageAsync(conversationId, message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
|
||||
|
||||
internal partial class CopyConversationMessagesTemplate
|
||||
{
|
||||
public CopyConversationMessagesTemplate(CopyConversationMessages model)
|
||||
{
|
||||
this.Model = this.Initialize(model);
|
||||
this.UseAgentProvider = true;
|
||||
}
|
||||
|
||||
public CopyConversationMessages Model { get; }
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version: 18.0.0.0
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
{
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Class to produce the template output
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
|
||||
internal partial class CreateConversationTemplate : ActionTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the template output
|
||||
/// </summary>
|
||||
public override string TransformText()
|
||||
{
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\n/// <summary>\n/// Creates a new conversation and stores the identifier value to " +
|
||||
"the \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Model.ConversationId));
|
||||
this.Write("\" variable.\n/// </summary>\ninternal sealed class ");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Name));
|
||||
this.Write("Executor(FormulaSession session, ResponseAgentProvider agentProvider) : ActionExe" +
|
||||
"cutor(id: \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Id));
|
||||
this.Write(@""", session)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);");
|
||||
|
||||
AssignVariable(this.ConversationId, "conversationId");
|
||||
this.Write("\n await context.AddEventAsync(new ConversationUpdateEvent(conversationId))" +
|
||||
".ConfigureAwait(false);\n\n return default;\n }\n}\n");
|
||||
return this.GenerationEnvironment.ToString();
|
||||
}
|
||||
|
||||
void AssignVariable(PropertyPath targetVariable, string valueVariable, bool tightFormat = false)
|
||||
{
|
||||
if (targetVariable is not null)
|
||||
{
|
||||
this.Write("\n await context.QueueStateUpdateAsync(key: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableName(targetVariable)));
|
||||
|
||||
this.Write("\", value: ");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(valueVariable));
|
||||
|
||||
this.Write(", scopeName: \"");
|
||||
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(VariableScope(targetVariable)));
|
||||
|
||||
this.Write("\").ConfigureAwait(false);");
|
||||
|
||||
|
||||
if (!tightFormat)
|
||||
{
|
||||
this.Write("\n ");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
<#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #>
|
||||
<#@ output extension=".cs" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ import namespace="Microsoft.Agents.ObjectModel" #>
|
||||
<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #>
|
||||
/// <summary>
|
||||
/// Creates a new conversation and stores the identifier value to the "<#= this.Model.ConversationId #>" variable.
|
||||
/// </summary>
|
||||
internal sealed class <#= this.Name #>Executor(FormulaSession session, ResponseAgentProvider agentProvider) : ActionExecutor(id: "<#= this.Id #>", session)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);<#
|
||||
AssignVariable(this.ConversationId, "conversationId");#>
|
||||
await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
|
||||
|
||||
internal partial class CreateConversationTemplate
|
||||
{
|
||||
public CreateConversationTemplate(CreateConversation model)
|
||||
{
|
||||
this.Model = this.Initialize(model);
|
||||
this.ConversationId = Throw.IfNull(this.Model.ConversationId);
|
||||
this.UseAgentProvider = true;
|
||||
}
|
||||
|
||||
public CreateConversation Model { get; }
|
||||
|
||||
public PropertyPath ConversationId { get; }
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version: 18.0.0.0
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
// ------------------------------------------------------------------------------
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
{
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Class to produce the template output
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
|
||||
internal partial class DefaultTemplate : ActionTemplate, IModeledAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Create the template output
|
||||
/// </summary>
|
||||
public override string TransformText()
|
||||
{
|
||||
this.Write("\n");
|
||||
this.Write("\n");
|
||||
this.Write("\nDelegateExecutor ");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.InstanceVariable));
|
||||
this.Write(" = new(id: \"");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Id));
|
||||
this.Write("\", ");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.RootVariable));
|
||||
this.Write(".Session");
|
||||
this.Write(this.ToStringHelper.ToStringWithCulture(this.Action is not null ? $", {this.Action}" : ""));
|
||||
this.Write(");\n");
|
||||
return this.GenerationEnvironment.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<#@ template language="C#" inherits="ActionTemplate, IModeledAction" visibility="internal" linePragmas="false" #>
|
||||
<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.Interpreter" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
DelegateExecutor <#= this.InstanceVariable #> = new(id: "<#= this.Id #>", <#= this.RootVariable #>.Session<#= this.Action is not null ? $", {this.Action}" : "" #>);
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
|
||||
|
||||
internal partial class DefaultTemplate
|
||||
{
|
||||
public DefaultTemplate(DialogAction model, string rootId, string? action = null)
|
||||
{
|
||||
this.Initialize(model);
|
||||
this.Action = action;
|
||||
this.InstanceVariable = this.Id.FormatName();
|
||||
this.RootVariable = rootId.FormatName();
|
||||
}
|
||||
|
||||
public string? Action { get; }
|
||||
public string InstanceVariable { get; }
|
||||
public string RootVariable { get; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user