mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflows - WIP Declarative action update (#1761)
* WIP * Fixed build errors (#1638) Comment and nullable type alignment * Sync to SDK update * Checkpoint * Checkpoint: Tests passing * Checkpoint: EndWorkflow * Add trace * .NET: Azure.AI.Agents Package Split + Initial Extensions (#1657) * Move packages * Update nuget.config * Address Xmldoc * Remove format from branches checks * Address Xmldocs * Add more details to the implementation * Moving Agent logic to ChatClient * Adding Name and Id overrides to AzureAIAgent * Updating extensions * Add GetAiAgent extensions * Adding support for version as name can conflict 409 using the Agents API with same name * Addressing more updates to the extensions * More improvements * Remove debugging code from sample * Address copilot feedback * Apply suggestions from co-pilot code review * Checkpoint * Update Directory.Packages.props Fix package version rollback: Azure.AI.Agents.Persistent (beta-6 => beta-7) * Add project reference * .NET: Add comprehensive unit tests for Microsoft.Agents.AI.AzureAIAgents extension methods (#1786) * Initial plan * Add comprehensive unit test project for Microsoft.Agents.AI.AzureAIAgents Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Add README documenting test project and package dependency requirements Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Fix documentation URL to use learn.microsoft.com Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Bump back AAAP 1.2.0-beta.7 * Address AI generated UT's * Remove UT Readme * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * .NET: Change model to be required just for prompt agent definition specific extensions (#1812) * Remove unneeded model from extensions * Add noop justification * Update Package Nameing: V1 -> AzureAI.Persistent / V2 -> AzureAI (#1829) * Checkpoint for merge * No build errors * .NET: Update Extensions for Strict Agent Definitions + Improvements (#1892) * Update Package Nameing: V1 -> AzureAI.Persistent / V2 -> AzureAI * Update agents and extensions to comply with strict agent definitions * More static updates * Address UT, and ResponseTool support * Improving reusability extensions * Addressing ResponseTools Unit Tests and extension setup * Adapted workaround on breaking AAA with OpenAI 2.6.0 * Small updates * Remove strictness when retrieving agents, improved XmlDocs * Improve sample comments * Update dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AgentsClientExtensionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address PR comments * Address UT failing * Address Copilot feedback * Address Copilot feedback * Address comment typo * Address PR feedback * Address typo * Add missing Extensions with ChatClientAgentOptions * Address comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updated package version (#1897) * Version update (#1901) * Checkpoint * Updated package version (#1906) * Checkpoint * Checkpoint * Checkpoint * Align with azure ai agent * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/MCPToolApproval/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactored external input * Update dotnet/samples/GettingStarted/Workflows/Declarative/MCPToolApproval/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Agent tools patch * Demos validated * Checkpoint * Hygiene * Checkpoint - Samples * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Checkpoint * Checkpoint - Deep Research * Update baseline * Update * Typo * Checkpoint * Typos * Sample cleanup * Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.AzureAI/AgentsClientExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Typo * Typo * Fix input loop * Sample - Function Calling / External Input * Typo * Finessed * Checkpoint * Fix feed * Checkpoint - so close * Ding dong! * "there" *** * Fixup comments * Fix sample * Code analysis * Header * Typo (variableName) * Remove dead code * Skip test (agent api ratchet) * Comment * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Typo --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
This commit is contained in:
@@ -94,10 +94,10 @@
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
|
||||
<!-- Workflows -->
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.1003.2" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1003.2" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1003.2" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
|
||||
<!-- Community -->
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<!-- Test -->
|
||||
@@ -105,8 +105,6 @@
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="Moq" Version="[4.18.4]" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.66.0-beta" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
|
||||
@@ -107,13 +107,19 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/">
|
||||
<File Path="samples/GettingStarted/Workflows/Declarative/README.md" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ConfirmInput/ConfirmInput.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/DeepResearch/DeepResearch.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ExecuteCode/ExecuteCode.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/GenerateCode/GenerateCode.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/Marketing/Marketing.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/StudentTeacher/StudentTeacher.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/Examples/">
|
||||
<File Path="../workflow-samples/ConfirmInput.yaml" />
|
||||
<File Path="../workflow-samples/DeepResearch.yaml" />
|
||||
<File Path="../workflow-samples/HumanInLoop.yaml" />
|
||||
<File Path="../workflow-samples/Marketing.yaml" />
|
||||
<File Path="../workflow-samples/MathChat.yaml" />
|
||||
<File Path="../workflow-samples/README.md" />
|
||||
@@ -284,8 +290,8 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
|
||||
@@ -11,4 +11,13 @@
|
||||
<ItemGroup Condition="'$(InjectSharedBuildTestCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\CodeTests\*.cs" LinkBase="Shared\CodeTests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedWorkflowsExecution)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Execution\*.cs" LinkBase="Shared\Workflows" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedWorkflowsSettings)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Workflows\Settings\*.cs" LinkBase="Shared\Workflows" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedFoundryAgents)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Foundry\Agents\*.cs" LinkBase="Shared\Foundry" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(MSBuildThisFileDirectory)..\..\..\..\..\..\workflow-samples\ConfirmInput.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.Workflows.Declarative.ConfirmInput;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate how to use the question action to request user input
|
||||
/// and confirm it matches the original input.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
IConfiguration configuration = Application.InitializeConfig();
|
||||
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
|
||||
|
||||
// Get input from command line or console
|
||||
string workflowInput = Application.GetInput(args);
|
||||
|
||||
// Create the workflow factory. This class demonstrates how to initialize a
|
||||
// declarative workflow from a YAML file. Once the workflow is created, it
|
||||
// can be executed just like any regular workflow.
|
||||
WorkflowFactory workflowFactory = new("ConfirmInput.yaml", foundryEndpoint);
|
||||
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
WorkflowRunner runner = new();
|
||||
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(MSBuildThisFileDirectory)..\..\..\..\..\..\workflow-samples\DeepResearch.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="wttr.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using OpenAI.Responses;
|
||||
using Shared.Foundry;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.Workflows.Declarative.DeepResearch;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate a declarative workflow that accomplishes a task
|
||||
/// using the Magentic orchestration pattern developed by AutoGen.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
IConfiguration configuration = Application.InitializeConfig();
|
||||
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
|
||||
|
||||
// Ensure sample agents exist in Foundry.
|
||||
await CreateAgentsAsync(foundryEndpoint, configuration);
|
||||
|
||||
// Get input from command line or console
|
||||
string workflowInput = Application.GetInput(args);
|
||||
|
||||
// Create the workflow factory. This class demonstrates how to initialize a
|
||||
// declarative workflow from a YAML file. Once the workflow is created, it
|
||||
// can be executed just like any regular workflow.
|
||||
WorkflowFactory workflowFactory = new("DeepResearch.yaml", foundryEndpoint);
|
||||
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
WorkflowRunner runner = new();
|
||||
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
|
||||
}
|
||||
|
||||
private static async Task CreateAgentsAsync(Uri foundryEndpoint, IConfiguration configuration)
|
||||
{
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "ResearchAgent",
|
||||
agentDefinition: DefineResearchAgent(configuration),
|
||||
agentDescription: "Planner agent for DeepResearch workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "PlannerAgent",
|
||||
agentDefinition: DefinePlannerAgent(configuration),
|
||||
agentDescription: "Planner agent for DeepResearch workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "ManagerAgent",
|
||||
agentDefinition: DefineManagerAgent(configuration),
|
||||
agentDescription: "Manager agent for DeepResearch workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "SummaryAgent",
|
||||
agentDefinition: DefineSummaryAgent(configuration),
|
||||
agentDescription: "Summary agent for DeepResearch workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "KnowledgeAgent",
|
||||
agentDefinition: DefineKnowledgeAgent(configuration),
|
||||
agentDescription: "Research agent for DeepResearch workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "CoderAgent",
|
||||
agentDefinition: DefineCoderAgent(configuration),
|
||||
agentDescription: "Coder agent for DeepResearch workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "WeatherAgent",
|
||||
agentDefinition: DefineWeatherAgent(configuration),
|
||||
agentDescription: "Weather agent for DeepResearch workflow");
|
||||
}
|
||||
|
||||
private static PromptAgentDefinition DefineResearchAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelFull))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability.
|
||||
Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from.
|
||||
|
||||
Here is the pre-survey:
|
||||
|
||||
1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none.
|
||||
2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself.
|
||||
3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation)
|
||||
4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc.
|
||||
|
||||
When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings:
|
||||
|
||||
1. GIVEN OR VERIFIED FACTS
|
||||
2. FACTS TO LOOK UP
|
||||
3. FACTS TO DERIVE
|
||||
4. EDUCATED GUESSES
|
||||
|
||||
DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so.
|
||||
""",
|
||||
Tools =
|
||||
{
|
||||
//AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
|
||||
// new BingGroundingSearchToolParameters(
|
||||
// [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))]))
|
||||
}
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefinePlannerAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions = // TODO: Use Structured Inputs / Prompt Template
|
||||
"""
|
||||
Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request.
|
||||
|
||||
Only select the following team which is listed as "- [Name]: [Description]"
|
||||
|
||||
- WeatherAgent: Able to retrieve weather information
|
||||
- CoderAgent: Able to write and execute Python code
|
||||
- KnowledgeAgent: Able to perform generic websearches
|
||||
|
||||
The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]"
|
||||
|
||||
Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task.
|
||||
"""
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineManagerAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions = // TODO: Use Structured Inputs / Prompt Template
|
||||
"""
|
||||
Recall we have assembled the following team:
|
||||
|
||||
- KnowledgeAgent: Able to perform generic websearches
|
||||
- CoderAgent: Able to write and execute Python code
|
||||
- WeatherAgent: Able to retrieve weather information
|
||||
|
||||
To make progress on the request, please answer the following questions, including necessary reasoning:
|
||||
- Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed)
|
||||
- Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times.
|
||||
- Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file)
|
||||
- Who should speak next? (select from: KnowledgeAgent, CoderAgent, WeatherAgent)
|
||||
- What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need)
|
||||
""",
|
||||
TextOptions =
|
||||
new ResponseTextOptions
|
||||
{
|
||||
TextFormat =
|
||||
ResponseTextFormat.CreateJsonSchemaFormat(
|
||||
"TaskEvaluation",
|
||||
BinaryData.FromString(
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_request_satisfied": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": { "type": "string" },
|
||||
"answer": { "type": "boolean" }
|
||||
},
|
||||
"required": ["reason", "answer"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"is_in_loop": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": { "type": "string" },
|
||||
"answer": { "type": "boolean" }
|
||||
},
|
||||
"required": ["reason", "answer"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"is_progress_being_made": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": { "type": "string" },
|
||||
"answer": { "type": "boolean" }
|
||||
},
|
||||
"required": ["reason", "answer"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"next_speaker": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": { "type": "string" },
|
||||
"answer": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["reason", "answer"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"instruction_or_question": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": { "type": "string" },
|
||||
"answer": { "type": "string" }
|
||||
},
|
||||
"required": ["reason", "answer"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["is_request_satisfied", "is_in_loop", "is_progress_being_made", "next_speaker", "instruction_or_question"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
"""),
|
||||
jsonSchemaFormatDescription: null,
|
||||
jsonSchemaIsStrict: true),
|
||||
}
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineSummaryAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
We have completed the task.
|
||||
|
||||
Based only on the conversation and without adding any new information,
|
||||
synthesize the result of the conversation as a complete response to the user task.
|
||||
|
||||
The user will only ever see this last response and not the entire conversation,
|
||||
so please ensure it is complete and self-contained.
|
||||
"""
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineKnowledgeAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Tools =
|
||||
{
|
||||
//AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
|
||||
// new BingGroundingSearchToolParameters(
|
||||
// [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))]))
|
||||
}
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineCoderAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You solve problem by writing and executing code.
|
||||
""",
|
||||
Tools =
|
||||
{
|
||||
ResponseTool.CreateCodeInterpreterTool(
|
||||
new(CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration()))
|
||||
}
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineWeatherAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are a weather expert.
|
||||
""",
|
||||
Tools =
|
||||
{
|
||||
AgentTool.CreateOpenApiTool(
|
||||
new OpenApiFunctionDefinition(
|
||||
"weather-forecast",
|
||||
BinaryData.FromString(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "wttr.json"))),
|
||||
new OpenApiAnonymousAuthDetails()))
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -12,7 +12,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
// Uncomment this to enable JSON checkpointing to the local file system.
|
||||
//#define CHECKPOINT_JSON
|
||||
|
||||
using System.Reflection;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Test.WorkflowProviders;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.DeclarativeCode;
|
||||
|
||||
@@ -24,157 +24,64 @@ internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
Program program = new(args);
|
||||
string? workflowInput = ParseWorkflowInput(args);
|
||||
|
||||
Program program = new(workflowInput);
|
||||
await program.ExecuteAsync();
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
Notify("\nWORKFLOW: Starting...");
|
||||
|
||||
string input = this.GetWorkflowInput();
|
||||
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
await this.Runner.ExecuteAsync(this.CreateWorkflow, input);
|
||||
|
||||
Notify("\nWORKFLOW: Done!\n");
|
||||
}
|
||||
|
||||
private Workflow CreateWorkflow()
|
||||
{
|
||||
// Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
|
||||
DeclarativeWorkflowOptions options =
|
||||
new(new AzureAgentProvider(this.FoundryEndpoint, new AzureCliCredential()))
|
||||
new(new AzureAgentProvider(new Uri(this.FoundryEndpoint), new AzureCliCredential()))
|
||||
{
|
||||
Configuration = this.Configuration
|
||||
};
|
||||
|
||||
// Use the generated provider to create a workflow instance.
|
||||
Workflow workflow = TestWorkflowProvider.CreateWorkflow<string>(options);
|
||||
|
||||
Notify("\nWORKFLOW: Starting...");
|
||||
|
||||
// Run the workflow, just like any other workflow
|
||||
string input = this.GetWorkflowInput();
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: input);
|
||||
await this.MonitorAndDisposeWorkflowRunAsync(run);
|
||||
|
||||
Notify("\nWORKFLOW: Done!");
|
||||
return SampleWorkflowProvider.CreateWorkflow<string>(options);
|
||||
}
|
||||
|
||||
private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
|
||||
|
||||
private static readonly Dictionary<string, string> s_nameCache = [];
|
||||
private static readonly HashSet<string> s_fileCache = [];
|
||||
|
||||
private string? WorkflowInput { get; }
|
||||
private string FoundryEndpoint { get; }
|
||||
private PersistentAgentsClient FoundryClient { get; }
|
||||
private IConfiguration Configuration { get; }
|
||||
private WorkflowRunner Runner { get; }
|
||||
|
||||
private Program(string[] args)
|
||||
private Program(string? workflowInput)
|
||||
{
|
||||
this.WorkflowInput = ParseWorkflowInput(args);
|
||||
this.WorkflowInput = workflowInput;
|
||||
|
||||
this.Configuration = InitializeConfig();
|
||||
|
||||
this.FoundryEndpoint = this.Configuration[ConfigKeyFoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {ConfigKeyFoundryEndpoint}");
|
||||
this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
|
||||
}
|
||||
this.FoundryEndpoint = this.Configuration[Application.Settings.FoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {Application.Settings.FoundryEndpoint}");
|
||||
|
||||
private async Task MonitorAndDisposeWorkflowRunAsync(StreamingRun run)
|
||||
{
|
||||
await using IAsyncDisposable disposeRun = run;
|
||||
|
||||
string? messageId = null;
|
||||
|
||||
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
|
||||
{
|
||||
switch (workflowEvent)
|
||||
this.Runner =
|
||||
new()
|
||||
{
|
||||
case ExecutorInvokedEvent executorInvoked:
|
||||
Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}");
|
||||
break;
|
||||
|
||||
case ExecutorCompletedEvent executorComplete:
|
||||
Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
|
||||
break;
|
||||
|
||||
case ExecutorFailedEvent executorFailure:
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure...");
|
||||
|
||||
case ConversationUpdateEvent invokeEvent:
|
||||
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
|
||||
break;
|
||||
|
||||
case AgentRunUpdateEvent streamEvent:
|
||||
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
|
||||
{
|
||||
messageId = streamEvent.Update.MessageId;
|
||||
|
||||
if (messageId is not null)
|
||||
{
|
||||
string? agentId = streamEvent.Update.AuthorName;
|
||||
if (agentId is not null)
|
||||
{
|
||||
if (!s_nameCache.TryGetValue(agentId, out string? realName))
|
||||
{
|
||||
PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
|
||||
s_nameCache[agentId] = agent.Name;
|
||||
realName = agent.Name;
|
||||
}
|
||||
agentId = realName;
|
||||
}
|
||||
agentId ??= nameof(ChatRole.Assistant);
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write($"\n{agentId.ToUpperInvariant()}:");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{messageId}]");
|
||||
}
|
||||
}
|
||||
|
||||
ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
|
||||
switch (chatUpdate?.RawRepresentation)
|
||||
{
|
||||
case MessageContentUpdate messageUpdate:
|
||||
string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId;
|
||||
if (fileId is not null && s_fileCache.Add(fileId))
|
||||
{
|
||||
BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId);
|
||||
await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
try
|
||||
{
|
||||
Console.ResetColor();
|
||||
Console.Write(streamEvent.Data);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
|
||||
case AgentRunResponseEvent messageEvent:
|
||||
try
|
||||
{
|
||||
Console.WriteLine();
|
||||
if (messageEvent.Response.AgentId is null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("ACTIVITY:");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(messageEvent.Response?.Text.Trim());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (messageEvent.Response.Usage is not null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
#if CHECKPOINT_JSON
|
||||
// Use an json file checkpoint store that will persist checkpoints to the local file system.
|
||||
UseJsonCheckpoints = true
|
||||
#else
|
||||
// Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process.
|
||||
UseJsonCheckpoints = false
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
private string GetWorkflowInput()
|
||||
@@ -231,19 +138,4 @@ internal sealed class Program
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content)
|
||||
{
|
||||
string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename));
|
||||
filePath = Path.ChangeExtension(filePath, ".png");
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, content.ToArray());
|
||||
|
||||
Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/C start {filePath}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -11,7 +11,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,18 +5,12 @@
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
#if CHECKPOINT_JSON
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
#endif
|
||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.DeclarativeWorkflow;
|
||||
|
||||
@@ -62,52 +56,13 @@ internal sealed class Program
|
||||
|
||||
Notify("\nWORKFLOW: Starting...");
|
||||
|
||||
// Run the workflow, just like any other workflow
|
||||
string input = this.GetWorkflowInput();
|
||||
|
||||
#if CHECKPOINT_JSON
|
||||
// Use a file-system based JSON checkpoint store to persist checkpoints to disk.
|
||||
DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:yyMMdd-hhmmss-ff}"));
|
||||
CheckpointManager checkpointManager = CheckpointManager.CreateJson(new FileSystemJsonCheckpointStore(checkpointFolder));
|
||||
#else
|
||||
// Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process.
|
||||
CheckpointManager checkpointManager = CheckpointManager.CreateInMemory();
|
||||
#endif
|
||||
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager);
|
||||
|
||||
bool isComplete = false;
|
||||
object? response = null;
|
||||
do
|
||||
{
|
||||
ExternalRequest? externalRequest = await this.MonitorAndDisposeWorkflowRunAsync(run, response);
|
||||
if (externalRequest is not null)
|
||||
{
|
||||
Notify("\nWORKFLOW: Yield");
|
||||
|
||||
if (this.LastCheckpoint is null)
|
||||
{
|
||||
throw new InvalidOperationException("Checkpoint information missing after external request.");
|
||||
}
|
||||
|
||||
// Process the external request.
|
||||
response = await this.HandleExternalRequestAsync(externalRequest);
|
||||
|
||||
// Let's resume on an entirely new workflow instance to demonstrate checkpoint portability.
|
||||
workflow = this.CreateWorkflow();
|
||||
|
||||
// Restore the latest checkpoint.
|
||||
Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}");
|
||||
Notify("\nWORKFLOW: Restore");
|
||||
|
||||
run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager, run.Run.RunId);
|
||||
}
|
||||
else
|
||||
{
|
||||
isComplete = true;
|
||||
}
|
||||
}
|
||||
while (!isComplete);
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
await this.Runner.ExecuteAsync(this.CreateWorkflow, input);
|
||||
|
||||
Notify("\nWORKFLOW: Done!\n");
|
||||
}
|
||||
@@ -116,19 +71,16 @@ internal sealed class Program
|
||||
/// Create the workflow from the declarative YAML. Includes definition of the
|
||||
/// <see cref="DeclarativeWorkflowOptions" /> and the associated <see cref="WorkflowAgentProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value assigned to <see cref="IncludeFunctions" /> controls on whether the function
|
||||
/// tools (<see cref="AIFunction"/>) initialized in the constructor are included for auto-invocation.
|
||||
/// </remarks>
|
||||
private Workflow CreateWorkflow()
|
||||
{
|
||||
// Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
|
||||
AzureAgentProvider agentProvider = new(this.FoundryEndpoint, new AzureCliCredential())
|
||||
// Create the agent provider that will service agent requests within the workflow.
|
||||
AzureAgentProvider agentProvider = new(new Uri(this.FoundryEndpoint), new AzureCliCredential())
|
||||
{
|
||||
// Functions included here will be auto-executed by the framework.
|
||||
Functions = IncludeFunctions ? this.FunctionMap.Values : null,
|
||||
Functions = this.Functions
|
||||
};
|
||||
|
||||
// Define the workflow options.
|
||||
DeclarativeWorkflowOptions options =
|
||||
new(agentProvider)
|
||||
{
|
||||
@@ -137,31 +89,16 @@ internal sealed class Program
|
||||
//LoggerFactory = null, // Assign to enable logging
|
||||
};
|
||||
|
||||
// Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
|
||||
return DeclarativeWorkflowBuilder.Build<string>(this.WorkflowFile, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration key used to identify the Foundry project endpoint.
|
||||
/// </summary>
|
||||
private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
|
||||
|
||||
/// <summary>
|
||||
/// Controls on whether the function tools (<see cref="AIFunction"/>) initialized
|
||||
/// in the constructor are included for auto-invocation.
|
||||
/// NOTE: By default, no functions exist as part of this sample.
|
||||
/// </summary>
|
||||
private const bool IncludeFunctions = true;
|
||||
|
||||
private static Dictionary<string, string> NameCache { get; } = [];
|
||||
private static HashSet<string> FileCache { get; } = [];
|
||||
|
||||
private string WorkflowFile { get; }
|
||||
private string? WorkflowInput { get; }
|
||||
private string FoundryEndpoint { get; }
|
||||
private PersistentAgentsClient FoundryClient { get; }
|
||||
private IConfiguration Configuration { get; }
|
||||
private CheckpointInfo? LastCheckpoint { get; set; }
|
||||
private Dictionary<string, AIFunction> FunctionMap { get; }
|
||||
private WorkflowRunner Runner { get; }
|
||||
private IList<AIFunction> Functions { get; }
|
||||
|
||||
private Program(string workflowFile, string? workflowInput)
|
||||
{
|
||||
@@ -170,246 +107,26 @@ internal sealed class Program
|
||||
|
||||
this.Configuration = InitializeConfig();
|
||||
|
||||
this.FoundryEndpoint = this.Configuration[ConfigKeyFoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {ConfigKeyFoundryEndpoint}");
|
||||
this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
|
||||
this.FoundryEndpoint = this.Configuration[Application.Settings.FoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {Application.Settings.FoundryEndpoint}");
|
||||
|
||||
List<AIFunction> functions =
|
||||
this.Functions =
|
||||
[
|
||||
// Manually define any custom functions that may be required by agents within the workflow.
|
||||
// By default, this sample does not include any functions.
|
||||
//AIFunctionFactory.Create(),
|
||||
];
|
||||
this.FunctionMap = functions.ToDictionary(f => f.Name);
|
||||
}
|
||||
|
||||
private async Task<ExternalRequest?> MonitorAndDisposeWorkflowRunAsync(Checkpointed<StreamingRun> run, object? response = null)
|
||||
{
|
||||
// Always dispose the run when done.
|
||||
await using IAsyncDisposable disposeRun = run;
|
||||
|
||||
bool hasStreamed = false;
|
||||
string? messageId = null;
|
||||
|
||||
await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync())
|
||||
{
|
||||
switch (workflowEvent)
|
||||
this.Runner =
|
||||
new(this.Functions)
|
||||
{
|
||||
case ExecutorInvokedEvent executorInvoked:
|
||||
Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
|
||||
break;
|
||||
|
||||
case ExecutorCompletedEvent executorCompleted:
|
||||
Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
|
||||
break;
|
||||
|
||||
case DeclarativeActionInvokedEvent actionInvoked:
|
||||
Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]");
|
||||
break;
|
||||
|
||||
case DeclarativeActionCompletedEvent actionComplete:
|
||||
Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]");
|
||||
break;
|
||||
|
||||
case ExecutorFailedEvent executorFailure:
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure...");
|
||||
|
||||
case SuperStepCompletedEvent checkpointCompleted:
|
||||
this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint;
|
||||
Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]");
|
||||
break;
|
||||
|
||||
case RequestInfoEvent requestInfo:
|
||||
Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
|
||||
if (response is not null)
|
||||
{
|
||||
ExternalResponse requestResponse = requestInfo.Request.CreateResponse(response);
|
||||
await run.Run.SendResponseAsync(requestResponse);
|
||||
response = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Yield to handle the external request
|
||||
return requestInfo.Request;
|
||||
}
|
||||
break;
|
||||
|
||||
case ConversationUpdateEvent invokeEvent:
|
||||
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
|
||||
break;
|
||||
|
||||
case MessageActivityEvent activityEvent:
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("\nACTIVITY:");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(activityEvent.Message.Trim());
|
||||
break;
|
||||
|
||||
case AgentRunUpdateEvent streamEvent:
|
||||
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
|
||||
{
|
||||
hasStreamed = false;
|
||||
messageId = streamEvent.Update.MessageId;
|
||||
|
||||
if (messageId is not null)
|
||||
{
|
||||
string? agentId = streamEvent.Update.AgentId;
|
||||
if (agentId is not null)
|
||||
{
|
||||
if (!NameCache.TryGetValue(agentId, out string? realName))
|
||||
{
|
||||
PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
|
||||
NameCache[agentId] = agent.Name;
|
||||
realName = agent.Name;
|
||||
}
|
||||
agentId = realName;
|
||||
}
|
||||
agentId ??= nameof(ChatRole.Assistant);
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write($"\n{agentId.ToUpperInvariant()}:");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{messageId}]");
|
||||
}
|
||||
}
|
||||
|
||||
ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
|
||||
switch (chatUpdate?.RawRepresentation)
|
||||
{
|
||||
case MessageContentUpdate messageUpdate:
|
||||
string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId;
|
||||
if (fileId is not null && FileCache.Add(fileId))
|
||||
{
|
||||
BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId);
|
||||
await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content);
|
||||
}
|
||||
break;
|
||||
case RequiredActionUpdate actionUpdate:
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.Write($"Calling tool: {actionUpdate.FunctionName}");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{actionUpdate.ToolCallId}]");
|
||||
break;
|
||||
}
|
||||
try
|
||||
{
|
||||
Console.ResetColor();
|
||||
Console.Write(streamEvent.Update.Text);
|
||||
hasStreamed |= !string.IsNullOrEmpty(streamEvent.Update.Text);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
|
||||
case AgentRunResponseEvent messageEvent:
|
||||
try
|
||||
{
|
||||
if (hasStreamed)
|
||||
{
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
if (messageEvent.Response.Usage is not null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No request to handle
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle request for external input, either from a human or a function tool invocation.
|
||||
/// </summary>
|
||||
private async ValueTask<object> HandleExternalRequestAsync(ExternalRequest request) =>
|
||||
request.Data.TypeId.TypeName switch
|
||||
{
|
||||
// Request for human input
|
||||
_ when request.Data.TypeId.IsMatch<AnswerRequest>() => HandleUserMessageRequest(request.DataAs<AnswerRequest>()!),
|
||||
// Request for function tool invocation. (Only active when functions are defined and IncludeFunctions is true.)
|
||||
_ when request.Data.TypeId.IsMatch<AgentFunctionToolRequest>() => await this.HandleToolRequestAsync(request.DataAs<AgentFunctionToolRequest>()!),
|
||||
// Request for user input, such as function or mcp tool approval
|
||||
_ when request.Data.TypeId.IsMatch<UserInputRequest>() => HandleUserInputRequest(request.DataAs<UserInputRequest>()!),
|
||||
// Unknown request type.
|
||||
_ => throw new InvalidOperationException($"Unsupported external request type: {request.GetType().Name}."),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handle request for human input.
|
||||
/// </summary>
|
||||
private static AnswerResponse HandleUserMessageRequest(AnswerRequest request)
|
||||
{
|
||||
string? userInput;
|
||||
do
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGreen;
|
||||
Console.Write($"\n{request.Prompt ?? "INPUT:"} ");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
userInput = Console.ReadLine();
|
||||
}
|
||||
while (string.IsNullOrWhiteSpace(userInput));
|
||||
|
||||
return new AnswerResponse(userInput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a function tool request by invoking the specified tools and returning the results.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This handler is only active when <see cref="IncludeFunctions"/> is set to true and
|
||||
/// one or more <see cref="AIFunction"/> instances are defined in the constructor.
|
||||
/// </remarks>
|
||||
private async ValueTask<AgentFunctionToolResponse> HandleToolRequestAsync(AgentFunctionToolRequest request)
|
||||
{
|
||||
Task<FunctionResultContent>[] functionTasks = request.FunctionCalls.Select(functionCall => InvokesToolAsync(functionCall)).ToArray();
|
||||
|
||||
await Task.WhenAll(functionTasks);
|
||||
|
||||
return AgentFunctionToolResponse.Create(request, functionTasks.Select(task => task.Result));
|
||||
|
||||
async Task<FunctionResultContent> InvokesToolAsync(FunctionCallContent functionCall)
|
||||
{
|
||||
AIFunction functionTool = this.FunctionMap[functionCall.Name];
|
||||
AIFunctionArguments? functionArguments = functionCall.Arguments is null ? null : new(functionCall.Arguments.NormalizePortableValues());
|
||||
object? result = await functionTool.InvokeAsync(functionArguments);
|
||||
return new FunctionResultContent(functionCall.CallId, JsonSerializer.Serialize(result));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle request for user input for mcp and function tool approval.
|
||||
/// </summary>
|
||||
private static UserInputResponse HandleUserInputRequest(UserInputRequest request)
|
||||
{
|
||||
return UserInputResponse.Create(request, ProcessRequests());
|
||||
|
||||
IEnumerable<UserInputResponseContent> ProcessRequests()
|
||||
{
|
||||
foreach (UserInputRequestContent approvalRequest in request.InputRequests)
|
||||
{
|
||||
// Here we are explicitly approving all requests.
|
||||
// In a real-world scenario, you would replace this logic to either solicit user approval or implement a more complex approval process.
|
||||
yield return
|
||||
approvalRequest switch
|
||||
{
|
||||
McpServerToolApprovalRequestContent mcpApprovalRequest => mcpApprovalRequest.CreateResponse(approved: true),
|
||||
FunctionApprovalRequestContent functionApprovalRequest => functionApprovalRequest.CreateResponse(approved: true),
|
||||
_ => throw new NotSupportedException($"Unsupported request of type {approvalRequest.GetType().Name}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
#if CHECKPOINT_JSON
|
||||
// Use an json file checkpoint store that will persist checkpoints to the local file system.
|
||||
UseJsonCheckpoints = true
|
||||
#else
|
||||
// Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process.
|
||||
UseJsonCheckpoints = false
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
private static string? ParseWorkflowFile(string[] args)
|
||||
@@ -516,19 +233,4 @@ internal sealed class Program
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content)
|
||||
{
|
||||
string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename));
|
||||
filePath = Path.ChangeExtension(filePath, ".png");
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, content.ToArray());
|
||||
|
||||
Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/C start {filePath}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="FunctionTools.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# This workflow demonstrates an agent that requires tool approval
|
||||
# in a loop responding to user input.
|
||||
#
|
||||
# Example input:
|
||||
# What is the soup of the day?
|
||||
#
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_search
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: MenuAgent
|
||||
input:
|
||||
externalLoop:
|
||||
when: =Upper(System.LastMessage.Text) <> "EXIT"
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Demo.Workflows.Declarative.FunctionTools;
|
||||
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
|
||||
public sealed class MenuPlugin
|
||||
{
|
||||
[Description("Provides a list items on the menu.")]
|
||||
public MenuItem[] GetMenu()
|
||||
{
|
||||
return s_menuItems;
|
||||
}
|
||||
|
||||
[Description("Provides a list of specials from the menu.")]
|
||||
public MenuItem[] GetSpecials()
|
||||
{
|
||||
return [.. s_menuItems.Where(i => i.IsSpecial)];
|
||||
}
|
||||
|
||||
[Description("Provides the price of the requested menu item.")]
|
||||
public float? GetItemPrice(
|
||||
[Description("The name of the menu item.")]
|
||||
string name)
|
||||
{
|
||||
return s_menuItems.FirstOrDefault(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase))?.Price;
|
||||
}
|
||||
|
||||
private static readonly MenuItem[] s_menuItems =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Category = "Soup",
|
||||
Name = "Clam Chowder",
|
||||
Price = 4.95f,
|
||||
IsSpecial = true,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "Soup",
|
||||
Name = "Tomato Soup",
|
||||
Price = 4.95f,
|
||||
IsSpecial = false,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "Salad",
|
||||
Name = "Cobb Salad",
|
||||
Price = 9.99f,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "Salad",
|
||||
Name = "House Salad",
|
||||
Price = 4.95f,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "Drink",
|
||||
Name = "Chai Tea",
|
||||
Price = 2.95f,
|
||||
IsSpecial = true,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "Drink",
|
||||
Name = "Soda",
|
||||
Price = 1.95f,
|
||||
},
|
||||
];
|
||||
|
||||
public sealed class MenuItem
|
||||
{
|
||||
public string Category { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public float Price { get; init; }
|
||||
public bool IsSpecial { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using OpenAI.Responses;
|
||||
using Shared.Foundry;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.Workflows.Declarative.FunctionTools;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate a workflow that responds to user input using an agent who
|
||||
/// with function tools assigned. Exits the loop when the user enters "exit".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
IConfiguration configuration = Application.InitializeConfig();
|
||||
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
|
||||
|
||||
// Ensure sample agents exist in Foundry.
|
||||
MenuPlugin menuPlugin = new();
|
||||
AIFunction[] functions =
|
||||
[
|
||||
AIFunctionFactory.Create(menuPlugin.GetMenu),
|
||||
AIFunctionFactory.Create(menuPlugin.GetSpecials),
|
||||
AIFunctionFactory.Create(menuPlugin.GetItemPrice),
|
||||
];
|
||||
|
||||
await CreateAgentAsync(foundryEndpoint, configuration, functions);
|
||||
|
||||
// Get input from command line or console
|
||||
string workflowInput = Application.GetInput(args);
|
||||
|
||||
// Create the workflow factory. This class demonstrates how to initialize a
|
||||
// declarative workflow from a YAML file. Once the workflow is created, it
|
||||
// can be executed just like any regular workflow.
|
||||
WorkflowFactory workflowFactory = new("FunctionTools.yaml", foundryEndpoint);
|
||||
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
WorkflowRunner runner = new(functions) { UseJsonCheckpoints = true };
|
||||
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
|
||||
}
|
||||
|
||||
private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration, AIFunction[] functions)
|
||||
{
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "MenuAgent",
|
||||
agentDefinition: DefineMenuAgent(configuration, functions),
|
||||
agentDescription: "Provides information about the restaurant menu");
|
||||
}
|
||||
|
||||
private static PromptAgentDefinition DefineMenuAgent(IConfiguration configuration, AIFunction[] functions)
|
||||
{
|
||||
PromptAgentDefinition agentDefinition =
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Answer the users questions on the menu.
|
||||
For questions or input that do not require searching the documentation, inform the
|
||||
user that you can only answer questions what's on the menu.
|
||||
"""
|
||||
};
|
||||
|
||||
foreach (AIFunction function in functions)
|
||||
{
|
||||
agentDefinition.Tools.Add(function.AsOpenAIResponseTool());
|
||||
}
|
||||
|
||||
return agentDefinition;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net9.0</TargetFrameworks>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(MSBuildThisFileDirectory)..\..\..\..\..\..\workflow-samples\Marketing.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.Foundry;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.Workflows.Declarative.Marketing;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate a declarative workflow with three agents (Analyst, Writer, Editor)
|
||||
/// sequentially engaging in a task.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
IConfiguration configuration = Application.InitializeConfig();
|
||||
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
|
||||
|
||||
// Ensure sample agents exist in Foundry.
|
||||
await CreateAgentsAsync(foundryEndpoint, configuration);
|
||||
|
||||
// Get input from command line or console
|
||||
string workflowInput = Application.GetInput(args);
|
||||
|
||||
// Create the workflow factory. This class demonstrates how to initialize a
|
||||
// declarative workflow from a YAML file. Once the workflow is created, it
|
||||
// can be executed just like any regular workflow.
|
||||
WorkflowFactory workflowFactory = new("Marketing.yaml", foundryEndpoint);
|
||||
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
WorkflowRunner runner = new();
|
||||
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
|
||||
}
|
||||
|
||||
private static async Task CreateAgentsAsync(Uri foundryEndpoint, IConfiguration configuration)
|
||||
{
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "AnalystAgent",
|
||||
agentDefinition: DefineAnalystAgent(configuration),
|
||||
agentDescription: "Analyst agent for Marketing workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "WriterAgent",
|
||||
agentDefinition: DefineWriterAgent(configuration),
|
||||
agentDescription: "Writer agent for Marketing workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "EditorAgent",
|
||||
agentDefinition: DefineEditorAgent(configuration),
|
||||
agentDescription: "Editor agent for Marketing workflow");
|
||||
}
|
||||
|
||||
private static PromptAgentDefinition DefineAnalystAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelFull))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are a marketing analyst. Given a product description, identify:
|
||||
- Key features
|
||||
- Target audience
|
||||
- Unique selling points
|
||||
""",
|
||||
Tools =
|
||||
{
|
||||
//AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
|
||||
// new BingGroundingSearchToolParameters(
|
||||
// [new BingGroundingSearchConfiguration(configuration[Application.Settings.FoundryGroundingTool])]))
|
||||
}
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineWriterAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelFull))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
|
||||
compose a compelling marketing copy (like a newsletter section) that highlights these points.
|
||||
Output should be short (around 150 words), output just the copy as a single text block.
|
||||
"""
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineEditorAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelFull))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
|
||||
give format and make it polished. Output the final improved copy as a single text block.
|
||||
"""
|
||||
};
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
# Summary
|
||||
|
||||
This demo showcases the ability to parse a declarative Foundry Workflow file (YAML) to build a `Workflow<>`
|
||||
be executed using the same pattern as any code-based workflow.
|
||||
These samples showcases the ability to parse a declarative Foundry Workflow file (YAML)
|
||||
to build a `Workflow` that may be executed using the same pattern as any code-based workflow.
|
||||
|
||||
## Configuration
|
||||
|
||||
This demo requires configuration to access agents an [Azure Foundry Project](https://learn.microsoft.com/azure/ai-foundry).
|
||||
These samples must be configured to create and use agents your
|
||||
[Azure Foundry Project](https://learn.microsoft.com/azure/ai-foundry).
|
||||
|
||||
#### Settings
|
||||
### Settings
|
||||
|
||||
We suggest using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets)
|
||||
to avoid the risk of leaking secrets into the repository, branches and pull requests.
|
||||
You can also use environment variables if you prefer.
|
||||
|
||||
To set your secrets as an environment variable (PowerShell):
|
||||
|
||||
```pwsh
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://..."
|
||||
```
|
||||
|
||||
etc...
|
||||
The configuraton required by the samples is:
|
||||
|
||||
|Setting Name| Description|
|
||||
|:--|:--|
|
||||
|FOUNDRY_PROJECT_ENDPOINT| The endpoint URL of your Azure Foundry Project.|
|
||||
|FOUNDRY_MODEL_DEPLOYMENT_NAME| The name of the model deployment to use
|
||||
|FOUNDRY_CONNECTION_GROUNDING_TOOL| The name of the Bing Grounding connection configured in your Azure Foundry Project.|
|
||||
|
||||
To set your secrets with .NET Secret Manager:
|
||||
|
||||
@@ -51,7 +51,7 @@ To set your secrets with .NET Secret Manager:
|
||||
5. Define setting that identifies your Azure Foundry Model Deployment (endpoint):
|
||||
|
||||
```
|
||||
dotnet user-secrets set "FOUNDRY_MODEL_DEPLOYMENT_NAME" "gpt-4.1"
|
||||
dotnet user-secrets set "FOUNDRY_MODEL_DEPLOYMENT_NAME" "gpt-5"
|
||||
```
|
||||
|
||||
6. Define setting that identifies your Bing Grounding connection:
|
||||
@@ -60,7 +60,15 @@ To set your secrets with .NET Secret Manager:
|
||||
dotnet user-secrets set "FOUNDRY_CONNECTION_GROUNDING_TOOL" "mybinggrounding"
|
||||
```
|
||||
|
||||
#### Authorization
|
||||
You may alternatively set your secrets as an environment variable (PowerShell):
|
||||
|
||||
```pwsh
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://..."
|
||||
$env:FOUNDRY_MODEL_DEPLOYMENT_NAME="gpt-5"
|
||||
$env:FOUNDRY_CONNECTION_GROUNDING_TOOL="mybinggrounding"
|
||||
```
|
||||
|
||||
### Authorization
|
||||
|
||||
Use [_Azure CLI_](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) to authorize access to your Azure Foundry Project:
|
||||
|
||||
@@ -69,34 +77,21 @@ az login
|
||||
az account get-access-token
|
||||
```
|
||||
|
||||
#### Agents
|
||||
|
||||
The sample workflows rely on agents defined in your Azure Foundry Project.
|
||||
|
||||
To create agents, run the [`Create.ps1`](../../../../../workflow-samples/setup/) script.
|
||||
This will create the agents used in the sample workflows in your Azure Foundry Project and format a script you can copy and use to configure your environment.
|
||||
|
||||
> Note: `Create.ps1` relies upon the `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_DEPLOYMENT_NAME`, and `FOUNDRY_CONNECTION_GROUNDING_TOOL` settings.
|
||||
|
||||
## Execution
|
||||
|
||||
Run the demo from the console by specifying a path to a declarative (YAML) workflow file.
|
||||
The repository has example workflows available in the root [`/workflow-samples`](../../../../../workflow-samples) folder.
|
||||
The samples may be executed within _Visual Studio_ or _VS Code_.
|
||||
|
||||
To run the sampes from the command line:
|
||||
|
||||
1. From the root of the repository, navigate the console to the project folder:
|
||||
|
||||
```sh
|
||||
cd dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow
|
||||
cd dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher
|
||||
```
|
||||
|
||||
2. Run the demo referencing a sample workflow by name:
|
||||
2. Run the demo and optionally provided input:
|
||||
|
||||
```sh
|
||||
dotnet run HelloWorld
|
||||
```
|
||||
|
||||
3. Run the demo with a path to any workflow file:
|
||||
|
||||
```sh
|
||||
dotnet run c:/myworkflows/HelloWorld.yaml
|
||||
dotnet run "How would you compute the value of PI?"
|
||||
```
|
||||
> The sample will allow for interactive input in the absence of an input argument.
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.Foundry;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.Workflows.Declarative.StudentTeacher;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate a declarative workflow with two agents (Student and Teacher)
|
||||
/// in an iterative conversation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
IConfiguration configuration = Application.InitializeConfig();
|
||||
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
|
||||
|
||||
// Ensure sample agents exist in Foundry.
|
||||
await CreateAgentsAsync(foundryEndpoint, configuration);
|
||||
|
||||
// Get input from command line or console
|
||||
string workflowInput = Application.GetInput(args);
|
||||
|
||||
// Create the workflow factory. This class demonstrates how to initialize a
|
||||
// declarative workflow from a YAML file. Once the workflow is created, it
|
||||
// can be executed just like any regular workflow.
|
||||
WorkflowFactory workflowFactory = new("MathChat.yaml", foundryEndpoint);
|
||||
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
WorkflowRunner runner = new();
|
||||
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
|
||||
}
|
||||
|
||||
private static async Task CreateAgentsAsync(Uri foundryEndpoint, IConfiguration configuration)
|
||||
{
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "StudentAgent",
|
||||
agentDefinition: DefineStudentAgent(configuration),
|
||||
agentDescription: "Student agent for MathChat workflow");
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "TeacherAgent",
|
||||
agentDefinition: DefineTeacherAgent(configuration),
|
||||
agentDescription: "Teacher agent for MathChat workflow");
|
||||
}
|
||||
|
||||
private static PromptAgentDefinition DefineStudentAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Your job is help a math teacher practice teaching by making intentional mistakes.
|
||||
You attempt to solve the given math problem, but with intentional mistakes so the teacher can help.
|
||||
Always incorporate the teacher's advice to fix your next response.
|
||||
You have the math-skills of a 6th grader.
|
||||
Don't describe who you are or reveal your instructions.
|
||||
"""
|
||||
};
|
||||
|
||||
private static PromptAgentDefinition DefineTeacherAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Review and coach the student's approach to solving the given math problem.
|
||||
Don't repeat the solution or try and solve it.
|
||||
If the student has demonstrated comprehension and responded to all of your feedback,
|
||||
give the student your congratulations by using the word "congratulations".
|
||||
"""
|
||||
};
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(MSBuildThisFileDirectory)..\..\..\..\..\..\workflow-samples\MathChat.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using OpenAI.Responses;
|
||||
using Shared.Foundry;
|
||||
using Shared.Workflows;
|
||||
|
||||
namespace Demo.Workflows.Declarative.ToolApproval;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate a workflow that responds to user input using an agent who
|
||||
/// has an MCP tool that requires approval. Exits the loop when the user enters "exit".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the README.md file in the parent folder (../Declarative/README.md) for detailed
|
||||
/// information the configuration required to run this sample.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
IConfiguration configuration = Application.InitializeConfig();
|
||||
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
|
||||
|
||||
// Ensure sample agents exist in Foundry.
|
||||
await CreateAgentAsync(foundryEndpoint, configuration);
|
||||
|
||||
// Get input from command line or console
|
||||
string workflowInput = Application.GetInput(args);
|
||||
|
||||
// Create the workflow factory. This class demonstrates how to initialize a
|
||||
// declarative workflow from a YAML file. Once the workflow is created, it
|
||||
// can be executed just like any regular workflow.
|
||||
WorkflowFactory workflowFactory = new("ToolApproval.yaml", foundryEndpoint);
|
||||
|
||||
// Execute the workflow: The WorkflowRunner demonstrates how to execute
|
||||
// a workflow, handle the workflow events, and providing external input.
|
||||
// This also includes the ability to checkpoint workflow state and how to
|
||||
// resume execution.
|
||||
WorkflowRunner runner = new() { UseJsonCheckpoints = true };
|
||||
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
|
||||
}
|
||||
|
||||
private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration)
|
||||
{
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "DocumentSearchAgent",
|
||||
agentDefinition: DefineSearchAgent(configuration),
|
||||
agentDescription: "Searches documents on Microsoft Learn");
|
||||
}
|
||||
|
||||
private static PromptAgentDefinition DefineSearchAgent(IConfiguration configuration) =>
|
||||
new(configuration.GetValue(Application.Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Answer the users questions by searching the Microsoft Learn documentation.
|
||||
For questions or input that do not require searching the documentation, inform the
|
||||
user that you can only answer questions related to Microsoft Learn documentation.
|
||||
""",
|
||||
Tools =
|
||||
{
|
||||
ResponseTool.CreateMcpTool(
|
||||
serverLabel: "microsoft_docs",
|
||||
serverUri: new Uri("https://learn.microsoft.com/api/mcp"),
|
||||
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval))
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="ToolApproval.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,38 @@
|
||||
#
|
||||
# This workflow demonstrates an agent that requires tool approval
|
||||
# in a loop responding to user input.
|
||||
#
|
||||
# Example input:
|
||||
# What is Microsoft Graph API used for?
|
||||
#
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_search
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: DocumentSearchAgent
|
||||
|
||||
- kind: RequestExternalInput
|
||||
id: request_requirements
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: check_completion
|
||||
conditions:
|
||||
|
||||
- condition: =Upper(System.LastMessage.Text) = "EXIT"
|
||||
id: check_done
|
||||
actions:
|
||||
|
||||
- kind: EndWorkflow
|
||||
id: all_done
|
||||
|
||||
elseActions:
|
||||
- kind: GotoAction
|
||||
id: goto_search
|
||||
actionId: invoke_search
|
||||
@@ -1,15 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Core;
|
||||
using Azure.Core.Pipeline;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative;
|
||||
|
||||
@@ -18,31 +21,23 @@ namespace Microsoft.Agents.AI.Workflows.Declarative;
|
||||
/// </summary>
|
||||
/// <remarks>This class is used to retrieve and manage AI agents associated with a Foundry project. It requires a
|
||||
/// project endpoint and credentials to authenticate requests.</remarks>
|
||||
/// <param name="projectEndpoint">The endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project.</param>
|
||||
/// <param name="projectEndpoint">A <see cref="Uri"/> instance representing the endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project.</param>
|
||||
/// <param name="projectCredentials">The credentials used to authenticate with the Foundry project. This must be a valid instance of <see cref="TokenCredential"/>.</param>
|
||||
/// <param name="httpClient">An optional <see cref="HttpClient"/> instance to be used for making HTTP requests. If not provided, a default client will be used.</param>
|
||||
public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider
|
||||
public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider
|
||||
{
|
||||
private static readonly Dictionary<string, MessageRole> s_roleMap =
|
||||
new()
|
||||
{
|
||||
[ChatRole.User.Value.ToUpperInvariant()] = MessageRole.User,
|
||||
[ChatRole.Assistant.Value.ToUpperInvariant()] = MessageRole.Agent,
|
||||
[ChatRole.System.Value.ToUpperInvariant()] = new MessageRole(ChatRole.System.Value),
|
||||
[ChatRole.Tool.Value.ToUpperInvariant()] = new MessageRole(ChatRole.Tool.Value),
|
||||
};
|
||||
private readonly Dictionary<string, AgentVersion> _versionCache = [];
|
||||
private readonly Dictionary<string, AIAgent> _agentCache = [];
|
||||
|
||||
private PersistentAgentsClient? _agentsClient;
|
||||
private AgentsClient? _agentsClient;
|
||||
private ConversationClient? _conversationClient;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<string> CreateConversationAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
PersistentAgentThread conversation =
|
||||
await this.GetAgentsClient().Threads.CreateThreadAsync(
|
||||
messages: null,
|
||||
toolResources: null,
|
||||
metadata: null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
AgentConversation conversation =
|
||||
await this.GetConversationClient()
|
||||
.CreateConversationAsync(options: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return conversation.Id;
|
||||
}
|
||||
@@ -50,61 +45,117 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatMessage> CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PersistentThreadMessage newMessage =
|
||||
await this.GetAgentsClient().Messages.CreateMessageAsync(
|
||||
ReadOnlyCollection<ResponseItem> newItems =
|
||||
await this.GetConversationClient().CreateConversationItemsAsync(
|
||||
conversationId,
|
||||
role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()],
|
||||
contentBlocks: GetContent(),
|
||||
attachments: null,
|
||||
metadata: GetMetadata(),
|
||||
items: GetResponseItems(),
|
||||
include: null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return ToChatMessage(newMessage);
|
||||
return newItems.AsChatMessages().Single();
|
||||
|
||||
Dictionary<string, string>? GetMetadata()
|
||||
IEnumerable<ResponseItem> GetResponseItems()
|
||||
{
|
||||
if (conversationMessage.AdditionalProperties is null)
|
||||
IEnumerable<ChatMessage> messages = [conversationMessage];
|
||||
|
||||
foreach (ResponseItem item in messages.AsOpenAIResponseItems())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return conversationMessage.AdditionalProperties.ToDictionary(prop => prop.Key, prop => prop.Value?.ToString() ?? string.Empty);
|
||||
}
|
||||
|
||||
IEnumerable<MessageInputContentBlock> GetContent()
|
||||
{
|
||||
foreach (AIContent content in conversationMessage.Contents)
|
||||
{
|
||||
MessageInputContentBlock? contentBlock =
|
||||
content switch
|
||||
{
|
||||
TextContent textContent => new MessageInputTextBlock(textContent.Text),
|
||||
HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)),
|
||||
UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())),
|
||||
DataContent dataContent when dataContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(dataContent.Uri)),
|
||||
_ => null // Unsupported content type
|
||||
};
|
||||
|
||||
if (contentBlock is not null)
|
||||
if (string.IsNullOrEmpty(item.Id))
|
||||
{
|
||||
yield return contentBlock;
|
||||
yield return item;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new ReferenceResponseItem(item.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default)
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> InvokeAgentAsync(
|
||||
string agentId,
|
||||
string? agentVersion,
|
||||
string? conversationId,
|
||||
IEnumerable<ChatMessage>? messages,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChatClientAgent agent =
|
||||
await this.GetAgentsClient().GetAIAgentAsync(
|
||||
agentId,
|
||||
new ChatOptions()
|
||||
{
|
||||
AllowMultipleToolCalls = this.AllowMultipleToolCalls,
|
||||
},
|
||||
clientFactory: null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
AgentVersion agentDefinition = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false);
|
||||
AIAgent agent = await this.GetAgentAsync(agentDefinition, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ChatOptions chatOptions =
|
||||
new()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
AllowMultipleToolCalls = this.AllowMultipleToolCalls,
|
||||
};
|
||||
|
||||
ChatClientAgentRunOptions runOptions = new(chatOptions);
|
||||
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentResponse =
|
||||
messages is not null ?
|
||||
agent.RunStreamingAsync([.. messages], null, runOptions, cancellationToken) :
|
||||
agent.RunStreamingAsync([new ChatMessage(ChatRole.User, string.Empty)], null, runOptions, cancellationToken);
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in agentResponse.ConfigureAwait(false))
|
||||
{
|
||||
update.AuthorName = agentDefinition.Name;
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AgentVersion> QueryAgentAsync(string agentName, string? agentVersion, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string agentKey = $"{agentName}:{agentVersion}";
|
||||
if (this._versionCache.TryGetValue(agentKey, out AgentVersion? targetAgent))
|
||||
{
|
||||
return targetAgent;
|
||||
}
|
||||
|
||||
AgentsClient client = this.GetAgentsClient();
|
||||
|
||||
if (string.IsNullOrEmpty(agentVersion))
|
||||
{
|
||||
AgentRecord agentRecord =
|
||||
await client.GetAgentAsync(
|
||||
agentName,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
targetAgent = agentRecord.Versions.Latest;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAgent =
|
||||
await client.GetAgentVersionAsync(
|
||||
agentName,
|
||||
agentVersion,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._versionCache[agentKey] = targetAgent;
|
||||
|
||||
return targetAgent;
|
||||
}
|
||||
|
||||
private async Task<AIAgent> GetAgentAsync(AgentVersion agentDefinition, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._agentCache.TryGetValue(agentDefinition.Id, out AIAgent? agent))
|
||||
{
|
||||
return agent;
|
||||
}
|
||||
|
||||
AgentsClient client = this.GetAgentsClient();
|
||||
|
||||
IList<AITool>? tools = null;
|
||||
if (agentDefinition.Definition is PromptAgentDefinition promptAgent)
|
||||
{
|
||||
tools =
|
||||
promptAgent.Tools
|
||||
.Select(tool => tool.AsAITool())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
agent = client.GetAIAgent(agentDefinition, tools, clientFactory: null, openAIClientOptions: null, requireInvocableTools: false, cancellationToken);
|
||||
|
||||
FunctionInvokingChatClient? functionInvokingClient = agent.GetService<FunctionInvokingChatClient>();
|
||||
if (functionInvokingClient is not null)
|
||||
@@ -127,14 +178,17 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
}
|
||||
}
|
||||
|
||||
this._agentCache[agentDefinition.Id] = agent;
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PersistentThreadMessage message = await this.GetAgentsClient().Messages.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false);
|
||||
return ToChatMessage(message);
|
||||
AgentResponseItem responseItem = await this.GetConversationClient().GetConversationItemAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false);
|
||||
ResponseItem[] items = [responseItem.AsOpenAIResponseItem()];
|
||||
return items.AsChatMessages().Single();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -146,25 +200,29 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
bool newestFirst = false,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ListSortOrder order = newestFirst ? ListSortOrder.Ascending : ListSortOrder.Descending;
|
||||
await foreach (PersistentThreadMessage message in this.GetAgentsClient().Messages.GetMessagesAsync(conversationId, runId: null, limit, order, after, before, cancellationToken).ConfigureAwait(false))
|
||||
AgentsListOrder order = newestFirst ? AgentsListOrder.Asc : AgentsListOrder.Desc;
|
||||
await foreach (AgentResponseItem responseItem in this.GetConversationClient().GetConversationItemsAsync(conversationId, limit, order, after, before, itemType: null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return ToChatMessage(message);
|
||||
ResponseItem[] items = [responseItem.AsOpenAIResponseItem()];
|
||||
foreach (ChatMessage message in items.AsChatMessages())
|
||||
{
|
||||
yield return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PersistentAgentsClient GetAgentsClient()
|
||||
private AgentsClient GetAgentsClient()
|
||||
{
|
||||
if (this._agentsClient is null)
|
||||
{
|
||||
PersistentAgentsAdministrationClientOptions clientOptions = new();
|
||||
AgentsClientOptions clientOptions = new();
|
||||
|
||||
if (httpClient is not null)
|
||||
{
|
||||
clientOptions.Transport = new HttpClientTransport(httpClient);
|
||||
clientOptions.Transport = new HttpClientPipelineTransport(httpClient);
|
||||
}
|
||||
|
||||
PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions);
|
||||
AgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions);
|
||||
|
||||
Interlocked.CompareExchange(ref this._agentsClient, newClient, null);
|
||||
}
|
||||
@@ -172,43 +230,15 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
return this._agentsClient;
|
||||
}
|
||||
|
||||
private static ChatMessage ToChatMessage(PersistentThreadMessage message)
|
||||
private ConversationClient GetConversationClient()
|
||||
{
|
||||
return
|
||||
new ChatMessage(new ChatRole(message.Role.ToString()), [.. GetContent()])
|
||||
{
|
||||
MessageId = message.Id,
|
||||
CreatedAt = message.CreatedAt,
|
||||
AdditionalProperties = GetMetadata()
|
||||
};
|
||||
|
||||
IEnumerable<AIContent> GetContent()
|
||||
if (this._conversationClient is null)
|
||||
{
|
||||
foreach (MessageContent contentItem in message.ContentItems)
|
||||
{
|
||||
AIContent? content =
|
||||
contentItem switch
|
||||
{
|
||||
MessageTextContent textContent => new TextContent(textContent.Text),
|
||||
MessageImageFileContent imageContent => new HostedFileContent(imageContent.FileId),
|
||||
_ => null // Unsupported content type
|
||||
};
|
||||
ConversationClient conversationClient = this.GetAgentsClient().GetConversationClient();
|
||||
|
||||
if (content is not null)
|
||||
{
|
||||
yield return content;
|
||||
}
|
||||
}
|
||||
Interlocked.CompareExchange(ref this._conversationClient, conversationClient, null);
|
||||
}
|
||||
|
||||
AdditionalPropertiesDictionary? GetMetadata()
|
||||
{
|
||||
if (message.Metadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AdditionalPropertiesDictionary(message.Metadata.Select(m => new KeyValuePair<string, object?>(m.Key, m.Value)));
|
||||
}
|
||||
return this._conversationClient;
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -61,7 +61,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
|
||||
EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true);
|
||||
EvaluateBoolExpression(this.Model.Output?.AutoSend, "autoSend", defaultValue: true);
|
||||
EvaluateMessageTemplate(this.Model.Input?.AdditionalInstructions, "additionalInstructions");
|
||||
EvaluateListExpression<ChatMessage>(this.Model.Input?.Messages, "inputMessages");
|
||||
this.Write(@"
|
||||
|
||||
@@ -71,7 +70,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
agentName,
|
||||
conversationId,
|
||||
autoSend,
|
||||
additionalInstructions,
|
||||
inputMessages,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
-2
@@ -19,7 +19,6 @@ internal sealed class <#= this.Name #>Executor(FormulaSession session, WorkflowA
|
||||
<#
|
||||
EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true);
|
||||
EvaluateBoolExpression(this.Model.Output?.AutoSend, "autoSend", defaultValue: true);
|
||||
EvaluateMessageTemplate(this.Model.Input?.AdditionalInstructions, "additionalInstructions");
|
||||
EvaluateListExpression<ChatMessage>(this.Model.Input?.Messages, "inputMessages");#>
|
||||
|
||||
AgentRunResponse agentResponse =
|
||||
@@ -28,7 +27,6 @@ internal sealed class <#= this.Name #>Executor(FormulaSession session, WorkflowA
|
||||
agentName,
|
||||
conversationId,
|
||||
autoSend,
|
||||
additionalInstructions,
|
||||
inputMessages,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more function tool requests.
|
||||
/// </summary>
|
||||
public sealed class AgentFunctionToolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of function tool requests.
|
||||
/// </summary>
|
||||
public IList<FunctionCallContent> FunctionCalls { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal AgentFunctionToolRequest(string agentName, IList<FunctionCallContent> functionCalls)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.FunctionCalls = functionCalls;
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more function tool responses.
|
||||
/// </summary>
|
||||
public sealed class AgentFunctionToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool response.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of tool responses.
|
||||
/// </summary>
|
||||
public IList<FunctionResultContent> FunctionResults { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal AgentFunctionToolResponse(string agentName, IList<FunctionResultContent> functionResults)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.FunctionResults = functionResults;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create an <see cref="AgentFunctionToolResponse"/> from an <see cref="AgentFunctionToolRequest"/>
|
||||
/// Ensures that all function calls in the request have a corresponding result.
|
||||
/// </summary>
|
||||
/// <param name="toolRequest">The tool request.</param>
|
||||
/// <param name="functionResults">One or more function results</param>
|
||||
/// <returns>An <see cref="AgentFunctionToolResponse"/> that can be provided to the workflow.</returns>
|
||||
/// <exception cref="DeclarativeActionException">Not all <see cref="AgentFunctionToolRequest.FunctionCalls"/> have a corresponding <see cref="FunctionResultContent"/>.</exception>
|
||||
public static AgentFunctionToolResponse Create(AgentFunctionToolRequest toolRequest, params IEnumerable<FunctionResultContent> functionResults)
|
||||
{
|
||||
HashSet<string> callIds = [.. toolRequest.FunctionCalls.Select(call => call.CallId)];
|
||||
HashSet<string> resultIds = [.. functionResults.Select(call => call.CallId)];
|
||||
|
||||
if (!callIds.SetEquals(resultIds))
|
||||
{
|
||||
throw new DeclarativeActionException($"Missing results for: {string.Join(",", callIds.Except(resultIds))}");
|
||||
}
|
||||
|
||||
return new AgentFunctionToolResponse(toolRequest.AgentName, [.. functionResults]);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for user input in response to a `Question` action.
|
||||
/// </summary>
|
||||
public sealed class AnswerRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// An optional prompt for the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This prompt is utilized for the "Question" action type in the Declarative Workflow,
|
||||
/// but is redundant when the user is responding to an agent since the agent's message
|
||||
/// is the implicit prompt.
|
||||
/// </remarks>
|
||||
public string? Prompt { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal AnswerRequest(string? prompt = null)
|
||||
{
|
||||
this.Prompt = prompt;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user input response.
|
||||
/// </summary>
|
||||
public sealed class AnswerResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The response value.
|
||||
/// </summary>
|
||||
public ChatMessage Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AnswerResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
[JsonConstructor]
|
||||
public AnswerResponse(ChatMessage value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AnswerResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
public AnswerResponse(string value)
|
||||
{
|
||||
this.Value = new ChatMessage(ChatRole.User, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for external input.
|
||||
/// </summary>
|
||||
public sealed class ExternalInputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The source message that triggered the request for external input.
|
||||
/// </summary>
|
||||
public AgentRunResponse AgentResponse { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal ExternalInputRequest(AgentRunResponse agentResponse)
|
||||
{
|
||||
this.AgentResponse = agentResponse;
|
||||
}
|
||||
|
||||
internal ExternalInputRequest(ChatMessage message)
|
||||
{
|
||||
this.AgentResponse = new AgentRunResponse(message);
|
||||
}
|
||||
|
||||
internal ExternalInputRequest(string text)
|
||||
{
|
||||
this.AgentResponse = new AgentRunResponse(new ChatMessage(ChatRole.User, text));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response to a <see cref="ExternalInputRequest"/>.
|
||||
/// </summary>
|
||||
public sealed class ExternalInputResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The message being provided as external input to the workflow.
|
||||
/// </summary>
|
||||
public IList<ChatMessage> Messages { get; }
|
||||
|
||||
internal bool HasMessages => this.Messages?.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ExternalInputResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">The external input message being provided to the workflow.</param>
|
||||
public ExternalInputResponse(ChatMessage message)
|
||||
{
|
||||
this.Messages = [message];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ExternalInputResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="messages">The external input messages being provided to the workflow.</param>
|
||||
[JsonConstructor]
|
||||
public ExternalInputResponse(IList<ChatMessage> messages)
|
||||
{
|
||||
this.Messages = messages;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more user-input requests.
|
||||
/// </summary>
|
||||
public sealed class UserInputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of user input requests.
|
||||
/// </summary>
|
||||
public IList<AIContent> InputRequests { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal UserInputRequest(string agentName, IList<AIContent> inputRequests)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.InputRequests = inputRequests;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more user-input responses.
|
||||
/// </summary>
|
||||
public sealed class UserInputResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of approval responses.
|
||||
/// </summary>
|
||||
public IList<AIContent> InputResponses { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal UserInputResponse(string agentName, IList<AIContent> inputResponses)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.InputResponses = inputResponses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create an <see cref="UserInputResponse"/> from a <see cref="UserInputRequest"/>
|
||||
/// Ensures that all requests have a corresponding result.
|
||||
/// </summary>
|
||||
/// <param name="inputRequest">The input request.</param>
|
||||
/// <param name="inputResponses">One or more responses</param>
|
||||
/// <returns>An <see cref="UserInputResponse"/> that can be provided to the workflow.</returns>
|
||||
/// <exception cref="DeclarativeActionException">Not all <see cref="AgentFunctionToolRequest.FunctionCalls"/> have a corresponding <see cref="FunctionResultContent"/>.</exception>
|
||||
public static UserInputResponse Create(UserInputRequest inputRequest, params IEnumerable<UserInputResponseContent> inputResponses)
|
||||
{
|
||||
HashSet<string> callIds = [.. inputRequest.InputRequests.OfType<UserInputRequestContent>().Select(call => call.Id)];
|
||||
HashSet<string> resultIds = [.. inputResponses.Select(call => call.Id)];
|
||||
|
||||
if (!callIds.SetEquals(resultIds))
|
||||
{
|
||||
throw new DeclarativeActionException($"Missing responses for: {string.Join(",", callIds.Except(resultIds))}");
|
||||
}
|
||||
|
||||
return new UserInputResponse(inputRequest.AgentName, [.. inputResponses]);
|
||||
}
|
||||
}
|
||||
+3
-43
@@ -1,24 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class AgentProviderExtensions
|
||||
{
|
||||
private static readonly HashSet<Azure.AI.Agents.Persistent.RunStatus> s_failureStatus =
|
||||
[
|
||||
Azure.AI.Agents.Persistent.RunStatus.Failed,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Cancelled,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Cancelling,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Expired,
|
||||
];
|
||||
|
||||
public static async ValueTask<AgentRunResponse> InvokeAgentAsync(
|
||||
this WorkflowAgentProvider agentProvider,
|
||||
string executorId,
|
||||
@@ -26,27 +16,10 @@ internal static class AgentProviderExtensions
|
||||
string agentName,
|
||||
string? conversationId,
|
||||
bool autoSend,
|
||||
string? additionalInstructions = null,
|
||||
IEnumerable<ChatMessage>? inputMessages = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get the specified agent.
|
||||
AIAgent agent = await agentProvider.GetAgentAsync(agentName, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Prepare the run options.
|
||||
ChatClientAgentRunOptions options =
|
||||
new(
|
||||
new ChatOptions()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
Instructions = additionalInstructions,
|
||||
});
|
||||
|
||||
// Initialize the agent thread.
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates =
|
||||
inputMessages is not null ?
|
||||
agent.RunStreamingAsync([.. inputMessages], null, options, cancellationToken) :
|
||||
agent.RunStreamingAsync(null, options, cancellationToken);
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, cancellationToken);
|
||||
|
||||
// Enable "autoSend" behavior if this is the workflow conversation.
|
||||
bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? workflowConversationId);
|
||||
@@ -60,13 +33,6 @@ internal static class AgentProviderExtensions
|
||||
|
||||
updates.Add(update);
|
||||
|
||||
if (update.RawRepresentation is ChatResponseUpdate chatUpdate &&
|
||||
chatUpdate.RawRepresentation is RunUpdate runUpdate &&
|
||||
s_failureStatus.Contains(runUpdate.Value.Status))
|
||||
{
|
||||
throw new DeclarativeActionException($"Unexpected failure invoking agent, run {runUpdate.Value.Status}: {agent.Name ?? agent.Id} [{runUpdate.Value.Id}/{conversationId}]");
|
||||
}
|
||||
|
||||
if (autoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update), cancellationToken).ConfigureAwait(false);
|
||||
@@ -80,16 +46,10 @@ internal static class AgentProviderExtensions
|
||||
await context.AddEventAsync(new AgentRunResponseEvent(executorId, response), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// If autoSend is enabled and this is not the workflow conversation, copy messages to the workflow conversation.
|
||||
if (autoSend && !isWorkflowConversation && workflowConversationId is not null)
|
||||
{
|
||||
// Copy messages with content that aren't function calls or results.
|
||||
IEnumerable<ChatMessage> messages =
|
||||
response.Messages.Where(
|
||||
message =>
|
||||
!string.IsNullOrEmpty(message.Text) &&
|
||||
!message.Contents.OfType<FunctionCallContent>().Any() &&
|
||||
!message.Contents.OfType<FunctionResultContent>().Any());
|
||||
foreach (ChatMessage message in messages)
|
||||
foreach (ChatMessage message in response.Messages)
|
||||
{
|
||||
await agentProvider.CreateMessageAsync(workflowConversationId, message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
+59
-39
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -43,16 +44,28 @@ internal static class JsonDocumentExtensions
|
||||
|
||||
private static Dictionary<string, object?> ParseRecord(this JsonElement currentElement, VariableType targetType)
|
||||
{
|
||||
if (targetType.Schema is null)
|
||||
{
|
||||
throw new DeclarativeActionException($"Object schema not defined for. {targetType.Type.Name}.");
|
||||
}
|
||||
IEnumerable<KeyValuePair<string, object?>> keyValuePairs =
|
||||
targetType.Schema is null ?
|
||||
ParseValues() :
|
||||
ParseSchema(targetType.Schema);
|
||||
|
||||
return ParseValues().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
return keyValuePairs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
IEnumerable<KeyValuePair<string, object?>> ParseValues()
|
||||
{
|
||||
foreach (KeyValuePair<string, VariableType> property in targetType.Schema)
|
||||
foreach (JsonProperty objectProperty in currentElement.EnumerateObject())
|
||||
{
|
||||
if (!objectProperty.Value.TryParseValue(targetType: null, out object? parsedValue))
|
||||
{
|
||||
throw new DeclarativeActionException($"Unsupported data type '{objectProperty.Value.ValueKind}' for property '{objectProperty.Name}'");
|
||||
}
|
||||
yield return new KeyValuePair<string, object?>(objectProperty.Name, parsedValue);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<KeyValuePair<string, object?>> ParseSchema(FrozenDictionary<string, VariableType> schema)
|
||||
{
|
||||
foreach (KeyValuePair<string, VariableType> property in schema)
|
||||
{
|
||||
object? parsedValue = null;
|
||||
if (!currentElement.TryGetProperty(property.Key, out JsonElement propertyElement))
|
||||
@@ -131,22 +144,22 @@ internal static class JsonDocumentExtensions
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool TryParseValue(this JsonElement propertyElement, VariableType targetType, out object? value) =>
|
||||
private static bool TryParseValue(this JsonElement propertyElement, VariableType? targetType, out object? value) =>
|
||||
propertyElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => TryParseString(propertyElement, targetType.Type, out value),
|
||||
JsonValueKind.Number => TryParseNumber(propertyElement, targetType.Type, out value),
|
||||
JsonValueKind.String => TryParseString(propertyElement, targetType?.Type, out value),
|
||||
JsonValueKind.Number => TryParseNumber(propertyElement, targetType?.Type, out value),
|
||||
JsonValueKind.True or JsonValueKind.False => TryParseBoolean(propertyElement, out value),
|
||||
JsonValueKind.Object => TryParseObject(propertyElement, targetType, out value),
|
||||
JsonValueKind.Array => TryParseList(propertyElement, targetType, out value),
|
||||
JsonValueKind.Null => TryParseNull(targetType.Type, out value),
|
||||
JsonValueKind.Null => TryParseNull(targetType?.Type, out value),
|
||||
_ => throw new DeclarativeActionException($"JSON element of type {propertyElement.ValueKind} is not supported."),
|
||||
};
|
||||
|
||||
private static bool TryParseNull(Type valueType, out object? value)
|
||||
private static bool TryParseNull(Type? valueType, out object? value)
|
||||
{
|
||||
// If the target type is not nullable, we cannot assign null to it
|
||||
if (!valueType.IsNullable())
|
||||
if (valueType?.IsNullable() == false)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
@@ -170,7 +183,7 @@ internal static class JsonDocumentExtensions
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseString(JsonElement propertyElement, Type valueType, out object? value)
|
||||
private static bool TryParseString(JsonElement propertyElement, Type? valueType, out object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -178,23 +191,30 @@ internal static class JsonDocumentExtensions
|
||||
if (propertyValue is null)
|
||||
{
|
||||
value = null;
|
||||
return valueType.IsNullable(); // Parse fails if value is null and requested type is not.
|
||||
return valueType?.IsNullable() ?? false; // Parse fails if value is null and requested type is not.
|
||||
}
|
||||
|
||||
switch (valueType)
|
||||
if (valueType is null)
|
||||
{
|
||||
case Type targetType when targetType == typeof(string):
|
||||
value = propertyValue;
|
||||
break;
|
||||
case Type targetType when targetType == typeof(DateTime):
|
||||
value = DateTime.Parse(propertyValue, provider: null, styles: DateTimeStyles.RoundtripKind);
|
||||
break;
|
||||
case Type targetType when targetType == typeof(TimeSpan):
|
||||
value = TimeSpan.Parse(propertyValue);
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
value = propertyValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (valueType)
|
||||
{
|
||||
case Type targetType when targetType == typeof(string):
|
||||
value = propertyValue;
|
||||
break;
|
||||
case Type targetType when targetType == typeof(DateTime):
|
||||
value = DateTime.Parse(propertyValue, provider: null, styles: DateTimeStyles.RoundtripKind);
|
||||
break;
|
||||
case Type targetType when targetType == typeof(TimeSpan):
|
||||
value = TimeSpan.Parse(propertyValue);
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -206,7 +226,7 @@ internal static class JsonDocumentExtensions
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNumber(JsonElement element, Type valueType, out object? value)
|
||||
private static bool TryParseNumber(JsonElement element, Type? valueType, out object? value)
|
||||
{
|
||||
// Try parsing as integer types first (most precise representation)
|
||||
if (element.TryGetInt32(out int intValue))
|
||||
@@ -234,8 +254,14 @@ internal static class JsonDocumentExtensions
|
||||
value = null;
|
||||
return false;
|
||||
|
||||
static bool ConvertToExpectedType(Type valueType, object sourceValue, out object? value)
|
||||
static bool ConvertToExpectedType(Type? valueType, object sourceValue, out object? value)
|
||||
{
|
||||
if (valueType is null)
|
||||
{
|
||||
value = sourceValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
value = Convert.ChangeType(sourceValue, valueType);
|
||||
@@ -249,23 +275,17 @@ internal static class JsonDocumentExtensions
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseObject(JsonElement propertyElement, VariableType targetType, out object? value)
|
||||
private static bool TryParseObject(JsonElement propertyElement, VariableType? targetType, out object? value)
|
||||
{
|
||||
if (!targetType.HasSchema)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = propertyElement.ParseRecord(targetType);
|
||||
value = propertyElement.ParseRecord(targetType ?? VariableType.RecordType);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseList(JsonElement propertyElement, VariableType targetType, out object? value)
|
||||
private static bool TryParseList(JsonElement propertyElement, VariableType? targetType, out object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = ParseTable(propertyElement, targetType);
|
||||
value = ParseTable(propertyElement, targetType ?? VariableType.ListType);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
|
||||
+3
-3
@@ -77,7 +77,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
this.State.Bind();
|
||||
}
|
||||
|
||||
private bool IsManagedScope(string? scopeName) => scopeName is not null && VariableScopeNames.IsValidName(scopeName);
|
||||
private static bool IsManagedScope(string? scopeName) => scopeName is not null && VariableScopeNames.IsValidName(scopeName);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<TValue?> ReadStateAsync<TValue>(string key, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
@@ -86,7 +86,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
// Not a managed scope, just pass through. This is valid when a declarative
|
||||
// workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
|
||||
_ when !this.IsManagedScope(scopeName) => await this.Source.ReadStateAsync<TValue>(key, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
_ when !IsManagedScope(scopeName) => await this.Source.ReadStateAsync<TValue>(key, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
// Retrieve formula values directly from the managed state to avoid conversion.
|
||||
_ when typeof(TValue) == typeof(FormulaValue) => (TValue?)(object?)this.State.Get(key, scopeName),
|
||||
// Retrieve native types from the source context to avoid conversion.
|
||||
@@ -100,7 +100,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
// Not a managed scope, just pass through. This is valid when a declarative
|
||||
// workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
|
||||
_ when !this.IsManagedScope(scopeName) => await this.Source.ReadOrInitStateAsync(key, initialStateFactory, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
_ when !IsManagedScope(scopeName) => await this.Source.ReadOrInitStateAsync(key, initialStateFactory, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
// Retrieve formula values directly from the managed state to avoid conversion.
|
||||
_ when typeof(TValue) == typeof(FormulaValue) => await EnsureFormulaValueAsync().ConfigureAwait(false),
|
||||
// Retrieve native types from the source context to avoid conversion.
|
||||
|
||||
+56
-23
@@ -239,6 +239,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
// Entry point for question
|
||||
QuestionExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
|
||||
// Transition to post action if complete
|
||||
string postId = Steps.Post(action.Id);
|
||||
this._workflowModel.AddLink(action.Id, postId, QuestionExecutor.IsComplete);
|
||||
@@ -249,13 +250,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
|
||||
// Define input action
|
||||
string inputId = QuestionExecutor.Steps.Input(action.Id);
|
||||
RequestPortAction inputPort = new(RequestPort.Create<AnswerRequest, AnswerResponse>(inputId));
|
||||
RequestPortAction inputPort = new(RequestPort.Create<ExternalInputRequest, ExternalInputResponse>(inputId));
|
||||
this._workflowModel.AddNode(inputPort, action.ParentId);
|
||||
this._workflowModel.AddLinkFromPeer(action.ParentId, inputId);
|
||||
|
||||
// Capture input response
|
||||
string captureId = QuestionExecutor.Steps.Capture(action.Id);
|
||||
this.ContinueWith(new DelegateActionExecutor<AnswerResponse>(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId);
|
||||
this.ContinueWith(new DelegateActionExecutor<ExternalInputResponse>(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId);
|
||||
|
||||
// Transition to post action if complete
|
||||
this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId, QuestionExecutor.IsComplete);
|
||||
@@ -263,6 +264,24 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this._workflowModel.AddLink(captureId, prepareId, message => !QuestionExecutor.IsComplete(message));
|
||||
}
|
||||
|
||||
protected override void Visit(RequestExternalInput item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
RequestExternalInputExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
|
||||
// Define input action
|
||||
string inputId = RequestExternalInputExecutor.Steps.Input(action.Id);
|
||||
RequestPortAction inputPort = new(RequestPort.Create<ExternalInputRequest, ExternalInputResponse>(inputId));
|
||||
this._workflowModel.AddNode(inputPort, action.ParentId);
|
||||
this._workflowModel.AddLinkFromPeer(action.ParentId, inputId);
|
||||
|
||||
// Capture input response
|
||||
string captureId = RequestExternalInputExecutor.Steps.Capture(action.Id);
|
||||
this.ContinueWith(new DelegateActionExecutor<ExternalInputResponse>(captureId, this._workflowState, action.CaptureResponseAsync), action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(EndDialog item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -285,6 +304,28 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CancelAllDialogs item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
// Represent action with default executor
|
||||
DefaultActionExecutor action = new(item, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
// Define a clean-start to ensure "end" is not a source for any edge
|
||||
this.RestartAfter(item.Id.Value, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CancelDialog item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
// Represent action with default executor
|
||||
DefaultActionExecutor action = new(item, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
// Define a clean-start to ensure "end" is not a source for any edge
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CreateConversation item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -318,33 +359,29 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this._workflowModel.AddLink(action.Id, postId, InvokeAzureAgentExecutor.RequiresNothing);
|
||||
|
||||
// Define request-port for function calling action
|
||||
string functionCallingPortId = InvokeAzureAgentExecutor.Steps.FunctionTool(action.Id);
|
||||
RequestPortAction functionCallingPort = new(RequestPort.Create<AgentFunctionToolRequest, AgentFunctionToolResponse>(functionCallingPortId));
|
||||
this._workflowModel.AddNode(functionCallingPort, action.ParentId);
|
||||
this._workflowModel.AddLink(action.Id, functionCallingPort.Id, InvokeAzureAgentExecutor.RequiresFunctionCall);
|
||||
|
||||
// Define request-port for user input, such as: mcp tool & function tool approval
|
||||
string userInputPortId = InvokeAzureAgentExecutor.Steps.UserInput(action.Id);
|
||||
RequestPortAction userInputPort = new(RequestPort.Create<UserInputRequest, UserInputResponse>(userInputPortId));
|
||||
this._workflowModel.AddNode(userInputPort, action.ParentId);
|
||||
this._workflowModel.AddLink(action.Id, userInputPortId, InvokeAzureAgentExecutor.RequiresUserInput);
|
||||
string externalInputPortId = InvokeAzureAgentExecutor.Steps.ExternalInput(action.Id);
|
||||
RequestPortAction externalInputPort = new(RequestPort.Create<ExternalInputRequest, ExternalInputResponse>(externalInputPortId));
|
||||
this._workflowModel.AddNode(externalInputPort, action.ParentId);
|
||||
this._workflowModel.AddLink(action.Id, externalInputPortId, InvokeAzureAgentExecutor.RequiresInput);
|
||||
|
||||
// Request ports always transitions to resume
|
||||
string resumeId = InvokeAzureAgentExecutor.Steps.Resume(action.Id);
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor<AgentFunctionToolResponse>(resumeId, this._workflowState, action.ResumeAsync), action.ParentId);
|
||||
this._workflowModel.AddLink(functionCallingPortId, resumeId);
|
||||
this._workflowModel.AddLink(userInputPortId, resumeId);
|
||||
// Transition to appropriate request port if more function calling is requested
|
||||
this._workflowModel.AddLink(resumeId, functionCallingPortId, InvokeAzureAgentExecutor.RequiresFunctionCall);
|
||||
// Transition to appropriate request port if more user input is requested
|
||||
this._workflowModel.AddLink(resumeId, userInputPortId, InvokeAzureAgentExecutor.RequiresUserInput);
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor<ExternalInputResponse>(resumeId, this._workflowState, action.ResumeAsync, emitResult: false), action.ParentId);
|
||||
this._workflowModel.AddLink(externalInputPortId, resumeId);
|
||||
// Transition to post action if complete
|
||||
this._workflowModel.AddLink(resumeId, postId, InvokeAzureAgentExecutor.RequiresNothing);
|
||||
// Transition to request port if more input is required
|
||||
this._workflowModel.AddLink(resumeId, externalInputPortId, InvokeAzureAgentExecutor.RequiresInput);
|
||||
|
||||
// Define post action
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeAzureResponse item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(RetrieveConversationMessage item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -462,10 +499,6 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
|
||||
protected override void Visit(ReplaceDialog item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(CancelAllDialogs item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(CancelDialog item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(EmitEvent item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(GetConversationMembers item) => this.NotSupported(item);
|
||||
|
||||
+27
-12
@@ -210,9 +210,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
protected override void Visit(Question item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
//this.Trace(item);
|
||||
}
|
||||
|
||||
//this.ContinueWith(new QuestionTemplate(item));
|
||||
protected override void Visit(RequestExternalInput item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(EndDialog item)
|
||||
@@ -237,6 +239,24 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CancelAllDialogs item)
|
||||
{
|
||||
// Represent action with default executor
|
||||
DefaultTemplate action = new(item, this._rootId);
|
||||
this.ContinueWith(action);
|
||||
// Define a clean-start to ensure "end" is not a source for any edge
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CancelDialog item)
|
||||
{
|
||||
// Represent action with default executor
|
||||
DefaultTemplate action = new(item, this._rootId);
|
||||
this.ContinueWith(action);
|
||||
// Define a clean-start to ensure "end" is not a source for any edge
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CreateConversation item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -265,6 +285,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
this.ContinueWith(new InvokeAzureAgentTemplate(item));
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeAzureResponse item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(RetrieveConversationMessage item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -317,17 +342,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
protected override void Visit(EditTable item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
//this.Trace(item);
|
||||
|
||||
//this.ContinueWith(new EditTableTemplate(item));
|
||||
}
|
||||
|
||||
protected override void Visit(EditTableV2 item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
//this.Trace(item);
|
||||
|
||||
//this.ContinueWith(new EditTableV2Template(item));
|
||||
}
|
||||
|
||||
protected override void Visit(ParseValue item)
|
||||
@@ -384,10 +403,6 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
|
||||
protected override void Visit(ReplaceDialog item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(CancelAllDialogs item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(CancelDialog item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(EmitEvent item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(GetConversationMembers item) => this.NotSupported(item);
|
||||
|
||||
@@ -23,7 +23,6 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA
|
||||
/// <param name="agentName">The name or identifier of the agent.</param>
|
||||
/// <param name="conversationId">The identifier of the conversation.</param>
|
||||
/// <param name="autoSend">Send the agent's response as workflow output. (default: true).</param>
|
||||
/// <param name="additionalInstructions">Optional additional instructions to the agent.</param>
|
||||
/// <param name="inputMessages">Optional messages to add to the conversation prior to invocation.</param>
|
||||
/// <param name="cancellationToken">A token that can be used to observe cancellation.</param>
|
||||
/// <returns></returns>
|
||||
@@ -32,8 +31,7 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA
|
||||
string agentName,
|
||||
string? conversationId,
|
||||
bool autoSend,
|
||||
string? additionalInstructions = null,
|
||||
IEnumerable<ChatMessage>? inputMessages = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, inputMessages, cancellationToken);
|
||||
=> agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, cancellationToken);
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<NoWarn>$(NoWarn);MEAI001;OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -34,8 +34,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+57
-56
@@ -2,6 +2,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
@@ -21,14 +22,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
{
|
||||
public static class Steps
|
||||
{
|
||||
public static string UserInput(string id) => $"{id}_{nameof(UserInput)}";
|
||||
public static string FunctionTool(string id) => $"{id}_{nameof(FunctionTool)}";
|
||||
public static string ExternalInput(string id) => $"{id}_{nameof(ExternalInput)}";
|
||||
public static string Resume(string id) => $"{id}_{nameof(Resume)}";
|
||||
}
|
||||
|
||||
public static bool RequiresFunctionCall(object? message) => message is AgentFunctionToolRequest;
|
||||
|
||||
public static bool RequiresUserInput(object? message) => message is UserInputRequest;
|
||||
public static bool RequiresInput(object? message) => message is ExternalInputRequest;
|
||||
|
||||
public static bool RequiresNothing(object? message) => message is ActionExecutorResult;
|
||||
|
||||
@@ -46,8 +44,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask ResumeAsync(IWorkflowContext context, AgentFunctionToolResponse message, CancellationToken cancellationToken) =>
|
||||
this.InvokeAgentAsync(context, [message.FunctionResults.ToChatMessage()], cancellationToken);
|
||||
public async ValueTask ResumeAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false);
|
||||
await this.InvokeAgentAsync(context, response.Messages, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -58,40 +59,52 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
{
|
||||
string? conversationId = this.GetConversationId();
|
||||
string agentName = this.GetAgentName();
|
||||
string? additionalInstructions = this.GetAdditionalInstructions();
|
||||
bool autoSend = this.GetAutoSendValue();
|
||||
|
||||
bool isComplete = true;
|
||||
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(agentResponse.Text))
|
||||
ChatMessage[] actionableMessages = FilterActionableContent(agentResponse).ToArray();
|
||||
if (actionableMessages.Length > 0)
|
||||
{
|
||||
// Identify function calls that have no associated result.
|
||||
List<UserInputRequestContent> inputRequests = GetUserInputRequests(agentResponse);
|
||||
if (inputRequests.Count > 0)
|
||||
{
|
||||
isComplete = false;
|
||||
UserInputRequest approvalRequest = new(agentName, inputRequests.OfType<AIContent>().ToArray());
|
||||
await context.SendMessageAsync(approvalRequest, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Identify function calls that have no associated result.
|
||||
List<FunctionCallContent> functionCalls = GetOrphanedFunctionCalls(agentResponse);
|
||||
if (functionCalls.Count > 0)
|
||||
{
|
||||
isComplete = false;
|
||||
AgentFunctionToolRequest toolRequest = new(agentName, functionCalls);
|
||||
await context.SendMessageAsync(toolRequest, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isComplete)
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
AgentRunResponse filteredResponse =
|
||||
new(actionableMessages)
|
||||
{
|
||||
AdditionalProperties = agentResponse.AdditionalProperties,
|
||||
AgentId = agentResponse.AgentId,
|
||||
CreatedAt = agentResponse.CreatedAt,
|
||||
ResponseId = agentResponse.ResponseId,
|
||||
Usage = agentResponse.Usage,
|
||||
};
|
||||
await context.SendMessageAsync(new ExternalInputRequest(filteredResponse), cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.AssignAsync(this.AgentOutput?.Messages?.Path, agentResponse.Messages.ToTable(), context).ConfigureAwait(false);
|
||||
|
||||
// Attempt to parse the last message as JSON and assign to the response object variable.
|
||||
try
|
||||
{
|
||||
JsonDocument jsonDocument = JsonDocument.Parse(agentResponse.Messages.Last().Text);
|
||||
Dictionary<string, object?> objectProperties = jsonDocument.ParseRecord(VariableType.RecordType);
|
||||
await this.AssignAsync(this.AgentOutput?.ResponseObject?.Path, objectProperties.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Not valid json, skip assignment.
|
||||
}
|
||||
|
||||
if (this.Model.Input?.ExternalLoop?.When is not null)
|
||||
{
|
||||
bool requestInput = this.Evaluator.GetValue(this.Model.Input.ExternalLoop.When).Value;
|
||||
if (requestInput)
|
||||
{
|
||||
ExternalInputRequest inputRequest = new(agentResponse);
|
||||
await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IEnumerable<ChatMessage>? GetInputMessages()
|
||||
@@ -107,7 +120,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
return userInput?.ToChatMessages();
|
||||
}
|
||||
|
||||
private static List<FunctionCallContent> GetOrphanedFunctionCalls(AgentRunResponse agentResponse)
|
||||
private static IEnumerable<ChatMessage> FilterActionableContent(AgentRunResponse agentResponse)
|
||||
{
|
||||
HashSet<string> functionResultIds =
|
||||
[.. agentResponse.Messages
|
||||
@@ -117,21 +130,21 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
.OfType<FunctionResultContent>()
|
||||
.Select(functionCall => functionCall.CallId))];
|
||||
|
||||
List<FunctionCallContent> functionCalls = [];
|
||||
foreach (FunctionCallContent functionCall in agentResponse.Messages.SelectMany(m => m.Contents.OfType<FunctionCallContent>()))
|
||||
foreach (ChatMessage responseMessage in agentResponse.Messages)
|
||||
{
|
||||
if (!functionResultIds.Contains(functionCall.CallId))
|
||||
if (responseMessage.Contents.Any(content => content is UserInputRequestContent))
|
||||
{
|
||||
functionCalls.Add(functionCall);
|
||||
yield return responseMessage;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (responseMessage.Contents.OfType<FunctionCallContent>().Any(functionCall => !functionResultIds.Contains(functionCall.CallId)))
|
||||
{
|
||||
yield return responseMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return functionCalls;
|
||||
}
|
||||
|
||||
private static List<UserInputRequestContent> GetUserInputRequests(AgentRunResponse agentResponse) =>
|
||||
agentResponse.Messages.SelectMany(m => m.Contents.OfType<UserInputRequestContent>()).ToList();
|
||||
|
||||
private string? GetConversationId()
|
||||
{
|
||||
if (this.Model.ConversationId is null)
|
||||
@@ -149,18 +162,6 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
this.AgentUsage.Name,
|
||||
$"{nameof(this.Model)}.{nameof(this.Model.Agent)}.{nameof(this.Model.Agent.Name)}")).Value;
|
||||
|
||||
private string? GetAdditionalInstructions()
|
||||
{
|
||||
string? additionalInstructions = null;
|
||||
|
||||
if (this.AgentInput?.AdditionalInstructions is not null)
|
||||
{
|
||||
additionalInstructions = this.Engine.Format(this.AgentInput.AdditionalInstructions);
|
||||
}
|
||||
|
||||
return additionalInstructions;
|
||||
}
|
||||
|
||||
private bool GetAutoSendValue()
|
||||
{
|
||||
if (this.AgentOutput?.AutoSend is null)
|
||||
|
||||
+6
-5
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Entities;
|
||||
@@ -75,22 +76,22 @@ internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider age
|
||||
public async ValueTask PrepareResponseAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false);
|
||||
AnswerRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt));
|
||||
ExternalInputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt));
|
||||
await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
|
||||
await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask CaptureResponseAsync(IWorkflowContext context, AnswerResponse message, CancellationToken cancellationToken)
|
||||
public async ValueTask CaptureResponseAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
FormulaValue? extractedValue = null;
|
||||
if (message.Value is null)
|
||||
if (!response.HasMessages)
|
||||
{
|
||||
string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt);
|
||||
await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim()), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, message.Value.Text);
|
||||
EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, string.Concat(response.Messages.Select(message => message.Text)));
|
||||
if (entityResult.IsValid)
|
||||
{
|
||||
extractedValue = entityResult.Value;
|
||||
@@ -121,7 +122,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider age
|
||||
if (workflowConversationId is not null)
|
||||
{
|
||||
// Input message always defined if values has been extracted.
|
||||
ChatMessage input = message.Value!;
|
||||
ChatMessage input = response.Messages.Last();
|
||||
await agentProvider.CreateMessageAsync(workflowConversationId, input, cancellationToken).ConfigureAwait(false);
|
||||
await context.SetLastMessageAsync(input).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class RequestExternalInputExecutor(RequestExternalInput model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state)
|
||||
: DeclarativeActionExecutor<RequestExternalInput>(model, state)
|
||||
{
|
||||
public static class Steps
|
||||
{
|
||||
public static string Input(string id) => $"{id}_{nameof(Input)}";
|
||||
public static string Capture(string id) => $"{id}_{nameof(Capture)}";
|
||||
}
|
||||
|
||||
protected override bool IsDiscreteAction => false;
|
||||
protected override bool EmitResultEvent => false;
|
||||
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ExternalInputRequest inputRequest = new(new AgentRunResponse());
|
||||
|
||||
await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async ValueTask CaptureResponseAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
string? workflowConversationId = context.GetWorkflowConversation();
|
||||
if (workflowConversationId is not null)
|
||||
{
|
||||
foreach (ChatMessage inputMessage in response.Messages)
|
||||
{
|
||||
await agentProvider.CreateMessageAsync(workflowConversationId, inputMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.Variable?.Path, response.Messages.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ Please refer to the [README](../../samples/GettingStarted/Workflows/Declarative/
|
||||
|**ConditionItem**|Represents a single conditional statement within a group. It evaluates a specific logical condition and determines the next step in the flow.
|
||||
|**ContinueLoop**|Skips the remaining steps in the current iteration and continues with the next loop cycle. Commonly used to bypass specific cases without exiting the loop entirely.
|
||||
|**EndConversation**|Terminates the current conversation session. It ensures any necessary cleanup or final actions are performed before closing.
|
||||
|**EndDialog**|Ends the current dialog or sub-dialog within a broader conversation flow. This helps modularize complex interactions.
|
||||
|**EndWorkflow**|Ends the current workflow or sub-workflow within a broader conversation flow. This helps modularize complex interactions.
|
||||
|**Foreach**|Iterates through a collection of items, executing a set of actions for each. Ideal for processing lists or batch operations.
|
||||
|**GotoAction**|Jumps directly to a specified action within the workflow. Enables non-linear navigation in the logic flow.
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ public abstract class WorkflowAgentProvider
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of additional tools an agent is able to automatically invoke.
|
||||
/// If an agent is configured with a function tool that is not available, a <see cref="RequestPort"/> is executed
|
||||
/// that provides an <see cref="AgentFunctionToolRequest"/> that describes the function calls requested. The caller may
|
||||
/// then respond with a corrsponding <see cref="AgentFunctionToolResponse"/> that includes the results of the function calls.
|
||||
/// that provides an <see cref="ExternalInputRequest"/> that describes the function calls requested. The caller may
|
||||
/// then respond with a corrsponding <see cref="ExternalInputResponse"/> that includes the results of the function calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These will not impact the requests sent to the model by the <see cref="FunctionInvokingChatClient"/>.
|
||||
@@ -56,14 +56,6 @@ public abstract class WorkflowAgentProvider
|
||||
/// </remarks>
|
||||
public bool AllowMultipleToolCalls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves an AI agent by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The unique identifier of the AI agent to retrieve. Cannot be null or empty.</param>
|
||||
/// <param name="cancellationToken">A token that propagates notification when operation should be canceled.</param>
|
||||
/// <returns>The task result contains the <see cref="AIAgent"/> associated.</returns>
|
||||
public abstract Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously creates a new conversation and returns its unique identifier.
|
||||
/// </summary>
|
||||
@@ -88,6 +80,17 @@ public abstract class WorkflowAgentProvider
|
||||
/// <returns>The requested message</returns>
|
||||
public abstract Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves an AI agent by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The unique identifier of the AI agent to retrieve. Cannot be null or empty.</param>
|
||||
/// <param name="agentVersion">An optional agent version.</param>
|
||||
/// <param name="conversationId">Optional identifier of the target conversation.</param>
|
||||
/// <param name="messages">The messages to include in the invocation.</param>
|
||||
/// <param name="cancellationToken">A token that propagates notification when operation should be canceled.</param>
|
||||
/// <returns>Asynchronous set of <see cref="AgentRunResponseUpdate"/>.</returns>
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> InvokeAgentAsync(string agentId, string? agentVersion, string? conversationId, IEnumerable<ChatMessage>? messages, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a set of messages from a conversation.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Build Code
|
||||
|
||||
Re-usable utility for building C# code in tests.
|
||||
|
||||
To use this in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedBuildTestCode>true</InjectSharedBuildTestCode>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0005
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents;
|
||||
|
||||
namespace Shared.Foundry;
|
||||
|
||||
internal static class AgentFactory
|
||||
{
|
||||
public static async ValueTask<AgentVersion> CreateAgentAsync(
|
||||
this AgentsClient agentsClient,
|
||||
string agentName,
|
||||
PromptAgentDefinition agentDefinition,
|
||||
string agentDescription)
|
||||
{
|
||||
AgentVersionCreationOptions options =
|
||||
new()
|
||||
{
|
||||
Description = agentDescription,
|
||||
Metadata =
|
||||
{
|
||||
{ "deleteme", bool.TrueString },
|
||||
{ "test", bool.TrueString },
|
||||
},
|
||||
};
|
||||
|
||||
AgentVersion agentVersion = await agentsClient.CreateAgentVersionAsync(agentName, agentDefinition, options).ConfigureAwait(false);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"PROMPT AGENT: {agentVersion.Name}:{agentVersion.Version}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
return agentVersion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Foundry Agents
|
||||
|
||||
Shared patterns for creating and utilizing Foundry agents.
|
||||
|
||||
To use this in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
# Workflow Execution
|
||||
|
||||
Common support for workflow execution.
|
||||
|
||||
To use this in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Shared.Workflows;
|
||||
|
||||
internal sealed class WorkflowFactory(string workflowFile, Uri foundryEndpoint)
|
||||
{
|
||||
public IList<AIFunction> Functions { get; init; } = [];
|
||||
|
||||
public IConfiguration? Configuration { get; init; }
|
||||
|
||||
// Assign to continue an existing conversation
|
||||
public string? ConversationId { get; init; }
|
||||
|
||||
// Assign to enable logging
|
||||
public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Create the workflow from the declarative YAML. Includes definition of the
|
||||
/// <see cref="DeclarativeWorkflowOptions" /> and the associated <see cref="WorkflowAgentProvider"/>.
|
||||
/// </summary>
|
||||
public Workflow CreateWorkflow()
|
||||
{
|
||||
// Create the agent provider that will service agent requests within the workflow.
|
||||
AzureAgentProvider agentProvider = new(foundryEndpoint, new AzureCliCredential())
|
||||
{
|
||||
// Functions included here will be auto-executed by the framework.
|
||||
Functions = this.Functions
|
||||
};
|
||||
|
||||
// Define the workflow options.
|
||||
DeclarativeWorkflowOptions options =
|
||||
new(agentProvider)
|
||||
{
|
||||
Configuration = this.Configuration,
|
||||
ConversationId = this.ConversationId,
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
};
|
||||
|
||||
string workflowPath = Path.Combine(AppContext.BaseDirectory, workflowFile);
|
||||
|
||||
// Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
|
||||
return DeclarativeWorkflowBuilder.Build<string>(workflowPath, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Uncomment to output unknown content types for debugging.
|
||||
//#define DEBUG_OUTPUT
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Shared.Workflows;
|
||||
|
||||
// Types are for evaluation purposes only and is subject to change or removal in future updates.
|
||||
#pragma warning disable OPENAI001
|
||||
#pragma warning disable OPENAICUA001
|
||||
#pragma warning disable MEAI001
|
||||
|
||||
internal sealed class WorkflowRunner
|
||||
{
|
||||
private Dictionary<string, AIFunction> FunctionMap { get; }
|
||||
private CheckpointInfo? LastCheckpoint { get; set; }
|
||||
|
||||
public static void Notify(string message, ConsoleColor? color = null)
|
||||
{
|
||||
Console.ForegroundColor = color ?? ConsoleColor.Cyan;
|
||||
try
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, checkpoints will be persisted to disk as JSON files.
|
||||
/// Otherwise an in-memory checkpoint store that will not persist checkpoints
|
||||
/// beyond the lifetime of the process.
|
||||
/// </summary>
|
||||
public bool UseJsonCheckpoints { get; init; }
|
||||
|
||||
public WorkflowRunner(params IEnumerable<AIFunction> functions)
|
||||
{
|
||||
this.FunctionMap = functions.ToDictionary(f => f.Name);
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(Func<Workflow> workflowProvider, string input)
|
||||
{
|
||||
Workflow workflow = workflowProvider.Invoke();
|
||||
|
||||
CheckpointManager checkpointManager;
|
||||
|
||||
if (this.UseJsonCheckpoints)
|
||||
{
|
||||
// Use a file-system based JSON checkpoint store to persist checkpoints to disk.
|
||||
DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:yyMMdd-hhmmss-ff}"));
|
||||
checkpointManager = CheckpointManager.CreateJson(new FileSystemJsonCheckpointStore(checkpointFolder));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process.
|
||||
checkpointManager = CheckpointManager.CreateInMemory();
|
||||
}
|
||||
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager).ConfigureAwait(false);
|
||||
|
||||
bool isComplete = false;
|
||||
ExternalResponse? requestResponse = null;
|
||||
do
|
||||
{
|
||||
ExternalRequest? externalRequest = await this.MonitorAndDisposeWorkflowRunAsync(run, requestResponse).ConfigureAwait(false);
|
||||
if (externalRequest is not null)
|
||||
{
|
||||
Notify("\nWORKFLOW: Yield\n", ConsoleColor.DarkYellow);
|
||||
|
||||
if (this.LastCheckpoint is null)
|
||||
{
|
||||
throw new InvalidOperationException("Checkpoint information missing after external request.");
|
||||
}
|
||||
|
||||
// Process the external request.
|
||||
object response = await this.HandleExternalRequestAsync(externalRequest).ConfigureAwait(false);
|
||||
requestResponse = externalRequest.CreateResponse(response);
|
||||
|
||||
// Let's resume on an entirely new workflow instance to demonstrate checkpoint portability.
|
||||
workflow = workflowProvider.Invoke();
|
||||
|
||||
// Restore the latest checkpoint.
|
||||
Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}");
|
||||
Notify("WORKFLOW: Restore", ConsoleColor.DarkYellow);
|
||||
|
||||
run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager, run.Run.RunId).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
isComplete = true;
|
||||
}
|
||||
}
|
||||
while (!isComplete);
|
||||
|
||||
Notify("\nWORKFLOW: Done!\n");
|
||||
}
|
||||
|
||||
public async Task<ExternalRequest?> MonitorAndDisposeWorkflowRunAsync(Checkpointed<StreamingRun> run, ExternalResponse? response = null)
|
||||
{
|
||||
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
|
||||
await using IAsyncDisposable disposeRun = run;
|
||||
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
|
||||
|
||||
bool hasStreamed = false;
|
||||
string? messageId = null;
|
||||
|
||||
bool shouldExit = false;
|
||||
ExternalRequest? externalResponse = null;
|
||||
|
||||
if (response is not null)
|
||||
{
|
||||
await run.Run.SendResponseAsync(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
switch (workflowEvent)
|
||||
{
|
||||
case ExecutorInvokedEvent executorInvoked:
|
||||
Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
|
||||
break;
|
||||
|
||||
case ExecutorCompletedEvent executorCompleted:
|
||||
Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
|
||||
break;
|
||||
|
||||
case DeclarativeActionInvokedEvent actionInvoked:
|
||||
Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]");
|
||||
break;
|
||||
|
||||
case DeclarativeActionCompletedEvent actionComplete:
|
||||
Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]");
|
||||
break;
|
||||
|
||||
case ExecutorFailedEvent executorFailure:
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure...");
|
||||
|
||||
case SuperStepCompletedEvent checkpointCompleted:
|
||||
this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint;
|
||||
Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]");
|
||||
if (externalResponse is not null)
|
||||
{
|
||||
shouldExit = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case RequestInfoEvent requestInfo:
|
||||
Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
|
||||
externalResponse = requestInfo.Request;
|
||||
break;
|
||||
|
||||
case ConversationUpdateEvent invokeEvent:
|
||||
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
|
||||
break;
|
||||
|
||||
case MessageActivityEvent activityEvent:
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("\nACTIVITY:");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(activityEvent.Message.Trim());
|
||||
break;
|
||||
|
||||
case AgentRunUpdateEvent streamEvent:
|
||||
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
|
||||
{
|
||||
hasStreamed = false;
|
||||
messageId = streamEvent.Update.MessageId;
|
||||
|
||||
if (messageId is not null)
|
||||
{
|
||||
string? agentName = streamEvent.Update.AuthorName ?? streamEvent.Update.AgentId ?? nameof(ChatRole.Assistant);
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write($"\n{agentName.ToUpperInvariant()}:");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{messageId}]");
|
||||
}
|
||||
}
|
||||
|
||||
ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
|
||||
switch (chatUpdate?.RawRepresentation)
|
||||
{
|
||||
case ImageGenerationCallResponseItem messageUpdate:
|
||||
await DownloadFileContentAsync(Path.GetFileName("response.png"), messageUpdate.ImageResultBytes).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case FunctionCallResponseItem actionUpdate:
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.Write($"Calling tool: {actionUpdate.FunctionName}");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{actionUpdate.CallId}]");
|
||||
break;
|
||||
|
||||
case McpToolCallItem actionUpdate:
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.Write($"Calling tool: {actionUpdate.ToolName}");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{actionUpdate.Id}]");
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Console.ResetColor();
|
||||
Console.Write(streamEvent.Update.Text);
|
||||
hasStreamed |= !string.IsNullOrEmpty(streamEvent.Update.Text);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
|
||||
case AgentRunResponseEvent messageEvent:
|
||||
try
|
||||
{
|
||||
if (hasStreamed)
|
||||
{
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
if (messageEvent.Response.Usage is not null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
#if DEBUG_OUTPUT
|
||||
Debug.WriteLine($"UNHANDLED: {workflowEvent.GetType().Name}");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldExit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return externalResponse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle request for external input.
|
||||
/// </summary>
|
||||
private async ValueTask<ExternalInputResponse> HandleExternalRequestAsync(ExternalRequest request)
|
||||
{
|
||||
ExternalInputRequest inputRequest =
|
||||
request.DataAs<ExternalInputRequest>() ??
|
||||
throw new InvalidOperationException($"Expected external request type: {request.GetType().Name}.");
|
||||
|
||||
List<ChatMessage> responseMessages = [];
|
||||
|
||||
foreach (ChatMessage message in inputRequest.AgentResponse.Messages)
|
||||
{
|
||||
await foreach (ChatMessage responseMessage in this.ProcessInputMessageAsync(message).ConfigureAwait(false))
|
||||
{
|
||||
responseMessages.Add(responseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (responseMessages.Count == 0)
|
||||
{
|
||||
// Must be request for user input.
|
||||
responseMessages.Add(HandleUserInputRequest(inputRequest));
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
return new ExternalInputResponse(responseMessages);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<ChatMessage> ProcessInputMessageAsync(ChatMessage message)
|
||||
{
|
||||
foreach (AIContent requestItem in message.Contents)
|
||||
{
|
||||
ChatMessage? responseMessage =
|
||||
requestItem switch
|
||||
{
|
||||
FunctionCallContent functionCall => await InvokeFunctionAsync(functionCall).ConfigureAwait(false),
|
||||
FunctionApprovalRequestContent functionApprovalRequest => ApproveFunction(functionApprovalRequest),
|
||||
McpServerToolApprovalRequestContent mcpApprovalRequest => ApproveMCP(mcpApprovalRequest),
|
||||
_ => HandleUnknown(requestItem),
|
||||
};
|
||||
|
||||
if (responseMessage is not null)
|
||||
{
|
||||
yield return responseMessage;
|
||||
}
|
||||
}
|
||||
|
||||
ChatMessage? HandleUnknown(AIContent request)
|
||||
{
|
||||
#if DEBUG_OUTPUT
|
||||
Notify($"INPUT - Unknown: {request.GetType().Name} [{request.RawRepresentation?.GetType().Name ?? "*"}]");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
ChatMessage ApproveFunction(FunctionApprovalRequestContent functionApprovalRequest)
|
||||
{
|
||||
Notify($"INPUT - Approving Function: {functionApprovalRequest.FunctionCall.Name}");
|
||||
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]);
|
||||
}
|
||||
|
||||
ChatMessage ApproveMCP(McpServerToolApprovalRequestContent mcpApprovalRequest)
|
||||
{
|
||||
Notify($"INPUT - Approving MCP: {mcpApprovalRequest.ToolCall.ToolName}");
|
||||
return new ChatMessage(ChatRole.User, [mcpApprovalRequest.CreateResponse(approved: true)]);
|
||||
}
|
||||
|
||||
async Task<ChatMessage> InvokeFunctionAsync(FunctionCallContent functionCall)
|
||||
{
|
||||
Notify($"INPUT - Executing Function: {functionCall.Name}");
|
||||
AIFunction functionTool = this.FunctionMap[functionCall.Name];
|
||||
AIFunctionArguments? functionArguments = functionCall.Arguments is null ? null : new(functionCall.Arguments.NormalizePortableValues());
|
||||
object? result = await functionTool.InvokeAsync(functionArguments).ConfigureAwait(false);
|
||||
return new ChatMessage(ChatRole.Tool, [new FunctionResultContent(functionCall.CallId, JsonSerializer.Serialize(result))]);
|
||||
}
|
||||
}
|
||||
|
||||
private static ChatMessage HandleUserInputRequest(ExternalInputRequest request)
|
||||
{
|
||||
string prompt =
|
||||
string.IsNullOrWhiteSpace(request.AgentResponse.Text) || request.AgentResponse.ResponseId is not null ?
|
||||
"INPUT:" :
|
||||
request.AgentResponse.Text;
|
||||
|
||||
string? userInput;
|
||||
do
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGreen;
|
||||
Console.Write($"{prompt} ");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
userInput = Console.ReadLine();
|
||||
}
|
||||
while (string.IsNullOrWhiteSpace(userInput));
|
||||
|
||||
return new ChatMessage(ChatRole.User, userInput);
|
||||
}
|
||||
|
||||
private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content)
|
||||
{
|
||||
string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename));
|
||||
filePath = Path.ChangeExtension(filePath, ".png");
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, content.ToArray()).ConfigureAwait(false);
|
||||
|
||||
Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/C start {filePath}"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Shared.Workflows;
|
||||
|
||||
internal static class Application
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration key used to identify the Foundry project endpoint.
|
||||
/// </summary>
|
||||
public static class Settings
|
||||
{
|
||||
public const string FoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
|
||||
public const string FoundryModelMini = "FOUNDRY_MODEL_DEPLOYMENT_NAME";
|
||||
public const string FoundryModelFull = "FOUNDRY_MEDIA_DEPLOYMENT_NAME";
|
||||
public const string FoundryGroundingTool = "FOUNDRY_CONNECTION_GROUNDING_TOOL";
|
||||
}
|
||||
|
||||
public static string GetInput(string[] args)
|
||||
{
|
||||
string? input = args.FirstOrDefault();
|
||||
|
||||
try
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGreen;
|
||||
|
||||
Console.Write("\nINPUT: ");
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
Console.WriteLine(input);
|
||||
return input;
|
||||
}
|
||||
while (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
input = Console.ReadLine();
|
||||
}
|
||||
|
||||
return input.Trim();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public static string GetValue(this IConfiguration configuration, string settingName) =>
|
||||
configuration[settingName] ??
|
||||
throw new InvalidOperationException($"Undefined configuration setting: {settingName}");
|
||||
|
||||
/// <summary>
|
||||
/// Initialize configuration and environment
|
||||
/// </summary>
|
||||
public static IConfigurationRoot InitializeConfig() =>
|
||||
new ConfigurationBuilder()
|
||||
.AddUserSecrets(Assembly.GetExecutingAssembly())
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Workflow Settings
|
||||
|
||||
Common support configuration and environment used in workflow samples.
|
||||
|
||||
To use this in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</PropertyGroup>
|
||||
```
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
|
||||
internal abstract class AgentProvider(IConfiguration configuration)
|
||||
{
|
||||
public static class Names
|
||||
{
|
||||
public const string FunctionTool = "FUNCTIONTOOL";
|
||||
public const string Marketing = "MARKETING";
|
||||
public const string MathChat = "MATHCHAT";
|
||||
}
|
||||
|
||||
public static class Settings
|
||||
{
|
||||
public const string FoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
|
||||
public const string FoundryModelMini = "FOUNDRY_MODEL_DEPLOYMENT_NAME";
|
||||
public const string FoundryModelFull = "FOUNDRY_MEDIA_DEPLOYMENT_NAME";
|
||||
public const string FoundryGroundingTool = "FOUNDRY_CONNECTION_GROUNDING_TOOL";
|
||||
}
|
||||
|
||||
public static AgentProvider Create(IConfiguration configuration, string providerType) =>
|
||||
providerType.ToUpperInvariant() switch
|
||||
{
|
||||
Names.FunctionTool => new FunctionToolAgentProvider(configuration),
|
||||
Names.Marketing => new MarketingAgentProvider(configuration),
|
||||
Names.MathChat => new MathChatAgentProvider(configuration),
|
||||
_ => new TestAgentProvider(configuration),
|
||||
};
|
||||
|
||||
public async ValueTask CreateAgentsAsync()
|
||||
{
|
||||
Uri foundryEndpoint = new(this.GetSetting(Settings.FoundryEndpoint));
|
||||
|
||||
await foreach (AgentVersion agent in this.CreateAgentsAsync(foundryEndpoint))
|
||||
{
|
||||
Console.WriteLine($"Created agent: {agent.Name}:{agent.Version})");
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint);
|
||||
|
||||
protected string GetSetting(string settingName) =>
|
||||
configuration[settingName] ??
|
||||
throw new InvalidOperationException($"Undefined configuration setting: {settingName}");
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using OpenAI.Responses;
|
||||
using Shared.Foundry;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
|
||||
internal sealed class FunctionToolAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
|
||||
{
|
||||
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
MenuPlugin menuPlugin = new();
|
||||
AIFunction[] functions =
|
||||
[
|
||||
AIFunctionFactory.Create(menuPlugin.GetMenu),
|
||||
AIFunctionFactory.Create(menuPlugin.GetSpecials),
|
||||
AIFunctionFactory.Create(menuPlugin.GetItemPrice),
|
||||
];
|
||||
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
yield return
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "MenuAgent",
|
||||
agentDefinition: this.DefineMenuAgent(functions),
|
||||
agentDescription: "Provides information about the restaurant menu");
|
||||
}
|
||||
|
||||
private PromptAgentDefinition DefineMenuAgent(AIFunction[] functions)
|
||||
{
|
||||
PromptAgentDefinition agentDefinition =
|
||||
new(this.GetSetting(Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Answer the users questions on the menu.
|
||||
For questions or input that do not require searching the documentation, inform the
|
||||
user that you can only answer questions what's on the menu.
|
||||
"""
|
||||
};
|
||||
|
||||
foreach (AIFunction function in functions)
|
||||
{
|
||||
agentDefinition.Tools.Add(function.AsOpenAIResponseTool());
|
||||
}
|
||||
|
||||
return agentDefinition;
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.Foundry;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
|
||||
internal sealed class MarketingAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
|
||||
{
|
||||
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
yield return
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "AnalystAgent",
|
||||
agentDefinition: this.DefineAnalystAgent(),
|
||||
agentDescription: "Analyst agent for Marketing workflow");
|
||||
|
||||
yield return
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "WriterAgent",
|
||||
agentDefinition: this.DefineWriterAgent(),
|
||||
agentDescription: "Writer agent for Marketing workflow");
|
||||
|
||||
yield return
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "EditorAgent",
|
||||
agentDefinition: this.DefineEditorAgent(),
|
||||
agentDescription: "Editor agent for Marketing workflow");
|
||||
}
|
||||
|
||||
private PromptAgentDefinition DefineAnalystAgent() =>
|
||||
new(this.GetSetting(Settings.FoundryModelFull))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are a marketing analyst. Given a product description, identify:
|
||||
- Key features
|
||||
- Target audience
|
||||
- Unique selling points
|
||||
""",
|
||||
Tools =
|
||||
{
|
||||
//AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
|
||||
// new BingGroundingSearchToolParameters(
|
||||
// [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))]))
|
||||
}
|
||||
};
|
||||
|
||||
private PromptAgentDefinition DefineWriterAgent() =>
|
||||
new(this.GetSetting(Settings.FoundryModelFull))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
|
||||
compose a compelling marketing copy (like a newsletter section) that highlights these points.
|
||||
Output should be short (around 150 words), output just the copy as a single text block.
|
||||
"""
|
||||
};
|
||||
|
||||
private PromptAgentDefinition DefineEditorAgent() =>
|
||||
new(this.GetSetting(Settings.FoundryModelFull))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
|
||||
give format and make it polished. Output the final improved copy as a single text block.
|
||||
"""
|
||||
};
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.Foundry;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
|
||||
internal sealed class MathChatAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
|
||||
{
|
||||
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
yield return
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "StudentAgent",
|
||||
agentDefinition: this.DefineStudentAgent(),
|
||||
agentDescription: "Student agent for MathChat workflow");
|
||||
|
||||
yield return
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "TeacherAgent",
|
||||
agentDefinition: this.DefineTeacherAgent(),
|
||||
agentDescription: "Teacher agent for MathChat workflow");
|
||||
}
|
||||
|
||||
private PromptAgentDefinition DefineStudentAgent() =>
|
||||
new(this.GetSetting(Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Your job is help a math teacher practice teaching by making intentional mistakes.
|
||||
You attempt to solve the given math problem, but with intentional mistakes so the teacher can help.
|
||||
Always incorporate the teacher's advice to fix your next response.
|
||||
You have the math-skills of a 6th grader.
|
||||
"""
|
||||
};
|
||||
|
||||
private PromptAgentDefinition DefineTeacherAgent() =>
|
||||
new(this.GetSetting(Settings.FoundryModelMini))
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
Review and coach the student's approach to solving the given math problem.
|
||||
Don't repeat the solution or try and solve it.
|
||||
If the student has demonstrated comprehension and responded to all of your feedback,
|
||||
give the student your congratulations by using the word "congratulations".
|
||||
"""
|
||||
};
|
||||
}
|
||||
+8
-7
@@ -5,32 +5,33 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
|
||||
#pragma warning disable CA1822
|
||||
|
||||
public sealed class MenuPlugin
|
||||
{
|
||||
public IEnumerable<AIFunction> GetTools()
|
||||
{
|
||||
yield return AIFunctionFactory.Create(this.GetMenu, name: $"{nameof(MenuPlugin)}_{nameof(GetMenu)}");
|
||||
yield return AIFunctionFactory.Create(this.GetSpecials, name: $"{nameof(MenuPlugin)}_{nameof(GetSpecials)}");
|
||||
yield return AIFunctionFactory.Create(this.GetItemPrice, name: $"{nameof(MenuPlugin)}_{nameof(GetItemPrice)}");
|
||||
yield return AIFunctionFactory.Create(this.GetMenu);
|
||||
yield return AIFunctionFactory.Create(this.GetSpecials);
|
||||
yield return AIFunctionFactory.Create(this.GetItemPrice);
|
||||
}
|
||||
|
||||
[KernelFunction, Description("Provides a list items on the menu.")]
|
||||
[Description("Provides a list items on the menu.")]
|
||||
public MenuItem[] GetMenu()
|
||||
{
|
||||
return s_menuItems;
|
||||
}
|
||||
|
||||
[KernelFunction, Description("Provides a list of specials from the menu.")]
|
||||
[Description("Provides a list of specials from the menu.")]
|
||||
public MenuItem[] GetSpecials()
|
||||
{
|
||||
return [.. s_menuItems.Where(i => i.IsSpecial)];
|
||||
}
|
||||
|
||||
[KernelFunction, Description("Provides the price of the requested menu item.")]
|
||||
[Description("Provides the price of the requested menu item.")]
|
||||
public float? GetItemPrice(
|
||||
[Description("The name of the menu item.")]
|
||||
string name)
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
type: foundry_agent
|
||||
name: BasicAgent
|
||||
description: Basic agent for integration tests
|
||||
model:
|
||||
id: ${FOUNDRY_MEDIA_DEPLOYMENT_NAME}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.Foundry;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
|
||||
internal sealed class TestAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
|
||||
{
|
||||
protected override async IAsyncEnumerable<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
|
||||
yield return
|
||||
await agentsClient.CreateAgentAsync(
|
||||
agentName: "TestAgent",
|
||||
agentDefinition: this.DefineMenuAgent(),
|
||||
agentDescription: "Provides information about the restaurant menu");
|
||||
}
|
||||
|
||||
private PromptAgentDefinition DefineMenuAgent() =>
|
||||
new(this.GetSetting(Settings.FoundryModelFull));
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
type: foundry_agent
|
||||
name: ToolAgent
|
||||
description: Agent with a function tool defined.
|
||||
model:
|
||||
id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME}
|
||||
tools:
|
||||
- id: MenuPlugin_GetMenu
|
||||
type: function
|
||||
description: Provides a list items on the menu.
|
||||
- id: MenuPlugin_GetSpecials
|
||||
type: function
|
||||
description: Provides a list of specials from the menu.
|
||||
- id: MenuPlugin_GetItemPrice
|
||||
type: function
|
||||
description: Provides the price of the requested menu item.
|
||||
+1
-35
@@ -1,10 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Azure;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -18,7 +15,7 @@ public sealed class AzureAgentProviderTest(ITestOutputHelper output) : Integrati
|
||||
public async Task ConversationTestAsync()
|
||||
{
|
||||
// Arrange
|
||||
AzureAgentProvider provider = new(this.FoundryConfiguration.Endpoint, new AzureCliCredential());
|
||||
AzureAgentProvider provider = new(this.TestEndpoint, new AzureCliCredential());
|
||||
// Act
|
||||
string conversationId = await provider.CreateConversationAsync();
|
||||
// Assert
|
||||
@@ -43,35 +40,4 @@ public sealed class AzureAgentProviderTest(ITestOutputHelper output) : Integrati
|
||||
Assert.NotNull(message);
|
||||
Assert.Equal(messages[3].Text, message.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAgentTestAsync()
|
||||
{
|
||||
// Arrange
|
||||
AzureAgentProvider provider = new(this.FoundryConfiguration.Endpoint, new AzureCliCredential());
|
||||
string agentName = $"TestAgent-{DateTime.UtcNow:yyMMdd-HHmmss-fff}";
|
||||
|
||||
string agent1Id = await this.CreateAgentAsync();
|
||||
string agent2Id = await this.CreateAgentAsync(agentName);
|
||||
|
||||
// Act
|
||||
AIAgent agent1 = await provider.GetAgentAsync(agent1Id);
|
||||
// Assert
|
||||
Assert.Equal(agent1Id, agent1.Id);
|
||||
|
||||
// Act
|
||||
AIAgent agent2 = await provider.GetAgentAsync(agent2Id);
|
||||
// Assert
|
||||
Assert.Equal(agent2Id, agent2.Id);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<RequestFailedException>(() => provider.GetAgentAsync(agentName));
|
||||
}
|
||||
|
||||
private async ValueTask<string> CreateAgentAsync(string? name = null)
|
||||
{
|
||||
PersistentAgentsClient client = new(this.FoundryConfiguration.Endpoint, new AzureCliCredential());
|
||||
PersistentAgent agent = await client.Administration.CreateAgentAsync(this.FoundryConfiguration.DeploymentName, name: name);
|
||||
return agent.Id;
|
||||
}
|
||||
}
|
||||
|
||||
+16
-5
@@ -4,6 +4,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -22,7 +23,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
[InlineData("ConversationMessages.yaml", "ConversationMessages.json")]
|
||||
[InlineData("ConversationMessages.yaml", "ConversationMessages.json", true)]
|
||||
public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
|
||||
this.RunWorkflowAsync(Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName), testcaseFileName, externalConveration);
|
||||
this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample: false), testcaseFileName, externalConveration);
|
||||
|
||||
[Theory]
|
||||
[InlineData("Marketing.yaml", "Marketing.json")]
|
||||
@@ -30,14 +31,24 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
[InlineData("MathChat.yaml", "MathChat.json", true)]
|
||||
[InlineData("DeepResearch.yaml", "DeepResearch.json", Skip = "Long running")]
|
||||
public Task ValidateScenarioAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
|
||||
this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName), testcaseFileName, externalConveration);
|
||||
this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample: true), testcaseFileName, externalConveration);
|
||||
|
||||
[Fact]
|
||||
public Task ValidateMultiTurnAsync() =>
|
||||
this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", "HumanInLoop.yaml"), "HumanInLoop.json", useJsonCheckpoint: true);
|
||||
[Theory]
|
||||
[InlineData("ConfirmInput.yaml", "ConfirmInput.json", true)]
|
||||
[InlineData("RequestExternalInput.yaml", "RequestExternalInput.json", false)]
|
||||
public Task ValidateMultiTurnAsync(string workflowFileName, string testcaseFileName, bool isSample) =>
|
||||
this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample), testcaseFileName, useJsonCheckpoint: true);
|
||||
|
||||
private static string GetWorkflowPath(string workflowFileName, bool isSample) =>
|
||||
isSample
|
||||
? Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName)
|
||||
: Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName);
|
||||
|
||||
protected override async Task RunAndVerifyAsync<TInput>(Testcase testcase, string workflowPath, DeclarativeWorkflowOptions workflowOptions, TInput input, bool useJsonCheckpoint)
|
||||
{
|
||||
AgentProvider agentProvider = AgentProvider.Create(this.Configuration, Path.GetFileNameWithoutExtension(workflowPath));
|
||||
await agentProvider.CreateAgentsAsync().ConfigureAwait(false);
|
||||
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<TInput>(workflowPath, workflowOptions);
|
||||
|
||||
WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowPath));
|
||||
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.AzureAI;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
|
||||
|
||||
internal static class AgentFactory
|
||||
{
|
||||
private static readonly Dictionary<string, string> s_agentDefinitions =
|
||||
new()
|
||||
{
|
||||
["FOUNDRY_AGENT_TEST"] = "TestAgent.yaml",
|
||||
["FOUNDRY_AGENT_TOOL"] = "ToolAgent.yaml",
|
||||
["FOUNDRY_AGENT_ANSWER"] = "QuestionAgent.yaml",
|
||||
["FOUNDRY_AGENT_STUDENT"] = "StudentAgent.yaml",
|
||||
["FOUNDRY_AGENT_TEACHER"] = "TeacherAgent.yaml",
|
||||
["FOUNDRY_AGENT_RESEARCHANALYST"] = "AnalystAgent.yaml",
|
||||
["FOUNDRY_AGENT_RESEARCHCODER"] = "CoderAgent.yaml",
|
||||
["FOUNDRY_AGENT_RESEARCHMANAGER"] = "ManagerAgent.yaml",
|
||||
["FOUNDRY_AGENT_RESEARCHWEATHER"] = "WeatherAgent.yaml",
|
||||
["FOUNDRY_AGENT_RESEARCHWEB"] = "WebAgent.yaml",
|
||||
};
|
||||
|
||||
private static FrozenDictionary<string, string?>? s_agentMap;
|
||||
|
||||
public static async Task<FrozenDictionary<string, string?>> GetAgentsAsync(AzureAIConfiguration config, IConfiguration configuration, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (s_agentMap is not null)
|
||||
{
|
||||
return s_agentMap;
|
||||
}
|
||||
|
||||
PersistentAgentsClient clientAgents = new(config.Endpoint, new AzureCliCredential());
|
||||
AIProjectClient clientProjects = new(new Uri(config.Endpoint), new AzureCliCredential());
|
||||
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
|
||||
kernelBuilder.Services.AddSingleton(clientAgents);
|
||||
kernelBuilder.Services.AddSingleton(clientProjects);
|
||||
kernelBuilder.Plugins.AddFromType<Agents.MenuPlugin>();
|
||||
AgentCreationOptions creationOptions = new() { Kernel = kernelBuilder.Build() };
|
||||
AzureAIAgentFactory factory = new();
|
||||
string repoRoot = WorkflowTest.GetRepoFolder();
|
||||
|
||||
return s_agentMap = (await Task.WhenAll(s_agentDefinitions.Select(kvp => CreateAgentAsync(kvp.Key, kvp.Value, cancellationToken)))).ToFrozenDictionary(t => t.Name, t => t.Id);
|
||||
|
||||
async Task<(string Name, string? Id)> CreateAgentAsync(string id, string file, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
string filePath = Path.Combine(Environment.CurrentDirectory, "Agents", file);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
filePath = Path.Combine(repoRoot, "workflow-samples", "setup", file);
|
||||
}
|
||||
Assert.True(File.Exists(filePath), $"Agent definition file not found: {file}");
|
||||
|
||||
Debug.WriteLine($"TEST AGENT: Creating - {file}");
|
||||
string agentText = File.ReadAllText(filePath);
|
||||
|
||||
Agent? agent = await factory.CreateAgentFromYamlAsync(agentText, creationOptions, configuration, cancellationToken);
|
||||
|
||||
Assert.NotNull(agent?.Name);
|
||||
|
||||
Debug.WriteLine($"TEST AGENT: {agent.Name} => {agent.Id} [{id}]");
|
||||
|
||||
return (id, agent.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine($"FAILURE: Error creating agent {id}: {exception.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-22
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.IntegrationTests;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
|
||||
@@ -21,25 +20,20 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
|
||||
public abstract class IntegrationTest : IDisposable
|
||||
{
|
||||
private IConfigurationRoot? _configuration;
|
||||
private AzureAIConfiguration? _foundryConfiguration;
|
||||
|
||||
protected IConfigurationRoot Configuration => this._configuration ??= InitializeConfig();
|
||||
|
||||
internal AzureAIConfiguration FoundryConfiguration
|
||||
{
|
||||
get
|
||||
{
|
||||
this._foundryConfiguration ??= this.Configuration.GetSection("AzureAI").Get<AzureAIConfiguration>();
|
||||
Assert.NotNull(this._foundryConfiguration);
|
||||
return this._foundryConfiguration;
|
||||
}
|
||||
}
|
||||
public Uri TestEndpoint { get; }
|
||||
|
||||
public TestOutputAdapter Output { get; }
|
||||
|
||||
protected IntegrationTest(ITestOutputHelper output)
|
||||
{
|
||||
this.Output = new TestOutputAdapter(output);
|
||||
this.TestEndpoint =
|
||||
new Uri(
|
||||
this.Configuration[AgentProvider.Settings.FoundryEndpoint] ??
|
||||
throw new InvalidOperationException($"Undefined configuration setting: {AgentProvider.Settings.FoundryEndpoint}"));
|
||||
Console.SetOut(this.Output);
|
||||
SetProduct();
|
||||
}
|
||||
@@ -70,15 +64,8 @@ public abstract class IntegrationTest : IDisposable
|
||||
|
||||
protected async ValueTask<DeclarativeWorkflowOptions> CreateOptionsAsync(bool externalConversation = false, params IEnumerable<AIFunction> functionTools)
|
||||
{
|
||||
FrozenDictionary<string, string?> agentMap = await AgentFactory.GetAgentsAsync(this.FoundryConfiguration, this.Configuration);
|
||||
|
||||
IConfiguration workflowConfig =
|
||||
new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(agentMap)
|
||||
.Build();
|
||||
|
||||
AzureAgentProvider agentProvider =
|
||||
new(this.FoundryConfiguration.Endpoint, new AzureCliCredential())
|
||||
new(this.TestEndpoint, new AzureCliCredential())
|
||||
{
|
||||
Functions = functionTools,
|
||||
};
|
||||
@@ -92,7 +79,6 @@ public abstract class IntegrationTest : IDisposable
|
||||
return
|
||||
new DeclarativeWorkflowOptions(agentProvider)
|
||||
{
|
||||
Configuration = workflowConfig,
|
||||
ConversationId = conversationId,
|
||||
LoggerFactory = this.Output
|
||||
};
|
||||
@@ -100,7 +86,6 @@ public abstract class IntegrationTest : IDisposable
|
||||
|
||||
private static IConfigurationRoot InitializeConfig() =>
|
||||
new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.Development.json", true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddUserSecrets(Assembly.GetExecutingAssembly())
|
||||
.Build();
|
||||
|
||||
+26
-20
@@ -17,23 +17,26 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
|
||||
internal sealed class WorkflowHarness(Workflow workflow, string runId)
|
||||
{
|
||||
private CheckpointManager? _checkpointManager;
|
||||
private CheckpointInfo? LastCheckpoint { get; set; }
|
||||
private CheckpointInfo? _lastCheckpoint;
|
||||
|
||||
public async Task<WorkflowEvents> RunTestcaseAsync<TInput>(Testcase testcase, TInput input, bool useJson = false) where TInput : notnull
|
||||
{
|
||||
WorkflowEvents workflowEvents = await this.RunWorkflowAsync(input, useJson);
|
||||
int requestCount = (workflowEvents.InputEvents.Count + 1) / 2;
|
||||
int requestCount = workflowEvents.InputEvents.Count;
|
||||
int responseCount = 0;
|
||||
while (requestCount > responseCount)
|
||||
{
|
||||
ExternalRequest request = workflowEvents.InputEvents[workflowEvents.InputEvents.Count - 1].Request;
|
||||
Assert.NotNull(testcase.Setup.Responses);
|
||||
Assert.NotEmpty(testcase.Setup.Responses);
|
||||
string inputText = testcase.Setup.Responses[responseCount].Value;
|
||||
Console.WriteLine($"ID: {request.RequestId}");
|
||||
Console.WriteLine($"INPUT: {inputText}");
|
||||
++responseCount;
|
||||
WorkflowEvents runEvents = await this.ResumeAsync(new AnswerResponse(inputText)).ConfigureAwait(false);
|
||||
ExternalResponse response = request.CreateResponse(new ExternalInputResponse(new ChatMessage(ChatRole.User, inputText)));
|
||||
WorkflowEvents runEvents = await this.ResumeAsync(response).ConfigureAwait(false);
|
||||
workflowEvents = new WorkflowEvents([.. workflowEvents.Events, .. runEvents.Events]);
|
||||
requestCount = (workflowEvents.InputEvents.Count + 1) / 2;
|
||||
requestCount = workflowEvents.InputEvents.Count;
|
||||
}
|
||||
|
||||
return workflowEvents;
|
||||
@@ -44,15 +47,15 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId)
|
||||
Console.WriteLine("RUNNING WORKFLOW...");
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, this.GetCheckpointManager(useJson), runId);
|
||||
IReadOnlyList<WorkflowEvent> workflowEvents = await MonitorAndDisposeWorkflowRunAsync(run).ToArrayAsync();
|
||||
this.LastCheckpoint = workflowEvents.OfType<SuperStepCompletedEvent>().LastOrDefault()?.CompletionInfo?.Checkpoint;
|
||||
this._lastCheckpoint = workflowEvents.OfType<SuperStepCompletedEvent>().LastOrDefault()?.CompletionInfo?.Checkpoint;
|
||||
return new WorkflowEvents(workflowEvents);
|
||||
}
|
||||
|
||||
public async Task<WorkflowEvents> ResumeAsync(object response)
|
||||
public async Task<WorkflowEvents> ResumeAsync(ExternalResponse response)
|
||||
{
|
||||
Console.WriteLine("\nRESUMING WORKFLOW...");
|
||||
Assert.NotNull(this.LastCheckpoint);
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, this.GetCheckpointManager(), runId);
|
||||
Assert.NotNull(this._lastCheckpoint);
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.ResumeStreamAsync(workflow, this._lastCheckpoint, this.GetCheckpointManager(), runId);
|
||||
IReadOnlyList<WorkflowEvent> workflowEvents = await MonitorAndDisposeWorkflowRunAsync(run, response).ToArrayAsync();
|
||||
return new WorkflowEvents(workflowEvents);
|
||||
}
|
||||
@@ -93,29 +96,32 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId)
|
||||
return this._checkpointManager;
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<WorkflowEvent> MonitorAndDisposeWorkflowRunAsync(Checkpointed<StreamingRun> run, object? response = null)
|
||||
private static async IAsyncEnumerable<WorkflowEvent> MonitorAndDisposeWorkflowRunAsync(Checkpointed<StreamingRun> run, ExternalResponse? response = null)
|
||||
{
|
||||
await using IAsyncDisposable disposeRun = run;
|
||||
|
||||
if (response is not null)
|
||||
{
|
||||
await run.Run.SendResponseAsync(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
bool exitLoop = false;
|
||||
bool hasRequest = false;
|
||||
|
||||
await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
bool exitLoop = false;
|
||||
|
||||
switch (workflowEvent)
|
||||
{
|
||||
case RequestInfoEvent requestInfo:
|
||||
Console.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
|
||||
if (response is not null)
|
||||
{
|
||||
ExternalResponse requestResponse = requestInfo.Request.CreateResponse(response);
|
||||
await run.Run.SendResponseAsync(requestResponse).ConfigureAwait(false);
|
||||
response = null;
|
||||
}
|
||||
else
|
||||
case SuperStepCompletedEvent:
|
||||
if (hasRequest)
|
||||
{
|
||||
exitLoop = true;
|
||||
}
|
||||
break;
|
||||
case RequestInfoEvent requestInfo:
|
||||
Console.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
|
||||
hasRequest = true;
|
||||
break;
|
||||
|
||||
case ConversationUpdateEvent conversationEvent:
|
||||
Console.WriteLine($"CONVERSATION: {conversationEvent.ConversationId}");
|
||||
|
||||
+11
-7
@@ -33,6 +33,9 @@ public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : Inte
|
||||
|
||||
private async Task RunWorkflowAsync(bool autoInvoke, params IEnumerable<AIFunction> functionTools)
|
||||
{
|
||||
AgentProvider agentProvider = AgentProvider.Create(this.Configuration, AgentProvider.Names.FunctionTool);
|
||||
await agentProvider.CreateAgentsAsync().ConfigureAwait(false);
|
||||
|
||||
string workflowPath = GetWorkflowPath("FunctionTool.yaml");
|
||||
Dictionary<string, AIFunction> functionMap = autoInvoke ? [] : functionTools.ToDictionary(tool => tool.Name, tool => tool);
|
||||
DeclarativeWorkflowOptions workflowOptions = await this.CreateOptionsAsync(externalConversation: false, autoInvoke ? functionTools : []);
|
||||
@@ -47,11 +50,11 @@ public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : Inte
|
||||
Assert.False(autoInvoke);
|
||||
|
||||
RequestInfoEvent inputEvent = workflowEvents.InputEvents[workflowEvents.InputEvents.Count - 1];
|
||||
AgentFunctionToolRequest? toolRequest = inputEvent.Request.Data.As<AgentFunctionToolRequest>();
|
||||
ExternalInputRequest? toolRequest = inputEvent.Request.Data.As<ExternalInputRequest>();
|
||||
Assert.NotNull(toolRequest);
|
||||
|
||||
List<(FunctionCallContent, AIFunction)> functionCalls = [];
|
||||
foreach (FunctionCallContent functionCall in toolRequest.FunctionCalls)
|
||||
foreach (FunctionCallContent functionCall in toolRequest.AgentResponse.Messages.SelectMany(message => message.Contents).OfType<FunctionCallContent>())
|
||||
{
|
||||
this.Output.WriteLine($"TOOL REQUEST: {functionCall.Name}");
|
||||
if (!functionMap.TryGetValue(functionCall.Name, out AIFunction? functionTool))
|
||||
@@ -62,11 +65,12 @@ public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : Inte
|
||||
functionCalls.Add((functionCall, functionTool));
|
||||
}
|
||||
|
||||
IList<FunctionResultContent> functionResults = await InvokeToolsAsync(functionCalls);
|
||||
IList<AIContent> functionResults = await InvokeToolsAsync(functionCalls);
|
||||
|
||||
++responseCount;
|
||||
|
||||
WorkflowEvents runEvents = await harness.ResumeAsync(AgentFunctionToolResponse.Create(toolRequest, functionResults)).ConfigureAwait(false);
|
||||
ChatMessage resultMessage = new(ChatRole.Tool, functionResults);
|
||||
WorkflowEvents runEvents = await harness.ResumeAsync(inputEvent.Request.CreateResponse(new ExternalInputResponse(resultMessage))).ConfigureAwait(false);
|
||||
workflowEvents = new WorkflowEvents([.. workflowEvents.Events, .. runEvents.Events]);
|
||||
}
|
||||
|
||||
@@ -79,13 +83,13 @@ public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : Inte
|
||||
Assert.NotEmpty(workflowEvents.InputEvents);
|
||||
}
|
||||
|
||||
Assert.Equal(autoInvoke ? 3 : 5, workflowEvents.AgentResponseEvents.Count);
|
||||
Assert.Equal(autoInvoke ? 3 : 4, workflowEvents.AgentResponseEvents.Count);
|
||||
Assert.All(workflowEvents.AgentResponseEvents, response => response.Response.Text.Contains("4.95"));
|
||||
}
|
||||
|
||||
private static async ValueTask<IList<FunctionResultContent>> InvokeToolsAsync(IEnumerable<(FunctionCallContent, AIFunction)> functionCalls)
|
||||
private static async ValueTask<IList<AIContent>> InvokeToolsAsync(IEnumerable<(FunctionCallContent, AIFunction)> functionCalls)
|
||||
{
|
||||
List<FunctionResultContent> results = [];
|
||||
List<AIContent> results = [];
|
||||
|
||||
foreach ((FunctionCallContent functionCall, AIFunction functionTool) in functionCalls)
|
||||
{
|
||||
|
||||
+12
-10
@@ -4,10 +4,11 @@ using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Files;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
|
||||
@@ -18,9 +19,9 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
|
||||
public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(output)
|
||||
{
|
||||
private const string WorkflowFileName = "MediaInput.yaml";
|
||||
private const string ImageReference = "https://upload.wikimedia.org/wikipedia/commons/5/56/White_shark.jpg";
|
||||
private const string ImageReference = "https://sample-files.com/downloads/documents/pdf/basic-text.pdf";
|
||||
|
||||
[Fact(Skip = "Service issue prevents this simple case")]
|
||||
[Fact]
|
||||
public async Task ValidateImageUrlAsync()
|
||||
{
|
||||
this.Output.WriteLine($"Image: {ImageReference}");
|
||||
@@ -30,20 +31,21 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o
|
||||
[Fact]
|
||||
public async Task ValidateImageDataAsync()
|
||||
{
|
||||
byte[] imageData = await DownloadImageAsync();
|
||||
byte[] imageData = await DownloadFileAsync();
|
||||
string encodedData = Convert.ToBase64String(imageData);
|
||||
string imageUrl = $"data:image/png;base64,{encodedData}";
|
||||
this.Output.WriteLine($"Image: {imageUrl.Substring(0, 112)}...");
|
||||
await this.ValidateImageAsync(new DataContent(imageUrl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "Not behaving will in git-hub build pipeline")]
|
||||
public async Task ValidateImageUploadAsync()
|
||||
{
|
||||
byte[] imageData = await DownloadImageAsync();
|
||||
PersistentAgentsClient client = new(this.FoundryConfiguration.Endpoint, new AzureCliCredential());
|
||||
byte[] imageData = await DownloadFileAsync();
|
||||
AgentsClient client = new(this.TestEndpoint, new AzureCliCredential());
|
||||
using MemoryStream contentStream = new(imageData);
|
||||
PersistentAgentFileInfo fileInfo = await client.Files.UploadFileAsync(contentStream, PersistentAgentFilePurpose.Agents, "image.jpg");
|
||||
OpenAIFileClient fileClient = client.GetOpenAIClient().GetOpenAIFileClient();
|
||||
OpenAIFile fileInfo = await fileClient.UploadFileAsync(contentStream, "basic-text.pdf", FileUploadPurpose.Assistants);
|
||||
try
|
||||
{
|
||||
this.Output.WriteLine($"Image: {fileInfo.Id}");
|
||||
@@ -51,11 +53,11 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.Files.DeleteFileAsync(fileInfo.Id);
|
||||
await fileClient.DeleteFileAsync(fileInfo.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<byte[]> DownloadImageAsync()
|
||||
private static async Task<byte[]> DownloadFileAsync()
|
||||
{
|
||||
using HttpClient client = new();
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0");
|
||||
|
||||
+2
-4
@@ -5,8 +5,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedIntegrationTestCode>true</InjectSharedIntegrationTestCode>
|
||||
<InjectSharedBuildTestCode>true</InjectSharedBuildTestCode>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedIntegrationTestCode>true</InjectSharedIntegrationTestCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -21,9 +22,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.Abstractions" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.AzureAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.Yaml" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"description": "Human in the loop sample - RequestExternalInput.yaml.",
|
||||
"setup": {
|
||||
"input": {
|
||||
"type": "String",
|
||||
"value": "1234"
|
||||
},
|
||||
"responses": [
|
||||
{
|
||||
"type": "String",
|
||||
"value": "1234"
|
||||
}
|
||||
]
|
||||
},
|
||||
"validation": {
|
||||
"conversation_count": 1,
|
||||
"min_action_count": 4,
|
||||
"max_action_count": -1,
|
||||
"min_response_count": 0,
|
||||
"actions": {
|
||||
"start": [
|
||||
"set_project"
|
||||
],
|
||||
"final": [
|
||||
"sendActivity_confirmed"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -9,7 +9,8 @@
|
||||
"validation": {
|
||||
"conversation_count": 3,
|
||||
"min_action_count": 3,
|
||||
"min_response_count": 2,
|
||||
"min_response_count": 3,
|
||||
"min_message_count": 4,
|
||||
"actions": {
|
||||
"start": [
|
||||
"invoke_inner1",
|
||||
|
||||
+1
@@ -10,6 +10,7 @@
|
||||
"conversation_count": 1,
|
||||
"min_action_count": 3,
|
||||
"min_response_count": 3,
|
||||
"min_message_count": 6,
|
||||
"actions": {
|
||||
"start": [
|
||||
"invoke_analyst",
|
||||
|
||||
+2
@@ -12,6 +12,8 @@
|
||||
"max_action_count": -1,
|
||||
"min_response_count": 2,
|
||||
"max_response_count": 8,
|
||||
"min_message_count": 4,
|
||||
"max_message_count": 17,
|
||||
"actions": {
|
||||
"start": [
|
||||
],
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"description": "Human in the loop sample - RequestExternalInput.yaml.",
|
||||
"setup": {
|
||||
"input": {
|
||||
"type": "String",
|
||||
"value": "n/a"
|
||||
},
|
||||
"responses": [
|
||||
{
|
||||
"type": "String",
|
||||
"value": "This is external input"
|
||||
}
|
||||
]
|
||||
},
|
||||
"validation": {
|
||||
"conversation_count": 1,
|
||||
"min_action_count": 2,
|
||||
"min_response_count": 0,
|
||||
"min_message_count": 1,
|
||||
"actions": {
|
||||
"start": [
|
||||
"get_input"
|
||||
],
|
||||
"final": [
|
||||
"show_input"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -12,43 +12,43 @@ trigger:
|
||||
- condition: =IsBlank(System.Conversation)
|
||||
id: conversation_check
|
||||
actions:
|
||||
- kind: EndDialog
|
||||
- kind: EndWorkflow
|
||||
id: conversation_bad
|
||||
|
||||
- condition: =IsBlank(System.Conversation.Id)
|
||||
id: conversation_id_check1
|
||||
actions:
|
||||
- kind: EndDialog
|
||||
- kind: EndWorkflow
|
||||
id: conversation_id_bad1
|
||||
|
||||
- condition: =IsBlank(System.ConversationId)
|
||||
id: conversation_id_check2
|
||||
actions:
|
||||
- kind: EndDialog
|
||||
- kind: EndWorkflow
|
||||
id: conversation_id_bad2
|
||||
|
||||
- condition: =IsBlank(System.LastMessage)
|
||||
id: message_check
|
||||
actions:
|
||||
- kind: EndDialog
|
||||
- kind: EndWorkflow
|
||||
id: message_bad
|
||||
|
||||
- condition: =IsBlank(System.LastMessage.Id)
|
||||
id: message_id_check1
|
||||
actions:
|
||||
- kind: EndDialog
|
||||
- kind: EndWorkflow
|
||||
id: message_id_bad1
|
||||
|
||||
- condition: =IsBlank(System.LastMessageId)
|
||||
id: message_id_check2
|
||||
actions:
|
||||
- kind: EndDialog
|
||||
- kind: EndWorkflow
|
||||
id: message_id_bad2
|
||||
|
||||
- condition: =IsBlank(System.LastMessageText)
|
||||
id: message_text_check
|
||||
actions:
|
||||
- kind: EndDialog
|
||||
- kind: EndWorkflow
|
||||
id: message_text_bad
|
||||
|
||||
elseActions:
|
||||
|
||||
+3
-3
@@ -9,13 +9,13 @@ trigger:
|
||||
id: invoke_greet
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_TOOL
|
||||
name: MenuAgent
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_menu
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_TOOL
|
||||
name: MenuAgent
|
||||
input:
|
||||
messages: =UserMessage("What's on today's menu?")
|
||||
|
||||
@@ -23,6 +23,6 @@ trigger:
|
||||
id: invoke_item
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_TOOL
|
||||
name: MenuAgent
|
||||
input:
|
||||
messages: =UserMessage("How much is the clam chowder?")
|
||||
|
||||
+6
-8
@@ -8,27 +8,25 @@ trigger:
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_inner1
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_TEST
|
||||
name: TestAgent
|
||||
input:
|
||||
messages: =UserMessage("Can an LLM think of funny jokes?")
|
||||
output:
|
||||
autoSend: false
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_inner2
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_TEST
|
||||
name: TestAgent
|
||||
input:
|
||||
messages: =UserMessage("Do you know the joke about the chicken crossing the road? Tell me an improved version of that joke.")
|
||||
output:
|
||||
autoSend: true
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_external
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_TEST
|
||||
name: TestAgent
|
||||
input:
|
||||
additionalInstructions: |-
|
||||
Rate the originality of this well known joke that is being re-told on a scale of 1 to 10.
|
||||
Take note on where improvements or changes were made.
|
||||
messages: =UserMessage("Rate the originality of this well known joke that is being re-told on a scale of 1 to 10. Take note on where improvements or changes were made.")
|
||||
output:
|
||||
messages: Local.RatingResponse
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ trigger:
|
||||
id: invoke_vision
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_TEST
|
||||
name: TestAgent
|
||||
input:
|
||||
additionalInstructions: |-
|
||||
Describe the image contained in the user request, if any;
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: RequestExternalInput
|
||||
id: get_input
|
||||
variable: Local.MyInput
|
||||
|
||||
- kind: SendMessage
|
||||
id: show_input
|
||||
message: "You provided: {Local.MyInput}"
|
||||
+1
-1
@@ -15,7 +15,7 @@ trigger:
|
||||
- kind: SetVariable
|
||||
id: set_user_name
|
||||
variable: Global.UserName
|
||||
value: =Env.USERNAME
|
||||
value: TestAgent
|
||||
|
||||
# Respond with input
|
||||
- kind: SendActivity
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ public sealed class DeclarativeEjectionTest(ITestOutputHelper output) : Workflow
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("AddConversationMessage.yaml")]
|
||||
[InlineData("CancelWorkflow.yaml")]
|
||||
[InlineData("ClearAllVariables.yaml")]
|
||||
[InlineData("CopyConversationMessages.yaml")]
|
||||
[InlineData("Condition.yaml")]
|
||||
@@ -23,7 +24,7 @@ public sealed class DeclarativeEjectionTest(ITestOutputHelper output) : Workflow
|
||||
[InlineData("EditTable.yaml")]
|
||||
[InlineData("EditTableV2.yaml")]
|
||||
[InlineData("EndConversation.yaml")]
|
||||
[InlineData("EndDialog.yaml")]
|
||||
[InlineData("EndWorkflow.yaml")]
|
||||
[InlineData("Goto.yaml")]
|
||||
[InlineData("InvokeAgent.yaml")]
|
||||
[InlineData("LoopBreak.yaml")]
|
||||
|
||||
-16
@@ -44,18 +44,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc
|
||||
BoolExpression.Expression("1 < 2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdditionalInstructions()
|
||||
{
|
||||
// Act, Assert
|
||||
this.ExecuteTest(
|
||||
nameof(VariableConversation),
|
||||
StringExpression.Literal("asst_123abc"),
|
||||
StringExpression.Variable(PropertyPath.TopicVariable("TestConversation")),
|
||||
"MyMessages",
|
||||
additionalInstructions: "Test instructions...");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InputMessagesVariable()
|
||||
{
|
||||
@@ -86,7 +74,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc
|
||||
StringExpression.Builder conversation,
|
||||
string? messagesVariable = null,
|
||||
BoolExpression.Builder? autoSend = null,
|
||||
string? additionalInstructions = null,
|
||||
ValueExpression.Builder? messages = null)
|
||||
{
|
||||
// Arrange
|
||||
@@ -97,7 +84,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc
|
||||
conversation,
|
||||
messagesVariable,
|
||||
autoSend,
|
||||
additionalInstructions is null ? null : (TemplateLine.Builder)TemplateLine.Parse(additionalInstructions),
|
||||
messages);
|
||||
|
||||
// Act
|
||||
@@ -117,7 +103,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc
|
||||
StringExpression.Builder conversation,
|
||||
string? messagesVariable = null,
|
||||
BoolExpression.Builder? autoSend = null,
|
||||
TemplateLine.Builder? additionalInstructions = null,
|
||||
ValueExpression.Builder? messages = null)
|
||||
{
|
||||
InitializablePropertyPath? outputMessages = null;
|
||||
@@ -141,7 +126,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc
|
||||
new AzureAgentInput.Builder
|
||||
{
|
||||
Messages = messages,
|
||||
AdditionalInstructions = additionalInstructions,
|
||||
},
|
||||
Output =
|
||||
new AzureAgentOutput.Builder
|
||||
|
||||
+3
-4
@@ -145,8 +145,9 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CancelWorkflow.yaml", 1, "end_all")]
|
||||
[InlineData("EndConversation.yaml", 1, "end_all")]
|
||||
[InlineData("EndDialog.yaml", 1, "end_all")]
|
||||
[InlineData("EndWorkflow.yaml", 1, "end_all")]
|
||||
[InlineData("EditTable.yaml", 2, "edit_var")]
|
||||
[InlineData("EditTableV2.yaml", 2, "edit_var")]
|
||||
[InlineData("ParseValue.yaml", 2, "parse_var")]
|
||||
@@ -170,8 +171,6 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
[InlineData(typeof(AdaptiveCardPrompt.Builder))]
|
||||
[InlineData(typeof(BeginDialog.Builder))]
|
||||
[InlineData(typeof(CSATQuestion.Builder))]
|
||||
[InlineData(typeof(CancelAllDialogs.Builder))]
|
||||
[InlineData(typeof(CancelDialog.Builder))]
|
||||
[InlineData(typeof(CreateSearchQuery.Builder))]
|
||||
[InlineData(typeof(DeleteActivity.Builder))]
|
||||
[InlineData(typeof(DisableTrigger.Builder))]
|
||||
@@ -231,7 +230,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
[InlineData("Condition.yaml", "setVariable_test")]
|
||||
[InlineData("ConditionElse.yaml", "setVariable_test")]
|
||||
[InlineData("EndConversation.yaml", "end_all")]
|
||||
[InlineData("EndDialog.yaml", "end_all")]
|
||||
[InlineData("EndWorkflow.yaml", "end_all")]
|
||||
[InlineData("EditTable.yaml", "edit_var")]
|
||||
[InlineData("EditTableV2.yaml", "edit_var")]
|
||||
[InlineData("Goto.yaml", "goto_end")]
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="AgentFunctionToolRequest"/> class
|
||||
/// </summary>
|
||||
public sealed class AgentFunctionToolRequestTest(ITestOutputHelper output) : EventTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VerifySerializationEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
AgentFunctionToolRequest copy = VerifyEventSerialization(new AgentFunctionToolRequest("testagent", []));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("testagent", copy.AgentName);
|
||||
Assert.Empty(copy.FunctionCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifySerializationWithRequests()
|
||||
{
|
||||
// Arrange & Act
|
||||
AgentFunctionToolRequest copy =
|
||||
VerifyEventSerialization(
|
||||
new AgentFunctionToolRequest(
|
||||
"agent",
|
||||
[
|
||||
new FunctionCallContent("call1", "result1"),
|
||||
new FunctionCallContent("call2", "result2", new Dictionary<string, object?>() { { "name", "Clam Chowder" } }),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("agent", copy.AgentName);
|
||||
Assert.Equal(2, copy.FunctionCalls.Count);
|
||||
Assert.IsType<FunctionCallContent>(copy.FunctionCalls[0]);
|
||||
Assert.Null(copy.FunctionCalls[0].Arguments);
|
||||
Assert.IsType<FunctionCallContent>(copy.FunctionCalls[1]);
|
||||
Assert.NotNull(copy.FunctionCalls[1].Arguments);
|
||||
Assert.NotEmpty(copy.FunctionCalls[1].Arguments!);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="AgentFunctionToolResponse"/> class
|
||||
/// </summary>
|
||||
public sealed class AgentFunctionToolResponseTest(ITestOutputHelper output) : EventTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VerifySerializationEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
AgentFunctionToolResponse copy = VerifyEventSerialization(new AgentFunctionToolResponse("testagent", []));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("testagent", copy.AgentName);
|
||||
Assert.Empty(copy.FunctionResults);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifySerializationWithResults()
|
||||
{
|
||||
// Arrange & Act
|
||||
AgentFunctionToolResponse copy =
|
||||
VerifyEventSerialization(
|
||||
new AgentFunctionToolResponse(
|
||||
"agent",
|
||||
[
|
||||
new FunctionResultContent("call1", "result1"),
|
||||
new FunctionResultContent("call2", "result2"),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("agent", copy.AgentName);
|
||||
Assert.Equal(2, copy.FunctionResults.Count);
|
||||
Assert.IsType<FunctionResultContent>(copy.FunctionResults[0]);
|
||||
Assert.Equal("call1", copy.FunctionResults[0].CallId);
|
||||
Assert.IsType<FunctionResultContent>(copy.FunctionResults[1]);
|
||||
Assert.Equal("call2", copy.FunctionResults[1].CallId);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Xunit.Abstractions;
|
||||
@@ -19,4 +20,17 @@ public abstract class EventTest(ITestOutputHelper output) : WorkflowTest(output)
|
||||
Assert.NotNull(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
protected static void AssertMessage(ChatMessage source, ChatMessage copy)
|
||||
{
|
||||
Assert.Equal(source.Role, copy.Role);
|
||||
Assert.Equal(source.Text, copy.Text);
|
||||
Assert.Equal(source.Contents.Count, copy.Contents.Count);
|
||||
}
|
||||
|
||||
protected static TContent AssertContent<TContent>(ChatMessage message) where TContent : AIContent
|
||||
{
|
||||
TContent[] contents = message.Contents.OfType<TContent>().ToArray();
|
||||
return Assert.Single(contents);
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="ExternalInputRequest"/> class
|
||||
/// </summary>
|
||||
public sealed class ExternalInputRequestTest(ITestOutputHelper output) : EventTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VerifySerializationWithText()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest source = new(new AgentRunResponse(new ChatMessage(ChatRole.User, "Wassup?")));
|
||||
|
||||
// Act
|
||||
ExternalInputRequest copy = VerifyEventSerialization(source);
|
||||
|
||||
// Assert
|
||||
ChatMessage messageCopy = Assert.Single(source.AgentResponse.Messages);
|
||||
AssertMessage(messageCopy, copy.AgentResponse.Messages[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifySerializationWithRequests()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest source =
|
||||
new(new AgentRunResponse(
|
||||
new ChatMessage(
|
||||
ChatRole.Assistant,
|
||||
[
|
||||
new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")),
|
||||
new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")),
|
||||
new FunctionCallContent("call3", "myfunc"),
|
||||
new TextContent("Heya"),
|
||||
])));
|
||||
|
||||
// Act
|
||||
ExternalInputRequest copy = VerifyEventSerialization(source);
|
||||
|
||||
// Assert
|
||||
ChatMessage messageCopy = Assert.Single(source.AgentResponse.Messages);
|
||||
Assert.Equal(messageCopy.Contents.Count, copy.AgentResponse.Messages[0].Contents.Count);
|
||||
|
||||
McpServerToolApprovalRequestContent mcpRequest = AssertContent<McpServerToolApprovalRequestContent>(messageCopy);
|
||||
Assert.Equal("call1", mcpRequest.Id);
|
||||
|
||||
FunctionApprovalRequestContent functionRequest = AssertContent<FunctionApprovalRequestContent>(messageCopy);
|
||||
Assert.Equal("call2", functionRequest.Id);
|
||||
|
||||
FunctionCallContent functionCall = AssertContent<FunctionCallContent>(messageCopy);
|
||||
Assert.Equal("call3", functionCall.CallId);
|
||||
|
||||
TextContent textContent = AssertContent<TextContent>(messageCopy);
|
||||
Assert.Equal("Heya", textContent.Text);
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="ExternalInputResponse"/> class
|
||||
/// </summary>
|
||||
public sealed class ExternalInputResponseTest(ITestOutputHelper output) : EventTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VerifySerializationEmpty()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputResponse source = new(new ChatMessage(ChatRole.User, "Wassup?"));
|
||||
|
||||
// Act
|
||||
ExternalInputResponse copy = VerifyEventSerialization(source);
|
||||
|
||||
// Assert
|
||||
ChatMessage messageCopy = Assert.Single(source.Messages);
|
||||
AssertMessage(messageCopy, copy.Messages[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifySerializationWithResponses()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputResponse source =
|
||||
new(new ChatMessage(
|
||||
ChatRole.Assistant,
|
||||
[
|
||||
new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")).CreateResponse(approved: true),
|
||||
new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")).CreateResponse(approved: true),
|
||||
new FunctionResultContent("call3", 33),
|
||||
new TextContent("Heya"),
|
||||
]));
|
||||
|
||||
// Act
|
||||
ExternalInputResponse copy = VerifyEventSerialization(source);
|
||||
|
||||
// Assert
|
||||
ChatMessage responseMessage = Assert.Single(source.Messages);
|
||||
Assert.Equal(responseMessage.Contents.Count, copy.Messages[0].Contents.Count);
|
||||
|
||||
McpServerToolApprovalResponseContent mcpApproval = AssertContent<McpServerToolApprovalResponseContent>(responseMessage);
|
||||
Assert.Equal("call1", mcpApproval.Id);
|
||||
|
||||
FunctionApprovalResponseContent functionApproval = AssertContent<FunctionApprovalResponseContent>(responseMessage);
|
||||
Assert.Equal("call2", functionApproval.Id);
|
||||
|
||||
FunctionResultContent functionResult = AssertContent<FunctionResultContent>(responseMessage);
|
||||
Assert.Equal("call3", functionResult.CallId);
|
||||
|
||||
TextContent textContent = AssertContent<TextContent>(responseMessage);
|
||||
Assert.Equal("Heya", textContent.Text);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="UserInputRequest"/> class
|
||||
/// </summary>
|
||||
public sealed class UserInputRequestTest(ITestOutputHelper output) : EventTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VerifySerializationEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
UserInputRequest copy = VerifyEventSerialization(new UserInputRequest("test agent", []));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test agent", copy.AgentName);
|
||||
Assert.Empty(copy.InputRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifySerializationWithRequests()
|
||||
{
|
||||
// Arrange & Act
|
||||
UserInputRequest copy =
|
||||
VerifyEventSerialization(
|
||||
new UserInputRequest(
|
||||
"agent",
|
||||
[
|
||||
new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")),
|
||||
new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("agent", copy.AgentName);
|
||||
Assert.Equal(2, copy.InputRequests.Count);
|
||||
McpServerToolApprovalRequestContent mcpRequest = Assert.IsType<McpServerToolApprovalRequestContent>(copy.InputRequests[0]);
|
||||
Assert.Equal("call1", mcpRequest.Id);
|
||||
FunctionApprovalRequestContent functionRequest = Assert.IsType<FunctionApprovalRequestContent>(copy.InputRequests[1]);
|
||||
Assert.Equal("call2", functionRequest.Id);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="UserInputResponse"/> class
|
||||
/// </summary>
|
||||
public sealed class UserInputResponseTest(ITestOutputHelper output) : EventTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VerifySerializationEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
UserInputResponse copy = VerifyEventSerialization(new UserInputResponse("testagent", []));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("testagent", copy.AgentName);
|
||||
Assert.Empty(copy.InputResponses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifySerializationWithResponses()
|
||||
{
|
||||
// Arrange & Act
|
||||
UserInputResponse copy =
|
||||
VerifyEventSerialization(
|
||||
new UserInputResponse(
|
||||
"agent",
|
||||
[
|
||||
new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")).CreateResponse(approved: true),
|
||||
new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")).CreateResponse(approved: true),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("agent", copy.AgentName);
|
||||
Assert.Equal(2, copy.InputResponses.Count);
|
||||
McpServerToolApprovalResponseContent mcpResponse = Assert.IsType<McpServerToolApprovalResponseContent>(copy.InputResponses[0]);
|
||||
Assert.Equal("call1", mcpResponse.Id);
|
||||
FunctionApprovalResponseContent functionResponse = Assert.IsType<FunctionApprovalResponseContent>(copy.InputResponses[1]);
|
||||
Assert.Equal("call2", functionResponse.Id);
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="AnswerRequest"/> class
|
||||
/// </summary>
|
||||
public sealed class UserMessageRequestTest(ITestOutputHelper output) : EventTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VerifySerialization()
|
||||
{
|
||||
// Arrange & Act
|
||||
AnswerRequest copy = VerifyEventSerialization(new AnswerRequest("wassup"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("wassup", copy.Prompt);
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="AnswerResponse"/> class
|
||||
/// </summary>
|
||||
public sealed class UserMessageResponseTest(ITestOutputHelper output) : EventTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VerifySerializationText()
|
||||
{
|
||||
// Arrange & Act
|
||||
AnswerResponse copy = VerifyEventSerialization(new AnswerResponse("test response"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test response", copy.Value.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifySerializationMessage()
|
||||
{
|
||||
// Arrange & Act
|
||||
AnswerResponse copy = VerifyEventSerialization(new AnswerResponse(new ChatMessage(ChatRole.User, "test response")));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test response", copy.Value.Text);
|
||||
}
|
||||
}
|
||||
+32
@@ -58,6 +58,38 @@ public sealed class JsonDocumentExtensionsTests
|
||||
Assert.Equal(expectedTimeSpan, result["time"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_Object_NoSchema_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
{
|
||||
"text": "hello",
|
||||
"numberInt": 7,
|
||||
"numberLong": 9223372036854775807,
|
||||
"numberDecimal": 12.5,
|
||||
"numberDouble": 3.99E99,
|
||||
"flag": true,
|
||||
"date": "2024-10-01T12:34:56Z",
|
||||
"time": "12:34:56"
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
Dictionary<string, object?> result = document.ParseRecord(VariableType.RecordType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("hello", result["text"]);
|
||||
Assert.Equal(7, result["numberInt"]);
|
||||
Assert.Equal(9223372036854775807L, result["numberLong"]);
|
||||
Assert.Equal(12.5m, result["numberDecimal"]);
|
||||
Assert.Equal(3.99E99, result["numberDouble"]);
|
||||
Assert.Equal(true, result["flag"]);
|
||||
Assert.Equal("2024-10-01T12:34:56Z", result["date"]);
|
||||
Assert.Equal("12:34:56", result["time"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRecord_Object_NestedRecord_Succeeds()
|
||||
{
|
||||
|
||||
+6
-6
@@ -14,8 +14,8 @@ public sealed class TemplateExtensionsTests
|
||||
{
|
||||
// Arrange
|
||||
RecalcEngine engine = new();
|
||||
IEnumerable<TemplateLine> template = new List<TemplateLine>
|
||||
{
|
||||
IEnumerable<TemplateLine> template =
|
||||
[
|
||||
new TemplateLine.Builder
|
||||
{
|
||||
Segments =
|
||||
@@ -24,7 +24,7 @@ public sealed class TemplateExtensionsTests
|
||||
new TextSegment.Builder { Value = "World" }
|
||||
}
|
||||
}.Build()
|
||||
};
|
||||
];
|
||||
|
||||
// Act
|
||||
string result = engine.Format(template);
|
||||
@@ -38,8 +38,8 @@ public sealed class TemplateExtensionsTests
|
||||
{
|
||||
// Arrange
|
||||
RecalcEngine engine = new();
|
||||
IEnumerable<TemplateLine> template = new List<TemplateLine>
|
||||
{
|
||||
IEnumerable<TemplateLine> template =
|
||||
[
|
||||
new TemplateLine.Builder
|
||||
{
|
||||
Segments =
|
||||
@@ -54,7 +54,7 @@ public sealed class TemplateExtensionsTests
|
||||
new TextSegment.Builder { Value = "Line 2" }
|
||||
}
|
||||
}.Build()
|
||||
};
|
||||
];
|
||||
|
||||
// Act
|
||||
string result = engine.Format(template);
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: my_workflow
|
||||
actions:
|
||||
|
||||
- kind: CancelWorkflow
|
||||
id: end_all
|
||||
|
||||
- kind: SendActivity
|
||||
id: send_activity_1
|
||||
activity: NEVER 1!
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user