mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72a193086e |
@@ -1,19 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "Python 3",
|
"name": "Python 3",
|
||||||
"image": "mcr.microsoft.com/devcontainers/python:3.14-bookworm",
|
"image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
"ghcr.io/va-h/devcontainers-features/uv:1": {},
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:3": {},
|
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {}
|
||||||
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
|
|
||||||
"ghcr.io/devcontainers/features/copilot-cli:1": {}
|
|
||||||
},
|
},
|
||||||
"postCreateCommand": "bash ./devsetup.sh",
|
"postCreateCommand": "bash ./devsetup.sh",
|
||||||
"workspaceFolder": "/workspaces/agent-framework/python/",
|
"workspaceFolder": "/workspaces/agent-framework/python/",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"GitHub.copilot",
|
|
||||||
"GitHub.vscode-github-actions",
|
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
"ms-windows-ai-studio.windows-ai-studio",
|
"ms-windows-ai-studio.windows-ai-studio",
|
||||||
"littlefoxteam.vscode-python-test-adapter"
|
"littlefoxteam.vscode-python-test-adapter"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ ignorePatterns:
|
|||||||
- pattern: "./blob"
|
- pattern: "./blob"
|
||||||
- pattern: "./issues"
|
- pattern: "./issues"
|
||||||
- pattern: "./discussions"
|
- pattern: "./discussions"
|
||||||
- pattern: "./pull"
|
- pattern: "./pulls"
|
||||||
- pattern: "https:\/\/platform.openai.com"
|
- pattern: "https:\/\/platform.openai.com"
|
||||||
- pattern: "http:\/\/localhost"
|
- pattern: "http:\/\/localhost"
|
||||||
- pattern: "http:\/\/127.0.0.1"
|
- pattern: "http:\/\/127.0.0.1"
|
||||||
|
|||||||
+4
-4
@@ -173,11 +173,11 @@ new SampleDefinition
|
|||||||
```csharp
|
```csharp
|
||||||
new SampleDefinition
|
new SampleDefinition
|
||||||
{
|
{
|
||||||
Name = "Workflow_Visualization",
|
Name = "Workflow_Declarative_GenerateCode",
|
||||||
ProjectPath = "samples/03-workflows/Visualization",
|
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
|
||||||
IsDeterministic = true,
|
IsDeterministic = true,
|
||||||
MustContain = ["Generating workflow visualization...", "Mermaid string:", "DiGraph string:"],
|
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
|
||||||
ExpectedOutputDescription = ["The output should show workflow visualization in Mermaid and DiGraph formats."],
|
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -117,18 +117,17 @@
|
|||||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj" />
|
<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_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_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
|
||||||
<Project Path="samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills/Agent_Step06_McpBasedSkills.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/Samples/02-agents/Harness/">
|
<Folder Name="/Samples/02-agents/Harness/">
|
||||||
<File Path="samples/02-agents/Harness/README.md" />
|
<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/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_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_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_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_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/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>
|
||||||
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
|
||||||
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
|
<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/ExecuteCode/ExecuteCode.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.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/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/HostedWorkflow/HostedWorkflow.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/InputArguments/InputArguments.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/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/InvokeHttpRequest/InvokeHttpRequest.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj" />
|
<Project Path="samples/03-workflows/Declarative/InvokeMcpTool/InvokeMcpTool.csproj" />
|
||||||
<Project Path="samples/03-workflows/Declarative/Marketing/Marketing.csproj" />
|
<Project Path="samples/03-workflows/Declarative/Marketing/Marketing.csproj" />
|
||||||
@@ -359,9 +359,6 @@
|
|||||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
|
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
|
||||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj" />
|
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj" />
|
||||||
</Folder>
|
</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/" />
|
||||||
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
|
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
|
||||||
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
|
<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.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.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.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.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.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.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.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/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.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/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/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.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.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.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.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.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.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.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.Harness.UnitTests/Microsoft.Agents.AI.Harness.UnitTests.csproj" />
|
||||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
|
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
|
||||||
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.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.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.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.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
|
||||||
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
<ItemGroup Condition="'$(InjectSharedIntegrationTestAzureCredentialsCode)' == 'true'">
|
<ItemGroup Condition="'$(InjectSharedIntegrationTestAzureCredentialsCode)' == 'true'">
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTestsAzureCredentials\*.cs" LinkBase="Shared\IntegrationTestsAzureCredentials" />
|
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTestsAzureCredentials\*.cs" LinkBase="Shared\IntegrationTestsAzureCredentials" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition="'$(InjectSharedBuildTestCode)' == 'true'">
|
||||||
|
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\CodeTests\*.cs" LinkBase="Shared\CodeTests" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup Condition="'$(InjectSharedWorkflowsExecution)' == 'true'">
|
<ItemGroup Condition="'$(InjectSharedWorkflowsExecution)' == 'true'">
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Execution\*.cs" LinkBase="Shared\Workflows" />
|
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Execution\*.cs" LinkBase="Shared\Workflows" />
|
||||||
</ItemGroup>
|
</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 ─────────────────────────────────────────────────
|
// ── AgentWithMemory ─────────────────────────────────────────────────
|
||||||
|
|
||||||
new SampleDefinition
|
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."],
|
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
|
new SampleDefinition
|
||||||
{
|
{
|
||||||
Name = "Workflow_Declarative_HostedWorkflow",
|
Name = "Workflow_Declarative_HostedWorkflow",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Central version prefix - applies to all nuget packages. -->
|
<!-- Central version prefix - applies to all nuget packages. -->
|
||||||
<VersionPrefix>1.7.0</VersionPrefix>
|
<VersionPrefix>1.6.2</VersionPrefix>
|
||||||
<RCNumber>1</RCNumber>
|
<RCNumber>1</RCNumber>
|
||||||
<DateSuffix>260526</DateSuffix>
|
<DateSuffix>260521</DateSuffix>
|
||||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
<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)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
|
||||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
||||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||||
<GitTag>1.7.0</GitTag>
|
<GitTag>1.6.2</GitTag>
|
||||||
|
|
||||||
<Configurations>Debug;Release;Publish</Configurations>
|
<Configurations>Debug;Release;Publish</Configurations>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYM
|
|||||||
var skillsProvider = new AgentSkillsProvider(
|
var skillsProvider = new AgentSkillsProvider(
|
||||||
Path.Combine(AppContext.BaseDirectory, "skills"),
|
Path.Combine(AppContext.BaseDirectory, "skills"),
|
||||||
SubprocessScriptRunner.RunAsync);
|
SubprocessScriptRunner.RunAsync);
|
||||||
|
|
||||||
// --- Agent Setup ---
|
// --- Agent Setup ---
|
||||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||||
.GetResponsesClient()
|
.GetResponsesClient()
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Console.WriteLine($"Agent: {response.Text}");
|
|||||||
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
|
/// Properties annotated with <see cref="AgentSkillResourceAttribute"/> are automatically
|
||||||
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
|
/// discovered as skill resources, and methods annotated with <see cref="AgentSkillScriptAttribute"/>
|
||||||
/// are automatically discovered as skill scripts. Alternatively,
|
/// 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>
|
/// </remarks>
|
||||||
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
|
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
|
||||||
{
|
{
|
||||||
|
|||||||
-24
@@ -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_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_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_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
|
## Key Concepts
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
foreach (string line in props.Title.Split('\n'))
|
foreach (string line in props.Title.Split('\n'))
|
||||||
{
|
{
|
||||||
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
||||||
|
Console.Write(AnsiEscapes.EraseEntireLine);
|
||||||
Console.Write(line);
|
Console.Write(line);
|
||||||
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
|
||||||
row++;
|
row++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
for (int i = 0; i < totalItems; i++)
|
for (int i = 0; i < totalItems; i++)
|
||||||
{
|
{
|
||||||
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
|
||||||
|
Console.Write(AnsiEscapes.EraseEntireLine);
|
||||||
|
|
||||||
bool isSelected = i == props.SelectedIndex;
|
bool isSelected = i == props.SelectedIndex;
|
||||||
bool isCustomTextOption = props.CustomTextPlaceholder != null && i == props.Items.Count;
|
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(props.Items[i]);
|
||||||
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
|
||||||
|
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
{
|
{
|
||||||
@@ -101,7 +101,6 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.Write(props.CustomText);
|
Console.Write(props.CustomText);
|
||||||
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
|
||||||
|
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
{
|
{
|
||||||
@@ -122,7 +121,6 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
|
|||||||
|
|
||||||
Console.Write(" ");
|
Console.Write(" ");
|
||||||
Console.Write(props.CustomTextPlaceholder);
|
Console.Write(props.CustomTextPlaceholder);
|
||||||
Console.Write(AnsiEscapes.EraseToEndOfLine);
|
|
||||||
Console.Write(AnsiEscapes.ResetAttributes);
|
Console.Write(AnsiEscapes.ResetAttributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,19 +17,16 @@ public record TextScrollPanelProps : ConsoleReactiveProps
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// State for <see cref="TextScrollPanel"/>.
|
/// State for <see cref="TextScrollPanel"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record TextScrollPanelState : ConsoleReactiveState;
|
/// <param name="RenderedCount">The number of items already rendered.</param>
|
||||||
|
public record TextScrollPanelState(int RenderedCount = 0) : ConsoleReactiveState;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A component that renders pre-rendered string items within a scroll area.
|
/// 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 items are considered finalized — only new items since the last render are output.
|
||||||
/// All prior items are considered finalized and are not re-rendered.
|
/// Use <see cref="Reset"/> to force a full re-render.
|
||||||
/// Use <see cref="Invalidate"/> to force a full re-render.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, TextScrollPanelState>
|
public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, TextScrollPanelState>
|
||||||
{
|
{
|
||||||
private int _renderedCount;
|
|
||||||
private int _lastItemOffsetFromBottom;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TextScrollPanel"/> class.
|
/// Initializes a new instance of the <see cref="TextScrollPanel"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -38,12 +35,12 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
|
|||||||
this.State = new TextScrollPanelState();
|
this.State = new TextScrollPanelState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override void Invalidate()
|
/// Resets the panel so all items will be re-rendered on the next Render call.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
{
|
{
|
||||||
this._renderedCount = 0;
|
this.State = new TextScrollPanelState();
|
||||||
this._lastItemOffsetFromBottom = 0;
|
|
||||||
base.Invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -54,59 +51,16 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
|
|||||||
return;
|
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,
|
// Output only new items since last rendered
|
||||||
// re-render the last one (it may have changed/grown) from its stored position.
|
for (int i = state.RenderedCount; i < props.Items.Count; i++)
|
||||||
int startIndex = this._renderedCount > 0 ? this._renderedCount - 1 : 0;
|
|
||||||
|
|
||||||
if (this._renderedCount > 0 && this._lastItemOffsetFromBottom > 0)
|
|
||||||
{
|
|
||||||
// Reposition cursor to where the last rendered item began
|
|
||||||
Console.Write(AnsiEscapes.MoveCursor(bottomRow - this._lastItemOffsetFromBottom, props.X));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// First render — position at the bottom of the scroll area
|
|
||||||
Console.Write(AnsiEscapes.MoveCursor(bottomRow, props.X));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render from startIndex onwards
|
|
||||||
for (int i = startIndex; i < props.Items.Count; i++)
|
|
||||||
{
|
{
|
||||||
Console.Write(props.Items[i]);
|
Console.Write(props.Items[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the offset from bottom for the start of the new last item
|
// Update state to track what we've rendered
|
||||||
int lastItemLines = CountLines(props.Items[^1]);
|
this.State = new TextScrollPanelState(props.Items.Count);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ListSelection _listSelection = new();
|
||||||
private readonly TextInput _textInput = new();
|
private readonly TextInput _textInput = new();
|
||||||
private readonly TextScrollPanel _textScrollPanel = new();
|
private readonly TextScrollPanel _textScrollPanel = new();
|
||||||
|
private readonly TextPanel _textPanel = new();
|
||||||
private readonly TextPanel _queuedPanel = new();
|
private readonly TextPanel _queuedPanel = new();
|
||||||
private readonly AgentStatus _agentStatus = new();
|
private readonly AgentStatus _agentStatus = new();
|
||||||
private readonly AgentModeAndHelp _modeAndHelp = new();
|
private readonly AgentModeAndHelp _modeAndHelp = new();
|
||||||
@@ -340,6 +341,16 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
|||||||
return;
|
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
|
// Calculate queued items panel height
|
||||||
int queuedPanelHeight = TextPanel.CalculateHeight(state.QueuedItems);
|
int queuedPanelHeight = TextPanel.CalculateHeight(state.QueuedItems);
|
||||||
|
|
||||||
@@ -433,7 +444,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
|
|||||||
int modeAndHelpHeight = showStatusAndHelp ? AgentModeAndHelp.CalculateHeight(modeAndHelpProps) : 0;
|
int modeAndHelpHeight = showStatusAndHelp ? AgentModeAndHelp.CalculateHeight(modeAndHelpProps) : 0;
|
||||||
|
|
||||||
int ruleHeight = TopBottomRule.CalculateHeight(ruleProps);
|
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);
|
int scrollBottom = Math.Max(1, state.ConsoleHeight - nonScrollHeight);
|
||||||
|
|
||||||
// If scroll region changed or a clear is needed, reset everything
|
// 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.ResetScrollRegion);
|
||||||
System.Console.Write(AnsiEscapes.EraseEntireScreen);
|
System.Console.Write(AnsiEscapes.EraseEntireScreen);
|
||||||
System.Console.Write(AnsiEscapes.EraseScrollbackBuffer);
|
System.Console.Write(AnsiEscapes.EraseScrollbackBuffer);
|
||||||
|
this._textScrollPanel.Reset();
|
||||||
|
this._resizedSinceLastRender = false;
|
||||||
|
|
||||||
// Invalidate all children so they re-render even if props haven't changed
|
// Invalidate all children so they re-render even if props haven't changed
|
||||||
this._rule.Invalidate();
|
this._rule.Invalidate();
|
||||||
this._textScrollPanel.Invalidate();
|
this._textScrollPanel.Invalidate();
|
||||||
|
this._textPanel.Invalidate();
|
||||||
this._queuedPanel.Invalidate();
|
this._queuedPanel.Invalidate();
|
||||||
this._agentStatus.Invalidate();
|
this._agentStatus.Invalidate();
|
||||||
this._modeAndHelp.Invalidate();
|
this._modeAndHelp.Invalidate();
|
||||||
this._textInput.Invalidate();
|
this._textInput.Invalidate();
|
||||||
this._listSelection.Invalidate();
|
this._listSelection.Invalidate();
|
||||||
|
|
||||||
this._resizedSinceLastRender = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._scrollRegionBottom = scrollBottom;
|
this._scrollRegionBottom = scrollBottom;
|
||||||
|
|
||||||
System.Console.Write(AnsiEscapes.SetScrollRegion(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
|
this._textScrollPanel.Props = new TextScrollPanelProps
|
||||||
{
|
{
|
||||||
X = 1,
|
X = 1,
|
||||||
Y = 1,
|
Y = 1,
|
||||||
Width = state.ConsoleWidth,
|
Width = state.ConsoleWidth,
|
||||||
Height = scrollBottom,
|
Height = scrollBottom,
|
||||||
Items = state.ScrollAreaContentItems,
|
Items = scrollItems,
|
||||||
};
|
};
|
||||||
this._textScrollPanel.Render();
|
this._textScrollPanel.Render();
|
||||||
|
|
||||||
// Render queued input items between scroll area and agent status
|
// Render the text panel for the last (dynamic) item just below the scroll region
|
||||||
int queuedPanelY = scrollBottom + 1;
|
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
|
this._queuedPanel.Props = new TextPanelProps
|
||||||
{
|
{
|
||||||
X = 1,
|
X = 1,
|
||||||
|
|||||||
+3
-3
@@ -5,17 +5,17 @@ using Microsoft.Extensions.AI;
|
|||||||
namespace Harness.Shared.Console.ToolFormatters;
|
namespace Harness.Shared.Console.ToolFormatters;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public sealed class ModeToolFormatter : ToolCallFormatter
|
public sealed class ModeToolFormatter : ToolCallFormatter
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <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/>
|
/// <inheritdoc/>
|
||||||
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
||||||
{
|
{
|
||||||
"mode_set" => FormatStringArg(call, "mode"),
|
"AgentMode_Set" => FormatStringArg(call, "mode"),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -7,20 +7,20 @@ using Microsoft.Extensions.AI;
|
|||||||
namespace Harness.Shared.Console.ToolFormatters;
|
namespace Harness.Shared.Console.ToolFormatters;
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// and structured output for complete/remove operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class TodoToolFormatter : ToolCallFormatter
|
public sealed class TodoToolFormatter : ToolCallFormatter
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <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/>
|
/// <inheritdoc/>
|
||||||
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
|
||||||
{
|
{
|
||||||
"todos_add" => FormatAddTodos(call),
|
"TodoList_Add" => FormatAddTodos(call),
|
||||||
"todos_complete" => FormatCompleteTodos(call),
|
"TodoList_Complete" => FormatCompleteTodos(call),
|
||||||
"todos_remove" => FormatIdList(call, "ids", "Remove"),
|
"TodoList_Remove" => FormatIdList(call, "ids", "Remove"),
|
||||||
_ => null,
|
_ => 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\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-14
@@ -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"]
|
|
||||||
-23
@@ -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"]
|
|
||||||
-40
@@ -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>
|
|
||||||
-215
@@ -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.
|
|
||||||
-41
@@ -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}
|
|
||||||
-100
@@ -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
|
|
||||||
}
|
|
||||||
-30
@@ -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.
|
|
||||||
-25
@@ -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 `# `.
|
|
||||||
+3
-3
@@ -27,9 +27,9 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
<PackageReference Include="Azure.AI.Projects" />
|
<PackageReference Include="Azure.AI.Projects" />
|
||||||
<PackageReference Include="Azure.Identity" />
|
<PackageReference Include="Azure.Identity" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+2
-2
@@ -33,8 +33,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
<PackageReference Include="Azure.AI.Projects" />
|
<PackageReference Include="Azure.AI.Projects" />
|
||||||
<PackageReference Include="Azure.Identity" />
|
<PackageReference Include="Azure.Identity" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -26,8 +26,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -26,9 +26,9 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -32,11 +32,11 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Hosting" Version="1.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Hosting" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -27,10 +27,10 @@
|
|||||||
|
|
||||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" 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.6.1-preview.260514.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
@@ -20,28 +20,8 @@ internal static class AGUIChatMessageExtensions
|
|||||||
this IEnumerable<AGUIMessage> aguiMessages,
|
this IEnumerable<AGUIMessage> aguiMessages,
|
||||||
JsonSerializerOptions jsonSerializerOptions)
|
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)
|
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);
|
var role = MapChatRole(message.Role);
|
||||||
|
|
||||||
switch (message)
|
switch (message)
|
||||||
@@ -104,14 +84,14 @@ internal static class AGUIChatMessageExtensions
|
|||||||
|
|
||||||
case AGUIAssistantMessage assistantMessage when assistantMessage.ToolCalls is { Length: > 0 }:
|
case AGUIAssistantMessage assistantMessage when assistantMessage.ToolCalls is { Length: > 0 }:
|
||||||
{
|
{
|
||||||
pendingContents ??= new List<AIContent>();
|
var contents = new List<AIContent>();
|
||||||
pendingId ??= message.Id;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(assistantMessage.Content))
|
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)
|
foreach (var toolCall in assistantMessage.ToolCalls)
|
||||||
{
|
{
|
||||||
Dictionary<string, object?>? arguments = null;
|
Dictionary<string, object?>? arguments = null;
|
||||||
@@ -122,12 +102,16 @@ internal static class AGUIChatMessageExtensions
|
|||||||
jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
|
jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingContents.Add(new FunctionCallContent(
|
contents.Add(new FunctionCallContent(
|
||||||
toolCall.Id,
|
toolCall.Id,
|
||||||
toolCall.Function.Name,
|
toolCall.Function.Name,
|
||||||
arguments));
|
arguments));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield return new ChatMessage(role, contents)
|
||||||
|
{
|
||||||
|
MessageId = message.Id
|
||||||
|
};
|
||||||
break;
|
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(
|
public static IEnumerable<AGUIMessage> AsAGUIMessages(
|
||||||
|
|||||||
@@ -448,36 +448,24 @@ internal static class ChatResponseUpdateAGUIExtensions
|
|||||||
};
|
};
|
||||||
|
|
||||||
string? currentMessageId = null;
|
string? currentMessageId = null;
|
||||||
string? textStreamingFallback = null;
|
string? streamingMessageId = null;
|
||||||
bool textInFallback = false;
|
|
||||||
string? currentReasoningBaseId = null;
|
string? currentReasoningBaseId = null;
|
||||||
string? currentReasoningId = null;
|
string? currentReasoningId = null;
|
||||||
string? currentReasoningMessageId = null;
|
string? currentReasoningMessageId = null;
|
||||||
await foreach (var chatResponse in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
|
await foreach (var chatResponse in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
// The text-event surface (TextMessageStart/Content/End) requires a non-empty
|
// Generate a fallback MessageId when the provider doesn't supply one.
|
||||||
// MessageId to be valid AGUI. Generate a fallback scoped to a contiguous run of
|
// This ensures all AGUI events have a valid messageId regardless of agent type.
|
||||||
// null/empty-MessageId chunks (one logical text message). Leave the raw
|
if (string.IsNullOrWhiteSpace(chatResponse.MessageId))
|
||||||
// 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))
|
|
||||||
{
|
{
|
||||||
textStreamingFallback ??= Guid.NewGuid().ToString("N");
|
chatResponse.MessageId = ContainsToolResult(chatResponse)
|
||||||
textMessageId = textStreamingFallback;
|
? Guid.NewGuid().ToString("N")
|
||||||
textInFallback = true;
|
: (streamingMessageId ??= Guid.NewGuid().ToString("N"));
|
||||||
}
|
|
||||||
else if (textInFallback)
|
|
||||||
{
|
|
||||||
textStreamingFallback = null;
|
|
||||||
textInFallback = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chatResponse is { Contents.Count: > 0 } &&
|
if (chatResponse is { Contents.Count: > 0 } &&
|
||||||
chatResponse.Contents[0] is TextContent &&
|
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
|
// Close any open reasoning block before opening a text message, so AG-UI
|
||||||
// events are properly bracketed. MEAI providers share one MessageId across
|
// events are properly bracketed. MEAI providers share one MessageId across
|
||||||
@@ -510,11 +498,11 @@ internal static class ChatResponseUpdateAGUIExtensions
|
|||||||
// Start the new message
|
// Start the new message
|
||||||
yield return new TextMessageStartEvent
|
yield return new TextMessageStartEvent
|
||||||
{
|
{
|
||||||
MessageId = textMessageId!,
|
MessageId = chatResponse.MessageId!,
|
||||||
Role = chatResponse.Role!.Value.Value
|
Role = chatResponse.Role!.Value.Value
|
||||||
};
|
};
|
||||||
|
|
||||||
currentMessageId = textMessageId;
|
currentMessageId = chatResponse.MessageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit text content if present
|
// Emit text content if present
|
||||||
@@ -589,15 +577,9 @@ internal static class ChatResponseUpdateAGUIExtensions
|
|||||||
currentReasoningMessageId = null;
|
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
|
yield return new ToolCallResultEvent
|
||||||
{
|
{
|
||||||
MessageId = $"result-{functionResultContent.CallId}",
|
MessageId = chatResponse.MessageId,
|
||||||
ToolCallId = functionResultContent.CallId,
|
ToolCallId = functionResultContent.CallId,
|
||||||
Content = SerializeResultContent(functionResultContent, jsonSerializerOptions) ?? "",
|
Content = SerializeResultContent(functionResultContent, jsonSerializerOptions) ?? "",
|
||||||
Role = AGUIRoles.Tool
|
Role = AGUIRoles.Tool
|
||||||
@@ -692,7 +674,7 @@ internal static class ChatResponseUpdateAGUIExtensions
|
|||||||
// Text content event
|
// Text content event
|
||||||
yield return new TextMessageContentEvent
|
yield return new TextMessageContentEvent
|
||||||
{
|
{
|
||||||
MessageId = textMessageId!,
|
MessageId = chatResponse.MessageId!,
|
||||||
#if !NET
|
#if !NET
|
||||||
Delta = Encoding.UTF8.GetString(dataContent.Data.ToArray())
|
Delta = Encoding.UTF8.GetString(dataContent.Data.ToArray())
|
||||||
#else
|
#else
|
||||||
@@ -744,4 +726,17 @@ internal static class ChatResponseUpdateAGUIExtensions
|
|||||||
_ => JsonSerializer.Serialize(functionResultContent.Result, options.GetTypeInfo(functionResultContent.Result.GetType())),
|
_ => 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
|
## 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))
|
- [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))
|
- Improve session cast error message quality and consistency ([#3973](https://github.com/microsoft/agent-framework/pull/3973))
|
||||||
|
|
||||||
## v1.0.0-preview.260212.1
|
## v1.0.0-preview.260212.1
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
@@ -12,6 +13,7 @@ using Microsoft.Agents.AI;
|
|||||||
using Microsoft.Agents.AI.Foundry;
|
using Microsoft.Agents.AI.Foundry;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Shared.DiagnosticIds;
|
||||||
using Microsoft.Shared.Diagnostics;
|
using Microsoft.Shared.Diagnostics;
|
||||||
using OpenAI.Responses;
|
using OpenAI.Responses;
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ namespace Azure.AI.Projects;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides extension methods for <see cref="AIProjectClient"/>.
|
/// Provides extension methods for <see cref="AIProjectClient"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
public static partial class AIProjectClientExtensions
|
public static partial class AIProjectClientExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -371,7 +374,6 @@ public static partial class AIProjectClientExtensions
|
|||||||
if (agentDefinition is DeclarativeAgentDefinition { Tools: { Count: > 0 } definitionTools })
|
if (agentDefinition is DeclarativeAgentDefinition { Tools: { Count: > 0 } definitionTools })
|
||||||
{
|
{
|
||||||
// Check if no tools were provided while the agent definition requires in-proc tools.
|
// 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))
|
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.");
|
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());
|
(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 })
|
if (requireInvocableTools && missingTools is { Count: > 0 })
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// Copyright (c) Microsoft. All rights reserved.
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Azure.AI.Extensions.OpenAI;
|
using Azure.AI.Extensions.OpenAI;
|
||||||
using Azure.AI.Projects.Agents;
|
using Azure.AI.Projects.Agents;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
|
using Microsoft.Shared.DiagnosticIds;
|
||||||
using Microsoft.Shared.Diagnostics;
|
using Microsoft.Shared.Diagnostics;
|
||||||
|
|
||||||
namespace Microsoft.Agents.AI.Foundry;
|
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
|
/// <c>to_prompt_agent(agent)</c> function for agents whose underlying chat client is a
|
||||||
/// <see cref="FoundryChatClient"/>.
|
/// <see cref="FoundryChatClient"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
public static class ChatClientAgentFoundryExtensions
|
public static class ChatClientAgentFoundryExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Azure.AI.Projects.Agents;
|
using Azure.AI.Projects.Agents;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
|
using Microsoft.Shared.DiagnosticIds;
|
||||||
using OpenAI.Responses;
|
using OpenAI.Responses;
|
||||||
|
|
||||||
#pragma warning disable OPENAI001
|
#pragma warning disable OPENAI001
|
||||||
@@ -26,6 +27,7 @@ namespace Microsoft.Agents.AI.Foundry;
|
|||||||
/// <c>FoundryAITool.CreateOpenApiTool(definition)</c>
|
/// <c>FoundryAITool.CreateOpenApiTool(definition)</c>
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
public static class FoundryAITool
|
public static class FoundryAITool
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ using System;
|
|||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
using System.ClientModel.Primitives;
|
using System.ClientModel.Primitives;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Azure.AI.Extensions.OpenAI;
|
using Azure.AI.Extensions.OpenAI;
|
||||||
using Azure.AI.Projects;
|
using Azure.AI.Projects;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Shared.DiagnosticIds;
|
||||||
using Microsoft.Shared.Diagnostics;
|
using Microsoft.Shared.Diagnostics;
|
||||||
|
|
||||||
namespace Microsoft.Agents.AI.Foundry;
|
namespace Microsoft.Agents.AI.Foundry;
|
||||||
@@ -34,6 +36,7 @@ namespace Microsoft.Agents.AI.Foundry;
|
|||||||
/// <c>AsAIAgent</c> extension methods on <see cref="AIProjectClient"/>.
|
/// <c>AsAIAgent</c> extension methods on <see cref="AIProjectClient"/>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
public sealed class FoundryAgent : DelegatingAIAgent
|
public sealed class FoundryAgent : DelegatingAIAgent
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -258,7 +261,6 @@ public sealed class FoundryAgent : DelegatingAIAgent
|
|||||||
return innerAgent;
|
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)
|
if (innerAgent.ChatClient.GetService<OpenAIRequestPolicies>() is { } policies)
|
||||||
{
|
{
|
||||||
OpenAIRequestPoliciesReflection.AddPolicyIfMissing(
|
OpenAIRequestPoliciesReflection.AddPolicyIfMissing(
|
||||||
@@ -266,7 +268,6 @@ public sealed class FoundryAgent : DelegatingAIAgent
|
|||||||
ClientHeadersPolicy.Instance,
|
ClientHeadersPolicy.Instance,
|
||||||
PipelinePosition.PerCall);
|
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);
|
return new ClientHeadersAgent(innerAgent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Azure.AI.Extensions.OpenAI;
|
using Azure.AI.Extensions.OpenAI;
|
||||||
using Azure.AI.Projects.Agents;
|
using Azure.AI.Projects.Agents;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
|
using Microsoft.Shared.DiagnosticIds;
|
||||||
using Microsoft.Shared.Diagnostics;
|
using Microsoft.Shared.Diagnostics;
|
||||||
using OpenAI.Files;
|
using OpenAI.Files;
|
||||||
using OpenAI.VectorStores;
|
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
|
/// <see cref="FoundryChatClient"/> at the agent level so callers do not need to drop down to
|
||||||
/// <c>agent.GetService<FoundryChatClient>().X()</c> for common workflows.
|
/// <c>agent.GetService<FoundryChatClient>().X()</c> for common workflows.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
public static class FoundryAgentExtensions
|
public static class FoundryAgentExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System;
|
|||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
using System.ClientModel.Primitives;
|
using System.ClientModel.Primitives;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -12,6 +13,7 @@ using Azure.AI.Extensions.OpenAI;
|
|||||||
using Azure.AI.Projects;
|
using Azure.AI.Projects;
|
||||||
using Azure.AI.Projects.Agents;
|
using Azure.AI.Projects.Agents;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
|
using Microsoft.Shared.DiagnosticIds;
|
||||||
using Microsoft.Shared.Diagnostics;
|
using Microsoft.Shared.Diagnostics;
|
||||||
using OpenAI.Files;
|
using OpenAI.Files;
|
||||||
using OpenAI.Responses;
|
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.
|
/// behind an Agent Endpoint. It is not synonymous with the Agent Endpoint mode itself.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
public sealed class FoundryChatClient : DelegatingChatClient
|
public sealed class FoundryChatClient : DelegatingChatClient
|
||||||
{
|
{
|
||||||
private readonly ChatClientMetadata _metadata;
|
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>
|
/// <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)
|
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)
|
if (innerClient?.GetService<OpenAIRequestPolicies>() is { } policies)
|
||||||
{
|
{
|
||||||
// OpenAIRequestPoliciesReflection.AddPolicyIfMissing performs a check-then-add against
|
// OpenAIRequestPoliciesReflection.AddPolicyIfMissing performs a check-then-add against
|
||||||
@@ -661,7 +663,6 @@ public sealed class FoundryChatClient : DelegatingChatClient
|
|||||||
AgentFrameworkUserAgentPolicy.Instance,
|
AgentFrameworkUserAgentPolicy.Instance,
|
||||||
PipelinePosition.PerCall);
|
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>
|
/// <summary>
|
||||||
@@ -674,7 +675,6 @@ public sealed class FoundryChatClient : DelegatingChatClient
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static void TryRegisterServedModelPolicy(IChatClient? innerClient)
|
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)
|
if (innerClient?.GetService<OpenAIRequestPolicies>() is { } policies)
|
||||||
{
|
{
|
||||||
OpenAIRequestPoliciesReflection.AddPolicyIfMissing(
|
OpenAIRequestPoliciesReflection.AddPolicyIfMissing(
|
||||||
@@ -682,7 +682,6 @@ public sealed class FoundryChatClient : DelegatingChatClient
|
|||||||
ServedModelPolicy.Instance,
|
ServedModelPolicy.Instance,
|
||||||
PipelinePosition.PerCall);
|
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>
|
/// <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.
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Azure.AI.Extensions.OpenAI;
|
using Azure.AI.Extensions.OpenAI;
|
||||||
using Azure.AI.Projects;
|
using Azure.AI.Projects;
|
||||||
using Azure.AI.Projects.Agents;
|
using Azure.AI.Projects.Agents;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
|
using Microsoft.Shared.DiagnosticIds;
|
||||||
using Microsoft.Shared.Diagnostics;
|
using Microsoft.Shared.Diagnostics;
|
||||||
using OpenAI.Responses;
|
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>
|
/// <item><description><b>Agent Endpoint (Mode 3)</b>: throw — no local definition exists to convert.</description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
internal static class FoundryPromptAgentConverter
|
internal static class FoundryPromptAgentConverter
|
||||||
{
|
{
|
||||||
/// <summary>Performs the conversion for an agent whose chat client and chat options are supplied.</summary>
|
/// <summary>Performs the conversion for an agent whose chat client and chat options are supplied.</summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
|
using Microsoft.Shared.DiagnosticIds;
|
||||||
|
|
||||||
namespace Microsoft.Agents.AI.Foundry;
|
namespace Microsoft.Agents.AI.Foundry;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ namespace Microsoft.Agents.AI.Foundry;
|
|||||||
/// <c>FoundryAITool.CreateHostedMcpToolbox(...)</c> factory overloads.
|
/// <c>FoundryAITool.CreateHostedMcpToolbox(...)</c> factory overloads.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
public sealed class HostedMcpToolboxAITool : HostedMcpServerTool
|
public sealed class HostedMcpToolboxAITool : HostedMcpServerTool
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
related per-agent endpoint surface). Flip back to IsReleased=true once Azure.AI.Projects
|
related per-agent endpoint surface). Flip back to IsReleased=true once Azure.AI.Projects
|
||||||
ships a stable 2.1.0. -->
|
ships a stable 2.1.0. -->
|
||||||
<InjectSharedThrow>true</InjectSharedThrow>
|
<InjectSharedThrow>true</InjectSharedThrow>
|
||||||
|
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Azure.AI.Extensions.OpenAI;
|
|||||||
/// Provides extension methods for <see cref="ProjectResponsesClient"/>
|
/// Provides extension methods for <see cref="ProjectResponsesClient"/>
|
||||||
/// to simplify the creation of AI agents that work with Azure AI services.
|
/// to simplify the creation of AI agents that work with Azure AI services.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||||
public static class ProjectResponsesClientExtensions
|
public static class ProjectResponsesClientExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,15 +4,13 @@
|
|||||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||||
<RootNamespace>Microsoft.Agents.AI.Mcp</RootNamespace>
|
<RootNamespace>Microsoft.Agents.AI.Mcp</RootNamespace>
|
||||||
<VersionSuffix>alpha</VersionSuffix>
|
<VersionSuffix>alpha</VersionSuffix>
|
||||||
<NoWarn>$(NoWarn);MEAI001;MAAI001;MCPEXP001</NoWarn>
|
<NoWarn>$(NoWarn);MEAI001;MCPEXP001</NoWarn>
|
||||||
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<InjectSharedThrow>true</InjectSharedThrow>
|
<InjectSharedThrow>true</InjectSharedThrow>
|
||||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
|
||||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
@@ -36,8 +34,4 @@
|
|||||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Mcp.UnitTests" />
|
<InternalsVisibleTo Include="Microsoft.Agents.AI.Mcp.UnitTests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+395
@@ -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));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -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();
|
||||||
|
}
|
||||||
+221
@@ -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);");
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -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
|
||||||
|
}
|
||||||
+172
@@ -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);");
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -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)#>";
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -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; }
|
||||||
|
}
|
||||||
+324
@@ -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);");
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -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; }
|
||||||
|
}
|
||||||
+78
@@ -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 ");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -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
Reference in New Issue
Block a user