.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:
Chris
2025-09-03 16:32:15 +00:00
committed by GitHub
co-authored by Eric Zhu Jacob Alber
parent c129d32ebe
commit 24ad03af6f
113 changed files with 8278 additions and 38 deletions
+5
View File
@@ -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" />
+15
View File
@@ -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
View File
@@ -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;
}
@@ -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)
{
}
}
@@ -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)
{
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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());
}
@@ -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}");
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
}
}
@@ -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);
}
}
@@ -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()})";
}
@@ -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;
}
}
@@ -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>
@@ -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);
}
}
}
}
@@ -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}
""");
}
}
}
@@ -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);
}
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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)
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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"));
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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>
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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";
}
}
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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
@@ -0,0 +1,10 @@
kind: AdaptiveDialog
beginDialog:
kind: OnActivity
id: my_workflow
type: Message
actions:
- kind: ClearAllVariables
id: clear_all
variables: [ConversationScopedVariables]
@@ -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
@@ -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
@@ -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}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+486
View File
@@ -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
+30
View File
@@ -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}"
+82
View File
@@ -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...
+24
View File
@@ -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}
+27
View File
@@ -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.
+405
View File
@@ -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
+10
View File
@@ -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