Compare commits

..
Author SHA1 Message Date
Peter Ibekwe 72a193086e Fix Foreach body exit wiring in declarative workflows 2026-05-22 15:19:36 -07:00
293 changed files with 10452 additions and 10558 deletions
+2 -6
View File
@@ -1,19 +1,15 @@
{
"name": "Python 3",
"image": "mcr.microsoft.com/devcontainers/python:3.14-bookworm",
"image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye",
"features": {
"ghcr.io/va-h/devcontainers-features/uv:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:3": {},
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
"ghcr.io/devcontainers/features/copilot-cli:1": {}
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {}
},
"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"
+1 -1
View File
@@ -8,7 +8,7 @@ ignorePatterns:
- pattern: "./blob"
- pattern: "./issues"
- pattern: "./discussions"
- pattern: "./pull"
- pattern: "./pulls"
- pattern: "https:\/\/platform.openai.com"
- pattern: "http:\/\/localhost"
- pattern: "http:\/\/127.0.0.1"
+4 -4
View File
@@ -173,11 +173,11 @@ new SampleDefinition
```csharp
new SampleDefinition
{
Name = "Workflow_Visualization",
ProjectPath = "samples/03-workflows/Visualization",
Name = "Workflow_Declarative_GenerateCode",
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
IsDeterministic = true,
MustContain = ["Generating workflow visualization...", "Mermaid string:", "DiGraph string:"],
ExpectedOutputDescription = ["The output should show workflow visualization in Mermaid and DiGraph formats."],
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
},
```
+7 -10
View File
@@ -117,18 +117,17 @@
<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" />
@@ -242,10 +241,11 @@
<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/InvokeFoundryToolboxMcp/InvokeFoundryToolboxMcp.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/InvokeHttpRequest/InvokeHttpRequest.csproj" />
<Project Path="samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj" />
<Project Path="samples/03-workflows/Declarative/Marketing/Marketing.csproj" />
@@ -359,9 +359,6 @@
<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" />
@@ -597,8 +594,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.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.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.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" />
@@ -624,8 +621,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.TestContainer/Foundry.Hosting.IntegrationTests.TestContainer.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.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" />
@@ -650,8 +647,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.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.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.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" />
-1
View File
@@ -26,7 +26,6 @@
"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",
+3
View File
@@ -11,6 +11,9 @@
<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,25 +363,6 @@ 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
@@ -439,6 +439,15 @@ 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",
+3 -3
View File
@@ -1,14 +1,14 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.7.0</VersionPrefix>
<VersionPrefix>1.6.2</VersionPrefix>
<RCNumber>1</RCNumber>
<DateSuffix>260526</DateSuffix>
<DateSuffix>260521</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.7.0</GitTag>
<GitTag>1.6.2</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -24,7 +24,6 @@ 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="AgentClassSkill{TSelf}.Resources"/> and <see cref="AgentClassSkill{TSelf}.Scripts"/> can be overridden.
/// <see cref="AgentSkill.Resources"/> and <see cref="AgentSkill.Scripts"/> can be overridden.
/// </remarks>
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
{
@@ -1,24 +0,0 @@
<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>
@@ -1,142 +0,0 @@
// 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;
}
@@ -1,34 +0,0 @@
# 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,7 +9,6 @@ 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,6 +52,7 @@ 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;
@@ -71,7 +72,6 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
}
Console.Write(props.Items[i]);
Console.Write(AnsiEscapes.EraseToEndOfLine);
if (isSelected)
{
@@ -101,7 +101,6 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
}
Console.Write(props.CustomText);
Console.Write(AnsiEscapes.EraseToEndOfLine);
if (isSelected)
{
@@ -122,7 +121,6 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
Console.Write(" ");
Console.Write(props.CustomTextPlaceholder);
Console.Write(AnsiEscapes.EraseToEndOfLine);
Console.Write(AnsiEscapes.ResetAttributes);
}
}
@@ -17,19 +17,16 @@ public record TextScrollPanelProps : ConsoleReactiveProps
/// <summary>
/// State for <see cref="TextScrollPanel"/>.
/// </summary>
public record TextScrollPanelState : ConsoleReactiveState;
/// <param name="RenderedCount">The number of items already rendered.</param>
public record TextScrollPanelState(int RenderedCount = 0) : ConsoleReactiveState;
/// <summary>
/// A component that renders pre-rendered string items within a scroll area.
/// 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.
/// All items are considered finalized — only new items since the last render are output.
/// Use <see cref="Reset"/> 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>
@@ -38,12 +35,12 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
this.State = new TextScrollPanelState();
}
/// <inheritdoc />
public override void Invalidate()
/// <summary>
/// Resets the panel so all items will be re-rendered on the next Render call.
/// </summary>
public void Reset()
{
this._renderedCount = 0;
this._lastItemOffsetFromBottom = 0;
base.Invalidate();
this.State = new TextScrollPanelState();
}
/// <inheritdoc />
@@ -54,59 +51,16 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
return;
}
int bottomRow = props.Y + props.Height - 1;
// Move cursor to the bottom of the scroll area
Console.Write(AnsiEscapes.MoveCursor(props.Y + props.Height - 1, props.X));
// 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++)
// Output only new items since last rendered
for (int i = state.RenderedCount; i < props.Items.Count; i++)
{
Console.Write(props.Items[i]);
}
// 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;
// Update state to track what we've rendered
this.State = new TextScrollPanelState(props.Items.Count);
}
}
@@ -1,36 +0,0 @@
// 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,6 +19,7 @@ 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();
@@ -340,6 +341,16 @@ 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);
@@ -433,7 +444,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
int modeAndHelpHeight = showStatusAndHelp ? AgentModeAndHelp.CalculateHeight(modeAndHelpProps) : 0;
int ruleHeight = TopBottomRule.CalculateHeight(ruleProps);
int nonScrollHeight = ruleHeight + agentStatusHeight + queuedPanelHeight + modeAndHelpHeight + 1; // +1 for bottom padding
int nonScrollHeight = ruleHeight + textPanelHeight + 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
@@ -444,36 +455,52 @@ 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
// 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()
: [];
this._textScrollPanel.Props = new TextScrollPanelProps
{
X = 1,
Y = 1,
Width = state.ConsoleWidth,
Height = scrollBottom,
Items = state.ScrollAreaContentItems,
Items = scrollItems,
};
this._textScrollPanel.Render();
// Render queued input items between scroll area and agent status
int queuedPanelY = scrollBottom + 1;
// 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;
this._queuedPanel.Props = new TextPanelProps
{
X = 1,
@@ -5,17 +5,17 @@ using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.ToolFormatters;
/// <summary>
/// Formats <c>mode_*</c> tool calls, showing the target mode for Set operations.
/// Formats <c>AgentMode_*</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("mode_", StringComparison.Ordinal);
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("AgentMode_", StringComparison.Ordinal);
/// <inheritdoc/>
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
{
"mode_set" => FormatStringArg(call, "mode"),
"AgentMode_Set" => FormatStringArg(call, "mode"),
_ => null,
};
@@ -7,20 +7,20 @@ using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.ToolFormatters;
/// <summary>
/// Formats <c>todos_*</c> tool calls with tree-view output for added items
/// Formats <c>TodoList_*</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("todos_", StringComparison.Ordinal);
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("TodoList_", StringComparison.Ordinal);
/// <inheritdoc/>
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
{
"todos_add" => FormatAddTodos(call),
"todos_complete" => FormatCompleteTodos(call),
"todos_remove" => FormatIdList(call, "ids", "Remove"),
"TodoList_Add" => FormatAddTodos(call),
"TodoList_Complete" => FormatCompleteTodos(call),
"TodoList_Remove" => FormatIdList(call, "ids", "Remove"),
_ => null,
};
@@ -0,0 +1,30 @@
<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>
@@ -0,0 +1,105 @@
// 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();
}
}
}
@@ -0,0 +1,28 @@
{
"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\""
}
}
}
@@ -1,14 +0,0 @@
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
@@ -1,26 +0,0 @@
# 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"]
@@ -1,23 +0,0 @@
# 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"]
@@ -1,40 +0,0 @@
<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>
@@ -1,215 +0,0 @@
// 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}).");
}
}
}
@@ -1,109 +0,0 @@
# 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.
@@ -1,41 +0,0 @@
# 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
@@ -1,14 +0,0 @@
# 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}
@@ -1,100 +0,0 @@
#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
}
@@ -1,30 +0,0 @@
---
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.
@@ -1,25 +0,0 @@
---
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 `# `.
@@ -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.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.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" />
</ItemGroup>
-->
@@ -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.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
@@ -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.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
@@ -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.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
@@ -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.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
@@ -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.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
@@ -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.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
@@ -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.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
@@ -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.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.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" />
</ItemGroup>
-->
@@ -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.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
@@ -32,11 +32,11 @@
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<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" />
<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" />
</ItemGroup>
-->
@@ -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.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" />
<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" />
</ItemGroup>
-->
@@ -20,28 +20,8 @@ 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)
@@ -104,14 +84,14 @@ internal static class AGUIChatMessageExtensions
case AGUIAssistantMessage assistantMessage when assistantMessage.ToolCalls is { Length: > 0 }:
{
pendingContents ??= new List<AIContent>();
pendingId ??= message.Id;
var contents = new List<AIContent>();
if (!string.IsNullOrEmpty(assistantMessage.Content))
{
pendingContents.Add(new TextContent(assistantMessage.Content));
contents.Add(new TextContent(assistantMessage.Content));
}
// Add tool calls
foreach (var toolCall in assistantMessage.ToolCalls)
{
Dictionary<string, object?>? arguments = null;
@@ -122,12 +102,16 @@ internal static class AGUIChatMessageExtensions
jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
}
pendingContents.Add(new FunctionCallContent(
contents.Add(new FunctionCallContent(
toolCall.Id,
toolCall.Function.Name,
arguments));
}
yield return new ChatMessage(role, contents)
{
MessageId = message.Id
};
break;
}
@@ -150,12 +134,6 @@ 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,36 +448,24 @@ internal static class ChatResponseUpdateAGUIExtensions
};
string? currentMessageId = null;
string? textStreamingFallback = null;
bool textInFallback = false;
string? streamingMessageId = null;
string? currentReasoningBaseId = null;
string? currentReasoningId = null;
string? currentReasoningMessageId = null;
await foreach (var chatResponse in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
{
// 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))
// 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))
{
textStreamingFallback ??= Guid.NewGuid().ToString("N");
textMessageId = textStreamingFallback;
textInFallback = true;
}
else if (textInFallback)
{
textStreamingFallback = null;
textInFallback = false;
chatResponse.MessageId = ContainsToolResult(chatResponse)
? Guid.NewGuid().ToString("N")
: (streamingMessageId ??= Guid.NewGuid().ToString("N"));
}
if (chatResponse is { Contents.Count: > 0 } &&
chatResponse.Contents[0] is TextContent &&
!string.Equals(currentMessageId, textMessageId, StringComparison.Ordinal))
!string.Equals(currentMessageId, chatResponse.MessageId, 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
@@ -510,11 +498,11 @@ internal static class ChatResponseUpdateAGUIExtensions
// Start the new message
yield return new TextMessageStartEvent
{
MessageId = textMessageId!,
MessageId = chatResponse.MessageId!,
Role = chatResponse.Role!.Value.Value
};
currentMessageId = textMessageId;
currentMessageId = chatResponse.MessageId;
}
// Emit text content if present
@@ -589,15 +577,9 @@ 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 = $"result-{functionResultContent.CallId}",
MessageId = chatResponse.MessageId,
ToolCallId = functionResultContent.CallId,
Content = SerializeResultContent(functionResultContent, jsonSerializerOptions) ?? "",
Role = AGUIRoles.Tool
@@ -692,7 +674,7 @@ internal static class ChatResponseUpdateAGUIExtensions
// Text content event
yield return new TextMessageContentEvent
{
MessageId = textMessageId!,
MessageId = chatResponse.MessageId!,
#if !NET
Delta = Encoding.UTF8.GetString(dataContent.Data.ToArray())
#else
@@ -744,4 +726,17 @@ 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
- 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))
- Improve session cast error message quality and consistency ([#3973](https://github.com/microsoft/agent-framework/pull/3973))
## v1.0.0-preview.260212.1
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -12,6 +13,7 @@ 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;
@@ -20,6 +22,7 @@ namespace Azure.AI.Projects;
/// <summary>
/// Provides extension methods for <see cref="AIProjectClient"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public static partial class AIProjectClientExtensions
{
/// <summary>
@@ -371,7 +374,6 @@ 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.");
@@ -404,7 +406,6 @@ 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,11 +1,13 @@
// 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;
@@ -15,6 +17,7 @@ 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,6 +5,7 @@ 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
@@ -26,6 +27,7 @@ namespace Microsoft.Agents.AI.Foundry;
/// <c>FoundryAITool.CreateOpenApiTool(definition)</c>
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public static class FoundryAITool
{
/// <summary>
@@ -4,12 +4,14 @@ 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;
@@ -34,6 +36,7 @@ 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>
@@ -258,7 +261,6 @@ 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(
@@ -266,7 +268,6 @@ 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,11 +2,13 @@
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;
@@ -21,6 +23,7 @@ namespace Microsoft.Agents.AI.Foundry;
/// <see cref="FoundryChatClient"/> at the agent level so callers do not need to drop down to
/// <c>agent.GetService&lt;FoundryChatClient&gt;().X()</c> for common workflows.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public static class FoundryAgentExtensions
{
/// <summary>
@@ -4,6 +4,7 @@ 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;
@@ -12,6 +13,7 @@ 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;
@@ -51,6 +53,7 @@ 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;
@@ -649,7 +652,6 @@ 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
@@ -661,7 +663,6 @@ 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>
@@ -674,7 +675,6 @@ 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,7 +682,6 @@ 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,12 +1,14 @@
// 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;
@@ -32,6 +34,7 @@ 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,6 +3,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Foundry;
@@ -21,6 +22,7 @@ namespace Microsoft.Agents.AI.Foundry;
/// <c>FoundryAITool.CreateHostedMcpToolbox(...)</c> factory overloads.
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public sealed class HostedMcpToolboxAITool : HostedMcpServerTool
{
/// <summary>
@@ -6,6 +6,7 @@
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,6 +13,7 @@ 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>
@@ -4,15 +4,13 @@
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<RootNamespace>Microsoft.Agents.AI.Mcp</RootNamespace>
<VersionSuffix>alpha</VersionSuffix>
<NoWarn>$(NoWarn);MEAI001;MAAI001;MCPEXP001</NoWarn>
<NoWarn>$(NoWarn);MEAI001;MCPEXP001</NoWarn>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
</PropertyGroup>
@@ -36,8 +34,4 @@
<InternalsVisibleTo Include="Microsoft.Agents.AI.Mcp.UnitTests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
</Project>
@@ -1,133 +0,0 @@
// 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 + "/";
}
}
@@ -1,59 +0,0 @@
// 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);
}
}
@@ -1,182 +0,0 @@
// 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);
}
@@ -1,26 +0,0 @@
// 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));
}
}
@@ -1,14 +0,0 @@
// 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;
@@ -1,96 +0,0 @@
// 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,27 @@
// 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;
}
}
@@ -0,0 +1,395 @@
// ------------------------------------------------------------------------------
// <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));
}
}
}
}
@@ -0,0 +1,67 @@
<#@ 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);
}
}
@@ -0,0 +1,31 @@
// 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();
}
@@ -0,0 +1,221 @@
// ------------------------------------------------------------------------------
// <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);");
}
}
}
}
@@ -0,0 +1,21 @@
<#@ 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;
}
}
@@ -0,0 +1,25 @@
// 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();
}
@@ -0,0 +1,339 @@
// 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
}
@@ -0,0 +1,172 @@
// ------------------------------------------------------------------------------
// <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);");
}
}
}
}
@@ -0,0 +1,34 @@
<#@ 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)#>";
}
}
@@ -0,0 +1,15 @@
// 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; }
}
@@ -0,0 +1,324 @@
// ------------------------------------------------------------------------------
// <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);");
}
}
}
}
@@ -0,0 +1,33 @@
<#@ 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;
}
}
@@ -0,0 +1,16 @@
// 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; }
}
@@ -0,0 +1,78 @@
// ------------------------------------------------------------------------------
// <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 ");
}
}
}
}
}
@@ -0,0 +1,19 @@
<#@ 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;
}
}
@@ -0,0 +1,20 @@
// 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; }
}
@@ -0,0 +1,40 @@
// ------------------------------------------------------------------------------
// <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();
}
}
}
@@ -0,0 +1,4 @@
<#@ 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}" : "" #>);
@@ -0,0 +1,21 @@
// 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; }
}
@@ -0,0 +1,51 @@
// ------------------------------------------------------------------------------
// <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;
/// <summary>
/// Class to produce the template output
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
internal partial class EdgeTemplate : CodeTemplate
{
/// <summary>
/// Create the template output
/// </summary>
public override string TransformText()
{
this.Write("\n");
this.Write("\n");
if (this.Condition is not null)
{
this.Write("\n builder.AddEdge(");
this.Write(this.ToStringHelper.ToStringWithCulture(this.SourceId));
this.Write(", ");
this.Write(this.ToStringHelper.ToStringWithCulture(this.TargetId));
this.Write(", (object? result) => ");
this.Write(this.ToStringHelper.ToStringWithCulture(this.Condition));
this.Write(");");
}
else
{
this.Write("\n builder.AddEdge(");
this.Write(this.ToStringHelper.ToStringWithCulture(this.SourceId));
this.Write(", ");
this.Write(this.ToStringHelper.ToStringWithCulture(this.TargetId));
this.Write(");");
}
this.Write("\n");
return this.GenerationEnvironment.ToString();
}
}
}
@@ -0,0 +1,10 @@
<#@ template language="C#" inherits="CodeTemplate" visibility="internal" linePragmas="false" #>
<#@ assembly name="System.Core" #>
<# if (this.Condition is not null)
{#>
builder.AddEdge(<#= this.SourceId #>, <#= this.TargetId #>, (object? result) => <#= this.Condition #>);<#
}
else
{#>
builder.AddEdge(<#= this.SourceId #>, <#= this.TargetId #>);<#
} #>
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
internal partial class EdgeTemplate
{
public EdgeTemplate(string sourceId, string targetId, string? condition = null)
{
this.SourceId = sourceId.FormatName();
this.TargetId = targetId.FormatName();
this.Condition = condition;
}
public string SourceId { get; }
public string TargetId { get; }
public string? Condition { get; }
}
@@ -0,0 +1,37 @@
// ------------------------------------------------------------------------------
// <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;
/// <summary>
/// Class to produce the template output
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
internal partial class EditTableV2Template : ActionTemplate
{
/// <summary>
/// Create the template output
/// </summary>
public override string TransformText()
{
this.Write("\n");
this.Write("\n");
this.Write("\n/// <summary>\n/// Modify items in a list\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 " +
" {\n return default;\n }\n}");
return this.GenerationEnvironment.ToString();
}
}
}
@@ -0,0 +1,14 @@
<#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
/// <summary>
/// Modify items in a list
/// </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)
{
return default;
}
}
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.ObjectModel;
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
internal partial class EditTableV2Template
{
public EditTableV2Template(EditTableV2 model)
{
this.Model = this.Initialize(model);
}
public EditTableV2 Model { get; }
}
@@ -0,0 +1,40 @@
// ------------------------------------------------------------------------------
// <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 EmptyTemplate : CodeTemplate, 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();
}
}
}
@@ -0,0 +1,4 @@
<#@ template language="C#" inherits="CodeTemplate, 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}" : "" #>);
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
internal partial class EmptyTemplate
{
public EmptyTemplate(string actionId, string rootId, string? action = null)
{
this.Id = actionId;
this.Name = this.Id.FormatType();
this.InstanceVariable = this.Id.FormatName();
this.RootVariable = rootId.FormatName();
this.Action = action;
}
public string Id { get; }
public string Name { get; }
public string InstanceVariable { get; }
public string RootVariable { get; }
public string? Action { get; }
}
@@ -0,0 +1,250 @@
// ------------------------------------------------------------------------------
// <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 ForeachTemplate : 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/// <summary>\n/// Loops over a list assignign the loop variable to \"");
this.Write(this.ToStringHelper.ToStringWithCulture(this.Model.Value));
this.Write("\" variable.\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)
{
private int _index;
private object[] _values = [];
public bool HasValue { get; private set; }
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
this._index = 0;");
EvaluateValueExpression(this.Model.Items, "evaluatedValue");
this.Write(@"
if (evaluatedValue == null)
{
this._values = [];
this.HasValue = false;
}
else
if (evaluatedValue is IEnumerable evaluatedList)
{
this._values = [.. evaluatedList];
}
else
{
this._values = [evaluatedValue];
}
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
return default;
}
public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
{
if (this.HasValue = this._index < this._values.Length)
{
object value = this._values[this._index];
");
AssignVariable(this.Value, "value", tightFormat: true);
if (this.Index is not null)
{
AssignVariable(this.Index, "this._index", tightFormat: true);
}
this.Write(@"
this._index++;
}
}
public async ValueTask CompleteAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
{
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
}
private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
{");
AssignVariable(this.Value, "UnassignedValue.Instance", tightFormat: true);
if (this.Index is not null)
{
AssignVariable(this.Index, "UnassignedValue.Instance", tightFormat: true);
}
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 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);");
}
}
}
}
@@ -0,0 +1,77 @@
<#@ 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" #>
<#@ include file="Snippets/EvaluateValueExpressionTemplate.tt" once="true" #>
/// <summary>
/// Loops over a list assignign the loop variable to "<#= this.Model.Value #>" variable.
/// </summary>
internal sealed class <#= this.Name #>Executor(FormulaSession session) : ActionExecutor(id: "<#= this.Id #>", session)
{
private int _index;
private object[] _values = [];
public bool HasValue { get; private set; }
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
this._index = 0;<#
EvaluateValueExpression(this.Model.Items, "evaluatedValue");#>
if (evaluatedValue == null)
{
this._values = [];
this.HasValue = false;
}
else
if (evaluatedValue is IEnumerable evaluatedList)
{
this._values = [.. evaluatedList];
}
else
{
this._values = [evaluatedValue];
}
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
return default;
}
public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
{
if (this.HasValue = this._index < this._values.Length)
{
object value = this._values[this._index];
<#
AssignVariable(this.Value, "value", tightFormat: true);
if (this.Index is not null)
{
AssignVariable(this.Index, "this._index", tightFormat: true);
}
#>
this._index++;
}
}
public async ValueTask CompleteAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
{
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
}
private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
{<#
AssignVariable(this.Value, "UnassignedValue.Instance", tightFormat: true);
if (this.Index is not null)
{
AssignVariable(this.Index, "UnassignedValue.Instance", tightFormat: true);
}
#>
}
}
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.ObjectModel;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
internal partial class ForeachTemplate
{
public ForeachTemplate(Foreach model)
{
this.Model = this.Initialize(model);
this.Index = this.Model.Index?.Path;
this.Value = Throw.IfNull(this.Model.Value);
}
public Foreach Model { get; }
public PropertyPath? Index { get; }
public PropertyPath Value { get; }
}
@@ -0,0 +1,38 @@
// ------------------------------------------------------------------------------
// <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;
/// <summary>
/// Class to produce the template output
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")]
internal partial class InstanceTemplate : CodeTemplate
{
/// <summary>
/// Create the template output
/// </summary>
public override string TransformText()
{
this.Write("\n");
this.Write("\n");
this.Write(this.ToStringHelper.ToStringWithCulture(this.ExecutorType));
this.Write("Executor ");
this.Write(this.ToStringHelper.ToStringWithCulture(this.InstanceVariable));
this.Write(" = new(");
this.Write(this.ToStringHelper.ToStringWithCulture(this.RootVariable));
this.Write(".Session");
this.Write(this.ToStringHelper.ToStringWithCulture(this.HasProvider ? ", options.AgentProvider" : ""));
this.Write(");");
return this.GenerationEnvironment.ToString();
}
}
}

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