mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add Declarative Workflow Interpreter (#401)
* test: Add Reflection/Invocation tests * fix: Terminate on Completion event * refactor: Update public API surface * feat: Add support for external requests * feat: Support hosting AIAgent instances in Workflows * fix: Fix routing to go through Executor.ExecuteAsync * test: Update samples for "must SendMessage" semantics * Add invoking samples to unit tests to avoid future breaks * fix: ExternalRequest should block Workflow completion * feat: Normalize API surface against Python * Also adds xmldoc to all public APIs * refactor: Normalize UnitTest and Sample namespaces * fix: Formatting * refactor: Normalize project/folder names * feat: Remove DynamicCodeExecution from ValueTaskTypeErasure * fix: Fix ILTrim warnings * docs: Add missing docs and fix typos * feat: Hosted Agents should report Run events * fix: Fix type propagation for ILTrim changes * refactor: Simplify DynamicallyAccessedMembers annotations * sample: Use static-Type construction of InputPort * feat: Support non-Streaming Run Mode * test: Add test for non-streaming execution * Conversion checkpoint * Fix namespace error * Restructure * Completion * Executor checkpoint * Conditional checkpoint * Cleanup * Exception cleanup * Sample cleanup * Updates * feat: Define Workflow and Executor APIs * feat: Define IExecutionContext and Events * feat: Simple Workflow Demos * refactor: Move Workflows classes to separate assembly * feat: Move FanOut/In to LowLevel API with new semantics * feat: Implement Local Execution * refactor: Assembly name .Workflow => .Workflows * feat: Enable Default Message Handling * also lifts Bind in MessageHandlerInfo to better be able to direclty invoke handlers (for AOT, later) * feat: Implement StreamingHandle APIs This allows the user to respond to WorkflowEvents with external messages, enabling HIL. * feat: Add checks for duplicate edges and chain cycles * feat: Add built-in WorkflowEvents * refactor: Pull classes into own files * refactor: Simplify Disposal pattern in Executor * refactor: Break EdgeRunner file into per-type files * refactor: Use Throw.IfNull() * refactor: Remove AddLoop() Per https://github.com/microsoft/agent-framework/pull/272#discussion_r2241739079 we decided this was not very useful. * refactor: Normalize use of ValueTask * fix: Build Break from removing .AddLoop * refactor: Explicit routing and RouteBuilder Split out reflection from MessageRouter implemention into build phase, enabling AOT compilation to drive RouteBuilding without reflection. * test: Add Reflection/Invocation tests * fix: Terminate on Completion event * refactor: Update public API surface * feat: Add support for external requests * feat: Support hosting AIAgent instances in Workflows * fix: Fix routing to go through Executor.ExecuteAsync * test: Update samples for "must SendMessage" semantics * Add invoking samples to unit tests to avoid future breaks * fix: ExternalRequest should block Workflow completion * feat: Normalize API surface against Python * Also adds xmldoc to all public APIs * refactor: Normalize UnitTest and Sample namespaces * fix: Formatting * refactor: Normalize project/folder names * feat: Remove DynamicCodeExecution from ValueTaskTypeErasure * fix: Fix ILTrim warnings * docs: Add missing docs and fix typos * feat: Hosted Agents should report Run events * fix: Fix type propagation for ILTrim changes * refactor: Simplify DynamicallyAccessedMembers annotations * sample: Use static-Type construction of InputPort * feat: Support non-Streaming Run Mode * test: Add test for non-streaming execution * refactor: Remove unused types * refactor: Simplify Event and EdgeData type hierarchies * feat: Add Switch (=Conditional Edge Group) control flow * Fix unit-tests * Add sample * Comment cleanup * Fix debug output * Formating helpers * feat: Define Workflow and Executor APIs * feat: Define IExecutionContext and Events * feat: Simple Workflow Demos * refactor: Move Workflows classes to separate assembly * feat: Move FanOut/In to LowLevel API with new semantics * feat: Implement Local Execution * refactor: Assembly name .Workflow => .Workflows * feat: Enable Default Message Handling * also lifts Bind in MessageHandlerInfo to better be able to direclty invoke handlers (for AOT, later) * feat: Implement StreamingHandle APIs This allows the user to respond to WorkflowEvents with external messages, enabling HIL. * feat: Add checks for duplicate edges and chain cycles * feat: Add built-in WorkflowEvents * refactor: Pull classes into own files * refactor: Simplify Disposal pattern in Executor * refactor: Break EdgeRunner file into per-type files * refactor: Use Throw.IfNull() * refactor: Remove AddLoop() Per https://github.com/microsoft/agent-framework/pull/272#discussion_r2241739079 we decided this was not very useful. * refactor: Normalize use of ValueTask * fix: Build Break from removing .AddLoop * refactor: Explicit routing and RouteBuilder Split out reflection from MessageRouter implemention into build phase, enabling AOT compilation to drive RouteBuilding without reflection. * test: Add Reflection/Invocation tests * fix: Terminate on Completion event * refactor: Update public API surface * feat: Add support for external requests * feat: Support hosting AIAgent instances in Workflows * fix: Fix routing to go through Executor.ExecuteAsync * test: Update samples for "must SendMessage" semantics * Add invoking samples to unit tests to avoid future breaks * fix: ExternalRequest should block Workflow completion * feat: Normalize API surface against Python * Also adds xmldoc to all public APIs * refactor: Normalize UnitTest and Sample namespaces * fix: Formatting * refactor: Normalize project/folder names * feat: Remove DynamicCodeExecution from ValueTaskTypeErasure * fix: Fix ILTrim warnings * docs: Add missing docs and fix typos * feat: Hosted Agents should report Run events * fix: Fix type propagation for ILTrim changes * refactor: Simplify DynamicallyAccessedMembers annotations * sample: Use static-Type construction of InputPort * feat: Support non-Streaming Run Mode * test: Add test for non-streaming execution * refactor: Remove unused types * refactor: Simplify Event and EdgeData type hierarchies * feat: Add Switch (=Conditional Edge Group) control flow * feat: Make .NET AutoSend the MessageHandler result * feat: Implement State APIs * Add tests * Fix merge from main * Test coverage * Message event * Comments and clean-up * Format * Cleanup * Test checkpoint * Clean-up - comments / test * Test baseline - 100% * More clean-up * Comments * Streaming...sort've... * Fix build / test * Stable * Checkpoint * Checkpoint * Stable * Update sample after merge * Add "Question" workflow * State clean-up checkpoint * State clean-up * Sample updated * Expression bug fix * Sample formatting * Add unit test * Comments * Scope cleanup * Refine cleanup * Fill gaps * fcs * Finalize data-types * Add unit-test * Debug cleanup * Bug fixes * Demo progress * Sample clean-up * Update samples * Sample updates * Sync demo workflows * Sample formatting * Sample formatting * Demo complete * Workflow formatting * Demo formatting #2 * Readme + Sample clean-up * Scope update * Update diagnostics * Variable initiaization * Rollback * Tune research summary * State management * Fix merge * Fix merge - demo * Add readme * Overload for workflow builder * Fault tolerance - scope equivalency * Fix feed * Update sample * Add default for "Bot" * Nuget.config patchwork * Scope assignment check * Rollback nuget.config haxx * Sample format * Namespace * Namespace * Agent-Provider * Clean-up extra files * Renaming * Update sample * Prune junk files * Clean-up * Use transform * agent provider fix * Typo * Null check fix * Fix merge * Checkpoint * Cleanup * Exception cleanup * Exception message * Clean-up * Sample config update * Update handling of "Env" scope * Sample agent templates * Add readme * Event cleanup * Rename event * Update workflows/README.md Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com> --------- Co-authored-by: Jacob Alber <jaalber@microsoft.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
co-authored by
Eric Zhu
Jacob Alber
parent
c129d32ebe
commit
24ad03af6f
@@ -80,6 +80,11 @@
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.9.0" />
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.74.1" />
|
||||
<!-- Workflows -->
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="2025.8.2-1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="2025.8.2-1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="2025.8.2-1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0-build.20250625-1002" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.5.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
|
||||
@@ -44,6 +44,17 @@
|
||||
<Folder Name="/Samples/GettingStarted/Telemetry/">
|
||||
<Project Path="samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/">
|
||||
<Project Path="samples/DeclarativeWorkflow/DeclarativeWorkflow.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Example/">
|
||||
<File Path="../workflows/DeepResearch.yaml" />
|
||||
<File Path="../workflows/HelloWorld.yaml" />
|
||||
<File Path="../workflows/MathChat.yaml" />
|
||||
<File Path="../workflows/Question.yaml" />
|
||||
<File Path="../workflows/README.md" />
|
||||
<File Path="../workflows/wttr.json" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/SemanticKernelMigration/" />
|
||||
<Folder Name="/Samples/SemanticKernelMigration/AzureAIFoundry/">
|
||||
<Project Path="samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/AzureAIFoundry_Step01_Basics.csproj" />
|
||||
@@ -80,6 +91,8 @@
|
||||
<File Path="Directory.Build.props" />
|
||||
<File Path="Directory.Build.targets" />
|
||||
<File Path="Directory.Packages.props" />
|
||||
<File Path="global.json" />
|
||||
<File Path="nuget.config" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/.github/" />
|
||||
<Folder Name="/Solution Items/.github/workflows/">
|
||||
@@ -175,6 +188,7 @@
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.Workflows.Declarative/Microsoft.Agents.Workflows.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.Workflows/Microsoft.Agents.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.A2A/Microsoft.Extensions.AI.Agents.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
|
||||
@@ -204,6 +218,7 @@
|
||||
</Folder>
|
||||
<Folder Name="/Tests/UnitTests/">
|
||||
<Project Path="tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Microsoft.Agents.Workflows.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.Workflows.UnitTests/Microsoft.Agents.Workflows.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Extensions.AI.Agents.A2A.UnitTests/Microsoft.Extensions.AI.Agents.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests.csproj" />
|
||||
|
||||
+11
-1
@@ -2,6 +2,16 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="CopilotStudio" value="https://pkgs.dev.azure.com/Power-Fx/7dd30b4a-31be-4ac9-a649-e6addd4d5b0a/_packaging/CopilotStudio/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="CopilotStudio">
|
||||
<package pattern="Microsoft.Bot.*" />
|
||||
<package pattern="Microsoft.PowerFx.*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net9.0</TargetFrameworks>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
|
||||
<NoWarn>$(NoWarn);CA1812</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.Workflows.Declarative\Microsoft.Agents.Workflows.Declarative.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,304 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Declarative;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Demo.DeclarativeWorkflow;
|
||||
|
||||
/// <summary>
|
||||
/// HOW TO: Create a workflow from a declartive (yaml based) definition.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Configuration</b>
|
||||
/// Define FOUNDRY_PROJECT_ENDPOINT as a user-secret or environment variable that
|
||||
/// points to your Foundry project endpoint.
|
||||
/// <b>Usage</b>
|
||||
/// Provide the path to the workflow definition file as the first argument.
|
||||
/// All other arguments are intepreted as a queue of inputs.
|
||||
/// When no input is queued, interactive input is requested from the console.
|
||||
/// </remarks>
|
||||
internal sealed class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
Program program = new(args);
|
||||
await program.ExecuteAsync();
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
// Read and parse the declarative workflow.
|
||||
Notify($"WORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}");
|
||||
|
||||
Stopwatch timer = Stopwatch.StartNew();
|
||||
|
||||
// Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
|
||||
DeclarativeWorkflowOptions options =
|
||||
new(new FoundryAgentProvider(this.FoundryEndpoint, new AzureCliCredential()))
|
||||
{
|
||||
Configuration = this.Configuration
|
||||
};
|
||||
Workflow<string> workflow = DeclarativeWorkflowBuilder.Build<string>(this.WorkflowFile, options);
|
||||
|
||||
Notify($"\nWORKFLOW: Defined {timer.Elapsed}");
|
||||
|
||||
Notify("\nWORKFLOW: Starting...");
|
||||
|
||||
// Run the workflow, just like any other workflow
|
||||
string input = this.GetWorkflowInput();
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
|
||||
await this.MonitorWorkflowRunAsync(run);
|
||||
|
||||
Notify("\nWORKFLOW: Done!");
|
||||
}
|
||||
|
||||
private const string DefaultWorkflow = "HelloWorld.yaml";
|
||||
private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
|
||||
|
||||
private static readonly Dictionary<string, string> s_nameCache = [];
|
||||
private static readonly HashSet<string> s_fileCache = [];
|
||||
|
||||
private string WorkflowFile { get; }
|
||||
private string? WorkflowInput { get; }
|
||||
private string FoundryEndpoint { get; }
|
||||
private PersistentAgentsClient FoundryClient { get; }
|
||||
private IConfiguration Configuration { get; }
|
||||
|
||||
private Program(string[] args)
|
||||
{
|
||||
this.WorkflowFile = ParseWorkflowFile(args);
|
||||
this.WorkflowInput = ParseWorkflowInput(args);
|
||||
|
||||
this.Configuration = InitializeConfig();
|
||||
|
||||
this.FoundryEndpoint = this.Configuration[ConfigKeyFoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {ConfigKeyFoundryEndpoint}");
|
||||
this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
|
||||
}
|
||||
|
||||
private async Task MonitorWorkflowRunAsync(StreamingRun run)
|
||||
{
|
||||
string? messageId = null;
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorInvokeEvent executorInvoked)
|
||||
{
|
||||
Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}");
|
||||
}
|
||||
else if (evt is ExecutorCompleteEvent executorComplete)
|
||||
{
|
||||
Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
|
||||
}
|
||||
else if (evt is ExecutorFailureEvent executorFailure)
|
||||
{
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
}
|
||||
else if (evt is ConversationUpdateEvent invokeEvent)
|
||||
{
|
||||
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
|
||||
}
|
||||
else if (evt is 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();
|
||||
}
|
||||
}
|
||||
else if (evt is 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ParseWorkflowFile(string[] args)
|
||||
{
|
||||
string workflowFile = args.FirstOrDefault() ?? DefaultWorkflow;
|
||||
|
||||
if (!File.Exists(workflowFile) && !Path.IsPathFullyQualified(workflowFile))
|
||||
{
|
||||
string? repoFolder = GetRepoFolder();
|
||||
if (repoFolder is not null)
|
||||
{
|
||||
workflowFile = Path.Combine(repoFolder, "Workflows", workflowFile);
|
||||
workflowFile = Path.ChangeExtension(workflowFile, ".yaml");
|
||||
}
|
||||
}
|
||||
|
||||
if (!File.Exists(workflowFile))
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to locate workflow: {Path.GetFullPath(workflowFile)}.");
|
||||
}
|
||||
|
||||
return workflowFile;
|
||||
|
||||
static string? GetRepoFolder()
|
||||
{
|
||||
DirectoryInfo? current = new(Directory.GetCurrentDirectory());
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(current.FullName, ".git")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetWorkflowInput()
|
||||
{
|
||||
string? input = this.WorkflowInput;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ParseWorkflowInput(string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] workflowInput = [.. args.Skip(1)];
|
||||
|
||||
return workflowInput.FirstOrDefault();
|
||||
}
|
||||
|
||||
// Load configuration from user-secrets
|
||||
private static IConfigurationRoot InitializeConfig() =>
|
||||
new ConfigurationBuilder()
|
||||
.AddUserSecrets(Assembly.GetExecutingAssembly())
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
private static void Notify(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
try
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
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}"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
|
||||
## Configuration
|
||||
|
||||
This demo requires configuration to access agents an [Azure Foundry Project](https://learn.microsoft.com/azure/ai-foundry).
|
||||
|
||||
#### 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://..."
|
||||
```
|
||||
|
||||
To set your secrets with .NET Secret Manager:
|
||||
|
||||
1. From the root of the respository, navigate the console to the project folder:
|
||||
|
||||
```
|
||||
cd dotnet/demos/DeclarativeWorkflow
|
||||
```
|
||||
|
||||
2. Examine existing secret definitions:
|
||||
|
||||
```
|
||||
dotnet user-secrets list
|
||||
```
|
||||
|
||||
3. If needed, perform first time initialization:
|
||||
|
||||
```
|
||||
dotnet user-secrets init
|
||||
```
|
||||
|
||||
4. Define setting that identifies your Azure Foundry Project (endpoint):
|
||||
|
||||
```
|
||||
dotnet user-secrets set "FOUNDRY_PROJECT_ENDPOINT" "https://..."
|
||||
```
|
||||
|
||||
#### Authorization
|
||||
|
||||
Use [_Azure CLI_](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) to authorize access to your Azure Foundry Project:
|
||||
|
||||
```
|
||||
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`](../../../workflows/) 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` setting.
|
||||
|
||||
## 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 [`/workflows`](../../../workflows) folder.
|
||||
|
||||
1. From the root of the respository, navigate the console to the project folder:
|
||||
|
||||
```sh
|
||||
cd dotnet/demos/DeclarativeWorkflow
|
||||
|
||||
```
|
||||
|
||||
2. Run the demo referencing a sample workflow by name:
|
||||
|
||||
```sh
|
||||
dotnet run HelloWorld
|
||||
```
|
||||
|
||||
3. Run the demo with a path to any workflow file:
|
||||
|
||||
```sh
|
||||
dotnet run c:/myworkflows/HelloWorld.yaml
|
||||
```
|
||||
@@ -14,7 +14,7 @@
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Agents.Persistent" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
@@ -47,14 +47,14 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="GettingStarted" />
|
||||
<Using Include="GettingStarted" />
|
||||
<Using Include="Microsoft.Shared.SampleUtilities" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="Xunit.Abstractions" />
|
||||
</ItemGroup>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Resources\*">
|
||||
@@ -62,4 +62,4 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that represents a message produced by a declarative workflow.
|
||||
/// </summary>
|
||||
public class ConversationUpdateEvent(string executorid, string conversationId) : ExecutorEvent(executorid, conversationId)
|
||||
{
|
||||
/// <summary>
|
||||
/// The conversation ID associated with the workflow.
|
||||
/// </summary>
|
||||
public string ConversationId { get; } = conversationId;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Yaml;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Builder for converting a Foundry workflow object-model YAML definition into a process.
|
||||
/// </summary>
|
||||
public static class DeclarativeWorkflowBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a process from the provided YAML definition of a CPS Topic ObjectModel.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of the input message</typeparam>
|
||||
/// <param name="workflowFile">The path to the workflow.</param>
|
||||
/// <param name="options">The execution context for the workflow.</param>
|
||||
/// <param name="inputTransform">An optional function to transform the input message into a <see cref="ChatMessage"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static Workflow<TInput> Build<TInput>(
|
||||
string workflowFile,
|
||||
DeclarativeWorkflowOptions options,
|
||||
Func<TInput, ChatMessage>? inputTransform = null)
|
||||
where TInput : notnull
|
||||
{
|
||||
using StreamReader yamlReader = File.OpenText(workflowFile);
|
||||
return Build<TInput>(yamlReader, options, inputTransform);
|
||||
}
|
||||
/// <summary>
|
||||
/// Builds a process from the provided YAML definition of a CPS Topic ObjectModel.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of the input message</typeparam>
|
||||
/// <param name="yamlReader">The reader that provides the workflow object model YAML.</param>
|
||||
/// <param name="options">The execution context for the workflow.</param>
|
||||
/// <param name="inputTransform">An optional function to transform the input message into a <see cref="ChatMessage"/>.</param>
|
||||
/// <returns>The <see cref="Workflow"/> that corresponds with the YAML object model.</returns>
|
||||
public static Workflow<TInput> Build<TInput>(
|
||||
TextReader yamlReader,
|
||||
DeclarativeWorkflowOptions options,
|
||||
Func<TInput, ChatMessage>? inputTransform = null)
|
||||
where TInput : notnull
|
||||
{
|
||||
BotElement rootElement = YamlSerializer.Deserialize<BotElement>(yamlReader) ?? throw new DeclarativeModelException("Workflow undefined.");
|
||||
|
||||
// ISSUE #486 - Use "Workflow" element for Foundry.
|
||||
if (rootElement is not AdaptiveDialog workflowElement)
|
||||
{
|
||||
throw new DeclarativeModelException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(AdaptiveDialog)}.");
|
||||
}
|
||||
|
||||
string rootId = WorkflowActionVisitor.RootId(workflowElement.BeginDialog?.Id.Value ?? "workflow");
|
||||
|
||||
WorkflowScopes scopes = new();
|
||||
scopes.Initialize(WrapWithBot(workflowElement), options.Configuration);
|
||||
DeclarativeWorkflowState state = new(options.CreateRecalcEngine(), scopes);
|
||||
DeclarativeWorkflowExecutor<TInput> rootExecutor =
|
||||
new(rootId,
|
||||
state,
|
||||
message => inputTransform?.Invoke(message) ?? DefaultTransform(message));
|
||||
|
||||
WorkflowActionVisitor visitor = new(rootExecutor, state, options);
|
||||
WorkflowElementWalker walker = new(rootElement, visitor);
|
||||
|
||||
return walker.GetWorkflow<TInput>();
|
||||
}
|
||||
|
||||
private static ChatMessage DefaultTransform(object message) =>
|
||||
message switch
|
||||
{
|
||||
ChatMessage chatMessage => chatMessage,
|
||||
string stringMessage => new ChatMessage(ChatRole.User, stringMessage),
|
||||
_ => new(ChatRole.User, $"{message}")
|
||||
};
|
||||
|
||||
// Wrap with bot to ensure schema is set.
|
||||
private static AdaptiveDialog WrapWithBot(AdaptiveDialog dialog)
|
||||
{
|
||||
BotDefinition bot
|
||||
= new BotDefinition.Builder
|
||||
{
|
||||
Components =
|
||||
{
|
||||
new DialogComponent.Builder
|
||||
{
|
||||
SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema",
|
||||
Dialog = new AdaptiveDialog.Builder(dialog),
|
||||
}
|
||||
}
|
||||
}.Build();
|
||||
|
||||
return bot.Descendants().OfType<AdaptiveDialog>().First();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for workflow execution.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeWorkflowOptions(WorkflowAgentProvider agentProvider)
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the agent provider.
|
||||
/// </summary>
|
||||
public WorkflowAgentProvider AgentProvider { get; } = Throw.IfNull(agentProvider, nameof(agentProvider));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the configuration settings for the workflow.
|
||||
/// </summary>
|
||||
public IConfiguration? Configuration { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optionally identifies a continued workflow conversation.
|
||||
/// </summary>
|
||||
public string? ConversationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the maximum number of nested calls allowed in a PowerFx formula.
|
||||
/// </summary>
|
||||
public int? MaximumCallDepth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the maximum allowed length for expressions evaluated in the workflow.
|
||||
/// </summary>
|
||||
public int? MaximumExpressionLength { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILoggerFactory"/> used to create loggers for workflow components.
|
||||
/// </summary>
|
||||
public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an exception that occurs during action execution.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeActionException : DeclarativeWorkflowException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeActionException"/> class.
|
||||
/// </summary>
|
||||
public DeclarativeActionException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeActionException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public DeclarativeActionException(string? message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeActionException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
public DeclarativeActionException(string? message, Exception? innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an exception that occurs when the declarative model is not supported.
|
||||
/// </summary>
|
||||
public class DeclarativeModelException : DeclarativeWorkflowException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeModelException"/> class.
|
||||
/// </summary>
|
||||
public DeclarativeModelException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeModelException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public DeclarativeModelException(string? message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeModelException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
public DeclarativeModelException(string? message, Exception? innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Represents any exception that occurs during the execution of a process workflow.
|
||||
/// </summary>
|
||||
public class DeclarativeWorkflowException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeWorkflowException"/> class.
|
||||
/// </summary>
|
||||
public DeclarativeWorkflowException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeWorkflowException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public DeclarativeWorkflowException(string? message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeWorkflowException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
public DeclarativeWorkflowException(string? message, Exception? innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class BotElementExtensions
|
||||
{
|
||||
public static string? GetParentId(this BotElement element) => element.Parent?.GetId();
|
||||
|
||||
public static string GetId(this BotElement element)
|
||||
{
|
||||
return element switch
|
||||
{
|
||||
DialogAction action => action.Id.Value,
|
||||
ConditionItem conditionItem => conditionItem.Id ?? throw new DeclarativeModelException($"Undefined identifier for {nameof(ConditionItem)} that is member of {conditionItem.GetParentId() ?? "(root)"}."),
|
||||
OnActivity activity => activity.Id.Value,
|
||||
_ => throw new DeclarativeModelException($"Unknown identify for element type: {element.GetType().Name}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class ChatMessageExtensions
|
||||
{
|
||||
// ISSUE #485 - Align with message type updated OM is available.
|
||||
public static RecordValue ToRecord(this ChatMessage message) =>
|
||||
RecordValue.NewRecordFromFields(message.GetMessageFields());
|
||||
|
||||
private static IEnumerable<NamedValue> GetMessageFields(this ChatMessage message)
|
||||
{
|
||||
yield return new NamedValue(nameof(DialogAction.Id), message.MessageId.ToFormulaValue());
|
||||
yield return new NamedValue(nameof(ChatMessage.Role), FormulaValue.New(message.Role.Value));
|
||||
yield return new NamedValue(nameof(ChatMessage.AuthorName), message.AuthorName.ToFormulaValue());
|
||||
yield return new NamedValue(nameof(ChatMessage.Text), message.Text.ToFormulaValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class DataValueExtensions
|
||||
{
|
||||
public static FormulaValue ToFormulaValue(this DataValue? value) =>
|
||||
value switch
|
||||
{
|
||||
null => FormulaValue.NewBlank(),
|
||||
BlankDataValue blankValue => BlankValue.NewBlank(),
|
||||
BooleanDataValue boolValue => FormulaValue.New(boolValue.Value),
|
||||
NumberDataValue numberValue => FormulaValue.New(numberValue.Value),
|
||||
FloatDataValue floatValue => FormulaValue.New(floatValue.Value),
|
||||
StringDataValue stringValue => FormulaValue.New(stringValue.Value),
|
||||
DateTimeDataValue dateTimeValue => FormulaValue.New(dateTimeValue.Value.DateTime),
|
||||
DateDataValue dateValue => FormulaValue.NewDateOnly(dateValue.Value),
|
||||
TimeDataValue timeValue => FormulaValue.New(timeValue.Value),
|
||||
TableDataValue tableValue =>
|
||||
FormulaValue.NewTable(
|
||||
tableValue.Values.FirstOrDefault()?.ParseRecordType() ?? RecordType.Empty(),
|
||||
tableValue.Values.Select(value => value.ToRecordValue())),
|
||||
RecordDataValue recordValue => recordValue.ToRecordValue(),
|
||||
OptionDataValue optionValue => FormulaValue.New(optionValue.Value.Value),
|
||||
_ => FormulaValue.NewError(new Microsoft.PowerFx.ExpressionError { Message = $"Unknown literal type: {value.GetType().Name}" }),
|
||||
};
|
||||
|
||||
public static FormulaType ToFormulaType(this DataValue? value) => value?.GetDataType().ToFormulaType() ?? FormulaType.Blank;
|
||||
|
||||
public static FormulaType ToFormulaType(this DataType? type) =>
|
||||
type switch
|
||||
{
|
||||
null => FormulaType.Blank,
|
||||
BooleanDataType => FormulaType.Boolean,
|
||||
NumberDataType => FormulaType.Decimal,
|
||||
FloatDataType => FormulaType.Number,
|
||||
StringDataType => FormulaType.String,
|
||||
DateTimeDataType => FormulaType.DateTime,
|
||||
DateDataType => FormulaType.Date,
|
||||
TimeDataType => FormulaType.Time,
|
||||
ColorDataType => FormulaType.Color,
|
||||
GuidDataType => FormulaType.Guid,
|
||||
FileDataType => FormulaType.Blob,
|
||||
RecordDataType => RecordType.Empty(),
|
||||
TableDataType => TableType.Empty(),
|
||||
OptionSetDataType => FormulaType.String,
|
||||
AnyType => FormulaType.UntypedObject,
|
||||
_ => FormulaType.Unknown,
|
||||
};
|
||||
|
||||
public static FormulaValue NewBlank(this DataType? type) => FormulaValue.NewBlank(type?.ToFormulaType() ?? FormulaType.Blank);
|
||||
|
||||
public static RecordValue ToRecordValue(this RecordDataValue recordDataValue) =>
|
||||
FormulaValue.NewRecordFromFields(
|
||||
recordDataValue.Properties.Select(
|
||||
property => new NamedValue(property.Key, property.Value.ToFormulaValue())));
|
||||
|
||||
public static RecordType ToRecordType(this RecordDataType record)
|
||||
{
|
||||
RecordType recordType = RecordType.Empty();
|
||||
foreach (KeyValuePair<string, PropertyInfo> property in record.Properties)
|
||||
{
|
||||
recordType = recordType.Add(property.Key, property.Value.Type.ToFormulaType());
|
||||
}
|
||||
return recordType;
|
||||
}
|
||||
|
||||
private static RecordType ParseRecordType(this RecordDataValue record)
|
||||
{
|
||||
RecordType recordType = RecordType.Empty();
|
||||
foreach (KeyValuePair<string, DataValue> property in record.Properties)
|
||||
{
|
||||
recordType = recordType.Add(property.Key, property.Value.ToFormulaType());
|
||||
}
|
||||
return recordType;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.PowerFx;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class DeclarativeWorkflowOptionsExtensions
|
||||
{
|
||||
private const int DefaultMaximumExpressionLength = 10000;
|
||||
|
||||
public static RecalcEngine CreateRecalcEngine(this DeclarativeWorkflowOptions? context) =>
|
||||
RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth);
|
||||
}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using BlankType = Microsoft.PowerFx.Types.BlankType;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class FormulaValueExtensions
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true };
|
||||
|
||||
public static FormulaValue NewBlank(this FormulaType? type) => FormulaValue.NewBlank(type ?? FormulaType.Blank);
|
||||
|
||||
public static FormulaValue ToFormulaValue(this object? value) =>
|
||||
value switch
|
||||
{
|
||||
null => FormulaValue.NewBlank(),
|
||||
FormulaValue formulaValue => formulaValue,
|
||||
bool booleanValue => FormulaValue.New(booleanValue),
|
||||
int decimalValue => FormulaValue.New(decimalValue),
|
||||
long decimalValue => FormulaValue.New(decimalValue),
|
||||
float decimalValue => FormulaValue.New(decimalValue),
|
||||
decimal decimalValue => FormulaValue.New(decimalValue),
|
||||
double numberValue => FormulaValue.New(numberValue),
|
||||
string stringValue => FormulaValue.New(stringValue),
|
||||
DateTime dateonlyValue when dateonlyValue.TimeOfDay == TimeSpan.Zero => FormulaValue.NewDateOnly(dateonlyValue),
|
||||
DateTime datetimeValue => FormulaValue.New(datetimeValue),
|
||||
TimeSpan timeValue => FormulaValue.New(timeValue),
|
||||
object when value is IEnumerable tableValue => tableValue.ToTable(),
|
||||
ExpandoObject expandoValue => expandoValue.ToRecord(),
|
||||
_ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"),
|
||||
};
|
||||
|
||||
public static FormulaType GetFormulaType(this object? value) =>
|
||||
value switch
|
||||
{
|
||||
null => FormulaType.Blank,
|
||||
bool => FormulaType.Boolean,
|
||||
int => FormulaType.Decimal,
|
||||
long => FormulaType.Decimal,
|
||||
float => FormulaType.Decimal,
|
||||
decimal => FormulaType.Decimal,
|
||||
double => FormulaType.Number,
|
||||
string => FormulaType.String,
|
||||
DateTime => FormulaType.DateTime,
|
||||
TimeSpan => FormulaType.Time,
|
||||
object when value is IEnumerable tableValue => tableValue.ToTableType(),
|
||||
ExpandoObject expandoValue => expandoValue.ToRecordType(),
|
||||
_ => FormulaType.Unknown,
|
||||
};
|
||||
|
||||
public static DataValue ToDataValue(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
BooleanValue booleanValue => BooleanDataValue.Create(booleanValue.Value),
|
||||
DecimalValue decimalValue => NumberDataValue.Create(decimalValue.Value),
|
||||
NumberValue numberValue => FloatDataValue.Create(numberValue.Value),
|
||||
DateValue dateValue => DateDataValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
DateTimeValue datetimeValue => DateTimeDataValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
TimeValue timeValue => TimeDataValue.Create(timeValue.Value),
|
||||
StringValue stringValue => StringDataValue.Create(stringValue.Value),
|
||||
BlankValue blankValue => DataValue.Blank(),
|
||||
VoidValue voidValue => DataValue.Blank(),
|
||||
RecordValue recordValue => recordValue.ToRecord(),
|
||||
TableValue tableValue => tableValue.ToTable(),
|
||||
_ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"),
|
||||
};
|
||||
|
||||
public static DataType GetDataType(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
null => DataType.Blank,
|
||||
BooleanValue => DataType.Boolean,
|
||||
DecimalValue => DataType.Number,
|
||||
NumberValue => DataType.Float,
|
||||
DateValue => DataType.Date,
|
||||
DateTimeValue => DataType.DateTime,
|
||||
TimeValue => DataType.Time,
|
||||
StringValue => DataType.String,
|
||||
BlankValue => DataType.Blank,
|
||||
ColorValue => DataType.Color,
|
||||
GuidValue => DataType.Guid,
|
||||
BlobValue => DataType.File,
|
||||
RecordValue recordValue => recordValue.Type.ToDataType(),
|
||||
TableValue tableValue => tableValue.Type.ToDataType(),
|
||||
UntypedObjectValue => DataType.Any,
|
||||
_ => DataType.Unspecified,
|
||||
};
|
||||
|
||||
public static DataType ToDataType(this FormulaType type) =>
|
||||
type switch
|
||||
{
|
||||
null => DataType.Blank,
|
||||
BooleanType => DataType.Boolean,
|
||||
DecimalType => DataType.Number,
|
||||
NumberType => DataType.Float,
|
||||
DateType => DataType.Date,
|
||||
DateTimeType => DataType.DateTime,
|
||||
TimeType => DataType.Time,
|
||||
StringType => DataType.String,
|
||||
BlankType => DataType.Blank,
|
||||
ColorType => DataType.Color,
|
||||
GuidType => DataType.Guid,
|
||||
BlobType => DataType.File,
|
||||
RecordType recordType => recordType.ToDataType(),
|
||||
TableType tableType => tableType.ToDataType(),
|
||||
UntypedObjectType => DataType.Any,
|
||||
_ => DataType.Unspecified,
|
||||
};
|
||||
|
||||
public static string Format(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
BooleanValue booleanValue => $"{booleanValue.Value}",
|
||||
DecimalValue decimalValue => $"{decimalValue.Value}",
|
||||
NumberValue numberValue => $"{numberValue.Value}",
|
||||
DateValue dateValue => $"{dateValue.GetConvertedValue(TimeZoneInfo.Utc)}",
|
||||
DateTimeValue datetimeValue => $"{datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)}",
|
||||
TimeValue timeValue => $"{timeValue.Value}",
|
||||
StringValue stringValue => stringValue.Value,
|
||||
BlankValue blankValue => string.Empty,
|
||||
VoidValue voidValue => string.Empty,
|
||||
ColorValue colorValue => colorValue.Value.ToString(),
|
||||
GuidValue guidValue => guidValue.Value.ToString("N"),
|
||||
TableValue tableValue => tableValue.ToJson().ToJsonString(s_options),
|
||||
RecordValue recordValue => recordValue.ToJson().ToJsonString(s_options),
|
||||
ErrorValue errorValue => $"Error:{Environment.NewLine}{string.Join(Environment.NewLine, errorValue.Errors.Select(error => $"{error.MessageKey}: {error.Message}"))}",
|
||||
_ => $"[{value.GetType().Name}]",
|
||||
};
|
||||
|
||||
public static TableDataValue ToTable(this TableValue value) =>
|
||||
TableDataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToRecord()).ToImmutableArray());
|
||||
|
||||
public static RecordDataValue ToRecord(this RecordValue value) =>
|
||||
RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()).ToImmutableArray());
|
||||
|
||||
private static RecordDataType ToDataType(this RecordType record)
|
||||
{
|
||||
RecordDataType recordType = new();
|
||||
foreach (string fieldName in record.FieldNames)
|
||||
{
|
||||
recordType.Properties.Add(fieldName, PropertyInfo.Create(record.GetFieldType(fieldName).ToDataType()));
|
||||
}
|
||||
return recordType;
|
||||
}
|
||||
|
||||
private static TableDataType ToDataType(this TableType table)
|
||||
{
|
||||
TableDataType tableType = new();
|
||||
foreach (string fieldName in table.FieldNames)
|
||||
{
|
||||
tableType.Properties.Add(fieldName, PropertyInfo.Create(table.GetFieldType(fieldName).ToDataType()));
|
||||
}
|
||||
return tableType;
|
||||
}
|
||||
|
||||
private static RecordType ToRecordType(this ExpandoObject value)
|
||||
{
|
||||
RecordType recordType = RecordType.Empty();
|
||||
foreach (KeyValuePair<string, object?> property in value)
|
||||
{
|
||||
recordType.Add(property.Key, property.Value.GetFormulaType());
|
||||
}
|
||||
return recordType;
|
||||
}
|
||||
|
||||
private static RecordValue ToRecord(this ExpandoObject value) =>
|
||||
FormulaValue.NewRecordFromFields(
|
||||
value.Select(
|
||||
property => new NamedValue(property.Key, property.Value.ToFormulaValue())));
|
||||
|
||||
private static TableType ToTableType(this IEnumerable value)
|
||||
{
|
||||
Type valueType = value.GetType();
|
||||
Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault();
|
||||
|
||||
if (elementType is not null)
|
||||
{
|
||||
if (elementType != typeof(ExpandoObject))
|
||||
{
|
||||
throw new DeclarativeModelException($"Invalid table element: {elementType.Name}");
|
||||
}
|
||||
|
||||
foreach (ExpandoObject element in value)
|
||||
{
|
||||
return element.ToRecordType().ToTable();
|
||||
}
|
||||
}
|
||||
|
||||
return TableType.Empty();
|
||||
}
|
||||
|
||||
private static TableValue ToTable(this IEnumerable value)
|
||||
{
|
||||
Type valueType = value.GetType();
|
||||
Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault();
|
||||
|
||||
if (elementType is null)
|
||||
{
|
||||
return FormulaValue.NewTable(RecordType.EmptySealed());
|
||||
}
|
||||
|
||||
if (elementType != typeof(ExpandoObject))
|
||||
{
|
||||
throw new DeclarativeModelException($"Invalid table element: {elementType.Name}");
|
||||
}
|
||||
|
||||
List<RecordValue> records = [.. value.OfType<ExpandoObject>().Select(element => element.ToRecord())];
|
||||
|
||||
return FormulaValue.NewTable(value.ToTableType().ToRecord(), records);
|
||||
}
|
||||
|
||||
private static KeyValuePair<string, DataValue> GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue());
|
||||
|
||||
private static JsonNode ToJson(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
BooleanValue booleanValue => JsonValue.Create(booleanValue.Value),
|
||||
DecimalValue decimalValue => JsonValue.Create(decimalValue.Value),
|
||||
NumberValue numberValue => JsonValue.Create(numberValue.Value),
|
||||
DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"),
|
||||
StringValue stringValue => JsonValue.Create(stringValue.Value),
|
||||
GuidValue guidValue => JsonValue.Create(guidValue.Value),
|
||||
RecordValue recordValue => recordValue.ToJson(),
|
||||
TableValue tableValue => tableValue.ToJson(),
|
||||
BlankValue blankValue => JsonValue.Create(string.Empty),
|
||||
_ => $"[{value.GetType().Name}]",
|
||||
};
|
||||
|
||||
private static JsonArray ToJson(this TableValue value)
|
||||
{
|
||||
return new([.. GetJsonElements()]);
|
||||
|
||||
IEnumerable<JsonNode> GetJsonElements()
|
||||
{
|
||||
foreach (DValue<RecordValue> row in value.Rows)
|
||||
{
|
||||
RecordValue recordValue = row.Value;
|
||||
yield return recordValue.ToJson();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject ToJson(this RecordValue value)
|
||||
{
|
||||
JsonObject jsonObject = [];
|
||||
foreach (NamedValue field in value.OriginalFields)
|
||||
{
|
||||
jsonObject.Add(field.Name, field.Value.ToJson());
|
||||
}
|
||||
return jsonObject;
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class PropertyPathExtensions
|
||||
{
|
||||
public static string Format(this PropertyPath path) => string.Join(".", path.Segments());
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class RecordDataTypeExtensions
|
||||
{
|
||||
public static RecordValue ParseRecord(this RecordDataType recordType, JsonElement currentElement)
|
||||
{
|
||||
return FormulaValue.NewRecordFromFields(ParseValues());
|
||||
|
||||
IEnumerable<NamedValue> ParseValues()
|
||||
{
|
||||
foreach (KeyValuePair<string, PropertyInfo> property in recordType.Properties)
|
||||
{
|
||||
JsonElement propertyElement = currentElement.GetProperty(property.Key);
|
||||
FormulaValue? parsedValue =
|
||||
property.Value.Type switch
|
||||
{
|
||||
StringDataType => StringValue.New(propertyElement.GetString()),
|
||||
NumberDataType => NumberValue.New(propertyElement.GetDecimal()),
|
||||
BooleanDataType => BooleanValue.New(propertyElement.GetBoolean()),
|
||||
DateTimeDataType => DateTimeValue.New(propertyElement.GetDateTime()),
|
||||
DateDataType => DateValue.New(propertyElement.GetDateTime()),
|
||||
TimeDataType => TimeValue.New(propertyElement.GetDateTimeOffset().TimeOfDay),
|
||||
RecordDataType recordType => recordType.ParseRecord(propertyElement),
|
||||
TableDataType tableType => ParseTable(tableType, propertyElement),
|
||||
_ => throw new InvalidOperationException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'"),
|
||||
};
|
||||
yield return new NamedValue(property.Key, parsedValue);
|
||||
}
|
||||
|
||||
static TableValue ParseTable(TableDataType tableType, JsonElement propertyElement)
|
||||
{
|
||||
RecordDataType recordType = tableType.ToRecord();
|
||||
return
|
||||
FormulaValue.NewTable(
|
||||
recordType.ToRecordType(),
|
||||
propertyElement.EnumerateArray().Select(tableElement => ParseRecord(recordType, tableElement)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
private static readonly Regex s_regex = new(@"^```(?:\w*)\s*([\s\S]*?)\s*```$", RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
public static string TrimJsonDelimiter(this string value)
|
||||
{
|
||||
Match match = s_regex.Match(value.Trim());
|
||||
if (match.Success)
|
||||
{
|
||||
return match.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
public static FormulaValue ToFormulaValue(this string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? FormulaValue.NewBlank() : FormulaValue.New(value);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class TemplateExtensions
|
||||
{
|
||||
public static string? Format(this RecalcEngine engine, IEnumerable<TemplateLine> template)
|
||||
{
|
||||
return string.Concat(template.Select(line => engine.Format(line)));
|
||||
}
|
||||
|
||||
public static string? Format(this RecalcEngine engine, TemplateLine? line)
|
||||
{
|
||||
return string.Concat(line?.Segments.Select(segment => engine.Format(segment)) ?? [string.Empty]);
|
||||
}
|
||||
|
||||
public static string? Format(this RecalcEngine engine, TemplateSegment segment)
|
||||
{
|
||||
if (segment is TextSegment textSegment)
|
||||
{
|
||||
return textSegment.Value;
|
||||
}
|
||||
|
||||
if (segment is ExpressionSegment expressionSegment)
|
||||
{
|
||||
if (expressionSegment.Expression is not null)
|
||||
{
|
||||
if (expressionSegment.Expression.ExpressionText is not null)
|
||||
{
|
||||
FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.ExpressionText);
|
||||
return expressionValue.Format();
|
||||
}
|
||||
if (expressionSegment.Expression.VariableReference is not null)
|
||||
{
|
||||
FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.VariableReference.ToString());
|
||||
return expressionValue.Format();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new DeclarativeModelException($"Unsupported segment type: {segment.GetType().Name}");
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed record class DeclarativeExecutorResult(string ExecutorId, object? Result = null);
|
||||
|
||||
internal abstract class DeclarativeActionExecutor<TAction>(TAction model, DeclarativeWorkflowState state) :
|
||||
WorkflowActionExecutor(model, state)
|
||||
where TAction : DialogAction
|
||||
{
|
||||
public new TAction Model => (TAction)base.Model;
|
||||
}
|
||||
|
||||
internal abstract class WorkflowActionExecutor :
|
||||
ReflectingExecutor<WorkflowActionExecutor>,
|
||||
IMessageHandler<DeclarativeExecutorResult>
|
||||
{
|
||||
public const string RootActionId = "(root)";
|
||||
|
||||
private static readonly ImmutableHashSet<string> s_mutableScopes =
|
||||
new HashSet<string>
|
||||
{
|
||||
VariableScopeNames.Topic,
|
||||
VariableScopeNames.Global,
|
||||
}.ToImmutableHashSet();
|
||||
|
||||
private string? _parentId;
|
||||
|
||||
protected WorkflowActionExecutor(DialogAction model, DeclarativeWorkflowState state)
|
||||
: base(model.Id.Value)
|
||||
{
|
||||
if (!model.HasRequiredProperties)
|
||||
{
|
||||
throw new DeclarativeModelException($"Missing required properties for element: {model.GetId()} ({model.GetType().Name}).");
|
||||
}
|
||||
|
||||
this.Model = model;
|
||||
this.State = state;
|
||||
}
|
||||
|
||||
public DialogAction Model { get; }
|
||||
|
||||
public string ParentId => this._parentId ??= this.Model.GetParentId() ?? RootActionId;
|
||||
|
||||
internal ILogger Logger { get; set; } = NullLogger<WorkflowActionExecutor>.Instance;
|
||||
|
||||
protected DeclarativeWorkflowState State { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context)
|
||||
{
|
||||
if (this.Model.Disabled)
|
||||
{
|
||||
Debug.WriteLine($"DISABLED {this.GetType().Name} [{this.Id}]");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.State.RestoreAsync(context, default).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
object? result = await this.ExecuteAsync(context, cancellationToken: default).ConfigureAwait(false);
|
||||
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id, result)).ConfigureAwait(false);
|
||||
}
|
||||
catch (DeclarativeActionException exception)
|
||||
{
|
||||
Debug.WriteLine($"ERROR [{this.Id}] {exception.GetType().Name}\n{exception.Message}");
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.WriteLine($"ERROR [{this.Id}] {exception.GetType().Name}\n{exception.Message}");
|
||||
throw new DeclarativeActionException($"Unhandled workflow failure - #{this.Id} ({this.Model.GetType().Name})", exception);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default);
|
||||
|
||||
protected async ValueTask AssignAsync(PropertyPath targetPath, FormulaValue result, IWorkflowContext context)
|
||||
{
|
||||
if (!s_mutableScopes.Contains(Throw.IfNull(targetPath.VariableScopeName)))
|
||||
{
|
||||
throw new DeclarativeModelException($"Invalid scope: {targetPath.VariableScopeName}");
|
||||
}
|
||||
|
||||
await this.State.SetAsync(targetPath, result, context).ConfigureAwait(false);
|
||||
|
||||
#if DEBUG
|
||||
string? resultValue = result.Format();
|
||||
string valuePosition = (resultValue?.IndexOf('\n') ?? -1) >= 0 ? Environment.NewLine : " ";
|
||||
Debug.WriteLine(
|
||||
$"""
|
||||
STATE: {this.GetType().Name} [{this.Id}]
|
||||
NAME: {targetPath.Format()}
|
||||
VALUE:{valuePosition}{result.Format()} ({result.GetType().Name})
|
||||
""");
|
||||
#endif
|
||||
}
|
||||
|
||||
protected DeclarativeActionException Exception(string text, Exception? exception = null)
|
||||
{
|
||||
string message = $"Unexpected workflow failure during {this.Model.GetType().Name} [{this.Id}]: {text}";
|
||||
return exception is null ? new(message) : new(message, exception);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
/// <summary>
|
||||
/// The root executor for a declarative workflow.
|
||||
/// </summary>
|
||||
internal sealed class DeclarativeWorkflowExecutor<TInput>(string workflowId, DeclarativeWorkflowState state, Func<TInput, ChatMessage> inputTransform) :
|
||||
ReflectingExecutor<DeclarativeWorkflowExecutor<TInput>>(workflowId),
|
||||
IMessageHandler<TInput>
|
||||
where TInput : notnull
|
||||
{
|
||||
public async ValueTask HandleAsync(TInput message, IWorkflowContext context)
|
||||
{
|
||||
ChatMessage input = inputTransform.Invoke(message);
|
||||
await state.SetLastMessageAsync(context, input).ConfigureAwait(false);
|
||||
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
/// <summary>
|
||||
/// Provides dynamic model for constructing a declarative workflow.
|
||||
/// </summary>
|
||||
internal sealed class DeclarativeWorkflowModel
|
||||
{
|
||||
public DeclarativeWorkflowModel(Executor rootStep)
|
||||
{
|
||||
this.DefineNode(rootStep);
|
||||
}
|
||||
|
||||
private Dictionary<string, ModelNode> Nodes { get; } = [];
|
||||
|
||||
private List<ModelLink> Links { get; } = [];
|
||||
|
||||
public int GetDepth(string? nodeId)
|
||||
{
|
||||
if (nodeId == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!this.Nodes.TryGetValue(nodeId, out ModelNode? sourceNode))
|
||||
{
|
||||
throw new DeclarativeModelException($"Unresolved step: {nodeId}.");
|
||||
}
|
||||
|
||||
return sourceNode.Depth;
|
||||
}
|
||||
|
||||
public void AddNode(Executor executor, string parentId, Action? completionHandler = null)
|
||||
{
|
||||
if (!this.Nodes.TryGetValue(parentId, out ModelNode? parentNode))
|
||||
{
|
||||
throw new DeclarativeModelException($"Unresolved parent for {executor.Id}: {parentId}.");
|
||||
}
|
||||
|
||||
ModelNode stepNode = this.DefineNode(executor, parentNode, executor.GetType(), completionHandler);
|
||||
|
||||
parentNode.Children.Add(stepNode);
|
||||
}
|
||||
|
||||
public void AddLinkFromPeer(string parentId, string targetId, Func<object?, bool>? condition = null)
|
||||
{
|
||||
if (!this.Nodes.TryGetValue(parentId, out ModelNode? parentNode))
|
||||
{
|
||||
throw new DeclarativeModelException($"Unresolved step: {parentId}.");
|
||||
}
|
||||
|
||||
if (parentNode.Children.Count == 0)
|
||||
{
|
||||
throw new DeclarativeModelException($"Cannot add a link from a node with no children: {parentId}.");
|
||||
}
|
||||
|
||||
ModelNode sourceNode = parentNode.Children.Count == 1 ? parentNode : parentNode.Children[parentNode.Children.Count - 2];
|
||||
|
||||
this.Links.Add(new ModelLink(sourceNode, targetId, condition));
|
||||
}
|
||||
|
||||
public void AddLink(string sourceId, string targetId, Func<object?, bool>? condition = null)
|
||||
{
|
||||
if (!this.Nodes.TryGetValue(sourceId, out ModelNode? sourceNode))
|
||||
{
|
||||
throw new DeclarativeModelException($"Unresolved step: {sourceId}.");
|
||||
}
|
||||
|
||||
this.Links.Add(new ModelLink(sourceNode, targetId, condition));
|
||||
}
|
||||
|
||||
public void ConnectNodes(WorkflowBuilder workflowBuilder)
|
||||
{
|
||||
foreach (ModelNode node in this.Nodes.Values.ToImmutableArray())
|
||||
{
|
||||
if (node.CompletionHandler is not null)
|
||||
{
|
||||
Debug.WriteLine($"> CLOSE: {node.Id} (x{node.Children.Count})");
|
||||
|
||||
node.CompletionHandler.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ModelLink link in this.Links)
|
||||
{
|
||||
if (!this.Nodes.TryGetValue(link.TargetId, out ModelNode? targetNode))
|
||||
{
|
||||
throw new DeclarativeModelException($"Unresolved target for {link.Source.Id}: {link.TargetId}.");
|
||||
}
|
||||
|
||||
Debug.WriteLine($"> CONNECT: {link.Source.Id} => {link.TargetId}{(link.Condition is null ? string.Empty : " (?)")}");
|
||||
|
||||
workflowBuilder.AddEdge(link.Source.Executor, targetNode.Executor, link.Condition);
|
||||
}
|
||||
}
|
||||
|
||||
private ModelNode DefineNode(Executor executor, ModelNode? parentNode = null, Type? executorType = null, Action? completionHandler = null)
|
||||
{
|
||||
ModelNode stepNode = new(executor, parentNode, executorType, completionHandler);
|
||||
|
||||
this.Nodes.Add(stepNode.Id, stepNode);
|
||||
|
||||
return stepNode;
|
||||
}
|
||||
|
||||
internal TAction? LocateParent<TAction>(string? itemId) where TAction : Executor
|
||||
{
|
||||
if (string.IsNullOrEmpty(itemId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
while (itemId != null)
|
||||
{
|
||||
if (!this.Nodes.TryGetValue(itemId, out ModelNode? itemNode))
|
||||
{
|
||||
throw new DeclarativeModelException($"Unresolved child: {itemId}.");
|
||||
}
|
||||
|
||||
if (itemNode.ExecutorType == typeof(TAction))
|
||||
{
|
||||
return (TAction)itemNode.Executor;
|
||||
}
|
||||
|
||||
itemId = itemNode.Parent?.Id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private sealed class ModelNode(Executor executor, ModelNode? parent = null, Type? executorType = null, Action? completionHandler = null)
|
||||
{
|
||||
public string Id => executor.Id;
|
||||
|
||||
public Executor Executor => executor;
|
||||
|
||||
public Type? ExecutorType => executorType;
|
||||
|
||||
public ModelNode? Parent { get; } = parent;
|
||||
|
||||
public List<ModelNode> Children { get; } = [];
|
||||
|
||||
public int Depth => this.Parent?.Depth + 1 ?? 0;
|
||||
|
||||
public Action? CompletionHandler => completionHandler;
|
||||
}
|
||||
|
||||
private sealed record class ModelLink(ModelNode Source, string TargetId, Func<object?, bool>? Condition = null);
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed class DeclarativeWorkflowState
|
||||
{
|
||||
private static readonly ImmutableHashSet<string> s_mutableScopes =
|
||||
new HashSet<string>
|
||||
{
|
||||
VariableScopeNames.Topic,
|
||||
VariableScopeNames.Global,
|
||||
VariableScopeNames.System,
|
||||
}.ToImmutableHashSet();
|
||||
|
||||
private readonly RecalcEngine _engine;
|
||||
private readonly WorkflowScopes _scopes;
|
||||
private WorkflowExpressionEngine? _expressionEngine;
|
||||
private int _isInitialized;
|
||||
|
||||
public DeclarativeWorkflowState(RecalcEngine engine, WorkflowScopes? scopes = null)
|
||||
{
|
||||
this._scopes = scopes ?? new WorkflowScopes();
|
||||
this._engine = engine;
|
||||
this._scopes.Bind(this._engine);
|
||||
}
|
||||
|
||||
public WorkflowExpressionEngine ExpressionEngine => this._expressionEngine ??= new WorkflowExpressionEngine(this._engine);
|
||||
|
||||
public void Reset(PropertyPath variablePath) =>
|
||||
this.Reset(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName));
|
||||
|
||||
public void Reset(string scopeName, string? varName = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(varName))
|
||||
{
|
||||
this._scopes.Clear(scopeName);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._scopes.Reset(varName, scopeName);
|
||||
}
|
||||
|
||||
this._scopes.Bind(this._engine, scopeName);
|
||||
}
|
||||
|
||||
public FormulaValue Get(PropertyPath variablePath) =>
|
||||
this.Get(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName));
|
||||
|
||||
public FormulaValue Get(string scope, string varName) =>
|
||||
this._scopes.Get(varName, scope);
|
||||
|
||||
public ValueTask SetAsync(PropertyPath variablePath, FormulaValue value, IWorkflowContext context) =>
|
||||
this.SetAsync(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName), value, context);
|
||||
|
||||
public async ValueTask SetAsync(string scopeName, string varName, FormulaValue value, IWorkflowContext context)
|
||||
{
|
||||
if (!s_mutableScopes.Contains(scopeName))
|
||||
{
|
||||
throw new DeclarativeModelException($"Invalid scope: {scopeName}");
|
||||
}
|
||||
|
||||
this._scopes.Set(varName, value, scopeName);
|
||||
this._scopes.Bind(this._engine, scopeName);
|
||||
|
||||
await context.QueueStateUpdateAsync(varName, value.ToObject(), scopeName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string? Format(IEnumerable<TemplateLine> template) => this._engine.Format(template);
|
||||
|
||||
public string? Format(TemplateLine? line) => this._engine.Format(line);
|
||||
|
||||
public async ValueTask RestoreAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref this._isInitialized, 1, 0) == 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.WhenAll(s_mutableScopes.Select(scopeName => ReadScopeAsync(scopeName).AsTask())).ConfigureAwait(false);
|
||||
|
||||
async ValueTask ReadScopeAsync(string scopeName)
|
||||
{
|
||||
HashSet<string> keys = await context.ReadStateKeysAsync(scopeName).ConfigureAwait(false);
|
||||
foreach (string key in keys)
|
||||
{
|
||||
object? value = await context.ReadStateAsync<object>(key, scopeName).ConfigureAwait(false);
|
||||
this._scopes.Set(key, value.ToFormulaValue(), scopeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal delegate ValueTask DelegateAction(IWorkflowContext context, CancellationToken cancellationToken);
|
||||
|
||||
internal sealed class DelegateActionExecutor : ReflectingExecutor<DelegateActionExecutor>, IMessageHandler<DeclarativeExecutorResult>
|
||||
{
|
||||
private readonly DelegateAction? _action;
|
||||
|
||||
public DelegateActionExecutor(string actionId, DelegateAction? action = null)
|
||||
: base(actionId)
|
||||
{
|
||||
this._action = action;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context)
|
||||
{
|
||||
if (this._action is not null)
|
||||
{
|
||||
await this._action.Invoke(context, default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
{
|
||||
private readonly WorkflowBuilder _workflowBuilder;
|
||||
private readonly DeclarativeWorkflowModel _workflowModel;
|
||||
private readonly DeclarativeWorkflowOptions _workflowOptions;
|
||||
private readonly DeclarativeWorkflowState _workflowState;
|
||||
|
||||
public WorkflowActionVisitor(
|
||||
Executor rootAction,
|
||||
DeclarativeWorkflowState state,
|
||||
DeclarativeWorkflowOptions options)
|
||||
{
|
||||
this._workflowBuilder = new WorkflowBuilder(rootAction);
|
||||
this._workflowModel = new DeclarativeWorkflowModel(rootAction);
|
||||
this._workflowOptions = options;
|
||||
this._workflowState = state;
|
||||
}
|
||||
|
||||
public bool HasUnsupportedActions { get; private set; }
|
||||
|
||||
public Workflow<TInput> Complete<TInput>()
|
||||
{
|
||||
// Process the cached links
|
||||
this._workflowModel.ConnectNodes(this._workflowBuilder);
|
||||
|
||||
// Build final workflow
|
||||
return this._workflowBuilder.Build<TInput>();
|
||||
}
|
||||
|
||||
protected override void Visit(ActionScope item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
string parentId = GetParentId(item);
|
||||
|
||||
// Handle case where root element is its own parent
|
||||
if (item.Id.Equals(parentId))
|
||||
{
|
||||
parentId = RootId(parentId);
|
||||
}
|
||||
|
||||
this.ContinueWith(this.CreateStep(item.Id.Value), parentId, condition: null, CompletionHandler);
|
||||
|
||||
// Complete the action scope.
|
||||
void CompletionHandler()
|
||||
{
|
||||
if (this._workflowModel.GetDepth(item.Id.Value) > 1)
|
||||
{
|
||||
string completionId = this.ContinuationFor(item.Id.Value); // End scope
|
||||
this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId); // Connect with final action
|
||||
this._workflowModel.AddLink(completionId, PostId(parentId)); // Merge with parent scope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitConditionItem(ConditionItem item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent<ConditionGroupExecutor>(item.GetParentId());
|
||||
if (conditionGroup is not null)
|
||||
{
|
||||
string stepId = ConditionGroupExecutor.Steps.Item(conditionGroup.Model, item);
|
||||
string parentId = GetParentId(item);
|
||||
this._workflowModel.AddNode(this.CreateStep(stepId), parentId, CompletionHandler);
|
||||
|
||||
base.VisitConditionItem(item);
|
||||
|
||||
// Complete the condition item.
|
||||
void CompletionHandler()
|
||||
{
|
||||
string completionId = this.ContinuationFor(stepId); // End items
|
||||
this._workflowModel.AddLink(completionId, PostId(conditionGroup.Id)); // Merge with parent scope
|
||||
|
||||
// Merge link when no action group is defined
|
||||
if (!item.Actions.Any())
|
||||
{
|
||||
this._workflowModel.AddLink(stepId, completionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Visit(ConditionGroup item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
ConditionGroupExecutor action = new(item, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
this.ContinuationFor(action.Id, action.ParentId);
|
||||
|
||||
string? lastConditionItemId = null;
|
||||
foreach (ConditionItem conditionItem in item.Conditions)
|
||||
{
|
||||
// Create conditional link for conditional action
|
||||
lastConditionItemId = ConditionGroupExecutor.Steps.Item(item, conditionItem);
|
||||
this._workflowModel.AddLink(action.Id, lastConditionItemId, (result) => action.IsMatch(conditionItem, result));
|
||||
|
||||
conditionItem.Accept(this);
|
||||
}
|
||||
|
||||
if (item.ElseActions?.Actions.Length > 0)
|
||||
{
|
||||
if (lastConditionItemId is not null)
|
||||
{
|
||||
// Create clean start for else action from prior conditions
|
||||
this.RestartAfter(lastConditionItemId, action.Id);
|
||||
}
|
||||
// Create conditional link for else action
|
||||
string stepId = ConditionGroupExecutor.Steps.Else(item);
|
||||
this._workflowModel.AddLink(action.Id, stepId, (result) => action.IsElse(result));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Visit(GotoAction item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
string parentId = GetParentId(item);
|
||||
this.ContinueWith(this.CreateStep(item.Id.Value), parentId);
|
||||
this._workflowModel.AddLink(item.Id.Value, item.ActionId.Value);
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
|
||||
protected override void Visit(Foreach item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
ForeachExecutor action = new(item, this._workflowState);
|
||||
string loopId = ForeachExecutor.Steps.Next(action.Id);
|
||||
this.ContinueWith(action, condition: null, CompletionHandler); // Foreach
|
||||
this.ContinueWith(this.CreateStep(loopId, action.TakeNextAsync), action.Id); // Loop Increment
|
||||
string continuationId = this.ContinuationFor(action.Id, action.ParentId); // Action continuation
|
||||
this._workflowModel.AddLink(loopId, continuationId, (_) => !action.HasValue);
|
||||
DelegateActionExecutor startAction = this.CreateStep(ForeachExecutor.Steps.Start(action.Id)); // Action start
|
||||
this._workflowModel.AddNode(startAction, action.Id);
|
||||
this._workflowModel.AddLink(loopId, startAction.Id, (_) => action.HasValue);
|
||||
|
||||
void CompletionHandler()
|
||||
{
|
||||
string endActionsId = ForeachExecutor.Steps.End(action.Id); // Loop continuation
|
||||
this.ContinueWith(this.CreateStep(endActionsId, action.ResetAsync), action.Id);
|
||||
this._workflowModel.AddLink(endActionsId, loopId);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Visit(BreakLoop item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
ForeachExecutor? loopExecutor = this._workflowModel.LocateParent<ForeachExecutor>(item.GetParentId());
|
||||
if (loopExecutor is not null)
|
||||
{
|
||||
string parentId = GetParentId(item);
|
||||
this.ContinueWith(this.CreateStep(item.Id.Value), parentId);
|
||||
this._workflowModel.AddLink(item.Id.Value, PostId(loopExecutor.Id));
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Visit(ContinueLoop item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
ForeachExecutor? loopExecutor = this._workflowModel.LocateParent<ForeachExecutor>(item.GetParentId());
|
||||
if (loopExecutor is not null)
|
||||
{
|
||||
string parentId = GetParentId(item);
|
||||
this.ContinueWith(this.CreateStep(item.Id.Value), parentId);
|
||||
this._workflowModel.AddLink(item.Id.Value, ForeachExecutor.Steps.Next(loopExecutor.Id));
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Visit(EndConversation item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
string parentId = GetParentId(item);
|
||||
this.ContinueWith(this.CreateStep(item.Id.Value), parentId);
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
|
||||
protected override void Visit(AnswerQuestionWithAI item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new AnswerQuestionWithAIExecutor(item, this._workflowOptions.AgentProvider, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(SetVariable item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new SetVariableExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(SetTextVariable item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new SetTextVariableExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(ClearAllVariables item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new ClearAllVariablesExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(ResetVariable item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new ResetVariableExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(EditTable item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new EditTableExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(EditTableV2 item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new EditTableV2Executor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(ParseValue item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new ParseValueExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(SendActivity item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new SendActivityExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
#region Not supported
|
||||
|
||||
protected override void Visit(DeleteActivity item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(GetActivityMembers item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(UpdateActivity item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(ActivateExternalTrigger item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(DisableTrigger item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(WaitForConnectorTrigger item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeConnectorAction item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeCustomModelAction item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeFlowAction item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeAIBuilderModelAction item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeSkillAction item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(AdaptiveCardPrompt item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(Question item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(CSATQuestion item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(OAuthInput item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(BeginDialog item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(UnknownDialogAction item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(EndDialog item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(RepeatDialog item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected override void Visit(HttpRequestAction item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(RecognizeIntent item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(TransferConversation item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(TransferConversationV2 item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(SignOutUser item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(LogCustomTelemetryEvent item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(DisconnectedNodeContainer item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(CreateSearchQuery item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(SearchKnowledgeSources item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(SearchAndSummarizeWithCustomModel item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(SearchAndSummarizeContent item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void ContinueWith(
|
||||
WorkflowActionExecutor executor,
|
||||
Func<object?, bool>? condition = null,
|
||||
Action? completionHandler = null)
|
||||
{
|
||||
executor.Logger = this._workflowOptions.LoggerFactory.CreateLogger(executor.Id);
|
||||
this.ContinueWith(executor, executor.ParentId, condition, completionHandler);
|
||||
}
|
||||
|
||||
private void ContinueWith(
|
||||
Executor executor,
|
||||
string parentId,
|
||||
Func<object?, bool>? condition = null,
|
||||
Action? completionHandler = null)
|
||||
{
|
||||
this._workflowModel.AddNode(executor, parentId, completionHandler);
|
||||
this._workflowModel.AddLinkFromPeer(parentId, executor.Id, condition);
|
||||
}
|
||||
|
||||
public static string RootId(string? actionId) => $"root_{actionId ?? "workflow"}";
|
||||
|
||||
private static string PostId(string actionId) => $"{actionId}_Post";
|
||||
|
||||
private static string GetParentId(BotElement item) =>
|
||||
item.GetParentId() ??
|
||||
throw new DeclarativeModelException($"Missing parent ID for action element: {item.GetId()} [{item.GetType().Name}].");
|
||||
|
||||
private string ContinuationFor(string parentId) => this.ContinuationFor(parentId, parentId);
|
||||
|
||||
private string ContinuationFor(string actionId, string parentId)
|
||||
{
|
||||
actionId = PostId(actionId);
|
||||
this._workflowModel.AddNode(this.CreateStep(actionId), parentId);
|
||||
return actionId;
|
||||
}
|
||||
|
||||
private void RestartAfter(string actionId, string parentId) =>
|
||||
this._workflowModel.AddNode(this.CreateStep($"{actionId}_Continue"), parentId);
|
||||
|
||||
private DelegateActionExecutor CreateStep(string actionId, DelegateAction? stepAction = null)
|
||||
{
|
||||
DelegateActionExecutor stepExecutor = new(actionId, stepAction);
|
||||
|
||||
return stepExecutor;
|
||||
}
|
||||
|
||||
private void NotSupported(DialogAction item)
|
||||
{
|
||||
Debug.WriteLine($"> UNKNOWN: {new string('\t', this._workflowModel.GetDepth(item.GetParentId()))}{FormatItem(item)} => {FormatParent(item)}");
|
||||
this.HasUnsupportedActions = true;
|
||||
}
|
||||
|
||||
private void Trace(BotElement item)
|
||||
{
|
||||
Debug.WriteLine($"> VISIT: {new string('\t', this._workflowModel.GetDepth(item.GetParentId()))}{FormatItem(item)} => {FormatParent(item)}");
|
||||
}
|
||||
|
||||
private void Trace(DialogAction item)
|
||||
{
|
||||
string? parentId = item.GetParentId();
|
||||
if (item.Id.Equals(parentId ?? string.Empty))
|
||||
{
|
||||
parentId = RootId(parentId);
|
||||
}
|
||||
Debug.WriteLine($"> VISIT: {new string('\t', this._workflowModel.GetDepth(parentId))}{FormatItem(item)} => {FormatParent(item)}");
|
||||
}
|
||||
|
||||
private static string FormatItem(BotElement element) => $"{element.GetType().Name} ({element.GetId()})";
|
||||
|
||||
private static string FormatParent(BotElement element) =>
|
||||
element.Parent is null ?
|
||||
throw new DeclarativeModelException($"Undefined parent for {element.GetType().Name} that is member of {element.GetId()}.") :
|
||||
$"{element.Parent.GetType().Name} ({element.GetParentId()})";
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed class WorkflowElementWalker : BotElementWalker
|
||||
{
|
||||
private readonly WorkflowActionVisitor _visitor;
|
||||
|
||||
public WorkflowElementWalker(BotElement rootElement, WorkflowActionVisitor visitor)
|
||||
{
|
||||
this._visitor = visitor;
|
||||
this.Visit(rootElement);
|
||||
}
|
||||
|
||||
public Workflow<TInput> GetWorkflow<TInput>() => this._visitor.Complete<TInput>();
|
||||
|
||||
public override bool DefaultVisit(BotElement definition)
|
||||
{
|
||||
if (definition is DialogAction action)
|
||||
{
|
||||
action.Accept(this._visitor);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Workflow Framework</Title>
|
||||
<Description>Contains the Microsoft Agent Declarative Workflow Framework.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Agents.Persistent" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
|
||||
<PackageReference Include="Microsoft.PowerFx.Interpreter" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.Workflows.Declarative.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class AnswerQuestionWithAIExecutor(AnswerQuestionWithAI model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState state)
|
||||
: DeclarativeActionExecutor<AnswerQuestionWithAI>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
StringExpression userInputExpression = Throw.IfNull(this.Model.UserInput, $"{nameof(this.Model)}.{nameof(this.Model.UserInput)}");
|
||||
|
||||
string agentInstructions = this.State.Format(this.Model.AdditionalInstructions) ?? string.Empty;
|
||||
// ISSUE #485 - Agent identifier embedded in instructions until updated OM is available.
|
||||
string agentId;
|
||||
string? additionalInstructions = null;
|
||||
int delimiterIndex = agentInstructions.IndexOf(',');
|
||||
if (delimiterIndex < 0)
|
||||
{
|
||||
agentId = agentInstructions.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
agentId = agentInstructions.Substring(0, delimiterIndex).Trim();
|
||||
additionalInstructions = agentInstructions.Substring(delimiterIndex + 1).Trim();
|
||||
}
|
||||
|
||||
AIAgent agent = await agentProvider.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string? userInput = null;
|
||||
if (this.Model.UserInput is not null)
|
||||
{
|
||||
EvaluationResult<string> expressionResult = this.State.ExpressionEngine.GetValue(userInputExpression);
|
||||
userInput = expressionResult.Value;
|
||||
}
|
||||
|
||||
ChatClientAgentRunOptions options =
|
||||
new(
|
||||
new ChatOptions()
|
||||
{
|
||||
Instructions = additionalInstructions,
|
||||
});
|
||||
|
||||
FormulaValue conversationValue =
|
||||
this.Model.AutoSend ? // ISSUE #485: Conversation implicitly managed until updated OM is available.
|
||||
this.State.GetConversationId() :
|
||||
this.State.GetInternalConversationId();
|
||||
|
||||
string? conversationId = null;
|
||||
if (conversationValue is StringValue stringValue)
|
||||
{
|
||||
await AssignConversationId(stringValue.Value).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
AgentThread agentThread = new() { ConversationId = conversationId };
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates =
|
||||
!string.IsNullOrWhiteSpace(userInput) ?
|
||||
agent.RunStreamingAsync(userInput, agentThread, options, cancellationToken) :
|
||||
agent.RunStreamingAsync(agentThread, options, cancellationToken);
|
||||
|
||||
string? messageId = null;
|
||||
List<AgentRunResponseUpdate> agentResponseUpdates = new(0x400);
|
||||
await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false))
|
||||
{
|
||||
agentResponseUpdates.Add(update);
|
||||
messageId ??= update.MessageId;
|
||||
await AssignConversationId(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false);
|
||||
if (this.Model.AutoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
AgentRunResponse agentResponse = agentResponseUpdates.ToAgentRunResponse();
|
||||
|
||||
ChatMessage response = agentResponse.Messages.Last();
|
||||
await this.State.SetLastMessageAsync(context, response).ConfigureAwait(false);
|
||||
if (this.Model.AutoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Assign conversation ID if it wasn't already assigned.
|
||||
if (conversationValue is not StringValue && conversationId is not null)
|
||||
{
|
||||
if (this.Model.AutoSend) // ISSUE #485: Conversation implicitly managed until updated OM is available.
|
||||
{
|
||||
await this.State.SetConversationIdAsync(context, conversationId).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.State.SetInternalConversationIdAsync(context, conversationId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
PropertyPath? variablePath = this.Model.Variable?.Path;
|
||||
if (variablePath is not null)
|
||||
{
|
||||
await this.AssignAsync(variablePath, response.ToRecord(), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
async ValueTask AssignConversationId(string? assignValue)
|
||||
{
|
||||
if (assignValue != null && conversationId == null)
|
||||
{
|
||||
conversationId = assignValue;
|
||||
await context.AddEventAsync(new ConversationUpdateEvent(this.Id, conversationId)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, DeclarativeWorkflowState state)
|
||||
: DeclarativeActionExecutor<ClearAllVariables>(model, state)
|
||||
{
|
||||
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
EvaluationResult<VariablesToClearWrapper> variablesResult = this.State.ExpressionEngine.GetValue<VariablesToClearWrapper>(this.Model.Variables);
|
||||
|
||||
variablesResult.Value.Handle(new ScopeHandler(this.Id, this.State));
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private sealed class ScopeHandler(string executorId, DeclarativeWorkflowState state) : IEnumVariablesToClearHandler
|
||||
{
|
||||
public void HandleAllGlobalVariables()
|
||||
{
|
||||
this.ClearAll(VariableScopeNames.Global);
|
||||
}
|
||||
|
||||
public void HandleConversationHistory()
|
||||
{
|
||||
// Not supported....
|
||||
}
|
||||
|
||||
public void HandleConversationScopedVariables()
|
||||
{
|
||||
this.ClearAll(WorkflowScopes.DefaultScopeName);
|
||||
}
|
||||
|
||||
public void HandleUnknownValue()
|
||||
{
|
||||
// No scope to clear for unknown values.
|
||||
}
|
||||
|
||||
public void HandleUserScopedVariables()
|
||||
{
|
||||
// Not supported....
|
||||
}
|
||||
|
||||
private void ClearAll(string scope)
|
||||
{
|
||||
state.Reset(scope);
|
||||
Debug.WriteLine(
|
||||
$"""
|
||||
STATE: {this.GetType().Name} [{executorId}]
|
||||
SCOPE: {scope}
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<ConditionGroup>
|
||||
{
|
||||
public static class Steps
|
||||
{
|
||||
public static string Item(ConditionGroup model, ConditionItem conditionItem)
|
||||
{
|
||||
if (conditionItem.Id is not null)
|
||||
{
|
||||
return conditionItem.Id;
|
||||
}
|
||||
int index = model.Conditions.IndexOf(conditionItem);
|
||||
return $"{model.Id}_Items{index}";
|
||||
}
|
||||
|
||||
public static string Else(ConditionGroup model) => model.ElseActions.Id.Value ?? $"{model.Id}_Else";
|
||||
}
|
||||
|
||||
public ConditionGroupExecutor(ConditionGroup model, DeclarativeWorkflowState state)
|
||||
: base(model, state)
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsMatch(ConditionItem conditionItem, object? result)
|
||||
{
|
||||
if (result is not DeclarativeExecutorResult message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(Steps.Item(this.Model, conditionItem), message.Result as string, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public bool IsElse(object? result)
|
||||
{
|
||||
if (result is not DeclarativeExecutorResult message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(Steps.Else(this.Model), message.Result as string, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
{
|
||||
for (int index = 0; index < this.Model.Conditions.Length; ++index)
|
||||
{
|
||||
ConditionItem conditionItem = this.Model.Conditions[index];
|
||||
if (conditionItem.Condition is null)
|
||||
{
|
||||
continue; // Skip if no condition is defined
|
||||
}
|
||||
|
||||
EvaluationResult<bool> expressionResult = this.State.ExpressionEngine.GetValue(conditionItem.Condition);
|
||||
if (expressionResult.Value)
|
||||
{
|
||||
return Steps.Item(this.Model, conditionItem);
|
||||
}
|
||||
}
|
||||
|
||||
return Steps.Else(this.Model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class EditTableExecutor(EditTable model, DeclarativeWorkflowState state) : DeclarativeActionExecutor<EditTable>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
PropertyPath variablePath = Throw.IfNull(this.Model.ItemsVariable?.Path, $"{nameof(this.Model)}.{nameof(this.Model.ItemsVariable)}");
|
||||
|
||||
FormulaValue table = this.State.Get(variablePath);
|
||||
if (table is not TableValue tableValue)
|
||||
{
|
||||
throw this.Exception($"Require '{variablePath.Format()}' to be a table, not: '{table.GetType().Name}'.");
|
||||
}
|
||||
|
||||
TableChangeType changeType = this.Model.ChangeType.Value;
|
||||
switch (this.Model.ChangeType.Value)
|
||||
{
|
||||
case TableChangeType.Add:
|
||||
ValueExpression addItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}");
|
||||
EvaluationResult<DataValue> addResult = this.State.ExpressionEngine.GetValue(addItemValue);
|
||||
RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), addResult.Value.ToFormulaValue());
|
||||
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false);
|
||||
break;
|
||||
case TableChangeType.Remove:
|
||||
ValueExpression removeItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}");
|
||||
EvaluationResult<DataValue> removeResult = this.State.ExpressionEngine.GetValue(removeItemValue);
|
||||
if (removeResult.Value is TableDataValue removeItemTable)
|
||||
{
|
||||
await tableValue.RemoveAsync(removeItemTable?.Values.Select(row => row.ToRecordValue()), all: true, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, RecordValue.Empty(), context).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
case TableChangeType.Clear:
|
||||
await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
break;
|
||||
case TableChangeType.TakeFirst:
|
||||
RecordValue? firstRow = tableValue.Rows.FirstOrDefault()?.Value;
|
||||
if (firstRow is not null)
|
||||
{
|
||||
await tableValue.RemoveAsync([firstRow], all: true, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, firstRow, context).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
case TableChangeType.TakeLast:
|
||||
RecordValue? lastRow = tableValue.Rows.LastOrDefault()?.Value;
|
||||
if (lastRow is not null)
|
||||
{
|
||||
await tableValue.RemoveAsync([lastRow], all: true, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, lastRow, context).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
static RecordValue BuildRecord(RecordType recordType, FormulaValue value)
|
||||
{
|
||||
return FormulaValue.NewRecordFromFields(recordType, GetValues());
|
||||
|
||||
IEnumerable<NamedValue> GetValues()
|
||||
{
|
||||
foreach (NamedFormulaType fieldType in recordType.GetFieldTypes())
|
||||
{
|
||||
if (value is RecordValue recordValue)
|
||||
{
|
||||
yield return new NamedValue(fieldType.Name, recordValue.GetField(fieldType.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new NamedValue(fieldType.Name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflowState state) : DeclarativeActionExecutor<EditTableV2>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
PropertyPath variablePath = Throw.IfNull(this.Model.ItemsVariable?.Path, $"{nameof(this.Model)}.{nameof(this.Model.ItemsVariable)}");
|
||||
|
||||
FormulaValue table = this.State.Get(variablePath);
|
||||
if (table is not TableValue tableValue)
|
||||
{
|
||||
throw this.Exception($"Require '{variablePath.Format()}' to be a table, not: '{table.GetType().Name}'.");
|
||||
}
|
||||
|
||||
EditTableOperation? changeType = this.Model.ChangeType;
|
||||
if (changeType is AddItemOperation addItemOperation)
|
||||
{
|
||||
ValueExpression addItemValue = Throw.IfNull(addItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}");
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.GetValue(addItemValue);
|
||||
RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormulaValue());
|
||||
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false);
|
||||
}
|
||||
else if (changeType is ClearItemsOperation)
|
||||
{
|
||||
await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
}
|
||||
else if (changeType is RemoveItemOperation removeItemOperation)
|
||||
{
|
||||
ValueExpression removeItemValue = Throw.IfNull(removeItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}");
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.GetValue(removeItemValue);
|
||||
if (expressionResult.Value.ToFormulaValue() is TableValue removeItemTable)
|
||||
{
|
||||
await tableValue.RemoveAsync(removeItemTable?.Rows.Select(row => row.Value), all: true, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (changeType is TakeLastItemOperation)
|
||||
{
|
||||
RecordValue? lastRow = tableValue.Rows.LastOrDefault()?.Value;
|
||||
if (lastRow is not null)
|
||||
{
|
||||
await tableValue.RemoveAsync([lastRow], all: true, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, lastRow, context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (changeType is TakeFirstItemOperation)
|
||||
{
|
||||
RecordValue? firstRow = tableValue.Rows.FirstOrDefault()?.Value;
|
||||
if (firstRow is not null)
|
||||
{
|
||||
await tableValue.RemoveAsync([firstRow], all: true, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, firstRow, context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
static RecordValue BuildRecord(RecordType recordType, FormulaValue value)
|
||||
{
|
||||
return FormulaValue.NewRecordFromFields(recordType, GetValues());
|
||||
|
||||
IEnumerable<NamedValue> GetValues()
|
||||
{
|
||||
foreach (NamedFormulaType fieldType in recordType.GetFieldTypes())
|
||||
{
|
||||
if (value is RecordValue recordValue)
|
||||
{
|
||||
yield return new NamedValue(fieldType.Name, recordValue.GetField(fieldType.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new NamedValue(fieldType.Name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
{
|
||||
public static class Steps
|
||||
{
|
||||
public static string Start(string id) => $"{id}_{nameof(Start)}";
|
||||
public static string Next(string id) => $"{id}_{nameof(Next)}";
|
||||
public static string End(string id) => $"{id}_{nameof(End)}";
|
||||
}
|
||||
|
||||
private int _index;
|
||||
private FormulaValue[] _values;
|
||||
|
||||
public ForeachExecutor(Foreach model, DeclarativeWorkflowState state)
|
||||
: base(model, state)
|
||||
{
|
||||
this._values = [];
|
||||
}
|
||||
|
||||
public bool HasValue { get; private set; }
|
||||
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
this._index = 0;
|
||||
|
||||
if (this.Model.Items is null)
|
||||
{
|
||||
this._values = [];
|
||||
this.HasValue = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.GetValue(this.Model.Items);
|
||||
if (expressionResult.Value is TableDataValue tableValue)
|
||||
{
|
||||
this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormulaValue())];
|
||||
}
|
||||
else
|
||||
{
|
||||
this._values = [expressionResult.Value.ToFormulaValue()];
|
||||
}
|
||||
}
|
||||
|
||||
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async ValueTask TakeNextAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.HasValue = this._index < this._values.Length)
|
||||
{
|
||||
FormulaValue value = this._values[this._index];
|
||||
|
||||
await this.State.SetAsync(Throw.IfNull(this.Model.Value), value, context).ConfigureAwait(false);
|
||||
|
||||
if (this.Model.Index is not null)
|
||||
{
|
||||
await this.State.SetAsync(this.Model.Index.Path, FormulaValue.New(this._index), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._index++;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
this.State.Reset(Throw.IfNull(this.Model.Value));
|
||||
if (this.Model.Index is not null)
|
||||
{
|
||||
this.State.Reset(this.Model.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class ParseValueExecutor(ParseValue model, DeclarativeWorkflowState state) :
|
||||
DeclarativeActionExecutor<ParseValue>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}");
|
||||
ValueExpression valueExpression = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}");
|
||||
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.GetValue(valueExpression);
|
||||
|
||||
FormulaValue? parsedResult = null;
|
||||
|
||||
if (expressionResult.Value is RecordDataValue recordValue)
|
||||
{
|
||||
parsedResult = recordValue.ToFormulaValue();
|
||||
}
|
||||
else if (expressionResult.Value is StringDataValue stringValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stringValue.Value))
|
||||
{
|
||||
parsedResult = FormulaValue.NewBlank(expressionResult.Value.GetDataType().ToFormulaType());
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedResult =
|
||||
this.Model.ValueType switch
|
||||
{
|
||||
StringDataType => StringValue.New(stringValue.Value),
|
||||
NumberDataType => NumberValue.New(stringValue.Value),
|
||||
BooleanDataType => BooleanValue.New(stringValue.Value),
|
||||
RecordDataType recordType => ParseRecord(recordType, stringValue.Value),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedResult is null)
|
||||
{
|
||||
throw this.Exception("Unable to parse value.");
|
||||
}
|
||||
|
||||
await this.AssignAsync(variablePath, parsedResult, context).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
|
||||
RecordValue ParseRecord(RecordDataType recordType, string rawText)
|
||||
{
|
||||
string jsonText = rawText.TrimJsonDelimiter();
|
||||
JsonDocument json = JsonDocument.Parse(jsonText);
|
||||
JsonElement currentElement = json.RootElement;
|
||||
try
|
||||
{
|
||||
return recordType.ParseRecord(currentElement);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw this.Exception("Failed to parse value.", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class ResetVariableExecutor(ResetVariable model, DeclarativeWorkflowState state) :
|
||||
DeclarativeActionExecutor<ResetVariable>(model, state)
|
||||
{
|
||||
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
PropertyPath variablePath = Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}");
|
||||
|
||||
this.State.Reset(this.Model.Variable);
|
||||
Debug.WriteLine(
|
||||
$"""
|
||||
STATE: {this.GetType().Name} [{this.Id}]
|
||||
NAME: {this.Model.Variable!.Format()}
|
||||
""");
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class SendActivityExecutor(SendActivity model, DeclarativeWorkflowState state) :
|
||||
DeclarativeActionExecutor<SendActivity>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.Model.Activity is MessageActivityTemplate messageActivity)
|
||||
{
|
||||
StringBuilder templateBuilder = new();
|
||||
if (!string.IsNullOrEmpty(messageActivity.Summary))
|
||||
{
|
||||
templateBuilder.AppendLine($"\t{messageActivity.Summary}");
|
||||
}
|
||||
|
||||
string? activityText = this.State.Format(messageActivity.Text)?.Trim();
|
||||
templateBuilder.AppendLine(activityText);
|
||||
|
||||
AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, templateBuilder.ToString().Trim())]);
|
||||
await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class SetTextVariableExecutor(SetTextVariable model, DeclarativeWorkflowState state)
|
||||
: DeclarativeActionExecutor<SetTextVariable>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}");
|
||||
|
||||
if (this.Model.Value is null)
|
||||
{
|
||||
await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
FormulaValue expressionResult = FormulaValue.New(this.State.Format(this.Model.Value));
|
||||
|
||||
await this.AssignAsync(variablePath, expressionResult, context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class SetVariableExecutor(SetVariable model, DeclarativeWorkflowState state)
|
||||
: DeclarativeActionExecutor<SetVariable>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}");
|
||||
|
||||
if (this.Model.Value is null)
|
||||
{
|
||||
await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.GetValue(this.Model.Value);
|
||||
|
||||
await this.AssignAsync(variablePath, expressionResult.Value.ToFormulaValue(), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
|
||||
internal static class RecalcEngineFactory
|
||||
{
|
||||
public static RecalcEngine Create(
|
||||
int? maximumExpressionLength = null,
|
||||
int? maximumCallDepth = null)
|
||||
{
|
||||
RecalcEngine engine = new(CreateConfig());
|
||||
|
||||
foreach (string scopeName in VariableScopeNames.AllScopes)
|
||||
{
|
||||
engine.UpdateVariable(scopeName, RecordValue.Empty());
|
||||
}
|
||||
|
||||
return engine;
|
||||
|
||||
PowerFxConfig CreateConfig()
|
||||
{
|
||||
PowerFxConfig config = new(Features.PowerFxV1);
|
||||
|
||||
if (maximumExpressionLength is not null)
|
||||
{
|
||||
config.MaximumExpressionLength = maximumExpressionLength.Value;
|
||||
}
|
||||
|
||||
if (maximumCallDepth is not null)
|
||||
{
|
||||
config.MaxCallDepth = maximumCallDepth.Value;
|
||||
}
|
||||
|
||||
config.EnableSetFunction();
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.SystemVariables;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
|
||||
internal static class SystemScope
|
||||
{
|
||||
public static class Names
|
||||
{
|
||||
public const string Activity = nameof(Activity);
|
||||
public const string Bot = nameof(Bot);
|
||||
public const string Conversation = nameof(Conversation);
|
||||
public const string ConversationId = nameof(SystemVariables.ConversationId);
|
||||
public const string InternalId = nameof(InternalId);
|
||||
public const string LastMessage = nameof(LastMessage);
|
||||
public const string LastMessageId = nameof(SystemVariables.LastMessageId);
|
||||
public const string LastMessageText = nameof(SystemVariables.LastMessageText);
|
||||
public const string Recognizer = nameof(Recognizer);
|
||||
public const string User = nameof(User);
|
||||
public const string UserLanguage = nameof(UserLanguage);
|
||||
}
|
||||
|
||||
public static ImmutableHashSet<string> AllNames { get; } = GetNames().ToImmutableHashSet();
|
||||
|
||||
public static IEnumerable<string> GetNames()
|
||||
{
|
||||
yield return Names.Activity;
|
||||
yield return Names.Bot;
|
||||
yield return Names.Conversation;
|
||||
yield return Names.ConversationId;
|
||||
yield return Names.InternalId;
|
||||
yield return Names.LastMessage;
|
||||
yield return Names.LastMessageId;
|
||||
yield return Names.LastMessageText;
|
||||
yield return Names.Recognizer;
|
||||
yield return Names.User;
|
||||
yield return Names.UserLanguage;
|
||||
}
|
||||
|
||||
public static void InitializeSystem(this WorkflowScopes scopes)
|
||||
{
|
||||
scopes.Set(Names.Activity, RecordValue.Empty(), VariableScopeNames.System);
|
||||
scopes.Set(Names.Bot, RecordValue.Empty(), VariableScopeNames.System);
|
||||
|
||||
scopes.Set(Names.LastMessage, FormulaType.String.NewBlank(), VariableScopeNames.System);
|
||||
Set(Names.LastMessageId);
|
||||
Set(Names.LastMessageText);
|
||||
|
||||
scopes.Set(
|
||||
Names.Conversation,
|
||||
RecordValue.NewRecordFromFields(
|
||||
new NamedValue("Id", FormulaType.String.NewBlank()),
|
||||
new NamedValue("LocalTimeZone", FormulaValue.New(TimeZoneInfo.Local.StandardName)),
|
||||
new NamedValue("LocalTimeZoneOffset", FormulaValue.New(TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow))),
|
||||
new NamedValue("InTestMode", FormulaValue.New(false))),
|
||||
VariableScopeNames.System);
|
||||
scopes.Set(Names.ConversationId, FormulaType.String.NewBlank(), VariableScopeNames.System);
|
||||
scopes.Set(Names.InternalId, FormulaType.String.NewBlank(), VariableScopeNames.System);
|
||||
|
||||
scopes.Set(
|
||||
Names.Recognizer,
|
||||
RecordValue.NewRecordFromFields(
|
||||
new NamedValue("Id", FormulaType.String.NewBlank()),
|
||||
new NamedValue("Text", FormulaType.String.NewBlank())),
|
||||
VariableScopeNames.System);
|
||||
|
||||
scopes.Set(
|
||||
Names.User,
|
||||
RecordValue.NewRecordFromFields(
|
||||
new NamedValue("Language", StringValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName))),
|
||||
VariableScopeNames.System);
|
||||
scopes.Set(Names.UserLanguage, StringValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName), VariableScopeNames.System);
|
||||
|
||||
void Set(string key, string? value = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
scopes.Set(key, FormulaType.String.NewBlank(), VariableScopeNames.System);
|
||||
}
|
||||
else
|
||||
{
|
||||
scopes.Set(key, FormulaValue.New(value), VariableScopeNames.System);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static FormulaValue GetConversationId(this DeclarativeWorkflowState state) =>
|
||||
state.Get(VariableScopeNames.System, Names.ConversationId);
|
||||
|
||||
public static async ValueTask SetConversationIdAsync(this DeclarativeWorkflowState state, IWorkflowContext context, string conversationId)
|
||||
{
|
||||
RecordValue conversation = (RecordValue)state.Get(VariableScopeNames.System, Names.Conversation);
|
||||
conversation.UpdateField("Id", FormulaValue.New(conversationId));
|
||||
await state.SetAsync(VariableScopeNames.System, Names.Conversation, conversation, context).ConfigureAwait(false);
|
||||
await state.SetAsync(VariableScopeNames.System, Names.ConversationId, FormulaValue.New(conversationId), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static FormulaValue GetInternalConversationId(this DeclarativeWorkflowState state) =>
|
||||
state.Get(VariableScopeNames.System, Names.InternalId);
|
||||
|
||||
public static ValueTask SetInternalConversationIdAsync(this DeclarativeWorkflowState state, IWorkflowContext context, string conversationId) =>
|
||||
state.SetAsync(VariableScopeNames.System, Names.InternalId, FormulaValue.New(conversationId), context);
|
||||
|
||||
public static async ValueTask SetLastMessageAsync(this DeclarativeWorkflowState state, IWorkflowContext context, ChatMessage message)
|
||||
{
|
||||
await state.SetAsync(VariableScopeNames.System, Names.LastMessage, message.ToRecord(), context).ConfigureAwait(false);
|
||||
await state.SetAsync(VariableScopeNames.System, Names.LastMessageId, message.MessageId is null ? FormulaValue.NewBlank(FormulaType.String) : FormulaValue.New(message.MessageId), context).ConfigureAwait(false);
|
||||
await state.SetAsync(VariableScopeNames.System, Names.LastMessageText, FormulaValue.New(message.Text), context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Bot.ObjectModel.Analysis;
|
||||
using Microsoft.Bot.ObjectModel.PowerFx;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
|
||||
internal static class WorkflowDiagnostics
|
||||
{
|
||||
private static readonly WorkflowFeatureConfiguration s_semanticFeatureConfig = new();
|
||||
|
||||
public static void Initialize<TElement>(this WorkflowScopes scopes, TElement workflowElement, IConfiguration? configuration) where TElement : BotElement, IDialogBase
|
||||
{
|
||||
scopes.InitializeSystem();
|
||||
|
||||
SemanticModel semanticModel = workflowElement.GetSemanticModel(new PowerFxExpressionChecker(s_semanticFeatureConfig), s_semanticFeatureConfig);
|
||||
scopes.InitializeEnvironment(semanticModel, configuration);
|
||||
scopes.InitializeDefaults(semanticModel, workflowElement.SchemaName.Value);
|
||||
}
|
||||
|
||||
private static void InitializeEnvironment(this WorkflowScopes scopes, SemanticModel semanticModel, IConfiguration? configuration)
|
||||
{
|
||||
foreach (string variableName in semanticModel.GetAllEnvironmentVariablesReferencedInTheBot())
|
||||
{
|
||||
string? environmentValue = configuration is not null ? configuration[variableName] : Environment.GetEnvironmentVariable(variableName);
|
||||
FormulaValue variableValue = string.IsNullOrEmpty(environmentValue) ? FormulaType.String.NewBlank() : FormulaValue.New(environmentValue);
|
||||
scopes.Set(variableName, variableValue, VariableScopeNames.Environment);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitializeDefaults(this WorkflowScopes scopes, SemanticModel semanticModel, string schemaName)
|
||||
{
|
||||
foreach (VariableInformationDiagnostic variableDiagnostic in semanticModel.GetVariables(schemaName).Where(x => !x.IsSystemVariable).Select(v => v.ToDiagnostic()))
|
||||
{
|
||||
if (variableDiagnostic is null || variableDiagnostic?.Path?.VariableName is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FormulaValue defaultValue = variableDiagnostic.ConstantValue?.ToFormulaValue() ?? variableDiagnostic.Type.NewBlank();
|
||||
|
||||
if (variableDiagnostic.Path.VariableScopeName?.Equals(VariableScopeNames.System, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
{
|
||||
if (!SystemScope.AllNames.Contains(variableDiagnostic.Path.VariableName))
|
||||
{
|
||||
throw new DeclarativeModelException($"Variable '{variableDiagnostic.Path.VariableName}' is not a supported system variable.");
|
||||
}
|
||||
}
|
||||
|
||||
scopes.Set(variableDiagnostic.Path.VariableName, defaultValue, variableDiagnostic.Path.VariableScopeName ?? WorkflowScopes.DefaultScopeName);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WorkflowFeatureConfiguration : IFeatureConfiguration
|
||||
{
|
||||
public long GetInt64Value(string settingName, long defaultValue) => defaultValue;
|
||||
|
||||
public string GetStringValue(string settingName, string defaultValue) => defaultValue;
|
||||
|
||||
public bool IsEnvironmentFeatureEnabled(string featureName, bool defaultValue) => true;
|
||||
|
||||
public bool IsTenantFeatureEnabled(string featureName, bool defaultValue) => true;
|
||||
}
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Bot.ObjectModel.Exceptions;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
|
||||
internal class WorkflowExpressionEngine : IExpressionEngine
|
||||
{
|
||||
private readonly RecalcEngine _engine;
|
||||
|
||||
public WorkflowExpressionEngine(RecalcEngine engine)
|
||||
{
|
||||
this._engine = engine;
|
||||
}
|
||||
|
||||
public EvaluationResult<bool> GetValue(BoolExpression boolean, WorkflowScopes? state = null) => this.GetValue(boolean, state, this.EvaluateScope);
|
||||
|
||||
public EvaluationResult<bool> GetValue(BoolExpression boolean, RecordDataValue state) => this.GetValue(boolean, state, this.EvaluateState);
|
||||
|
||||
public EvaluationResult<string> GetValue(StringExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope);
|
||||
|
||||
public EvaluationResult<string> GetValue(StringExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState);
|
||||
|
||||
public EvaluationResult<DataValue> GetValue(ValueExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope);
|
||||
|
||||
public EvaluationResult<DataValue> GetValue(ValueExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState);
|
||||
|
||||
public EvaluationResult<long> GetValue(IntExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope);
|
||||
|
||||
public EvaluationResult<long> GetValue(IntExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState);
|
||||
|
||||
public EvaluationResult<double> GetValue(NumberExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope);
|
||||
|
||||
public EvaluationResult<double> GetValue(NumberExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState);
|
||||
|
||||
public EvaluationResult<TValue?> GetValue<TValue>(ObjectExpression<TValue> expression, WorkflowScopes? state = null) where TValue : BotElement => this.GetValue(expression, state, this.EvaluateScope);
|
||||
|
||||
public EvaluationResult<TValue?> GetValue<TValue>(ObjectExpression<TValue> expression, RecordDataValue state) where TValue : BotElement => this.GetValue(expression, state, this.EvaluateState);
|
||||
|
||||
public ImmutableArray<T> GetValue<T>(ArrayExpression<T> expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope).Value;
|
||||
|
||||
public ImmutableArray<T> GetValue<T>(ArrayExpression<T> expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState).Value;
|
||||
|
||||
public ImmutableArray<T> GetValue<T>(ArrayExpressionOnly<T> expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope).Value;
|
||||
|
||||
public ImmutableArray<T> GetValue<T>(ArrayExpressionOnly<T> expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState).Value;
|
||||
|
||||
public EvaluationResult<TValue> GetValue<TValue>(EnumExpression<TValue> expression, WorkflowScopes? state = null) where TValue : EnumWrapper =>
|
||||
this.GetValue<TValue, WorkflowScopes>(expression, state, this.EvaluateScope);
|
||||
|
||||
public EvaluationResult<TValue> GetValue<TValue>(EnumExpression<TValue> expression, RecordDataValue state) where TValue : EnumWrapper =>
|
||||
this.GetValue<TValue, RecordDataValue>(expression, state, this.EvaluateState);
|
||||
|
||||
public DialogSchemaName GetValue(DialogExpression expression, RecordDataValue state)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public EvaluationResult<string> GetValue(AdaptiveCardExpression expression, RecordDataValue state)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public EvaluationResult<FileDataValue?> GetValue(FileExpression expression, RecordDataValue state)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private EvaluationResult<bool> GetValue<TState>(BoolExpression expression, TState state, Func<ExpressionBase, TState, EvaluationResult<FormulaValue>> evaluator)
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return new EvaluationResult<bool>(expression.LiteralValue, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
if (expressionResult.Value is BlankValue)
|
||||
{
|
||||
return new EvaluationResult<bool>(default, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
if (expressionResult.Value is not BooleanValue formulaValue)
|
||||
{
|
||||
throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.Boolean);
|
||||
}
|
||||
|
||||
return new EvaluationResult<bool>(formulaValue.Value, expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
private EvaluationResult<string> GetValue<TState>(StringExpression expression, TState state, Func<ExpressionBase, TState, EvaluationResult<FormulaValue>> evaluator)
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return new EvaluationResult<string>(expression.LiteralValue, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
if (expressionResult.Value is BlankValue)
|
||||
{
|
||||
return new EvaluationResult<string>(string.Empty, expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
if (expressionResult.Value is RecordValue recordValue)
|
||||
{
|
||||
return new EvaluationResult<string>(recordValue.Format(), expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
if (expressionResult.Value is not StringValue formulaValue)
|
||||
{
|
||||
throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.String);
|
||||
}
|
||||
|
||||
return new EvaluationResult<string>(formulaValue.Value, expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
private EvaluationResult<long> GetValue<TState>(IntExpression expression, TState state, Func<ExpressionBase, TState, EvaluationResult<FormulaValue>> evaluator)
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return new EvaluationResult<long>(expression.LiteralValue, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
if (expressionResult.Value is BlankValue)
|
||||
{
|
||||
return new EvaluationResult<long>(default, expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
if (expressionResult.Value is not DecimalValue formulaValue)
|
||||
{
|
||||
throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.Number);
|
||||
}
|
||||
|
||||
return new EvaluationResult<long>(Convert.ToInt64(formulaValue.Value), expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
private EvaluationResult<double> GetValue<TState>(NumberExpression expression, TState state, Func<ExpressionBase, TState, EvaluationResult<FormulaValue>> evaluator)
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return new EvaluationResult<double>(expression.LiteralValue, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
if (expressionResult.Value is BlankValue)
|
||||
{
|
||||
return new EvaluationResult<double>(default, expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
if (expressionResult.Value is DecimalValue decimalValue)
|
||||
{
|
||||
return new EvaluationResult<double>(Convert.ToDouble(decimalValue.Value), expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
if (expressionResult.Value is not NumberValue formulaValue)
|
||||
{
|
||||
throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.Float);
|
||||
}
|
||||
|
||||
return new EvaluationResult<double>(formulaValue.Value, expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
private EvaluationResult<DataValue> GetValue<TState>(ValueExpression expression, TState? state, Func<ExpressionBase, TState?, EvaluationResult<FormulaValue>> evaluator)
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return new EvaluationResult<DataValue>(expression.LiteralValue ?? BlankDataValue.Instance, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
return new EvaluationResult<DataValue>(expressionResult.Value.ToDataValue(), expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
private EvaluationResult<TValue> GetValue<TValue, TState>(EnumExpression<TValue> expression, TState? state, Func<ExpressionBase, TState?, EvaluationResult<FormulaValue>> evaluator) where TValue : EnumWrapper
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return new EvaluationResult<TValue>(expression.LiteralValue, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
return expressionResult.Value switch
|
||||
{
|
||||
BlankValue => new EvaluationResult<TValue>(EnumWrapper.Create<TValue>(0), expressionResult.Sensitivity),
|
||||
StringValue s when s.Value is not null => new EvaluationResult<TValue>(EnumWrapper.Create<TValue>(s.Value), expressionResult.Sensitivity),
|
||||
StringValue => new EvaluationResult<TValue>(EnumWrapper.Create<TValue>(0), expressionResult.Sensitivity),
|
||||
NumberValue number => new EvaluationResult<TValue>(EnumWrapper.Create<TValue>((int)number.Value), expressionResult.Sensitivity),
|
||||
_ => throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.String),
|
||||
};
|
||||
}
|
||||
|
||||
private EvaluationResult<TValue?> GetValue<TValue, TState>(ObjectExpression<TValue> expression, TState state, Func<ExpressionBase, TState, EvaluationResult<FormulaValue>> evaluator) where TValue : BotElement
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
if (expression.LiteralValue != null)
|
||||
{
|
||||
return new EvaluationResult<TValue?>(expression.LiteralValue, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
if (expressionResult.Value is BlankValue)
|
||||
{
|
||||
return new EvaluationResult<TValue?>(null, expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
if (expressionResult.Value is not RecordValue formulaValue)
|
||||
{
|
||||
throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.TableFromEnumerable<TValue>());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new EvaluationResult<TValue?>(ObjectExpressionParser<TValue>.Parse(formulaValue.ToRecord()), expressionResult.Sensitivity);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new CannotParseObjectExpressionOutputException(typeof(TValue), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private EvaluationResult<ImmutableArray<TValue>> GetValue<TState, TValue>(ArrayExpression<TValue> expression, TState state, Func<ExpressionBase, TState, EvaluationResult<FormulaValue>> evaluator)
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
return new EvaluationResult<ImmutableArray<TValue>>(expression.LiteralValue, SensitivityLevel.None);
|
||||
}
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
return new EvaluationResult<ImmutableArray<TValue>>(ParseArrayResults<TValue>(expressionResult.Value), expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
private EvaluationResult<ImmutableArray<TValue>> GetValue<TState, TValue>(ArrayExpressionOnly<TValue> expression, TState state, Func<ExpressionBase, TState, EvaluationResult<FormulaValue>> evaluator)
|
||||
{
|
||||
Throw.IfNull(expression, nameof(expression));
|
||||
|
||||
EvaluationResult<FormulaValue> expressionResult = evaluator.Invoke(expression, state);
|
||||
|
||||
return new EvaluationResult<ImmutableArray<TValue>>(ParseArrayResults<TValue>(expressionResult.Value), expressionResult.Sensitivity);
|
||||
}
|
||||
|
||||
private static ImmutableArray<TValue> ParseArrayResults<TValue>(FormulaValue value)
|
||||
{
|
||||
if (value is BlankValue)
|
||||
{
|
||||
return ImmutableArray<TValue>.Empty;
|
||||
}
|
||||
|
||||
if (value is not TableValue tableValue)
|
||||
{
|
||||
throw new InvalidExpressionOutputTypeException(value.GetDataType(), DataType.TableFromEnumerable<TValue>());
|
||||
}
|
||||
|
||||
TableDataValue tableDataValue = tableValue.ToTable();
|
||||
try
|
||||
{
|
||||
List<TValue> list = [];
|
||||
foreach (RecordDataValue row in tableDataValue.Values)
|
||||
{
|
||||
TValue? s = TableItemParser<TValue>.Parse(row);
|
||||
if (s != null)
|
||||
{
|
||||
list.Add(s);
|
||||
}
|
||||
}
|
||||
return list.ToImmutableArray();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new CannotParseObjectExpressionOutputException(typeof(TValue), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private EvaluationResult<FormulaValue> EvaluateState(ExpressionBase expression, RecordDataValue? state)
|
||||
{
|
||||
if (state is not null)
|
||||
{
|
||||
foreach (KeyValuePair<string, DataValue> kvp in state.Properties)
|
||||
{
|
||||
if (kvp.Value is RecordDataValue scopeRecord)
|
||||
{
|
||||
Bind(kvp.Key, scopeRecord.ToRecordValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.Evaluate(expression);
|
||||
|
||||
void Bind(string scopeName, RecordValue stateRecord)
|
||||
{
|
||||
this._engine.DeleteFormula(scopeName);
|
||||
this._engine.UpdateVariable(scopeName, stateRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private EvaluationResult<FormulaValue> EvaluateScope(ExpressionBase expression, WorkflowScopes? state = null)
|
||||
{
|
||||
state?.Bind(this._engine);
|
||||
|
||||
return this.Evaluate(expression);
|
||||
}
|
||||
|
||||
private EvaluationResult<FormulaValue> Evaluate(ExpressionBase expression)
|
||||
{
|
||||
string? expressionText =
|
||||
expression.IsVariableReference ?
|
||||
expression.VariableReference?.Format() :
|
||||
expression.ExpressionText;
|
||||
|
||||
return new(this._engine.Eval(expressionText), SensitivityLevel.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
|
||||
/// <summary>
|
||||
/// Contains all action scopes for a process.
|
||||
/// </summary>
|
||||
internal sealed class WorkflowScopes
|
||||
{
|
||||
// ISSUE #488 - Update default scope for workflows to `Workflow` (instead of `Topic`)
|
||||
public const string DefaultScopeName = VariableScopeNames.Topic;
|
||||
|
||||
private readonly ImmutableDictionary<string, WorkflowScope> _scopes;
|
||||
|
||||
public WorkflowScopes()
|
||||
{
|
||||
this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => scopeName, scopeName => new WorkflowScope(scopeName)).ToImmutableDictionary();
|
||||
}
|
||||
|
||||
public FormulaValue Get(string variableName, string? scopeName = null)
|
||||
{
|
||||
if (this._scopes[scopeName ?? WorkflowScopes.DefaultScopeName].TryGetValue(variableName, out FormulaValue? value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return FormulaValue.NewBlank();
|
||||
}
|
||||
|
||||
public void Clear(string scopeName) => this._scopes[scopeName].Reset();
|
||||
|
||||
public void Reset(string variableName, string? scopeName = null) => this._scopes[scopeName ?? WorkflowScopes.DefaultScopeName].Reset(variableName);
|
||||
|
||||
public void Set(string variableName, FormulaValue value, string? scopeName = null) => this._scopes[scopeName ?? WorkflowScopes.DefaultScopeName][variableName] = value;
|
||||
|
||||
public RecordValue BuildRecord(string scopeName) => this._scopes[scopeName].BuildRecord();
|
||||
|
||||
public RecordDataValue BuildState()
|
||||
{
|
||||
return DataValue.RecordFromFields(BuildStateFields());
|
||||
|
||||
IEnumerable<KeyValuePair<string, DataValue>> BuildStateFields()
|
||||
{
|
||||
foreach (KeyValuePair<string, WorkflowScope> kvp in this._scopes)
|
||||
{
|
||||
yield return new(kvp.Key, kvp.Value.BuildState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind(RecalcEngine engine, string? type = null)
|
||||
{
|
||||
if (type is not null)
|
||||
{
|
||||
Bind(type);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string scopeName in VariableScopeNames.AllScopes)
|
||||
{
|
||||
Bind(scopeName);
|
||||
}
|
||||
}
|
||||
|
||||
void Bind(string scopeName)
|
||||
{
|
||||
RecordValue scopeRecord = this.BuildRecord(scopeName);
|
||||
engine.DeleteFormula(scopeName);
|
||||
engine.UpdateVariable(scopeName, scopeRecord);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The set of variables for a specific action scope.
|
||||
/// </summary>
|
||||
private sealed class WorkflowScope(string scopeName) : Dictionary<string, FormulaValue>
|
||||
{
|
||||
public string Name => scopeName;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (string variableName in this.Keys.ToArray())
|
||||
{
|
||||
this.Reset(variableName);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset(string variableName)
|
||||
{
|
||||
if (this.TryGetValue(variableName, out FormulaValue? value))
|
||||
{
|
||||
this[variableName] = value.Type.NewBlank();
|
||||
}
|
||||
}
|
||||
|
||||
public RecordValue BuildRecord()
|
||||
{
|
||||
return FormulaValue.NewRecordFromFields(GetFields());
|
||||
|
||||
IEnumerable<NamedValue> GetFields()
|
||||
{
|
||||
foreach (KeyValuePair<string, FormulaValue> kvp in this)
|
||||
{
|
||||
yield return new NamedValue(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RecordDataValue BuildState()
|
||||
{
|
||||
RecordDataValue.Builder recordBuilder = new();
|
||||
|
||||
foreach (KeyValuePair<string, FormulaValue> kvp in this)
|
||||
{
|
||||
recordBuilder.Properties.Add(kvp.Key, kvp.Value.ToDataValue());
|
||||
}
|
||||
|
||||
return recordBuilder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Core;
|
||||
using Azure.Core.Pipeline;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for workflow agent providers.
|
||||
/// </summary>
|
||||
public abstract class WorkflowAgentProvider
|
||||
{
|
||||
/// <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 cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="AIAgent"/> associated
|
||||
/// with the specified <paramref name="agentId"/>. Returns <see langword="null"/> if no agent is found.</returns>
|
||||
public abstract Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to interact with Foundry agents within a specified project context.
|
||||
/// </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="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 FoundryAgentProvider(string projectEndpoint, TokenCredential? projectCredentials = null, HttpClient? httpClient = null) : WorkflowAgentProvider
|
||||
{
|
||||
private PersistentAgentsClient? _agentsClient;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AIAgent agent = await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private PersistentAgentsClient GetAgentsClient()
|
||||
{
|
||||
if (this._agentsClient is null)
|
||||
{
|
||||
PersistentAgentsAdministrationClientOptions clientOptions = new();
|
||||
|
||||
if (httpClient is not null)
|
||||
{
|
||||
clientOptions.Transport = new HttpClientTransport(httpClient);
|
||||
}
|
||||
|
||||
PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials ?? new DefaultAzureCredential(), clientOptions);
|
||||
|
||||
Interlocked.CompareExchange(ref this._agentsClient, newClient, null);
|
||||
}
|
||||
|
||||
return this._agentsClient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when an agent run produces an update.
|
||||
/// </summary>
|
||||
public class AgentRunResponseEvent : ExecutorEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentRunUpdateEvent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="executorId">The identifier of the executor that generated this event.</param>
|
||||
/// <param name="response"></param>
|
||||
public AgentRunResponseEvent(string executorId, AgentRunResponse response) : base(executorId, data: response)
|
||||
{
|
||||
this.Response = response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content of the agent response.
|
||||
/// </summary>
|
||||
public AgentRunResponse Response { get; }
|
||||
}
|
||||
@@ -140,19 +140,19 @@ internal class StateManager
|
||||
|
||||
public async ValueTask PublishUpdatesAsync(IStepTracer? tracer)
|
||||
{
|
||||
Dictionary<ScopeId, Dictionary<string, List<StateUpdate>>> updatesByScope = new();
|
||||
Dictionary<ScopeId, Dictionary<string, List<StateUpdate>>> updatesByScope = [];
|
||||
|
||||
// Aggregate the updates for each scope
|
||||
foreach (UpdateKey key in this._queuedUpdates.Keys)
|
||||
{
|
||||
if (!updatesByScope.TryGetValue(key.ScopeId, out Dictionary<string, List<StateUpdate>>? scopeUpdates))
|
||||
{
|
||||
updatesByScope[key.ScopeId] = scopeUpdates = new();
|
||||
updatesByScope[key.ScopeId] = scopeUpdates = [];
|
||||
}
|
||||
|
||||
if (!scopeUpdates.TryGetValue(key.Key, out List<StateUpdate>? stateUpdates))
|
||||
{
|
||||
scopeUpdates[key.Key] = stateUpdates = new();
|
||||
scopeUpdates[key.Key] = stateUpdates = [];
|
||||
}
|
||||
|
||||
stateUpdates.Add(this._queuedUpdates[key]);
|
||||
|
||||
@@ -94,7 +94,7 @@ public abstract class Executor : IIdentified
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new TargetInvocationException($"Error invoking handler for {message.GetType()}", result.Exception!);
|
||||
throw new TargetInvocationException($"Error invoking handler for {message.GetType()}", result.Exception);
|
||||
}
|
||||
|
||||
if (result.IsVoid)
|
||||
|
||||
@@ -9,4 +9,11 @@ namespace Microsoft.Agents.Workflows;
|
||||
/// </summary>
|
||||
/// <param name="executorId">The unique identifier of the executor that has failed.</param>
|
||||
/// <param name="err">The exception representing the error.</param>
|
||||
public sealed class ExecutorFailureEvent(string executorId, Exception? err) : ExecutorEvent(executorId, data: err);
|
||||
public sealed class ExecutorFailureEvent(string executorId, Exception? err)
|
||||
: ExecutorEvent(executorId, data: err)
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that caused the executor to fail. This may be <c>null</c> if no exception was thrown.
|
||||
/// </summary>
|
||||
public new Exception? Data => err;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ public interface IWorkflowContext
|
||||
ValueTask SendMessageAsync(object message, string? targetId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a state value from the workflow's state store. If no scope is provided, the executor's private
|
||||
/// scope is used.
|
||||
/// Reads a state value from the workflow's state store. If no scope is provided, the executor's
|
||||
/// default scope is used.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the state value.</typeparam>
|
||||
/// <param name="key">The key of the state value.</param>
|
||||
|
||||
@@ -18,8 +18,8 @@ internal class InProcessRunnerContext<TExternalInput> : IRunnerContext
|
||||
{
|
||||
private StepContext _nextStep = new();
|
||||
private readonly Dictionary<string, ExecutorRegistration> _executorRegistrations;
|
||||
private readonly Dictionary<string, Executor> _executors = new();
|
||||
private readonly Dictionary<string, ExternalRequest> _externalRequests = new();
|
||||
private readonly Dictionary<string, Executor> _executors = [];
|
||||
private readonly Dictionary<string, ExternalRequest> _externalRequests = [];
|
||||
|
||||
public InProcessRunnerContext(Workflow workflow, ILogger? logger = null)
|
||||
{
|
||||
@@ -96,7 +96,7 @@ internal class InProcessRunnerContext<TExternalInput> : IRunnerContext
|
||||
|
||||
public bool CompleteRequest(string requestId) => this._externalRequests.Remove(requestId);
|
||||
|
||||
public readonly List<WorkflowEvent> QueuedEvents = new();
|
||||
public readonly List<WorkflowEvent> QueuedEvents = [];
|
||||
|
||||
internal StateManager StateManager { get; } = new();
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed class SwitchBuilder
|
||||
List<(Func<object?, bool> Predicate, HashSet<int> OutgoingIndicies)> caseMap = this._caseMap;
|
||||
HashSet<int> defaultIndicies = this._defaultIndicies;
|
||||
|
||||
return builder.AddFanOutEdge(source, CasePartitioner, this._executors.ToArray());
|
||||
return builder.AddFanOutEdge(source, CasePartitioner, [.. this._executors]);
|
||||
|
||||
IEnumerable<int> CasePartitioner(object? input, int targetCount)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
@@ -27,10 +26,8 @@ public sealed class AgentProxy : AIAgent
|
||||
/// <param name="client">The actor client used to communicate with the agent.</param>
|
||||
public AgentProxy(string name, IActorClient client)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
this._client = client;
|
||||
this.Name = name;
|
||||
this._client = Throw.IfNull(client, nameof(client));
|
||||
this.Name = Throw.IfNullOrEmpty(name, nameof(name));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -53,9 +50,9 @@ public sealed class AgentProxy : AIAgent
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
var agentThread = GetAgentThreadId(thread);
|
||||
return await this.RunAsync(messages, agentThread, cancellationToken).ConfigureAwait(false);
|
||||
Throw.IfNull(messages, nameof(messages));
|
||||
string agentThreadId = GetAgentThreadId(thread);
|
||||
return await this.RunAsync(messages, agentThreadId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -65,9 +62,9 @@ public sealed class AgentProxy : AIAgent
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
var agentThread = GetAgentThreadId(thread);
|
||||
await foreach (var item in this.RunStreamingAsync(messages, agentThread, cancellationToken).ConfigureAwait(false))
|
||||
Throw.IfNull(messages, nameof(messages));
|
||||
string agentThreadId = GetAgentThreadId(thread);
|
||||
await foreach (var item in this.RunStreamingAsync(messages, agentThreadId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
@@ -75,7 +72,6 @@ public sealed class AgentProxy : AIAgent
|
||||
|
||||
private async Task<AgentRunResponse> RunAsync(IReadOnlyCollection<ChatMessage> messages, string threadId, CancellationToken cancellationToken)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
var handle = await this.RunCoreAsync(messages, threadId, cancellationToken).ConfigureAwait(false);
|
||||
var response = await handle.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||
return response.Status switch
|
||||
@@ -93,7 +89,6 @@ public sealed class AgentProxy : AIAgent
|
||||
string threadId,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
var response = await this.RunCoreAsync(messages, threadId, cancellationToken).ConfigureAwait(false);
|
||||
var updateTypeInfo = AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponseUpdate));
|
||||
await foreach (var update in response.WatchUpdatesAsync(cancellationToken).ConfigureAwait(false))
|
||||
@@ -130,9 +125,7 @@ public sealed class AgentProxy : AIAgent
|
||||
|
||||
private async Task<ActorResponseHandle> RunCoreAsync(IReadOnlyCollection<ChatMessage> messages, string threadId, CancellationToken cancellationToken)
|
||||
{
|
||||
Debug.Assert(messages is not null);
|
||||
Debug.Assert(threadId is not null);
|
||||
var newMessages = new List<ChatMessage>(messages);
|
||||
List<ChatMessage> newMessages = [.. messages];
|
||||
|
||||
var runRequest = new AgentRunRequest
|
||||
{
|
||||
@@ -140,7 +133,7 @@ public sealed class AgentProxy : AIAgent
|
||||
};
|
||||
|
||||
string messageId = newMessages.LastOrDefault()?.MessageId ?? Guid.NewGuid().ToString();
|
||||
var actorRequest = new ActorRequest(
|
||||
ActorRequest actorRequest = new(
|
||||
actorId: new ActorId(this.Name, threadId),
|
||||
messageId,
|
||||
method: AgentActorConstants.RunMethodName,
|
||||
|
||||
@@ -18,6 +18,9 @@ public sealed class TestConfiguration
|
||||
/// <summary>Gets the configuration settings for the Azure OpenAI integration.</summary>
|
||||
public static AzureOpenAIConfig AzureOpenAI => LoadSection<AzureOpenAIConfig>();
|
||||
|
||||
/// <summary>Gets the configuration settings for the AzureAI integration.</summary>
|
||||
public static AzureAIConfig AzureAI => LoadSection<AzureAIConfig>();
|
||||
|
||||
/// <summary>Represents the configuration settings required to interact with the OpenAI service.</summary>
|
||||
public class OpenAIConfig
|
||||
{
|
||||
@@ -72,11 +75,6 @@ public sealed class TestConfiguration
|
||||
this._configRoot = configRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration settings for the AzureAI integration.
|
||||
/// </summary>
|
||||
public static AzureAIConfig AzureAI => LoadSection<AzureAIConfig>();
|
||||
|
||||
private static T LoadSection<T>([CallerMemberName] string? caller = null)
|
||||
{
|
||||
if (s_instance is null)
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.Core;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests;
|
||||
|
||||
public class DeclarativeWorkflowContextTests
|
||||
{
|
||||
[Fact]
|
||||
public void InitializeDefaultValues()
|
||||
{
|
||||
// Act
|
||||
Mock<WorkflowAgentProvider> mockProvider = new(MockBehavior.Strict);
|
||||
DeclarativeWorkflowOptions context = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(mockProvider.Object, context.AgentProvider);
|
||||
Assert.Null(context.MaximumCallDepth);
|
||||
Assert.Null(context.MaximumExpressionLength);
|
||||
Assert.Same(NullLoggerFactory.Instance, context.LoggerFactory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitializeExplicitValues()
|
||||
{
|
||||
// Arrange
|
||||
TokenCredential credentials = new DefaultAzureCredential();
|
||||
int maxCallDepth = 10;
|
||||
int maxExpressionLength = 100;
|
||||
ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { });
|
||||
|
||||
// Act
|
||||
Mock<WorkflowAgentProvider> mockProvider = new(MockBehavior.Strict);
|
||||
DeclarativeWorkflowOptions context = new(mockProvider.Object)
|
||||
{
|
||||
MaximumCallDepth = maxCallDepth,
|
||||
MaximumExpressionLength = maxExpressionLength,
|
||||
LoggerFactory = loggerFactory
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(mockProvider.Object, context.AgentProvider);
|
||||
Assert.Equal(maxCallDepth, context.MaximumCallDepth);
|
||||
Assert.Equal(maxExpressionLength, context.MaximumExpressionLength);
|
||||
Assert.Same(loggerFactory, context.LoggerFactory);
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests declarative workflow exceptions.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeWorkflowExceptionTest(ITestOutputHelper output) : WorkflowTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void WorkflowExecutionException()
|
||||
{
|
||||
AssertDefault<DeclarativeActionException>(() => throw new DeclarativeActionException());
|
||||
AssertMessage<DeclarativeActionException>((message) => throw new DeclarativeActionException(message));
|
||||
AssertInner<DeclarativeActionException>((message, inner) => throw new DeclarativeActionException(message, inner));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkflowModelException()
|
||||
{
|
||||
AssertDefault<DeclarativeModelException>(() => throw new DeclarativeModelException());
|
||||
AssertMessage<DeclarativeModelException>((message) => throw new DeclarativeModelException(message));
|
||||
AssertInner<DeclarativeModelException>((message, inner) => throw new DeclarativeModelException(message, inner));
|
||||
}
|
||||
|
||||
private static void AssertDefault<TException>(Action throwAction) where TException : Exception
|
||||
{
|
||||
TException exception = Assert.Throws<TException>(() => throwAction.Invoke());
|
||||
Assert.NotEmpty(exception.Message);
|
||||
Assert.Null(exception.InnerException);
|
||||
}
|
||||
|
||||
private static void AssertMessage<TException>(Action<string> throwAction) where TException : Exception
|
||||
{
|
||||
const string message = "Test exception message";
|
||||
TException exception = Assert.Throws<TException>(() => throwAction.Invoke(message));
|
||||
Assert.Equal(message, exception.Message);
|
||||
Assert.Null(exception.InnerException);
|
||||
}
|
||||
|
||||
private static void AssertInner<TException>(Action<string, Exception> throwAction) where TException : Exception
|
||||
{
|
||||
const string message = "Test exception message";
|
||||
NotSupportedException innerException = new("Inner exception message");
|
||||
TException exception = Assert.Throws<TException>(() => throwAction.Invoke(message, innerException));
|
||||
Assert.Equal(message, exception.Message);
|
||||
Assert.Equal(innerException, exception.InnerException);
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Moq;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowBuilder"/>.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : WorkflowTest(output)
|
||||
{
|
||||
private ImmutableList<WorkflowEvent> WorkflowEvents { get; set; } = ImmutableList<WorkflowEvent>.Empty;
|
||||
|
||||
private ImmutableDictionary<Type, int> WorkflowEventCounts { get; set; } = ImmutableDictionary<Type, int>.Empty;
|
||||
|
||||
[Theory]
|
||||
[InlineData("BadEmpty.yaml")]
|
||||
[InlineData("BadId.yaml")]
|
||||
[InlineData("BadKind.yaml")]
|
||||
public async Task InvalidWorkflow(string workflowFile)
|
||||
{
|
||||
await Assert.ThrowsAsync<DeclarativeModelException>(() => this.RunWorkflow(workflowFile));
|
||||
this.AssertNotExecuted("end_all");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoopEachAction()
|
||||
{
|
||||
await this.RunWorkflow("LoopEach.yaml");
|
||||
this.AssertExecutionCount(expectedCount: 35);
|
||||
this.AssertExecuted("foreach_loop");
|
||||
this.AssertExecuted("end_all");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoopBreakAction()
|
||||
{
|
||||
await this.RunWorkflow("LoopBreak.yaml");
|
||||
this.AssertExecutionCount(expectedCount: 7);
|
||||
this.AssertExecuted("foreach_loop");
|
||||
this.AssertExecuted("breakLoop_now");
|
||||
this.AssertExecuted("end_all");
|
||||
this.AssertNotExecuted("setVariable_loop");
|
||||
this.AssertNotExecuted("sendActivity_loop");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoopContinueAction()
|
||||
{
|
||||
await this.RunWorkflow("LoopContinue.yaml");
|
||||
this.AssertExecutionCount(expectedCount: 23);
|
||||
this.AssertExecuted("foreach_loop");
|
||||
this.AssertExecuted("continueLoop_now");
|
||||
this.AssertExecuted("end_all");
|
||||
this.AssertNotExecuted("setVariable_loop");
|
||||
this.AssertNotExecuted("sendActivity_loop");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GotoAction()
|
||||
{
|
||||
await this.RunWorkflow("Goto.yaml");
|
||||
this.AssertExecutionCount(expectedCount: 2);
|
||||
this.AssertExecuted("goto_end");
|
||||
this.AssertExecuted("end_all");
|
||||
this.AssertNotExecuted("sendActivity_1");
|
||||
this.AssertNotExecuted("sendActivity_2");
|
||||
this.AssertNotExecuted("sendActivity_3");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(12)]
|
||||
[InlineData(37)]
|
||||
public async Task ConditionAction(int input)
|
||||
{
|
||||
await this.RunWorkflow("Condition.yaml", input);
|
||||
this.AssertExecutionCount(expectedCount: 9);
|
||||
this.AssertExecuted("setVariable_test");
|
||||
this.AssertExecuted("conditionGroup_test");
|
||||
if (input % 2 == 0)
|
||||
{
|
||||
this.AssertExecuted("conditionItem_even");
|
||||
this.AssertExecuted("sendActivity_even");
|
||||
this.AssertNotExecuted("conditionItem_odd");
|
||||
this.AssertNotExecuted("sendActivity_odd");
|
||||
this.AssertMessage("EVEN");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AssertExecuted("conditionItem_odd");
|
||||
this.AssertExecuted("sendActivity_odd");
|
||||
this.AssertNotExecuted("conditionItem_even");
|
||||
this.AssertNotExecuted("sendActivity_even");
|
||||
this.AssertMessage("ODD");
|
||||
}
|
||||
this.AssertExecuted("end_all");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(12, 7)]
|
||||
[InlineData(37, 9)]
|
||||
public async Task ConditionActionWithElse(int input, int expectedActions)
|
||||
{
|
||||
await this.RunWorkflow("ConditionElse.yaml", input);
|
||||
this.AssertExecutionCount(expectedActions);
|
||||
this.AssertExecuted("setVariable_test");
|
||||
this.AssertExecuted("conditionGroup_test");
|
||||
if (input % 2 == 0)
|
||||
{
|
||||
this.AssertExecuted("sendActivity_else");
|
||||
this.AssertNotExecuted("conditionItem_odd");
|
||||
this.AssertNotExecuted("sendActivity_odd");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AssertExecuted("conditionItem_odd");
|
||||
this.AssertExecuted("sendActivity_odd");
|
||||
this.AssertNotExecuted("sendActivity_else");
|
||||
}
|
||||
this.AssertExecuted("end_all");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Single.yaml", 1, "end_all")]
|
||||
[InlineData("EditTable.yaml", 2, "edit_var")]
|
||||
[InlineData("EditTableV2.yaml", 2, "edit_var")]
|
||||
[InlineData("ParseValue.yaml", 1, "parse_var")]
|
||||
[InlineData("SetTextVariable.yaml", 1, "set_text")]
|
||||
[InlineData("ClearAllVariables.yaml", 1, "clear_all")]
|
||||
[InlineData("ResetVariable.yaml", 2, "clear_var")]
|
||||
public async Task ExecuteAction(string workflowFile, int expectedCount, string expectedId)
|
||||
{
|
||||
await this.RunWorkflow(workflowFile);
|
||||
this.AssertExecutionCount(expectedCount);
|
||||
this.AssertExecuted(expectedId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(ActivateExternalTrigger.Builder))]
|
||||
[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))]
|
||||
[InlineData(typeof(DisconnectedNodeContainer.Builder))]
|
||||
[InlineData(typeof(EmitEvent.Builder))]
|
||||
[InlineData(typeof(EndDialog.Builder))]
|
||||
[InlineData(typeof(GetActivityMembers.Builder))]
|
||||
[InlineData(typeof(GetConversationMembers.Builder))]
|
||||
[InlineData(typeof(HttpRequestAction.Builder))]
|
||||
[InlineData(typeof(InvokeAIBuilderModelAction.Builder))]
|
||||
[InlineData(typeof(InvokeConnectorAction.Builder))]
|
||||
[InlineData(typeof(InvokeCustomModelAction.Builder))]
|
||||
[InlineData(typeof(InvokeFlowAction.Builder))]
|
||||
[InlineData(typeof(InvokeSkillAction.Builder))]
|
||||
[InlineData(typeof(LogCustomTelemetryEvent.Builder))]
|
||||
[InlineData(typeof(OAuthInput.Builder))]
|
||||
[InlineData(typeof(Question.Builder))]
|
||||
[InlineData(typeof(RecognizeIntent.Builder))]
|
||||
[InlineData(typeof(RepeatDialog.Builder))]
|
||||
[InlineData(typeof(ReplaceDialog.Builder))]
|
||||
[InlineData(typeof(SearchAndSummarizeContent.Builder))]
|
||||
[InlineData(typeof(SearchAndSummarizeWithCustomModel.Builder))]
|
||||
[InlineData(typeof(SearchKnowledgeSources.Builder))]
|
||||
[InlineData(typeof(SignOutUser.Builder))]
|
||||
[InlineData(typeof(TransferConversation.Builder))]
|
||||
[InlineData(typeof(TransferConversationV2.Builder))]
|
||||
[InlineData(typeof(UnknownDialogAction.Builder))]
|
||||
[InlineData(typeof(UpdateActivity.Builder))]
|
||||
[InlineData(typeof(WaitForConnectorTrigger.Builder))]
|
||||
public void UnsupportedAction(Type type)
|
||||
{
|
||||
DialogAction.Builder? unsupportedAction = (DialogAction.Builder?)Activator.CreateInstance(type);
|
||||
Assert.NotNull(unsupportedAction);
|
||||
unsupportedAction.Id = "action_bad";
|
||||
AdaptiveDialog.Builder dialogBuilder =
|
||||
new()
|
||||
{
|
||||
BeginDialog =
|
||||
new OnActivity.Builder()
|
||||
{
|
||||
Id = "workflow",
|
||||
Actions = [unsupportedAction]
|
||||
}
|
||||
};
|
||||
AdaptiveDialog dialog = dialogBuilder.Build();
|
||||
|
||||
WorkflowScopes scopes = new();
|
||||
Mock<WorkflowAgentProvider> mockAgentProvider = new(MockBehavior.Strict);
|
||||
DeclarativeWorkflowOptions options = new(mockAgentProvider.Object);
|
||||
WorkflowActionVisitor visitor = new(new RootExecutor(), new DeclarativeWorkflowState(RecalcEngineFactory.Create()), options);
|
||||
WorkflowElementWalker walker = new(dialog, visitor);
|
||||
Assert.True(visitor.HasUnsupportedActions);
|
||||
}
|
||||
|
||||
private void AssertExecutionCount(int expectedCount)
|
||||
{
|
||||
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorInvokeEvent)]);
|
||||
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorCompleteEvent)]);
|
||||
}
|
||||
|
||||
private void AssertNotExecuted(string executorId)
|
||||
{
|
||||
Assert.DoesNotContain(this.WorkflowEvents.OfType<ExecutorInvokeEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.DoesNotContain(this.WorkflowEvents.OfType<ExecutorCompleteEvent>(), e => e.ExecutorId == executorId);
|
||||
}
|
||||
|
||||
private void AssertExecuted(string executorId)
|
||||
{
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorInvokeEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorCompleteEvent>(), e => e.ExecutorId == executorId);
|
||||
}
|
||||
|
||||
private void AssertMessage(string message)
|
||||
{
|
||||
Assert.Contains(this.WorkflowEvents.OfType<AgentRunResponseEvent>(), e => string.Equals(e.Response.Messages[0].Text.Trim(), message, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private Task RunWorkflow(string workflowPath) => this.RunWorkflow<string>(workflowPath, string.Empty);
|
||||
|
||||
private async Task RunWorkflow<TInput>(string workflowPath, TInput workflowInput) where TInput : notnull
|
||||
{
|
||||
using StreamReader yamlReader = File.OpenText(Path.Combine("Workflows", workflowPath));
|
||||
Mock<WorkflowAgentProvider> mockAgentProvider = new(MockBehavior.Strict);
|
||||
DeclarativeWorkflowOptions workflowContext = new(mockAgentProvider.Object) { LoggerFactory = this.Output };
|
||||
|
||||
Workflow<TInput> workflow = DeclarativeWorkflowBuilder.Build<TInput>(yamlReader, workflowContext);
|
||||
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, workflowInput);
|
||||
|
||||
this.WorkflowEvents = run.WatchStreamAsync().ToEnumerable().ToImmutableList();
|
||||
foreach (WorkflowEvent workflowEvent in this.WorkflowEvents)
|
||||
{
|
||||
if (workflowEvent is ExecutorInvokeEvent invokeEvent)
|
||||
{
|
||||
DeclarativeExecutorResult? message = invokeEvent.Data as DeclarativeExecutorResult;
|
||||
this.Output.WriteLine($"EXEC: {invokeEvent.ExecutorId} << {message?.ExecutorId ?? "?"} [{message?.Result ?? "-"}]");
|
||||
}
|
||||
else if (workflowEvent is AgentRunResponseEvent messageEvent)
|
||||
{
|
||||
this.Output.WriteLine($"MESSAGE: {messageEvent.Response.Messages[0].Text.Trim()}");
|
||||
}
|
||||
}
|
||||
this.WorkflowEventCounts = this.WorkflowEvents.GroupBy(e => e.GetType()).ToImmutableDictionary(e => e.Key, e => e.Count());
|
||||
}
|
||||
|
||||
private sealed class RootExecutor() :
|
||||
ReflectingExecutor<RootExecutor>(WorkflowActionVisitor.RootId("workflow")),
|
||||
IMessageHandler<string>
|
||||
{
|
||||
public async ValueTask HandleAsync(string message, IWorkflowContext context)
|
||||
{
|
||||
await context.SendMessageAsync($"{this.Id}: {DateTime.UtcNow.ToShortTimeString()}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.Extensions;
|
||||
|
||||
public class FormulaValueExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void BooleanValue()
|
||||
{
|
||||
BooleanValue formulaValue = FormulaValue.New(true);
|
||||
DataValue dataValue = formulaValue.ToDataValue();
|
||||
BooleanDataValue typedValue = Assert.IsType<BooleanDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
BooleanValue formulaCopy = Assert.IsType<BooleanValue>(dataValue.ToFormulaValue());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal(bool.TrueString, formulaValue.Format());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringValues()
|
||||
{
|
||||
StringValue formulaValue = FormulaValue.New("test value");
|
||||
Assert.Equal(StringDataType.Instance, formulaValue.GetDataType());
|
||||
|
||||
DataValue dataValue = formulaValue.ToDataValue();
|
||||
StringDataValue typedValue = Assert.IsType<StringDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
StringValue formulaCopy = Assert.IsType<StringValue>(typedValue.ToFormulaValue());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal(formulaValue.Value, formulaValue.Format());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecimalValues()
|
||||
{
|
||||
DecimalValue formulaValue = FormulaValue.New(45.3m);
|
||||
Assert.Equal(NumberDataType.Instance, formulaValue.GetDataType());
|
||||
|
||||
DataValue dataValue = formulaValue.ToDataValue();
|
||||
NumberDataValue typedValue = Assert.IsType<NumberDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
DecimalValue formulaCopy = Assert.IsType<DecimalValue>(typedValue.ToFormulaValue());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal("45.3", formulaValue.Format());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumberValues()
|
||||
{
|
||||
NumberValue formulaValue = FormulaValue.New(3.1415926535897);
|
||||
Assert.Equal(FloatDataType.Instance, formulaValue.GetDataType());
|
||||
|
||||
DataValue dataValue = formulaValue.ToDataValue();
|
||||
FloatDataValue typedValue = Assert.IsType<FloatDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
NumberValue formulaCopy = Assert.IsType<NumberValue>(typedValue.ToFormulaValue());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal("3.1415926535897", formulaValue.Format());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlankValues()
|
||||
{
|
||||
BlankValue formulaValue = FormulaValue.NewBlank();
|
||||
Assert.Equal(DataType.Blank, formulaValue.GetDataType());
|
||||
|
||||
BlankDataValue dataCopy = Assert.IsType<BlankDataValue>(formulaValue.ToDataValue());
|
||||
|
||||
Assert.Equal(string.Empty, formulaValue.Format());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VoidValues()
|
||||
{
|
||||
VoidValue formulaValue = FormulaValue.NewVoid();
|
||||
Assert.Equal(DataType.Unspecified, formulaValue.GetDataType());
|
||||
BlankDataValue dataCopy = Assert.IsType<BlankDataValue>(formulaValue.ToDataValue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DateValues()
|
||||
{
|
||||
DateTime timestamp = DateTime.UtcNow.Date;
|
||||
DateValue formulaValue = FormulaValue.NewDateOnly(timestamp);
|
||||
Assert.Equal(DataType.Date, formulaValue.GetDataType());
|
||||
|
||||
DataValue dataValue = formulaValue.ToDataValue();
|
||||
DateDataValue typedValue = Assert.IsType<DateDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
|
||||
|
||||
DateValue formulaCopy = Assert.IsType<DateValue>(dataValue.ToFormulaValue());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
|
||||
|
||||
Assert.Equal($"{timestamp}", formulaValue.Format());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DateTimeValues()
|
||||
{
|
||||
DateTime timestamp = DateTime.UtcNow;
|
||||
DateTimeValue formulaValue = FormulaValue.New(timestamp);
|
||||
Assert.Equal(DataType.DateTime, formulaValue.GetDataType());
|
||||
|
||||
DataValue dataValue = formulaValue.ToDataValue();
|
||||
DateTimeDataValue typedValue = Assert.IsType<DateTimeDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
|
||||
|
||||
DateTimeValue formulaCopy = Assert.IsType<DateTimeValue>(typedValue.ToFormulaValue());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
|
||||
|
||||
Assert.Equal($"{timestamp}", formulaValue.Format());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimeValues()
|
||||
{
|
||||
TimeValue formulaValue = FormulaValue.New(TimeSpan.Parse("10:35"));
|
||||
Assert.Equal(DataType.Time, formulaValue.GetDataType());
|
||||
|
||||
DataValue dataValue = formulaValue.ToDataValue();
|
||||
TimeDataValue typedValue = Assert.IsType<TimeDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
TimeValue formulaCopy = Assert.IsType<TimeValue>(typedValue.ToFormulaValue());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal("10:35:00", formulaValue.Format());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordValues()
|
||||
{
|
||||
RecordValue formulaValue = FormulaValue.NewRecordFromFields(
|
||||
new NamedValue("FieldA", FormulaValue.New("Value1")),
|
||||
new NamedValue("FieldB", FormulaValue.New("Value2")),
|
||||
new NamedValue("FieldC", FormulaValue.New("Value3")));
|
||||
Assert.Equal(DataType.EmptyRecord, formulaValue.GetDataType());
|
||||
|
||||
RecordDataValue dataValue = formulaValue.ToRecord();
|
||||
Assert.Equal(formulaValue.Fields.Count(), dataValue.Properties.Count);
|
||||
foreach (KeyValuePair<string, DataValue> property in dataValue.Properties)
|
||||
{
|
||||
Assert.Contains(property.Key, formulaValue.Fields.Select(field => field.Name));
|
||||
}
|
||||
|
||||
RecordValue formulaCopy = Assert.IsType<RecordValue>(dataValue.ToFormulaValue(), exactMatch: false);
|
||||
Assert.Equal(formulaCopy.Fields.Count(), dataValue.Properties.Count);
|
||||
foreach (NamedValue field in formulaCopy.Fields)
|
||||
{
|
||||
Assert.Contains(field.Name, dataValue.Properties.Keys);
|
||||
}
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
{
|
||||
"FieldA": "Value1",
|
||||
"FieldB": "Value2",
|
||||
"FieldC": "Value3"
|
||||
}
|
||||
""",
|
||||
formulaValue.Format().Replace(Environment.NewLine, "\n"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TableValues()
|
||||
{
|
||||
RecordValue recordValue = FormulaValue.NewRecordFromFields(
|
||||
new NamedValue("FieldA", FormulaValue.New("Value1")),
|
||||
new NamedValue("FieldB", FormulaValue.New("Value2")),
|
||||
new NamedValue("FieldC", FormulaValue.New("Value3")));
|
||||
TableValue formulaValue = TableValue.NewTable(recordValue.Type, [recordValue]);
|
||||
|
||||
TableDataValue dataValue = formulaValue.ToTable();
|
||||
Assert.Equal(formulaValue.Rows.Count(), dataValue.Values.Length);
|
||||
|
||||
TableValue formulaCopy = Assert.IsType<TableValue>(dataValue.ToFormulaValue(), exactMatch: false);
|
||||
Assert.Equal(formulaCopy.Rows.Count(), dataValue.Values.Length);
|
||||
|
||||
Assert.Equal(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"FieldA": "Value1",
|
||||
"FieldB": "Value2",
|
||||
"FieldC": "Value3"
|
||||
}
|
||||
]
|
||||
""",
|
||||
formulaValue.Format().Replace(Environment.NewLine, "\n"));
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.Extensions;
|
||||
|
||||
public class StringExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void TrimJsonWithDelimiter()
|
||||
{
|
||||
// Arrange
|
||||
const string Input =
|
||||
"""
|
||||
```json
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
```
|
||||
""";
|
||||
|
||||
// Act
|
||||
string result = Input.TrimJsonDelimiter();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"""
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
""",
|
||||
result);
|
||||
}
|
||||
[Fact]
|
||||
public void TrimJsonWithPadding()
|
||||
{
|
||||
// Arrange
|
||||
const string Input =
|
||||
"""
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
```
|
||||
""";
|
||||
|
||||
// Act
|
||||
string result = Input.TrimJsonDelimiter();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"""
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
""",
|
||||
result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimJsonWithUnqualifiedDelimiter()
|
||||
{
|
||||
// Arrange
|
||||
const string Input =
|
||||
"""
|
||||
```
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
```
|
||||
""";
|
||||
|
||||
// Act
|
||||
string result = Input.TrimJsonDelimiter();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"""
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
""",
|
||||
result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimJsonWithoutDelimiter()
|
||||
{
|
||||
// Arrange
|
||||
const string Input =
|
||||
"""
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
string result = Input.TrimJsonDelimiter();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"""
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
""",
|
||||
result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimJsonWithoutDelimiterWithPadding()
|
||||
{
|
||||
// Arrange
|
||||
const string Input =
|
||||
"""
|
||||
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
string result = Input.TrimJsonDelimiter();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"""
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
""",
|
||||
result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimMissingWithDelimiter()
|
||||
{
|
||||
// Arrange
|
||||
const string Input =
|
||||
"""
|
||||
```json
|
||||
```
|
||||
""";
|
||||
|
||||
// Act
|
||||
string result = Input.TrimJsonDelimiter();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimEmptyString()
|
||||
{
|
||||
// Act
|
||||
string result = string.Empty.TrimJsonDelimiter();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.Interpreter;
|
||||
|
||||
/// <summary>
|
||||
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowModel"/>.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeWorkflowModelTest(ITestOutputHelper output) : WorkflowTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetDepthForDefault()
|
||||
{
|
||||
DeclarativeWorkflowModel model = new(this.CreateExecutor("root"));
|
||||
Assert.Equal(0, model.GetDepth(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDepthForMissingNode()
|
||||
{
|
||||
DeclarativeWorkflowModel model = new(this.CreateExecutor("root"));
|
||||
Assert.Throws<DeclarativeModelException>(() => model.GetDepth("missing"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectMissingNode()
|
||||
{
|
||||
TestExecutor rootExecutor = this.CreateExecutor("root");
|
||||
DeclarativeWorkflowModel model = new(rootExecutor);
|
||||
model.AddLink("root", "missing");
|
||||
WorkflowBuilder workflowBuilder = new(rootExecutor);
|
||||
Assert.Throws<DeclarativeModelException>(() => model.ConnectNodes(workflowBuilder));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddToMissingParent()
|
||||
{
|
||||
DeclarativeWorkflowModel model = new(this.CreateExecutor("root"));
|
||||
Assert.Throws<DeclarativeModelException>(() => model.AddNode(this.CreateExecutor("next"), "missing"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LinkFromMissingSource()
|
||||
{
|
||||
DeclarativeWorkflowModel model = new(this.CreateExecutor("root"));
|
||||
Assert.Throws<DeclarativeModelException>(() => model.AddLink("missing", "anything"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LocateMissingParent()
|
||||
{
|
||||
DeclarativeWorkflowModel model = new(this.CreateExecutor("root"));
|
||||
Assert.Null(model.LocateParent<TestExecutor>(null));
|
||||
Assert.Throws<DeclarativeModelException>(() => model.LocateParent<TestExecutor>("missing"));
|
||||
}
|
||||
|
||||
private TestExecutor CreateExecutor(string id) => new(id);
|
||||
|
||||
internal sealed class TestExecutor(string actionId) :
|
||||
ReflectingExecutor<TestExecutor>(actionId),
|
||||
IMessageHandler<string>
|
||||
{
|
||||
public async ValueTask HandleAsync(string message, IWorkflowContext context)
|
||||
{
|
||||
await context.SendMessageAsync($"{this.Id}: {DateTime.UtcNow.ToShortTimeString()}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);IDE1006</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.Workflows.Declarative\Microsoft.Agents.Workflows.Declarative.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Workflows\*.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ClearAllVariablesExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task ClearWorkflowScope()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("NoVar", FormulaValue.New("Old value"));
|
||||
|
||||
ClearAllVariables model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ClearWorkflowScope)),
|
||||
VariablesToClear.ConversationScopedVariables);
|
||||
|
||||
// Act
|
||||
ClearAllVariablesExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyUndefined("NoVar");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearUndefinedScope()
|
||||
{
|
||||
// Arrange
|
||||
ClearAllVariables model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ClearUndefinedScope)),
|
||||
VariablesToClear.UserScopedVariables);
|
||||
|
||||
// Act
|
||||
ClearAllVariablesExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyUndefined("NoVar");
|
||||
}
|
||||
|
||||
private ClearAllVariables CreateModel(string displayName, VariablesToClear variableTarget)
|
||||
{
|
||||
ClearAllVariables.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Variables = EnumExpression<VariablesToClearWrapper>.Literal(VariablesToClearWrapper.Get(variableTarget)),
|
||||
};
|
||||
|
||||
ClearAllVariables model = this.AssignParent<ClearAllVariables>(actionBuilder);
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ParseValueExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task ParseTable()
|
||||
{
|
||||
// Arrange
|
||||
RecordDataType.Builder recordBuilder =
|
||||
new()
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
{"key1", new PropertyInfo.Builder() { Type = DataType.String } },
|
||||
}
|
||||
};
|
||||
ParseValue model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ParseTable)),
|
||||
recordBuilder,
|
||||
@"{ ""key1"": ""val1"" }");
|
||||
|
||||
// Act
|
||||
ParseValueExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyState("Target", FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseBoolean()
|
||||
{
|
||||
// Arrange
|
||||
ParseValue model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ParseTable)),
|
||||
new BooleanDataType.Builder(),
|
||||
"True");
|
||||
|
||||
// Act
|
||||
ParseValueExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyState("Target", FormulaValue.New(true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNumber()
|
||||
{
|
||||
// Arrange
|
||||
ParseValue model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ParseNumber)),
|
||||
new NumberDataType.Builder(),
|
||||
"42");
|
||||
|
||||
// Act
|
||||
ParseValueExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyState("Target", FormulaValue.New(42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseString()
|
||||
{
|
||||
// Arrange
|
||||
ParseValue model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ParseString)),
|
||||
new StringDataType.Builder(),
|
||||
"Hello, World!");
|
||||
|
||||
// Act
|
||||
ParseValueExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyState("Target", FormulaValue.New("Hello, World!"));
|
||||
}
|
||||
|
||||
private ParseValue CreateModel(string displayName, DataType.Builder typeBuilder, string sourceText)
|
||||
{
|
||||
ParseValue.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
ValueType = typeBuilder,
|
||||
Variable = PropertyPath.TopicVariable("Target"),
|
||||
Value = new ValueExpression.Builder(ValueExpression.Literal(StringDataValue.Create(sourceText))),
|
||||
};
|
||||
|
||||
ParseValue model = this.AssignParent<ParseValue>(actionBuilder);
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ResetVariableExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task ResetDefinedValue()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("MyVar1", FormulaValue.New("Value #1"));
|
||||
this.Scopes.Set("MyVar2", FormulaValue.New("Value #2"));
|
||||
|
||||
ResetVariable model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ResetDefinedValue)),
|
||||
FormatVariablePath("MyVar1"));
|
||||
|
||||
// Act
|
||||
ResetVariableExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyUndefined("MyVar1");
|
||||
this.VerifyState("MyVar2", FormulaValue.New("Value #2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResetUndefinedValue()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("MyVar1", FormulaValue.New("Value #1"));
|
||||
|
||||
ResetVariable model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(ResetUndefinedValue)),
|
||||
FormatVariablePath("NoVar"));
|
||||
|
||||
// Act
|
||||
ResetVariableExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyUndefined("NoVar");
|
||||
this.VerifyState("MyVar1", FormulaValue.New("Value #1"));
|
||||
}
|
||||
|
||||
private ResetVariable CreateModel(string displayName, string variablePath)
|
||||
{
|
||||
ResetVariable.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Variable = InitializablePropertyPath.Create(variablePath),
|
||||
};
|
||||
|
||||
ResetVariable model = this.AssignParent<ResetVariable>(actionBuilder);
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="SendActivityExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class SendActivityExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task CaptureActivity()
|
||||
{
|
||||
// Arrange
|
||||
SendActivity model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(CaptureActivity)),
|
||||
"Test activity message");
|
||||
|
||||
// Act
|
||||
SendActivityExecutor action = new(model, this.GetState());
|
||||
WorkflowEvent[] events = await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
Assert.Contains(events, e => e is AgentRunResponseEvent);
|
||||
}
|
||||
|
||||
private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null)
|
||||
{
|
||||
MessageActivityTemplate.Builder activityBuilder =
|
||||
new()
|
||||
{
|
||||
Summary = summary,
|
||||
Text = { TemplateLine.Parse(activityMessage) },
|
||||
};
|
||||
SendActivity.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Activity = activityBuilder.Build(),
|
||||
};
|
||||
|
||||
SendActivity model = this.AssignParent<SendActivity>(actionBuilder);
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="SetTextVariableExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task SetLiteralValue()
|
||||
{
|
||||
// Arrange
|
||||
SetTextVariable model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(SetLiteralValue)),
|
||||
FormatVariablePath("TextVar"),
|
||||
"Text variable value");
|
||||
|
||||
// Act
|
||||
SetTextVariableExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyState("TextVar", FormulaValue.New("Text variable value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateExistingValue()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("TextVar", FormulaValue.New("Old value"));
|
||||
|
||||
SetTextVariable model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(nameof(UpdateExistingValue)),
|
||||
FormatVariablePath("TextVar"),
|
||||
"New value");
|
||||
|
||||
// Act
|
||||
SetTextVariableExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyState("TextVar", FormulaValue.New("New value"));
|
||||
}
|
||||
|
||||
private SetTextVariable CreateModel(string displayName, string variablePath, string textValue)
|
||||
{
|
||||
SetTextVariable.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Variable = InitializablePropertyPath.Create(variablePath),
|
||||
Value = TemplateLine.Parse(textValue),
|
||||
};
|
||||
|
||||
SetTextVariable model = this.AssignParent<SetTextVariable>(actionBuilder);
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="SetVariableExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class SetVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void InvalidModel()
|
||||
{
|
||||
// Arrange, Act, Assert
|
||||
Assert.Throws<DeclarativeModelException>(() => new SetVariableExecutor(new SetVariable(), this.GetState()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetNumericValue()
|
||||
{
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetNumericValue),
|
||||
variableName: "TestVariable",
|
||||
variableValue: new NumberDataValue(42),
|
||||
expectedValue: FormulaValue.New(42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetStringValue()
|
||||
{
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetStringValue),
|
||||
variableName: "TestVariable",
|
||||
variableValue: new StringDataValue("Text"),
|
||||
expectedValue: FormulaValue.New("Text"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetBooleanValue()
|
||||
{
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetBooleanValue),
|
||||
variableName: "TestVariable",
|
||||
variableValue: new BooleanDataValue(true),
|
||||
expectedValue: FormulaValue.New(true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetBooleanExpression()
|
||||
{
|
||||
// Arrange
|
||||
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("true || false"));
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetBooleanExpression),
|
||||
variableName: "TestVariable",
|
||||
valueExpression: expressionBuilder,
|
||||
expectedValue: FormulaValue.New(true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetNumberExpression()
|
||||
{
|
||||
// Arrange
|
||||
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("9 - 3"));
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetBooleanExpression),
|
||||
variableName: "TestVariable",
|
||||
valueExpression: expressionBuilder,
|
||||
expectedValue: FormulaValue.New(6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetStringExpression()
|
||||
{
|
||||
// Arrange
|
||||
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression(@"Concatenate(""A"", ""B"", ""C"")"));
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetBooleanExpression),
|
||||
variableName: "TestVariable",
|
||||
valueExpression: expressionBuilder,
|
||||
expectedValue: FormulaValue.New("ABC"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetBooleanVariable()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("Source", FormulaValue.New(true));
|
||||
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetBooleanExpression),
|
||||
variableName: "TestVariable",
|
||||
valueExpression: expressionBuilder,
|
||||
expectedValue: FormulaValue.New(true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetNumberVariable()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("Source", FormulaValue.New(321));
|
||||
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetBooleanExpression),
|
||||
variableName: "TestVariable",
|
||||
valueExpression: expressionBuilder,
|
||||
expectedValue: FormulaValue.New(321));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetStringVariable()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("Source", FormulaValue.New("Test"));
|
||||
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(SetBooleanExpression),
|
||||
variableName: "TestVariable",
|
||||
valueExpression: expressionBuilder,
|
||||
expectedValue: FormulaValue.New("Test"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateExistingValue()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("VarA", FormulaValue.New(33));
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTest(
|
||||
displayName: nameof(UpdateExistingValue),
|
||||
variableName: "VarA",
|
||||
variableValue: new NumberDataValue(42),
|
||||
expectedValue: FormulaValue.New(42));
|
||||
}
|
||||
|
||||
private Task ExecuteTest(
|
||||
string displayName,
|
||||
string variableName,
|
||||
DataValue variableValue,
|
||||
FormulaValue expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
ValueExpression.Builder expressionBuilder = new(ValueExpression.Literal(variableValue));
|
||||
|
||||
// Act & Assert
|
||||
return this.ExecuteTest(displayName, variableName, expressionBuilder, expectedValue);
|
||||
}
|
||||
|
||||
private async Task ExecuteTest(
|
||||
string displayName,
|
||||
string variableName,
|
||||
ValueExpression.Builder valueExpression,
|
||||
FormulaValue expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
SetVariable model =
|
||||
this.CreateModel(
|
||||
displayName,
|
||||
FormatVariablePath(variableName),
|
||||
valueExpression);
|
||||
|
||||
this.Scopes.Set(variableName, FormulaValue.New(33));
|
||||
|
||||
// Act
|
||||
SetVariableExecutor action = new(model, this.GetState());
|
||||
await this.Execute(action);
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
this.VerifyState(variableName, expectedValue);
|
||||
}
|
||||
|
||||
private SetVariable CreateModel(string displayName, string variablePath, ValueExpression.Builder valueExpression)
|
||||
{
|
||||
SetVariable.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Variable = InitializablePropertyPath.Create(variablePath),
|
||||
Value = valueExpression,
|
||||
};
|
||||
|
||||
SetVariable model = this.AssignParent<SetVariable>(actionBuilder);
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Base test class for <see cref="WorkflowActionExecutor"/> implementations.
|
||||
/// </summary>
|
||||
public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : WorkflowTest(output)
|
||||
{
|
||||
internal WorkflowScopes Scopes { get; } = new();
|
||||
|
||||
internal DeclarativeWorkflowState GetState() => new(RecalcEngineFactory.Create(), this.Scopes);
|
||||
|
||||
protected ActionId CreateActionId() => new($"{this.GetType().Name}_{Guid.NewGuid():N}");
|
||||
|
||||
protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}";
|
||||
|
||||
internal async Task<WorkflowEvent[]> Execute(WorkflowActionExecutor executor)
|
||||
{
|
||||
TestWorkflowExecutor workflowExecutor = new();
|
||||
WorkflowBuilder workflowBuilder = new(workflowExecutor);
|
||||
workflowBuilder.AddEdge(workflowExecutor, executor);
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build<WorkflowScopes>(), this.Scopes);
|
||||
WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync();
|
||||
return events;
|
||||
}
|
||||
|
||||
internal void VerifyModel(DialogAction model, WorkflowActionExecutor action)
|
||||
{
|
||||
Assert.Equal(model.Id, action.Id);
|
||||
Assert.Equal(model, action.Model);
|
||||
}
|
||||
|
||||
protected void VerifyState(string variableName, FormulaValue expectedValue) => this.VerifyState(variableName, VariableScopeNames.Topic, expectedValue);
|
||||
|
||||
internal void VerifyState(string variableName, string scopeName, FormulaValue expectedValue)
|
||||
{
|
||||
FormulaValue actualValue = this.Scopes.Get(variableName, scopeName);
|
||||
Assert.Equal(expectedValue.Format(), actualValue.Format());
|
||||
}
|
||||
|
||||
protected void VerifyUndefined(string variableName) => this.VerifyUndefined(variableName, VariableScopeNames.Topic);
|
||||
|
||||
internal void VerifyUndefined(string variableName, string scopeName)
|
||||
{
|
||||
Assert.IsType<BlankValue>(this.Scopes.Get(variableName, scopeName));
|
||||
}
|
||||
|
||||
protected TAction AssignParent<TAction>(DialogAction.Builder actionBuilder) where TAction : DialogAction
|
||||
{
|
||||
OnActivity.Builder activityBuilder =
|
||||
new()
|
||||
{
|
||||
Id = new("root"),
|
||||
};
|
||||
|
||||
activityBuilder.Actions.Add(actionBuilder);
|
||||
|
||||
OnActivity model = activityBuilder.Build();
|
||||
|
||||
return (TAction)model.Actions[0];
|
||||
}
|
||||
|
||||
internal sealed class TestWorkflowExecutor() :
|
||||
ReflectingExecutor<TestWorkflowExecutor>(nameof(TestWorkflowExecutor)),
|
||||
IMessageHandler<WorkflowScopes>
|
||||
{
|
||||
public async ValueTask HandleAsync(WorkflowScopes message, IWorkflowContext context)
|
||||
{
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx;
|
||||
|
||||
public class RecalcEngineFactoryTests(ITestOutputHelper output) : RecalcEngineTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void VariableUpdateTest()
|
||||
{
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
FormulaValue evalResult;
|
||||
|
||||
engine.UpdateVariable("single", FormulaValue.New(1));
|
||||
evalResult = engine.Eval("single");
|
||||
Console.WriteLine($"# {evalResult.Format()}");
|
||||
|
||||
RecordValue recordSub =
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue("sub", FormulaValue.New(3.14)));
|
||||
RecordValue recordRoot =
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue("another", FormulaValue.NewBlank()),
|
||||
new NamedValue("val", FormulaValue.New(2.82)),
|
||||
new NamedValue("root", recordSub));
|
||||
engine.DeleteFormula("Topic");
|
||||
engine.UpdateVariable("Topic", recordRoot);
|
||||
evalResult = engine.Eval("Topic");
|
||||
Console.WriteLine($"# {evalResult.Format()}");
|
||||
evalResult = engine.Eval("Topic.val");
|
||||
Console.WriteLine($"# {evalResult.Format()}");
|
||||
evalResult = engine.Eval("Topic.root");
|
||||
Console.WriteLine($"# {evalResult.Format()}");
|
||||
evalResult = engine.Eval("Topic.root.sub");
|
||||
Console.WriteLine($"# {evalResult.Format()}");
|
||||
//recordRoot.UpdateField("another", FormulaValue.New("abc"));
|
||||
RecordValue recordRoot2 =
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue("another", FormulaValue.New("abc")),
|
||||
new NamedValue("val", FormulaValue.New(2.82)),
|
||||
new NamedValue("root", recordSub));
|
||||
engine.DeleteFormula("Topic");
|
||||
engine.UpdateVariable("Topic", recordRoot2);
|
||||
evalResult = engine.Eval("Topic.another");
|
||||
Console.WriteLine($"# {evalResult.Format()}");
|
||||
engine.UpdateVariable("Topic.another", FormulaValue.New(-1));
|
||||
evalResult = engine.Eval("Topic.another");
|
||||
Console.WriteLine($"# {evalResult.Format()}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultNotNull()
|
||||
{
|
||||
// Act
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(engine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewInstanceEachTime()
|
||||
{
|
||||
// Act
|
||||
RecalcEngine engine1 = this.CreateEngine();
|
||||
RecalcEngine engine2 = this.CreateEngine();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(engine1);
|
||||
Assert.NotNull(engine2);
|
||||
Assert.NotSame(engine1, engine2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasSetFunctionEnabled()
|
||||
{
|
||||
// Arrange
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act
|
||||
CheckResult result = engine.Check("1+1");
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCorrectMaximumExpressionLength()
|
||||
{
|
||||
// Arrange
|
||||
RecalcEngine engine = RecalcEngineFactory.Create(2000, 3);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2000, engine.Config.MaximumExpressionLength);
|
||||
Assert.Equal(3, engine.Config.MaxCallDepth);
|
||||
|
||||
// Act: Create a long expression that is within the limit
|
||||
string goodExpression = string.Concat(GenerateExpression(999));
|
||||
CheckResult goodResult = engine.Check(goodExpression);
|
||||
|
||||
// Assert
|
||||
Assert.True(goodResult.IsSuccess);
|
||||
|
||||
// Act: Create a long expression that exceeds the limit
|
||||
string longExpression = string.Concat(GenerateExpression(1001));
|
||||
CheckResult longResult = engine.Check(longExpression);
|
||||
|
||||
// Assert
|
||||
Assert.False(longResult.IsSuccess);
|
||||
|
||||
static IEnumerable<string> GenerateExpression(int elements)
|
||||
{
|
||||
yield return "1";
|
||||
for (int i = 0; i < elements - 1; i++)
|
||||
{
|
||||
yield return "+1";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.PowerFx;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx;
|
||||
|
||||
/// <summary>
|
||||
/// Base test class for PowerFx engine tests.
|
||||
/// </summary>
|
||||
public abstract class RecalcEngineTest(ITestOutputHelper output) : WorkflowTest(output)
|
||||
{
|
||||
internal WorkflowScopes Scopes { get; } = new();
|
||||
|
||||
protected RecalcEngine CreateEngine(int maximumExpressionLength = 500) => RecalcEngineFactory.Create(maximumExpressionLength);
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx;
|
||||
|
||||
public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void FormatTemplateLines()
|
||||
{
|
||||
// Arrange
|
||||
List<TemplateLine> template =
|
||||
[
|
||||
TemplateLine.Parse("Hello"),
|
||||
TemplateLine.Parse(" "),
|
||||
TemplateLine.Parse("World"),
|
||||
];
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act
|
||||
string? result = engine.Format(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello World", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTemplateLinesEmpty()
|
||||
{
|
||||
// Arrange
|
||||
List<TemplateLine> template = [];
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act
|
||||
string? result = engine.Format(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTemplateLine()
|
||||
{
|
||||
// Arrange
|
||||
TemplateLine line = TemplateLine.Parse("Test");
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act
|
||||
string? result = engine.Format(line);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Test", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTemplateLineNull()
|
||||
{
|
||||
// Arrange
|
||||
TemplateLine? line = null;
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act
|
||||
string? result = engine.Format(line);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTextSegment()
|
||||
{
|
||||
// Arrange
|
||||
TemplateSegment textSegment = TextSegment.FromText("Hello World");
|
||||
TemplateLine line = new([textSegment]);
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act
|
||||
string? result = engine.Format(line);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello World", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatExpressionSegment()
|
||||
{
|
||||
// Arrange
|
||||
ExpressionSegment expressionSegment = new(ValueExpression.Expression("1 + 1"));
|
||||
TemplateLine line = new([expressionSegment]);
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act
|
||||
string? result = engine.Format(line);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("2", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatVariableSegment()
|
||||
{
|
||||
// Arrange
|
||||
this.Scopes.Set("Source", FormulaValue.New("Hello World"));
|
||||
ExpressionSegment expressionSegment = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
|
||||
TemplateLine line = new([expressionSegment]);
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
this.Scopes.Bind(engine);
|
||||
|
||||
// Act
|
||||
string? result = engine.Format(line);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello World", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatExpressionSegmentUndefined()
|
||||
{
|
||||
// Arrange
|
||||
ExpressionSegment expressionSegment = new();
|
||||
TemplateLine line = new([expressionSegment]);
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeModelException>(() => engine.Format(line));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMultipleSegments()
|
||||
{
|
||||
// Arrange
|
||||
TemplateSegment textSegment = TextSegment.FromText("Hello ");
|
||||
ExpressionSegment expressionSegment = new(ValueExpression.Expression(@"""World"""));
|
||||
TemplateLine line = new([textSegment, expressionSegment]);
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
|
||||
// Act
|
||||
string? result = engine.Format(line);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello World", result);
|
||||
}
|
||||
}
|
||||
+705
@@ -0,0 +1,705 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Bot.ObjectModel.Exceptions;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx;
|
||||
|
||||
public class WorkflowExpressionEngineTests : RecalcEngineTest
|
||||
{
|
||||
private static class Variables
|
||||
{
|
||||
public const string GlobalValue = nameof(GlobalValue);
|
||||
public const string BoolValue = nameof(BoolValue);
|
||||
public const string StringValue = nameof(StringValue);
|
||||
public const string IntValue = nameof(IntValue);
|
||||
public const string NumberValue = nameof(NumberValue);
|
||||
public const string EnumValue = nameof(EnumValue);
|
||||
public const string ObjectValue = nameof(ObjectValue);
|
||||
public const string ArrayValue = nameof(ArrayValue);
|
||||
public const string BlankValue = nameof(BlankValue);
|
||||
}
|
||||
|
||||
public static readonly RecordValue ObjectData = FormulaValue.NewRecordFromFields(new NamedValue(nameof(EnvironmentVariableReference.SchemaName), FormulaValue.New("test")));
|
||||
public static readonly TableValue TableData = FormulaValue.NewSingleColumnTable(FormulaValue.New("a"), FormulaValue.New("b"));
|
||||
|
||||
public WorkflowExpressionEngineTests(ITestOutputHelper output)
|
||||
: base(output)
|
||||
{
|
||||
this.Scopes.Set(Variables.GlobalValue, FormulaValue.New(255), VariableScopeNames.Global);
|
||||
this.Scopes.Set(Variables.BoolValue, FormulaValue.New(true), VariableScopeNames.Topic);
|
||||
this.Scopes.Set(Variables.StringValue, FormulaValue.New("Hello World"), VariableScopeNames.Topic);
|
||||
this.Scopes.Set(Variables.IntValue, FormulaValue.New(long.MaxValue), VariableScopeNames.Topic);
|
||||
this.Scopes.Set(Variables.NumberValue, FormulaValue.New(33.3), VariableScopeNames.Topic);
|
||||
this.Scopes.Set(Variables.EnumValue, FormulaValue.New(nameof(VariablesToClear.ConversationScopedVariables)), VariableScopeNames.Topic);
|
||||
this.Scopes.Set(Variables.ObjectValue, ObjectData, VariableScopeNames.Topic);
|
||||
this.Scopes.Set(Variables.ArrayValue, TableData, VariableScopeNames.Topic);
|
||||
this.Scopes.Set(Variables.BlankValue, FormulaValue.NewBlank(), VariableScopeNames.Topic);
|
||||
}
|
||||
|
||||
#region Unsupported Expression Tests
|
||||
|
||||
[Fact]
|
||||
public void AdaptiveCardExpressionGetValueUnsupported()
|
||||
{
|
||||
this.EvaluateUnsupportedExpression(expressionEngine => expressionEngine.GetValue(AdaptiveCardExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)), this.Scopes.BuildState()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DialogExpressionGetValueUnsupported()
|
||||
{
|
||||
this.EvaluateUnsupportedExpression(expressionEngine => expressionEngine.GetValue(DialogExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)), this.Scopes.BuildState()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileExpressionGetValueUnsupported()
|
||||
{
|
||||
this.EvaluateUnsupportedExpression(expressionEngine => expressionEngine.GetValue(FileExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)), this.Scopes.BuildState()));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BoolExpression Tests
|
||||
|
||||
[Fact]
|
||||
public void BoolExpressionGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<ArgumentNullException>((BoolExpression)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoolExpressionGetValueForInvalid()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<InvalidExpressionOutputTypeException>(BoolExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoolExpressionGetValueForLiteral()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
BoolExpression.Literal(true),
|
||||
expectedValue: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoolExpressionGetValueForBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
BoolExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: false);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void BoolExpressionGetValueForVariable(bool useState)
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
BoolExpression.Variable(PropertyPath.TopicVariable(Variables.BoolValue)),
|
||||
expectedValue: true,
|
||||
useState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoolExpressionGetValueForFormula()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
BoolExpression.Expression("true || false"),
|
||||
expectedValue: true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StringExpression Tests
|
||||
|
||||
[Fact]
|
||||
public void StringExpressionGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<ArgumentNullException>((StringExpression)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringExpressionGetValueForInvalid()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<InvalidExpressionOutputTypeException>(StringExpression.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringExpressionGetValueForStringExpressionBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
StringExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringExpressionGetValueForLiteral()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
StringExpression.Literal("test"),
|
||||
expectedValue: "test");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void StringExpressionGetValueForVariable(bool useState)
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
StringExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)),
|
||||
expectedValue: "Hello World",
|
||||
useState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringExpressionGetValueForFormula()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
StringExpression.Expression(@"""A"" & ""B"""),
|
||||
expectedValue: "AB");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringExpressionGetValueForRecord()
|
||||
{
|
||||
// Arrange
|
||||
RecordValue state = FormulaValue.NewRecordFromFields([new NamedValue("test", FormulaValue.New("value"))]);
|
||||
this.Scopes.Set("TestRecord", state, VariableScopeNames.Global);
|
||||
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
StringExpression.Variable(PropertyPath.Create("Global.TestRecord")),
|
||||
expectedValue:
|
||||
"""
|
||||
{
|
||||
"test": "value"
|
||||
}
|
||||
""".Replace("\n", Environment.NewLine));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IntExpression Tests
|
||||
|
||||
[Fact]
|
||||
public void IntExpressionGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<ArgumentNullException>((IntExpression)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntExpressionGetValueForInvalid()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<InvalidExpressionOutputTypeException>(IntExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntExpressionGetValueForIntExpressionBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
IntExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntExpressionGetValueForLiteral()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
IntExpression.Literal(7),
|
||||
expectedValue: 7);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void IntExpressionGetValueForVariable(bool useState)
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
IntExpression.Variable(PropertyPath.TopicVariable(Variables.IntValue)),
|
||||
expectedValue: long.MaxValue,
|
||||
useState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntExpressionGetValueForFormula()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
IntExpression.Expression("1 + 6"),
|
||||
expectedValue: 7);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NumberExpression Tests
|
||||
|
||||
[Fact]
|
||||
public void NumberExpressionGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<ArgumentNullException>((NumberExpression)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumberExpressionGetValueForInvalid()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<InvalidExpressionOutputTypeException>(NumberExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumberExpressionGetValueForBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
NumberExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumberExpressionGetValueForLiteral()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
NumberExpression.Literal(3.14),
|
||||
expectedValue: 3.14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumberExpressionGetValueForVariable()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
NumberExpression.Variable(PropertyPath.TopicVariable(Variables.NumberValue)),
|
||||
expectedValue: 33.3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumberExpressionGetValueForFormula()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
NumberExpression.Expression("31.1 + 2.2"),
|
||||
expectedValue: 33.3);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DataValueExpression Tests
|
||||
|
||||
[Fact]
|
||||
public void DataValueExpressionGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<ArgumentNullException>((ValueExpression)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataValueExpressionGetValueForDataValueExpressionBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ValueExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: DataValue.Blank());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataValueExpressionGetValueForLiteral()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ValueExpression.Literal(DataValue.Create("test")),
|
||||
expectedValue: DataValue.Create("test"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DataValueExpressionGetValueForVariable(bool useState)
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ValueExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)),
|
||||
expectedValue: DataValue.Create("Hello World"),
|
||||
useState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataValueExpressionGetValueForFormula()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ValueExpression.Expression(@"""A"" & ""B"""),
|
||||
expectedValue: DataValue.Create("AB"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnumExpression Tests
|
||||
|
||||
[Fact]
|
||||
public void EnumExpressionGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<VariablesToClearWrapper, ArgumentNullException>((EnumExpression<VariablesToClearWrapper>)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnumExpressionGetValueForInvalid()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<VariablesToClearWrapper, InvalidExpressionOutputTypeException>(EnumExpression<VariablesToClearWrapper>.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnumExpressionGetValueForLiteral()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression<VariablesToClearWrapper>(
|
||||
EnumExpression<VariablesToClearWrapper>.Literal(VariablesToClearWrapper.Get(VariablesToClear.ConversationScopedVariables)),
|
||||
expectedValue: VariablesToClear.ConversationScopedVariables);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnumExpressionGetValueForBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression<VariablesToClearWrapper>(
|
||||
EnumExpression<VariablesToClearWrapper>.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: VariablesToClear.ConversationScopedVariables);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void EnumExpressionGetValueForVariable(bool useState)
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression<VariablesToClearWrapper>(
|
||||
EnumExpression<VariablesToClearWrapper>.Variable(PropertyPath.TopicVariable(Variables.EnumValue)),
|
||||
expectedValue: VariablesToClear.ConversationScopedVariables,
|
||||
useState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnumExpressionGetValueForFormula()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression<VariablesToClearWrapper>(
|
||||
EnumExpression<VariablesToClearWrapper>.Expression(@"""ConversationScoped"" & ""Variables"""),
|
||||
expectedValue: VariablesToClear.ConversationScopedVariables);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ObjectExpression Tests
|
||||
|
||||
[Fact]
|
||||
public void ObjectExpressionGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<RecordDataValue, ArgumentNullException>((ObjectExpression<RecordDataValue>)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectExpressionGetValueForInvalid()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<RecordDataValue, InvalidExpressionOutputTypeException>(ObjectExpression<RecordDataValue>.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectExpressionGetValueForLiteral()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
RecordDataValue.Builder recordBuilder = new();
|
||||
recordBuilder.Properties.Add(nameof(EnvironmentVariableReference.SchemaName), new StringDataValue("test"));
|
||||
RecordDataValue objectRecord = recordBuilder.Build();
|
||||
EnvironmentVariableReference element = new EnvironmentVariableReference.Builder() { SchemaName = "test" }.Build();
|
||||
this.EvaluateExpression(
|
||||
ObjectExpression<RecordDataValue>.Literal(objectRecord),
|
||||
expectedValue: objectRecord);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectExpressionGetValueForBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ObjectExpression<RecordDataValue>.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ObjectExpressionGetValueForVariable(bool useState)
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ObjectExpression<RecordDataValue>.Variable(PropertyPath.TopicVariable(Variables.ObjectValue)),
|
||||
expectedValue: ObjectData.ToRecord(),
|
||||
useState);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ArrayExpression Tests
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<string, ArgumentNullException>((ArrayExpression<string>)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionGetValueForInvalid()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<string, InvalidExpressionOutputTypeException>(ArrayExpression<string>.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionGetValueForLiteral()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
string[] input = ["a", "b"];
|
||||
this.EvaluateExpression(
|
||||
ArrayExpression<string>.Literal(input.ToImmutableArray()),
|
||||
expectedValue: input);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionGetValueForBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ArrayExpression<string>.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: []);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ArrayExpressionGetValueForVariable(bool useState)
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ArrayExpression<string>.Variable(PropertyPath.TopicVariable(Variables.ArrayValue)),
|
||||
expectedValue: ["a", "b"],
|
||||
useState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionGetValueForFormula()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ArrayExpression<string>.Expression(@"[""a"", ""b""]"),
|
||||
expectedValue: ["a", "b"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ArrayExpressionOnly Tests
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionOnlyGetValueForNull()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<string, ArgumentNullException>((ArrayExpressionOnly<string>)null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionOnlyGetValueForInvalid()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateInvalidExpression<string, InvalidExpressionOutputTypeException>(ArrayExpressionOnly<string>.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionOnlyGetValueForBlank()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ArrayExpressionOnly<string>.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
|
||||
expectedValue: []);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ArrayExpressionOnlyGetValueForVariable(bool useState)
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ArrayExpressionOnly<string>.Variable(PropertyPath.TopicVariable(Variables.ArrayValue)),
|
||||
expectedValue: ["a", "b"],
|
||||
useState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayExpressionOnlyGetValueForFormula()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
this.EvaluateExpression(
|
||||
ArrayExpressionOnly<string>.Expression(@"[""a"", ""b""]"),
|
||||
expectedValue: ["a", "b"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private EvaluationResult<bool> EvaluateExpression(BoolExpression expression, bool expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
|
||||
=> this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity);
|
||||
|
||||
private void EvaluateInvalidExpression<TException>(BoolExpression expression)
|
||||
where TException : Exception
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression, this.Scopes));
|
||||
|
||||
private EvaluationResult<string> EvaluateExpression(StringExpression expression, string expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
|
||||
=> this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity);
|
||||
|
||||
private void EvaluateInvalidExpression<TException>(StringExpression expression)
|
||||
where TException : Exception
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression, this.Scopes));
|
||||
|
||||
private EvaluationResult<long> EvaluateExpression(IntExpression expression, long expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
|
||||
=> this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity);
|
||||
|
||||
private void EvaluateInvalidExpression<TException>(IntExpression expression)
|
||||
where TException : Exception
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression, this.Scopes));
|
||||
|
||||
private EvaluationResult<double> EvaluateExpression(NumberExpression expression, double expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
|
||||
=> this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity);
|
||||
|
||||
private void EvaluateInvalidExpression<TException>(NumberExpression expression)
|
||||
where TException : Exception
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression, this.Scopes));
|
||||
|
||||
private EvaluationResult<DataValue> EvaluateExpression(ValueExpression expression, DataValue expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
|
||||
=> this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity);
|
||||
|
||||
private void EvaluateInvalidExpression<TException>(ValueExpression expression)
|
||||
where TException : Exception
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression, this.Scopes));
|
||||
|
||||
private EvaluationResult<TEnum> EvaluateExpression<TEnum>(EnumExpression<TEnum> expression, TEnum expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
|
||||
where TEnum : EnumWrapper
|
||||
=> this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue<TEnum>(expression, this.Scopes) : evaluator.GetValue<TEnum>(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity);
|
||||
|
||||
private void EvaluateInvalidExpression<TEnum, TException>(EnumExpression<TEnum> expression)
|
||||
where TException : Exception
|
||||
where TEnum : EnumWrapper
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue<TEnum>(expression, this.Scopes));
|
||||
|
||||
private EvaluationResult<TValue?> EvaluateExpression<TValue>(ObjectExpression<TValue> expression, TValue? expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
|
||||
where TValue : BotElement
|
||||
=> this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue<TValue>(expression, this.Scopes) : evaluator.GetValue<TValue>(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity);
|
||||
|
||||
private void EvaluateInvalidExpression<TValue, TException>(ObjectExpression<TValue> expression)
|
||||
where TException : Exception
|
||||
where TValue : BotElement
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue<TValue>(expression, this.Scopes));
|
||||
|
||||
private ImmutableArray<TValue> EvaluateExpression<TValue>(ArrayExpression<TValue> expression, TValue[] expectedValue, bool useState = false)
|
||||
=> this.EvaluateArrayExpression((evaluator) => useState ? evaluator.GetValue<TValue>(expression, this.Scopes) : evaluator.GetValue<TValue>(expression, this.Scopes.BuildState()), expectedValue);
|
||||
|
||||
private void EvaluateInvalidExpression<TValue, TException>(ArrayExpression<TValue> expression)
|
||||
where TException : Exception
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue<TValue>(expression, this.Scopes));
|
||||
|
||||
private ImmutableArray<TValue> EvaluateExpression<TValue>(ArrayExpressionOnly<TValue> expression, TValue[] expectedValue, bool useState = false)
|
||||
=> this.EvaluateArrayExpression((evaluator) => useState ? evaluator.GetValue<TValue>(expression, this.Scopes) : evaluator.GetValue<TValue>(expression, this.Scopes.BuildState()), expectedValue);
|
||||
|
||||
private void EvaluateInvalidExpression<TValue, TException>(ArrayExpressionOnly<TValue> expression)
|
||||
where TException : Exception
|
||||
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue<TValue>(expression, this.Scopes));
|
||||
|
||||
private EvaluationResult<TValue> EvaluateExpression<TValue>(
|
||||
Func<WorkflowExpressionEngine, EvaluationResult<TValue>> evaluator,
|
||||
TValue? expectedValue,
|
||||
SensitivityLevel expectedSensitivity = SensitivityLevel.None)
|
||||
{
|
||||
// Arrange
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
this.Scopes.Bind(engine);
|
||||
WorkflowExpressionEngine expressionEngine = new(engine);
|
||||
|
||||
// Act
|
||||
EvaluationResult<TValue> result = evaluator.Invoke(expressionEngine);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedValue, result.Value);
|
||||
Assert.Equal(expectedSensitivity, result.Sensitivity);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ImmutableArray<TValue> EvaluateArrayExpression<TValue>(
|
||||
Func<WorkflowExpressionEngine, ImmutableArray<TValue>> evaluator,
|
||||
TValue[] expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
this.Scopes.Bind(engine);
|
||||
WorkflowExpressionEngine expressionEngine = new(engine);
|
||||
|
||||
// Act
|
||||
ImmutableArray<TValue> result = evaluator.Invoke(expressionEngine);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedValue.Length, result.Length);
|
||||
Assert.Equivalent(expectedValue, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EvaluateInvalidExpression<TException>(Action<WorkflowExpressionEngine> evaluator) where TException : Exception
|
||||
{
|
||||
// Arrange
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
this.Scopes.Bind(engine);
|
||||
WorkflowExpressionEngine expressionEngine = new(engine);
|
||||
|
||||
// Act
|
||||
Assert.Throws<TException>(() => evaluator.Invoke(expressionEngine));
|
||||
}
|
||||
|
||||
private void EvaluateUnsupportedExpression(Action<WorkflowExpressionEngine> evaluator)
|
||||
{
|
||||
// Arrange
|
||||
RecalcEngine engine = this.CreateEngine();
|
||||
this.Scopes.Bind(engine);
|
||||
WorkflowExpressionEngine expressionEngine = new(engine);
|
||||
|
||||
// Act
|
||||
Assert.Throws<NotSupportedException>(() => evaluator.Invoke(expressionEngine));
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx;
|
||||
|
||||
public class WorkflowScopesTests
|
||||
{
|
||||
[Fact]
|
||||
public void ConstructorInitializesAllScopes()
|
||||
{
|
||||
// Arrange & Act
|
||||
WorkflowScopes scopes = new();
|
||||
|
||||
// Assert
|
||||
RecordValue envRecord = scopes.BuildRecord(VariableScopeNames.Environment);
|
||||
RecordValue topicRecord = scopes.BuildRecord(VariableScopeNames.Topic);
|
||||
RecordValue globalRecord = scopes.BuildRecord(VariableScopeNames.Global);
|
||||
RecordValue systemRecord = scopes.BuildRecord(VariableScopeNames.System);
|
||||
|
||||
Assert.NotNull(envRecord);
|
||||
Assert.NotNull(topicRecord);
|
||||
Assert.NotNull(globalRecord);
|
||||
Assert.NotNull(systemRecord);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRecordWhenEmpty()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
|
||||
// Act
|
||||
RecordValue record = scopes.BuildRecord(VariableScopeNames.Topic);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(record);
|
||||
Assert.Empty(record.Fields);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRecordContainsSetValues()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
FormulaValue testValue = FormulaValue.New("test");
|
||||
scopes.Set("key1", testValue, VariableScopeNames.Topic);
|
||||
|
||||
// Act
|
||||
RecordValue record = scopes.BuildRecord(VariableScopeNames.Topic);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(record);
|
||||
Assert.Single(record.Fields);
|
||||
Assert.Equal("key1", record.Fields.First().Name);
|
||||
Assert.Equal(testValue, record.Fields.First().Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRecordForAllScopeTypes()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
FormulaValue testValue = FormulaValue.New("test");
|
||||
|
||||
// Act & Assert
|
||||
scopes.Set("envKey", testValue, VariableScopeNames.Environment);
|
||||
RecordValue envRecord = scopes.BuildRecord(VariableScopeNames.Environment);
|
||||
Assert.Single(envRecord.Fields);
|
||||
|
||||
scopes.Set("topicKey", testValue, VariableScopeNames.Topic);
|
||||
RecordValue topicRecord = scopes.BuildRecord(VariableScopeNames.Topic);
|
||||
Assert.Single(topicRecord.Fields);
|
||||
|
||||
scopes.Set("globalKey", testValue, VariableScopeNames.Global);
|
||||
RecordValue globalRecord = scopes.BuildRecord(VariableScopeNames.Global);
|
||||
Assert.Single(globalRecord.Fields);
|
||||
|
||||
scopes.Set("systemKey", testValue, VariableScopeNames.System);
|
||||
RecordValue systemRecord = scopes.BuildRecord(VariableScopeNames.System);
|
||||
Assert.Single(systemRecord.Fields);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetWithImplicitScope()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
FormulaValue testValue = FormulaValue.New("test");
|
||||
scopes.Set("key1", testValue, VariableScopeNames.Topic);
|
||||
|
||||
// Act
|
||||
FormulaValue result = scopes.Get("key1");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(testValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetWithSpecifiedScope()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
FormulaValue testValue = FormulaValue.New("test");
|
||||
scopes.Set("key1", testValue, VariableScopeNames.Global);
|
||||
|
||||
// Act
|
||||
FormulaValue result = scopes.Get("key1", VariableScopeNames.Global);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(testValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetDefaultScope()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
FormulaValue testValue = FormulaValue.New("test");
|
||||
|
||||
// Act
|
||||
scopes.Set("key1", testValue);
|
||||
|
||||
// Assert
|
||||
FormulaValue result = scopes.Get("key1", VariableScopeNames.Topic);
|
||||
Assert.Equal(testValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetSpecifiedScope()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
FormulaValue testValue = FormulaValue.New("test");
|
||||
|
||||
// Act
|
||||
scopes.Set("key1", testValue, VariableScopeNames.System);
|
||||
|
||||
// Assert
|
||||
FormulaValue result = scopes.Get("key1", VariableScopeNames.System);
|
||||
Assert.Equal(testValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOverwritesExistingValue()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
FormulaValue initialValue = FormulaValue.New("initial");
|
||||
FormulaValue newValue = FormulaValue.New("new");
|
||||
|
||||
// Act
|
||||
scopes.Set("key1", initialValue, VariableScopeNames.Topic);
|
||||
scopes.Set("key1", newValue, VariableScopeNames.Topic);
|
||||
|
||||
// Assert
|
||||
FormulaValue result = scopes.Get("key1", VariableScopeNames.Topic);
|
||||
Assert.Equal(newValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveSpecifiedScope()
|
||||
{
|
||||
// Arrange
|
||||
WorkflowScopes scopes = new();
|
||||
FormulaValue testValue = FormulaValue.New("test");
|
||||
|
||||
// Act
|
||||
scopes.Set("key1", testValue);
|
||||
|
||||
// Assert
|
||||
FormulaValue result = scopes.Get("key1");
|
||||
Assert.Equal(testValue, result);
|
||||
|
||||
// Act
|
||||
scopes.Reset("key1");
|
||||
|
||||
// Assert
|
||||
FormulaValue resultBlank = scopes.Get("key1");
|
||||
Assert.IsType<BlankValue>(resultBlank);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests;
|
||||
|
||||
public sealed class TestOutputAdapter(ITestOutputHelper output) : TextWriter, ILogger, ILoggerFactory
|
||||
{
|
||||
private readonly Stack<string> _scopes = [];
|
||||
|
||||
public override Encoding Encoding { get; } = Encoding.UTF8;
|
||||
|
||||
public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException();
|
||||
|
||||
public ILogger CreateLogger(string categoryName) => this;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public override void WriteLine(object? value = null) => this.SafeWrite($"{value}");
|
||||
|
||||
public override void WriteLine(string? format, params object?[] arg) => this.SafeWrite(string.Format(format ?? string.Empty, arg));
|
||||
|
||||
public override void WriteLine(string? value) => this.SafeWrite(value ?? string.Empty);
|
||||
|
||||
public override void Write(object? value = null) => this.SafeWrite($"{value}");
|
||||
|
||||
public override void Write(char[]? buffer) => this.SafeWrite(new string(buffer));
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull
|
||||
{
|
||||
this._scopes.Push($"{state}");
|
||||
return new LoggerScope(() => this._scopes.Pop());
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
string message = formatter(state, exception);
|
||||
string scope = this._scopes.Count > 0 ? $"[{this._scopes.Peek()}] " : string.Empty;
|
||||
output.WriteLine($"{scope}{message}");
|
||||
}
|
||||
|
||||
private void SafeWrite(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
output.WriteLine(value ?? string.Empty);
|
||||
}
|
||||
catch (InvalidOperationException exception) when (exception.Message == "There is no currently active test.")
|
||||
{
|
||||
// This exception is thrown when the test output is accessed outside of a test context.
|
||||
// We can ignore it since we are not in a test context.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoggerScope(Action action) : IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this._disposed)
|
||||
{
|
||||
action.Invoke();
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for workflow tests.
|
||||
/// </summary>
|
||||
public abstract class WorkflowTest : IDisposable
|
||||
{
|
||||
public TestOutputAdapter Output { get; }
|
||||
|
||||
protected WorkflowTest(ITestOutputHelper output)
|
||||
{
|
||||
this.Output = new TestOutputAdapter(output);
|
||||
System.Console.SetOut(this.Output);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(isDisposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool isDisposing)
|
||||
{
|
||||
if (isDisposing)
|
||||
{
|
||||
this.Output.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? VariableScopeNames.Topic}.{variableName}";
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# empty yaml
|
||||
- id: 1
|
||||
- id: 2
|
||||
- id: 3
|
||||
@@ -0,0 +1,7 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
type: Message
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
@@ -0,0 +1,8 @@
|
||||
kind: ToolDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: ClearAllVariables
|
||||
id: clear_all
|
||||
variables: [ConversationScopedVariables]
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_test
|
||||
variable: Topic.TestValue
|
||||
value: =Value(System.LastMessage.Text)
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_test
|
||||
conditions:
|
||||
- id: conditionItem_odd
|
||||
condition: =Mod(Topic.TestValue, 2) = 1
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_odd
|
||||
activity: ODD
|
||||
|
||||
- id: conditionItem_even
|
||||
condition: =Mod(Topic.TestValue, 2) = 0
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_even
|
||||
activity: EVEN
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_test
|
||||
variable: Topic.TestValue
|
||||
value: =Value(System.LastMessage.Text)
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_test
|
||||
conditions:
|
||||
- id: conditionItem_odd
|
||||
condition: =Mod(Topic.TestValue, 2) = 1
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_odd
|
||||
activity: ODD
|
||||
elseActions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_else
|
||||
activity: EVEN
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: set_var
|
||||
variable: Topic.MyTable
|
||||
value: =[{id: 3}]
|
||||
- kind: EditTable
|
||||
id: edit_var
|
||||
itemsVariable: Topic.MyTable
|
||||
changeType: Add
|
||||
value: ={id: 7}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: set_var
|
||||
variable: Topic.MyTable
|
||||
value: =[{id: 3}]
|
||||
- kind: EditTableV2
|
||||
id: edit_var
|
||||
itemsVariable: Topic.MyTable
|
||||
changeType:
|
||||
kind: AddItemOperation
|
||||
value: ={id: 7}
|
||||
@@ -0,0 +1,25 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_end
|
||||
actionId: end_all
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_1
|
||||
activity: NEVER 1!
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_2
|
||||
activity: NEVER 2!
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_3
|
||||
activity: NEVER 3!
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: setVariable_count
|
||||
variable: Topic.Count
|
||||
value: =0
|
||||
|
||||
- kind: Foreach
|
||||
id: foreach_loop
|
||||
items: =["a", "b", "c", "d", "e", "f"]
|
||||
index: Topic.LoopIndex
|
||||
value: Topic.LoopValue
|
||||
actions:
|
||||
- kind: BreakLoop
|
||||
id: breakLoop_now
|
||||
- kind: SetVariable
|
||||
id: setVariable_loop
|
||||
variable: Topic.Count
|
||||
value: =Topic.Count + 1
|
||||
- kind: SendActivity
|
||||
id: sendActivity_loop
|
||||
activity: x{Topic.Count} - {Topic.LoopIndex}:{Topic.LoopValue}
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: setVariable_count
|
||||
variable: Topic.Count
|
||||
value: =0
|
||||
|
||||
- kind: Foreach
|
||||
id: foreach_loop
|
||||
items: =["a", "b", "c", "d", "e", "f"]
|
||||
index: Topic.LoopIndex
|
||||
value: Topic.LoopValue
|
||||
actions:
|
||||
- kind: ContinueLoop
|
||||
id: continueLoop_now
|
||||
- kind: SetVariable
|
||||
id: setVariable_loop
|
||||
variable: Topic.Count
|
||||
value: =Topic.Count + 1
|
||||
- kind: SendActivity
|
||||
id: sendActivity_loop
|
||||
activity: x{Topic.Count} - {Topic.LoopIndex}:{Topic.LoopValue}
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
@@ -0,0 +1,27 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: setVariable_count
|
||||
variable: Topic.Count
|
||||
value: =0
|
||||
|
||||
- kind: Foreach
|
||||
id: foreach_loop
|
||||
items: =["a", "b", "c", "d", "e", "f"]
|
||||
index: Topic.LoopIndex
|
||||
value: Topic.LoopValue
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: setVariable_loop
|
||||
variable: Topic.Count
|
||||
value: =Topic.Count + 1
|
||||
- kind: SendActivity
|
||||
id: sendActivity_loop
|
||||
activity: x{Topic.Count} - {Topic.LoopIndex}:{Topic.LoopValue}
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: ParseValue
|
||||
id: parse_var
|
||||
variable: Topic.MyVar
|
||||
value: "42"
|
||||
valueType: Number
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: set_var
|
||||
variable: Topic.MyVar
|
||||
value: 42
|
||||
- kind: ResetVariable
|
||||
id: clear_var
|
||||
variable: Topic.MyVar
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: SetTextVariable
|
||||
id: set_text
|
||||
variable: Topic.TestVar
|
||||
value: "Test content"
|
||||
@@ -0,0 +1,8 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
@@ -0,0 +1,486 @@
|
||||
#
|
||||
# This workflow demonstrates a multi-agent orchestrator that attempts to address complex user requests.
|
||||
#
|
||||
# For this workflow, several agents used, each with a prompt specific to their role:
|
||||
#
|
||||
# 1. Analyst Agent: Able to analyze the current task.
|
||||
# Enable "Bing Grounding Tool" in the agent settings.
|
||||
# See: ./setup/AnalystAgent.yaml
|
||||
#
|
||||
# 2. Manager Agent: Able to create plans and delegate tasks to other agents.
|
||||
# See: ./setup/ManagerAgent.yaml
|
||||
#
|
||||
# 3. Research Agent:
|
||||
# Enable "Bing Grounding" in the agent settings.
|
||||
# See: ./setup/WebAgent.yaml
|
||||
#
|
||||
# With instructions:
|
||||
#
|
||||
# Only provide requested information in a way that is throughfully organized and formatted.
|
||||
# Never include any analysis or code.
|
||||
# Never generate a file.
|
||||
# Avoid repeating yourself.
|
||||
#
|
||||
# 4. Coder Agent:
|
||||
# Enable "Code Interpreter" in the agent settings.
|
||||
# See: ./setup/CoderAgent.yaml
|
||||
#
|
||||
# 5. Weather Agent: Able to retrieve factual information from the web.
|
||||
# Enable "Open API" in the agent settings using the wttr.json schema.
|
||||
# See: ./setup/WeatherAgent.yaml
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: activity_xyz123
|
||||
type: Message
|
||||
actions:
|
||||
- kind: SetVariable
|
||||
id: setVariable_aASlmF
|
||||
displayName: List all available agents for this orchestrator
|
||||
variable: Topic.AvailableAgents
|
||||
value: |-
|
||||
=[
|
||||
{
|
||||
name: "WeatherAgent",
|
||||
description: "Able to retrieve weather information",
|
||||
agentid: Env.FOUNDRY_AGENT_RESEARCHWEATHER
|
||||
},
|
||||
{
|
||||
name: "CoderAgent",
|
||||
description: "Able to write and execute Python code",
|
||||
agentid: Env.FOUNDRY_AGENT_RESEARCHCODER
|
||||
},
|
||||
{
|
||||
name: "WebAgent",
|
||||
description: "Able to perform generic websearches",
|
||||
agentid: Env.FOUNDRY_AGENT_RESEARCHWEB
|
||||
}
|
||||
]
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_V6yEbo
|
||||
displayName: Get a summary of all the agents for use in prompts
|
||||
variable: Topic.TeamDescription
|
||||
value: "=Concat(ForAll(Topic.AvailableAgents, $\"- \" & name & $\": \" & description), Value, \"\n\")"
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_NZ2u0l
|
||||
displayName: Set Task
|
||||
variable: Topic.InputTask
|
||||
value: =System.LastMessage.Text
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_10u2ZN
|
||||
displayName: Set Task
|
||||
variable: Topic.SeedTask
|
||||
value: =Topic.InputTask
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_yFsbRy
|
||||
activity: Analyzing facts...
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_UDoMUw
|
||||
displayName: Get Facts
|
||||
autoSend: false
|
||||
variable: Topic.TaskFacts
|
||||
userInput: =Topic.InputTask
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHANALYST},
|
||||
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.
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_yFsbRz
|
||||
activity: Creating a plan...
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_DsBaJU
|
||||
displayName: Create a Plan
|
||||
autoSend: false
|
||||
variable: Topic.Plan
|
||||
userInput: =""
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHMANAGER},
|
||||
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]"
|
||||
|
||||
{Topic.TeamDescription}
|
||||
|
||||
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.
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_Kk2LDL
|
||||
displayName: Define instructions
|
||||
variable: Topic.TaskInstructions
|
||||
value: |-
|
||||
="# TASK
|
||||
Address the following user request:
|
||||
|
||||
" & Topic.InputTask & "
|
||||
|
||||
|
||||
# TEAM
|
||||
Use the following team to answer this request:
|
||||
|
||||
" & Topic.TeamDescription & "
|
||||
|
||||
|
||||
# FACTS
|
||||
Consider this initial fact sheet:
|
||||
|
||||
" & Trim(Topic.TaskFacts.Text) & "
|
||||
|
||||
|
||||
# PLAN
|
||||
Here is the plan to follow as best as possible:
|
||||
|
||||
" & Topic.Plan.Text
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_bwNZiM
|
||||
activity: {Topic.TaskInstructions}
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_o3BQkf
|
||||
displayName: Progress Ledger Prompt
|
||||
autoSend: false
|
||||
variable: Topic.ProgressLedgerUpdate
|
||||
userInput: =Topic.AgentResponseText
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHMANAGER},
|
||||
Recall we are working on the following request:
|
||||
|
||||
{Topic.InputTask}
|
||||
|
||||
And we have assembled the following team:
|
||||
|
||||
{Topic.TeamDescription}
|
||||
|
||||
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: {Concat(Topic.AvailableAgents, name, ",")})
|
||||
- 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)
|
||||
|
||||
Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA:
|
||||
|
||||
{{
|
||||
"is_request_satisfied": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"is_in_loop": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"is_progress_being_made": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"next_speaker": {{
|
||||
"reason": string,
|
||||
"answer": string (select from: {Concat(Topic.AvailableAgents, name, ",")})
|
||||
}},
|
||||
"instruction_or_question": {{
|
||||
"reason": string,
|
||||
"answer": string
|
||||
}}
|
||||
}}
|
||||
|
||||
- kind: ParseValue
|
||||
id: parse_rNZtlV
|
||||
displayName: Parse ledger response
|
||||
variable: Topic.TypedProgressLedger
|
||||
value: =Topic.ProgressLedgerUpdate.Text
|
||||
valueType:
|
||||
kind: Record
|
||||
properties:
|
||||
instruction_or_question:
|
||||
type:
|
||||
kind: Record
|
||||
properties:
|
||||
answer: String
|
||||
reason: String
|
||||
|
||||
is_in_loop:
|
||||
type:
|
||||
kind: Record
|
||||
properties:
|
||||
answer: Boolean
|
||||
reason: String
|
||||
|
||||
is_progress_being_made:
|
||||
type:
|
||||
kind: Record
|
||||
properties:
|
||||
answer: Boolean
|
||||
reason: String
|
||||
|
||||
is_request_satisfied:
|
||||
type:
|
||||
kind: Record
|
||||
properties:
|
||||
answer: Boolean
|
||||
reason: String
|
||||
|
||||
next_speaker:
|
||||
type:
|
||||
kind: Record
|
||||
properties:
|
||||
answer: String
|
||||
reason: String
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_mVIecC
|
||||
conditions:
|
||||
- id: conditionItem_fj432c
|
||||
condition: =Topic.TypedProgressLedger.is_request_satisfied.answer
|
||||
displayName: If Done
|
||||
actions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_kdl3mC
|
||||
activity: Completed! {Topic.TypedProgressLedger.is_request_satisfied.reason}
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_Ke3l1d
|
||||
displayName: Generate Response
|
||||
variable: Topic.FinalResponse
|
||||
userInput: =Topic.SeedTask
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHMANAGER},
|
||||
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 every see this last response and not the entire conversation, so please ensure it is complete and self-contained.
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_SVoNSV
|
||||
|
||||
- id: conditionItem_yiqund
|
||||
condition: =Topic.TypedProgressLedger.is_in_loop.answer || Not(Topic.TypedProgressLedger.is_progress_being_made.answer)
|
||||
displayName: If Stalling
|
||||
actions:
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_H5lXdD
|
||||
displayName: Increase stall count
|
||||
variable: Topic.StallCount
|
||||
value: =Topic.StallCount + 1
|
||||
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_vBTQd3
|
||||
conditions:
|
||||
|
||||
- id: conditionItem_fpaNL9
|
||||
condition: =.TypedProgressLedger.is_in_loop.answer
|
||||
displayName: Is Loop
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_fpaNL9
|
||||
activity: {Topic.TypedProgressLedger.is_in_loop.reason}
|
||||
|
||||
- id: conditionItem_NnqvXh
|
||||
condition: =Not(Topic.TypedProgressLedger.is_progress_being_made.answer)
|
||||
displayName: Is No Progress
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_NnqvXh
|
||||
activity: {Topic.TypedProgressLedger.is_progress_being_made.reason}
|
||||
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_xzNrdM
|
||||
conditions:
|
||||
- id: conditionItem_NlQTBv
|
||||
condition: =Topic.StallCount > 2
|
||||
displayName: Stall Count Exceeded
|
||||
actions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_H5lXdD
|
||||
activity: Unable to make sufficient progress...
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_4s1Z27
|
||||
conditions:
|
||||
- id: conditionItem_EXAlhZ
|
||||
condition: =Topic.RestartCount > 2
|
||||
actions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_xKxFUU
|
||||
activity: Stopping after attempting {Topic.RestartCount} restarts...
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_GHVrFh
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_cwNZiM
|
||||
activity: Re-analyzing facts...
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_wFJ123
|
||||
displayName: Get New Facts Prompt
|
||||
autoSend: false
|
||||
variable: Topic.TaskFacts
|
||||
userInput: |-
|
||||
="As a reminder, we are working to solve the following task:
|
||||
|
||||
" & Topic.InputTask
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHANALYST},
|
||||
It's clear we aren't making as much progress as we would like, but we may have learned something new.
|
||||
Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful.
|
||||
Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc.
|
||||
Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited.
|
||||
This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning.
|
||||
|
||||
Here is the old fact sheet:
|
||||
|
||||
{Topic.TaskFacts}
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_dsBaJU
|
||||
activity: Re-analyzing plan...
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_uEJ456
|
||||
displayName: Create new Plan Prompt
|
||||
autoSend: false
|
||||
variable: Topic.Plan
|
||||
userInput: =""
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHMANAGER},
|
||||
Please briefly explain what went wrong on this last run (the root cause of the failure),
|
||||
and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes.
|
||||
As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition
|
||||
(do not involve any other outside people since we cannot contact anyone else):
|
||||
|
||||
{Topic.TeamDescription}
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_jW7tmM
|
||||
displayName: Set Plan as Context
|
||||
variable: Topic.TaskInstructions
|
||||
value: |-
|
||||
="# TASK
|
||||
Address the following user request:
|
||||
|
||||
" & Topic.InputTask & "
|
||||
|
||||
|
||||
# TEAM
|
||||
Use the following team to answer this request:
|
||||
|
||||
" & Topic.TeamDescription & "
|
||||
|
||||
|
||||
# FACTS
|
||||
Consider this initial fact sheet:
|
||||
|
||||
" & Topic.TaskFacts.Text & "
|
||||
|
||||
|
||||
# PLAN
|
||||
Here is the plan to follow as best as possible:
|
||||
|
||||
" & Topic.Plan.Text
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_6J2snP
|
||||
displayName: Reset Stall count
|
||||
variable: Topic.StallCount
|
||||
value: 0
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_S6HCgh
|
||||
displayName: Increase Restart count
|
||||
variable: Topic.RestartCount
|
||||
value: =Topic.RestartCount + 1
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_LzfJ8u
|
||||
actionId: question_o3BQkf
|
||||
|
||||
elseActions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_L7ooQO
|
||||
activity: |-
|
||||
({Topic.TypedProgressLedger.next_speaker.reason})
|
||||
|
||||
{Topic.TypedProgressLedger.next_speaker.answer} - {Topic.TypedProgressLedger.instruction_or_question.answer}
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_L7ooQO
|
||||
variable: Topic.StallCount
|
||||
value: 0
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_nxN1mE
|
||||
variable: Topic.NextSpeaker
|
||||
value: =Search(Topic.AvailableAgents, Topic.TypedProgressLedger.next_speaker.answer, name)
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: conditionGroup_QFPiF5
|
||||
conditions:
|
||||
- id: conditionItem_GmigcU
|
||||
condition: =CountRows(Topic.NextSpeaker) = 1
|
||||
displayName: If next Agent tool Exists
|
||||
actions:
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_orsBf06
|
||||
variable: Topic.AgentResponse
|
||||
userInput: =Topic.SeedTask
|
||||
additionalInstructions: |-
|
||||
{First(Topic.NextSpeaker).agentid},
|
||||
{Topic.TypedProgressLedger.instruction_or_question.answer}
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_XzNrdM
|
||||
variable: Topic.AgentResponseText
|
||||
value: =Topic.AgentResponse.Text
|
||||
|
||||
- kind: ResetVariable
|
||||
id: setVariable_8eIx2A
|
||||
displayName: Clear seed task
|
||||
variable: Topic.SeedTask
|
||||
|
||||
elseActions:
|
||||
- kind: SendActivity
|
||||
id: sendActivity_BhcsI7
|
||||
activity: Unable to choose next agent...
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_BhcsI7
|
||||
displayName: Increase stall count
|
||||
variable: Topic.StallCount
|
||||
value: =Topic.StallCount + 1
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_76Hne8
|
||||
actionId: question_o3BQkf
|
||||
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# This workflow provides the most basic example of providing a response that includes the user and environment input.
|
||||
#
|
||||
# No agent setup is required to run this workflow.
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
# Capture input
|
||||
- kind: SetVariable
|
||||
id: setvar_userinput
|
||||
variable: Topic.UserInput
|
||||
value: =System.LastMessageText
|
||||
|
||||
# Capture environment variable
|
||||
- kind: SetVariable
|
||||
id: setvar_username
|
||||
variable: Global.UserName
|
||||
value: =Env.USERNAME
|
||||
|
||||
# Respond with input
|
||||
- kind: SendActivity
|
||||
id: sendActivity_demo
|
||||
activity: |-
|
||||
Hello {Global.UserName},
|
||||
You said, "{Topic.UserInput}"
|
||||
@@ -0,0 +1,82 @@
|
||||
#
|
||||
# This workflow demonstrates conversation between two agents: a student and a teacher.
|
||||
# The student attempts to solve the input problem and the teacher provides guidance.
|
||||
#
|
||||
# For this workflow, two agents are used, each with a prompt specific to their role.
|
||||
#
|
||||
# Student:
|
||||
# See: ./setup/StudentAgent.yaml
|
||||
#
|
||||
# With 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.
|
||||
|
||||
# Teacher:
|
||||
# See: ./setup/TeacherAgent.yaml
|
||||
#
|
||||
# With 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 congraluations by using the word "congratulations".
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: SetVariable
|
||||
id: set_project
|
||||
variable: Topic.Project
|
||||
value: =System.LastMessage.Text
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_student
|
||||
userInput: =Topic.Project
|
||||
additionalInstructions: {Env.FOUNDRY_AGENT_STUDENT}
|
||||
|
||||
- kind: ResetVariable
|
||||
id: reset_project
|
||||
variable: Topic.Project
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_teacher
|
||||
userInput: =""
|
||||
additionalInstructions: {Env.FOUNDRY_AGENT_TEACHER}
|
||||
|
||||
- kind: SetVariable
|
||||
id: set_count_increment
|
||||
variable: Topic.TurnCount
|
||||
value: =Topic.TurnCount + 1
|
||||
|
||||
- kind: ConditionGroup
|
||||
id: check_completion
|
||||
conditions:
|
||||
|
||||
- condition: =!IsBlank(Find("congratulations", Lower(System.LastMessage.Text)))
|
||||
id: check_turn_done
|
||||
actions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_done
|
||||
activity: GOLD STAR!
|
||||
|
||||
- condition: =Topic.TurnCount < 4
|
||||
id: check_turn_count
|
||||
actions:
|
||||
|
||||
- kind: GotoAction
|
||||
id: goto_student_agent
|
||||
actionId: question_student
|
||||
|
||||
elseActions:
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_tired
|
||||
activity: Let's try again later...
|
||||
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# This workflow demonstrates a single agent interaction based on user input.
|
||||
#
|
||||
# Any Foundry Agent may be used to provide the response.
|
||||
# See: ./setup/QuestionAgent.yaml
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
# Respond with input
|
||||
- kind: SendActivity
|
||||
id: sendActivity_demo
|
||||
activity: "Working..."
|
||||
|
||||
# Use AI to answer the question
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_demo
|
||||
variable: Topic.Answer
|
||||
userInput: =System.LastMessage.Text
|
||||
additionalInstructions: {Env.FOUNDRY_AGENT_ANSWER}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Declarative Workflows
|
||||
|
||||
This folder contains sample workflow definitions than be ran using the
|
||||
[Declarative Workflow](../dotnet/demos/DeclarativeWorkflow) demo.
|
||||
|
||||
Each workflow is defined in a single YAML file and contains
|
||||
comments with additional information specific to that workflow.
|
||||
|
||||
A _Declarative Workflow_ may be executed locally no different from any `Workflow` defined by code.
|
||||
The difference is that the workflow definition is loaded from a YAML file instead of being defined in code.
|
||||
|
||||
```c#
|
||||
Workflow<string> workflow = DeclarativeWorkflowBuilder.Build<string>("HelloWorld.yaml", options);
|
||||
```
|
||||
|
||||
Workflows may also be hosted in your _Azure Foundry Project_.
|
||||
|
||||
> _Python_ support in the works!
|
||||
|
||||
#### Agents
|
||||
|
||||
The sample workflows rely on agents defined in your Azure Foundry Project.
|
||||
|
||||
To create agents, run the [`Create.ps1`](./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` setting. See [README.md](../dotnet/demos/DeclarativeWorkflow/README.md) from the demo for configuration details.
|
||||
@@ -0,0 +1,405 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
[Aa][Rr][Mm]64[Ee][Cc]/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# AWS SAM Build and Temporary Artifacts folder
|
||||
.aws-sam
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
@@ -0,0 +1,10 @@
|
||||
type: foundry_agent
|
||||
name: ResearchAnalyst
|
||||
description: Demo agent for DeepResearch workflow
|
||||
model:
|
||||
id: ${AzureAI:ModelDeployment}
|
||||
tools:
|
||||
- type: bing_grounding
|
||||
options:
|
||||
tool_connections:
|
||||
- ${AzureAI:BingConnectionId}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user