mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into python-middleware
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<LangVersion>12</LangVersion>
|
||||
<LangVersion>13</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);IDE0290;IDE0079;NU5128;CS8002</NoWarn>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
@@ -86,14 +87,16 @@
|
||||
<!-- 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" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.915-1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.915-1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.915-1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0-build.20250821-1001" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.6.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="Moq" Version="[4.18.4]" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.65.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.65.0-beta" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
|
||||
@@ -78,9 +78,9 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/Examples/">
|
||||
<File Path="../workflows/DeepResearch.yaml" />
|
||||
<File Path="../workflows/HelloWorld.yaml" />
|
||||
<File Path="../workflows/HumanInLoop.yaml" />
|
||||
<File Path="../workflows/Marketing.yaml" />
|
||||
<File Path="../workflows/MathChat.yaml" />
|
||||
<File Path="../workflows/Question.yaml" />
|
||||
<File Path="../workflows/README.md" />
|
||||
<File Path="../workflows/wttr.json" />
|
||||
</Folder>
|
||||
@@ -96,8 +96,8 @@
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Checkpoint/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/HumanInTheLoop/">
|
||||
@@ -265,6 +265,7 @@
|
||||
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj" />
|
||||
<Project Path="tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj" />
|
||||
<Project Path="tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletion.IntegrationTests.csproj" />
|
||||
<Project Path="tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj" />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
|
||||
<NoWarn>$(NoWarn);CA1812</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Declarative;
|
||||
using Microsoft.Agents.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
@@ -39,17 +40,11 @@ internal sealed class Program
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
// Read and parse the declarative workflow.
|
||||
Notify($"WORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}");
|
||||
Notify($"\nWORKFLOW: 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);
|
||||
Workflow<string> workflow = this.CreateWorkflow();
|
||||
|
||||
Notify($"\nWORKFLOW: Defined {timer.Elapsed}");
|
||||
|
||||
@@ -57,23 +52,69 @@ internal sealed class Program
|
||||
|
||||
// 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!");
|
||||
CheckpointManager checkpointManager = new();
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager);
|
||||
|
||||
bool isComplete = false;
|
||||
InputResponse? response = null;
|
||||
do
|
||||
{
|
||||
ExternalRequest? inputRequest = await this.MonitorWorkflowRunAsync(run, response);
|
||||
if (inputRequest is not null)
|
||||
{
|
||||
Notify("\nWORKFLOW: Yield");
|
||||
|
||||
if (this.LastCheckpoint is null)
|
||||
{
|
||||
throw new InvalidOperationException("Checkpoint information missing after external request.");
|
||||
}
|
||||
|
||||
// Process the external request.
|
||||
response = HandleExternalRequest(inputRequest);
|
||||
|
||||
// Let's resume on an entirely new workflow instance to demonstrate checkpoint portability.
|
||||
workflow = this.CreateWorkflow();
|
||||
|
||||
// Restore the latest checkpoint.
|
||||
Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}");
|
||||
Notify("\nWORKFLOW: Restore");
|
||||
run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
isComplete = true;
|
||||
}
|
||||
}
|
||||
while (!isComplete);
|
||||
|
||||
Notify("\nWORKFLOW: Done!\n");
|
||||
}
|
||||
|
||||
private Workflow<string> CreateWorkflow()
|
||||
{
|
||||
// Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
|
||||
DeclarativeWorkflowOptions options =
|
||||
new(new AzureAgentProvider(this.FoundryEndpoint, new AzureCliCredential()))
|
||||
{
|
||||
Configuration = this.Configuration
|
||||
};
|
||||
Workflow<string> workflow = DeclarativeWorkflowBuilder.Build<string>(this.WorkflowFile, options);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
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 static Dictionary<string, string> NameCache { get; } = [];
|
||||
private static HashSet<string> FileCache { get; } = [];
|
||||
|
||||
private string WorkflowFile { get; }
|
||||
private string? WorkflowInput { get; }
|
||||
private string FoundryEndpoint { get; }
|
||||
private PersistentAgentsClient FoundryClient { get; }
|
||||
private IConfiguration Configuration { get; }
|
||||
private CheckpointInfo? LastCheckpoint { get; set; }
|
||||
|
||||
private Program(string[] args)
|
||||
{
|
||||
@@ -86,112 +127,147 @@ internal sealed class Program
|
||||
this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
|
||||
}
|
||||
|
||||
private async Task MonitorWorkflowRunAsync(StreamingRun run)
|
||||
private async Task<ExternalRequest?> MonitorWorkflowRunAsync(Checkpointed<StreamingRun> run, InputResponse? response = null)
|
||||
{
|
||||
string? messageId = null;
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorInvokedEvent executorInvoked)
|
||||
switch (workflowEvent)
|
||||
{
|
||||
Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
|
||||
}
|
||||
else if (evt is ExecutorCompletedEvent executorCompleted)
|
||||
{
|
||||
Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
|
||||
}
|
||||
if (evt is DeclarativeActionInvokeEvent actionInvoked)
|
||||
{
|
||||
Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]");
|
||||
}
|
||||
else if (evt is DeclarativeActionCompleteEvent actionComplete)
|
||||
{
|
||||
Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]");
|
||||
}
|
||||
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;
|
||||
case ExecutorInvokedEvent executorInvoked:
|
||||
Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
|
||||
break;
|
||||
|
||||
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}]");
|
||||
}
|
||||
}
|
||||
case ExecutorCompletedEvent executorCompleted:
|
||||
Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
|
||||
break;
|
||||
|
||||
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)
|
||||
case DeclarativeActionInvokeEvent actionInvoked:
|
||||
Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]");
|
||||
break;
|
||||
|
||||
case DeclarativeActionCompleteEvent actionComplete:
|
||||
Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]");
|
||||
break;
|
||||
|
||||
case ExecutorFailureEvent executorFailure:
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
break;
|
||||
|
||||
case SuperStepCompletedEvent checkpointCompleted:
|
||||
this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint;
|
||||
Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]");
|
||||
break;
|
||||
|
||||
case RequestInfoEvent requestInfo:
|
||||
Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
|
||||
if (response is not null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("ACTIVITY:");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(messageEvent.Response?.Text.Trim());
|
||||
ExternalResponse requestResponse = requestInfo.Request.CreateResponse<InputResponse>(response);
|
||||
await run.Run.SendResponseAsync(requestResponse).ConfigureAwait(false);
|
||||
response = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return requestInfo.Request;
|
||||
}
|
||||
break;
|
||||
|
||||
case ConversationUpdateEvent invokeEvent:
|
||||
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
|
||||
break;
|
||||
|
||||
case MessageActivityEvent activityEvent:
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("\nACTIVITY:");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(activityEvent.Message.Trim());
|
||||
break;
|
||||
|
||||
case AgentRunUpdateEvent streamEvent:
|
||||
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
|
||||
{
|
||||
messageId = streamEvent.Update.MessageId;
|
||||
|
||||
if (messageId is not null)
|
||||
{
|
||||
string? agentId = streamEvent.Update.AuthorName;
|
||||
if (agentId is not null)
|
||||
{
|
||||
if (!NameCache.TryGetValue(agentId, out string? realName))
|
||||
{
|
||||
PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
|
||||
NameCache[agentId] = agent.Name;
|
||||
realName = agent.Name;
|
||||
}
|
||||
agentId = realName;
|
||||
}
|
||||
agentId ??= nameof(ChatRole.Assistant);
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write($"\n{agentId.ToUpperInvariant()}:");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{messageId}]");
|
||||
}
|
||||
}
|
||||
|
||||
ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
|
||||
switch (chatUpdate?.RawRepresentation)
|
||||
{
|
||||
case MessageContentUpdate messageUpdate:
|
||||
string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId;
|
||||
if (fileId is not null && FileCache.Add(fileId))
|
||||
{
|
||||
BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId);
|
||||
await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
try
|
||||
{
|
||||
Console.ResetColor();
|
||||
Console.Write(streamEvent.Data);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
|
||||
case AgentRunResponseEvent messageEvent:
|
||||
try
|
||||
{
|
||||
Console.WriteLine();
|
||||
if (messageEvent.Response.Usage is not null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
private static InputResponse HandleExternalRequest(ExternalRequest request)
|
||||
{
|
||||
InputRequest? message = request.Data as InputRequest;
|
||||
string? userInput = null;
|
||||
do
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGreen;
|
||||
Console.Write($"\n{message?.Prompt ?? "INPUT:"} ");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
userInput = Console.ReadLine();
|
||||
}
|
||||
while (string.IsNullOrWhiteSpace(userInput));
|
||||
|
||||
return new InputResponse(userInput);
|
||||
}
|
||||
|
||||
private static string ParseWorkflowFile(string[] args)
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Core;
|
||||
using Azure.Core.Pipeline;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <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 AzureAgentProvider(string projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider
|
||||
{
|
||||
private static readonly Dictionary<string, MessageRole> s_roleMap =
|
||||
new()
|
||||
{
|
||||
[ChatRole.User.Value.ToUpperInvariant()] = MessageRole.User,
|
||||
[ChatRole.Assistant.Value.ToUpperInvariant()] = MessageRole.Agent,
|
||||
[ChatRole.System.Value.ToUpperInvariant()] = new MessageRole(ChatRole.System.Value),
|
||||
[ChatRole.Tool.Value.ToUpperInvariant()] = new MessageRole(ChatRole.Tool.Value),
|
||||
};
|
||||
|
||||
private PersistentAgentsClient? _agentsClient;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<string> CreateConversationAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
PersistentAgentThread conversation = await this.GetAgentsClient().Threads.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return conversation.Id;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.GetAgentsClient().Messages.CreateMessageAsync(
|
||||
conversationId,
|
||||
role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()],
|
||||
// TODO: PersistentAgent bug blocks supporting multiple content types:
|
||||
// https://github.com/Azure/azure-sdk-for-net/issues/52571
|
||||
//contentBlocks: GetContent(),
|
||||
content: conversationMessage.Text,
|
||||
attachments: null,
|
||||
metadata: GetMetadata(),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Dictionary<string, string>? GetMetadata()
|
||||
{
|
||||
if (conversationMessage.AdditionalProperties is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return conversationMessage.AdditionalProperties.ToDictionary(prop => prop.Key, prop => prop.Value?.ToString() ?? string.Empty);
|
||||
}
|
||||
|
||||
// TODO: PersistentAgent bug blocks supporting multiple content types:
|
||||
// https://github.com/Azure/azure-sdk-for-net/issues/52571
|
||||
//IEnumerable<MessageInputContentBlock> GetContent()
|
||||
//{
|
||||
// foreach (AIContent content in conversationMessage.Contents)
|
||||
// {
|
||||
// MessageInputContentBlock? contentBlock =
|
||||
// content switch
|
||||
// {
|
||||
// TextContent textContent => new MessageInputTextBlock(textContent.Text),
|
||||
// HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)),
|
||||
// UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())),
|
||||
// _ => null // Unsupported content type
|
||||
// };
|
||||
|
||||
// if (contentBlock is not null)
|
||||
// {
|
||||
// yield return contentBlock;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PersistentThreadMessage message = await this.GetAgentsClient().Messages.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false);
|
||||
return ToChatMessage(message);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<ChatMessage> GetMessagesAsync(
|
||||
string conversationId,
|
||||
int? limit = null,
|
||||
string? after = null,
|
||||
string? before = null,
|
||||
bool newestFirst = false,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ListSortOrder order = newestFirst ? ListSortOrder.Ascending : ListSortOrder.Descending;
|
||||
await foreach (PersistentThreadMessage message in this.GetAgentsClient().Messages.GetMessagesAsync(conversationId, runId: null, limit, order, after, before, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return ToChatMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
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, clientOptions);
|
||||
|
||||
Interlocked.CompareExchange(ref this._agentsClient, newClient, null);
|
||||
}
|
||||
|
||||
return this._agentsClient;
|
||||
}
|
||||
|
||||
private static ChatMessage ToChatMessage(PersistentThreadMessage message)
|
||||
{
|
||||
return
|
||||
new ChatMessage(new ChatRole(message.Role.ToString()), [.. GetContent()])
|
||||
{
|
||||
MessageId = message.Id,
|
||||
CreatedAt = message.CreatedAt,
|
||||
AdditionalProperties = GetMetadata()
|
||||
};
|
||||
|
||||
IEnumerable<AIContent> GetContent()
|
||||
{
|
||||
foreach (MessageContent contentItem in message.ContentItems)
|
||||
{
|
||||
AIContent? content =
|
||||
contentItem switch
|
||||
{
|
||||
MessageTextContent textContent => new TextContent(textContent.Text),
|
||||
MessageImageFileContent imageContent => new HostedFileContent(imageContent.FileId),
|
||||
_ => null // Unsupported content type
|
||||
};
|
||||
|
||||
if (content is not null)
|
||||
{
|
||||
yield return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AdditionalPropertiesDictionary? GetMetadata()
|
||||
{
|
||||
if (message.Metadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AdditionalPropertiesDictionary(message.Metadata.Select(m => new KeyValuePair<string, object?>(m.Key, m.Value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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;
|
||||
@@ -22,7 +21,7 @@ public static class DeclarativeWorkflowBuilder
|
||||
/// </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="options">Configuration options for workflow execution.</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>(
|
||||
@@ -34,12 +33,13 @@ public static class DeclarativeWorkflowBuilder
|
||||
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="options">Configuration options for workflow execution.</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>(
|
||||
@@ -56,20 +56,20 @@ public static class DeclarativeWorkflowBuilder
|
||||
throw new DeclarativeModelException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(AdaptiveDialog)}.");
|
||||
}
|
||||
|
||||
string rootId = WorkflowActionVisitor.RootId(workflowElement.BeginDialog?.Id.Value ?? "workflow");
|
||||
string rootId = WorkflowActionVisitor.Steps.Root(workflowElement.BeginDialog?.Id.Value);
|
||||
|
||||
WorkflowScopes scopes = new();
|
||||
scopes.Initialize(WrapWithBot(workflowElement), options.Configuration);
|
||||
DeclarativeWorkflowState state = new(options.CreateRecalcEngine(), scopes);
|
||||
WorkflowFormulaState state = new(options.CreateRecalcEngine());
|
||||
state.Initialize(workflowElement.WrapWithBot(), options.Configuration);
|
||||
DeclarativeWorkflowExecutor<TInput> rootExecutor =
|
||||
new(rootId,
|
||||
state,
|
||||
message => inputTransform?.Invoke(message) ?? DefaultTransform(message));
|
||||
|
||||
WorkflowActionVisitor visitor = new(rootExecutor, state, options);
|
||||
WorkflowElementWalker walker = new(rootElement, visitor);
|
||||
WorkflowElementWalker walker = new(visitor);
|
||||
walker.Visit(rootElement);
|
||||
|
||||
return walker.GetWorkflow<TInput>();
|
||||
return visitor.Complete<TInput>();
|
||||
}
|
||||
|
||||
private static ChatMessage DefaultTransform(object message) =>
|
||||
@@ -79,23 +79,4 @@ public static class DeclarativeWorkflowBuilder
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that broadcasts the conversation identifier.
|
||||
/// </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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has been invoked.
|
||||
/// </summary>
|
||||
public class DeclarativeActionInvokeEvent(string actionId, DialogAction action, string? priorActionId) : WorkflowEvent(action)
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action id.
|
||||
/// </summary>
|
||||
public string ActionId => actionId;
|
||||
|
||||
/// <summary>
|
||||
/// The declarative action type name.
|
||||
/// </summary>
|
||||
public string ActionType => action.GetType().Name;
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the parent action.
|
||||
/// </summary>
|
||||
public string? ParentActionId => action.GetParentId();
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the previous action.
|
||||
/// </summary>
|
||||
public string? PriorActionId => priorActionId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has completed.
|
||||
/// </summary>
|
||||
public class DeclarativeActionCompleteEvent(string actionId, DialogAction action) : WorkflowEvent(action)
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action identifier.
|
||||
/// </summary>
|
||||
public string ActionId => actionId;
|
||||
|
||||
/// <summary>
|
||||
/// The declarative action type name.
|
||||
/// </summary>
|
||||
public string ActionType => action.GetType().Name;
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the parent action.
|
||||
/// </summary>
|
||||
public string? ParentActionId => action.GetParentId();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Entities;
|
||||
|
||||
internal sealed record class EntityExtractionResult
|
||||
{
|
||||
public EntityExtractionResult(FormulaValue? value)
|
||||
{
|
||||
this.Value = value;
|
||||
this.ErrorMessage = null;
|
||||
}
|
||||
|
||||
public EntityExtractionResult(string errorMessage)
|
||||
{
|
||||
this.Value = null;
|
||||
this.ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public FormulaValue? Value { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
public bool IsValid => this.Value is not null;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Net.Mail;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Entities;
|
||||
|
||||
internal static partial class EntityExtractor
|
||||
{
|
||||
private const string NumberUnitRegExExpression = @"(?<value>[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?|\d*\.\d+)";
|
||||
|
||||
#if NET
|
||||
[GeneratedRegex(NumberUnitRegExExpression, RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NumberUnitRegex();
|
||||
#else
|
||||
private static Regex NumberUnitRegex() => s_numberUnitRegex;
|
||||
private static readonly Regex s_numberUnitRegex = new(NumberUnitRegExExpression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
#endif
|
||||
|
||||
public static EntityExtractionResult Parse(EntityReference? entity, string value) =>
|
||||
entity switch
|
||||
{
|
||||
null => UndefinedEntity(value),
|
||||
AgePrebuiltEntity => TryParseNumberUnit(value, "age"),
|
||||
BooleanPrebuiltEntity => TryParseBoolean(value),
|
||||
CityPrebuiltEntity => TryParseString(value),
|
||||
ColorPrebuiltEntity => TryParseString(value),
|
||||
ContinentPrebuiltEntity => TryParseString(value),
|
||||
CountryOrRegionPrebuiltEntity => TryParseString(value),
|
||||
DatePrebuiltEntity => TryParseDate(value),
|
||||
DateTimeNoTimeZonePrebuiltEntity => TryParseDateTimeNoTimeZone(value),
|
||||
DateTimePrebuiltEntity => TryParseDateTime(value),
|
||||
DurationPrebuiltEntity => TryParseDuration(value),
|
||||
EmailPrebuiltEntity => TryParseEmail(value),
|
||||
EventPrebuiltEntity => TryParseString(value),
|
||||
LanguagePrebuiltEntity => TryParseString(value),
|
||||
MoneyPrebuiltEntity => TryParseNumberUnit(value, "money"),
|
||||
NumberPrebuiltEntity => TryParseNumber(value),
|
||||
PercentagePrebuiltEntity => TryParseNumberUnit(value, "percentage"),
|
||||
PhoneNumberPrebuiltEntity => TryParseString(value),
|
||||
PointOfInterestPrebuiltEntity => TryParseString(value),
|
||||
SpeedPrebuiltEntity => TryParseNumberUnit(value, "speed"),
|
||||
StatePrebuiltEntity => TryParseString(value),
|
||||
StreetAddressPrebuiltEntity => TryParseString(value),
|
||||
StringPrebuiltEntity => TryParseString(value),
|
||||
TemperaturePrebuiltEntity => TryParseNumberUnit(value, "temperature"),
|
||||
URLPrebuiltEntity => TryParseURL(value),
|
||||
WeightPrebuiltEntity => TryParseNumberUnit(value, "weight"),
|
||||
_ => UnsupportedEntity(entity),
|
||||
};
|
||||
|
||||
private static EntityExtractionResult TryParseBoolean(string value)
|
||||
{
|
||||
if (bool.TryParse(value, out bool parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid boolean value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseDate(string value)
|
||||
{
|
||||
if (DateTime.TryParse(value, out DateTime parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue.Date));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid date value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseDateTimeNoTimeZone(string value)
|
||||
{
|
||||
if (DateTime.TryParse(value, out DateTime parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(
|
||||
FormulaValue.New(
|
||||
DateTime.SpecifyKind(parsedValue, DateTimeKind.Unspecified)));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid date value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseDateTime(string value)
|
||||
{
|
||||
if (DateTime.TryParse(value, out DateTime parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid date-time value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseDuration(string value)
|
||||
{
|
||||
if (TimeSpan.TryParse(value, out TimeSpan parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid duration value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseEmail(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
MailAddress parsedValue = new(value);
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue.Address));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new EntityExtractionResult($"Invalid email value: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseNumberUnit(string value, string type)
|
||||
{
|
||||
Match m = NumberUnitRegex().Match(value);
|
||||
if (m.Success)
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(m.Groups[0].Value));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid {type} value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseNumber(string value)
|
||||
{
|
||||
if (double.TryParse(value, out double parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid double value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseString(string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(value));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult("Empty value");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseURL(string value)
|
||||
{
|
||||
if (Uri.TryCreate(value, UriKind.Absolute, out Uri? uriResult))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(uriResult.AbsoluteUri));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid double value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult UndefinedEntity(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.NewBlank());
|
||||
}
|
||||
|
||||
return new EntityExtractionResult(FormulaValue.New(value));
|
||||
}
|
||||
|
||||
private static EntityExtractionResult UnsupportedEntity(EntityReference entity) =>
|
||||
new($"Unsupported entity: {entity.GetType().Name}");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that broadcasts the conversation identifier.
|
||||
/// </summary>
|
||||
public sealed class ConversationUpdateEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The conversation ID associated with the workflow.
|
||||
/// </summary>
|
||||
public string ConversationId { get; }
|
||||
|
||||
internal ConversationUpdateEvent(string conversationId)
|
||||
: base(conversationId)
|
||||
{
|
||||
this.ConversationId = conversationId;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has been invoked.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeActionInvokeEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action id.
|
||||
/// </summary>
|
||||
public string ActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The declarative action type name.
|
||||
/// </summary>
|
||||
public string ActionType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the parent action.
|
||||
/// </summary>
|
||||
public string? ParentActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the previous action.
|
||||
/// </summary>
|
||||
public string? PriorActionId { get; }
|
||||
|
||||
internal DeclarativeActionInvokeEvent(DialogAction action, string? priorActionId) : base(action)
|
||||
{
|
||||
this.ActionId = action.GetId();
|
||||
this.ActionType = action.GetType().Name;
|
||||
this.ParentActionId = action.GetParentId();
|
||||
this.PriorActionId = priorActionId;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has completed.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeActionCompleteEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action identifier.
|
||||
/// </summary>
|
||||
public string ActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The declarative action type name.
|
||||
/// </summary>
|
||||
public string ActionType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the parent action.
|
||||
/// </summary>
|
||||
public string? ParentActionId { get; }
|
||||
|
||||
internal DeclarativeActionCompleteEvent(DialogAction action) : base(action)
|
||||
{
|
||||
this.ActionId = action.GetId();
|
||||
this.ActionType = action.GetType().Name;
|
||||
this.ParentActionId = action.GetParentId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for user input.
|
||||
/// </summary>
|
||||
public sealed class InputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The prompt message to display to the user.
|
||||
/// </summary>
|
||||
public string Prompt { get; }
|
||||
|
||||
internal InputRequest(string prompt)
|
||||
{
|
||||
this.Prompt = prompt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user input response.
|
||||
/// </summary>
|
||||
public sealed class InputResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The response value.
|
||||
/// </summary>
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
public InputResponse(string value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that broadcasts the conversation identifier.
|
||||
/// </summary>
|
||||
public sealed class MessageActivityEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The conversation ID associated with the workflow.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
internal MessageActivityEvent(string message) : base(message)
|
||||
{
|
||||
this.Message = message;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
|
||||
/// <summary>
|
||||
/// Represents an exception that occurs when the declarative model is not supported.
|
||||
/// </summary>
|
||||
public class DeclarativeModelException : DeclarativeWorkflowException
|
||||
public sealed class DeclarativeModelException : DeclarativeWorkflowException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeModelException"/> class.
|
||||
|
||||
+227
-5
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
@@ -9,15 +12,234 @@ 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());
|
||||
|
||||
public static TableValue ToTable(this IEnumerable<ChatMessage> messages) =>
|
||||
FormulaValue.NewTable(s_messageRecordType, messages.Select(message => message.ToRecord()));
|
||||
|
||||
public static IEnumerable<ChatMessage> ToChatMessages(this DataValue messages)
|
||||
{
|
||||
if (messages is TableDataValue table)
|
||||
{
|
||||
return table.ToChatMessages();
|
||||
}
|
||||
|
||||
if (messages is RecordDataValue record)
|
||||
{
|
||||
return [record.ToChatMessage()];
|
||||
}
|
||||
|
||||
if (messages is StringDataValue text)
|
||||
{
|
||||
return [text.ToChatMessage()];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public static IEnumerable<ChatMessage> ToChatMessages(this TableDataValue messages)
|
||||
{
|
||||
foreach (DataValue message in messages.Values)
|
||||
{
|
||||
if (message is RecordDataValue record)
|
||||
{
|
||||
if (record.Properties.Count == 1 && record.Properties.TryGetValue("Value", out DataValue? singleColumn))
|
||||
{
|
||||
record = singleColumn as RecordDataValue ?? record;
|
||||
}
|
||||
ChatMessage? convertedMessage = record.ToChatMessage();
|
||||
if (convertedMessage is not null)
|
||||
{
|
||||
yield return convertedMessage;
|
||||
}
|
||||
}
|
||||
else if (message is StringDataValue text)
|
||||
{
|
||||
yield return ToChatMessage(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ChatMessage? ToChatMessage(this DataValue message)
|
||||
{
|
||||
if (message is RecordDataValue record)
|
||||
{
|
||||
return record.ToChatMessage();
|
||||
}
|
||||
|
||||
if (message is StringDataValue text)
|
||||
{
|
||||
return text.ToChatMessage();
|
||||
}
|
||||
|
||||
if (message is BlankDataValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new DeclarativeActionException($"Unable to convert {message.GetDataType()} to {nameof(ChatMessage)}.");
|
||||
}
|
||||
|
||||
public static ChatMessage ToChatMessage(this RecordDataValue message) =>
|
||||
new(message.GetRole(), [.. message.GetContent()])
|
||||
{
|
||||
AdditionalProperties = message.GetProperty<RecordDataValue>("metadata").ToMetadata()
|
||||
};
|
||||
|
||||
public static ChatMessage ToChatMessage(this StringDataValue message) => new(ChatRole.User, message.Value);
|
||||
|
||||
public static AdditionalPropertiesDictionary? ToMetadata(this RecordDataValue? metadata)
|
||||
{
|
||||
if (metadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
AdditionalPropertiesDictionary properties = [];
|
||||
|
||||
foreach (KeyValuePair<string, DataValue> property in metadata.Properties)
|
||||
{
|
||||
properties[property.Key] = property.Value.ToObject();
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
public static ChatRole ToChatRole(this AgentMessageRole role) =>
|
||||
role switch
|
||||
{
|
||||
AgentMessageRole.Agent => ChatRole.Assistant,
|
||||
AgentMessageRole.User => ChatRole.User,
|
||||
_ => ChatRole.User
|
||||
};
|
||||
|
||||
public static ChatRole ToChatRole(this AgentMessageRole? role) => role?.ToChatRole() ?? ChatRole.User;
|
||||
|
||||
public static AIContent? ToContent(this AgentMessageContentType contentType, string? contentValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return
|
||||
contentType switch
|
||||
{
|
||||
AgentMessageContentType.ImageUrl => new UriContent(contentValue, "image/*"),
|
||||
AgentMessageContentType.ImageFile => new HostedFileContent(contentValue),
|
||||
_ => new TextContent(contentValue)
|
||||
};
|
||||
}
|
||||
|
||||
private static ChatRole GetRole(this RecordDataValue message)
|
||||
{
|
||||
StringDataValue? roleValue = message.GetProperty<StringDataValue>(TypeSchema.Message.Fields.Role);
|
||||
if (roleValue is null || string.IsNullOrWhiteSpace(roleValue.Value))
|
||||
{
|
||||
return ChatRole.User;
|
||||
}
|
||||
|
||||
AgentMessageRole? role = null;
|
||||
if (Enum.TryParse(roleValue.Value, out AgentMessageRole parsedRole))
|
||||
{
|
||||
role = parsedRole;
|
||||
}
|
||||
|
||||
return role.ToChatRole();
|
||||
}
|
||||
|
||||
private static IEnumerable<AIContent> GetContent(this RecordDataValue message)
|
||||
{
|
||||
TableDataValue? content = message.GetProperty<TableDataValue>(TypeSchema.Message.Fields.Content);
|
||||
if (content is not null)
|
||||
{
|
||||
foreach (RecordDataValue contentItem in content.Values)
|
||||
{
|
||||
StringDataValue? contentValue = contentItem?.GetProperty<StringDataValue>(TypeSchema.Message.Fields.ContentValue);
|
||||
if (contentValue is null || string.IsNullOrWhiteSpace(contentValue.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
yield return
|
||||
contentItem?.GetProperty<StringDataValue>(TypeSchema.Message.Fields.ContentType)?.Value switch
|
||||
{
|
||||
TypeSchema.Message.ContentTypes.ImageUrl => new UriContent(contentValue.Value, "image/*"),
|
||||
TypeSchema.Message.ContentTypes.ImageFile => new HostedFileContent(contentValue.Value),
|
||||
_ => new TextContent(contentValue.Value)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TValue? GetProperty<TValue>(this RecordDataValue record, string name)
|
||||
where TValue : DataValue
|
||||
{
|
||||
if (record.Properties.TryGetValue(name, out DataValue? value) && value is TValue dataValue)
|
||||
{
|
||||
return dataValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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());
|
||||
yield return new NamedValue(TypeSchema.Message.Fields.Id, message.MessageId.ToFormula());
|
||||
yield return new NamedValue(TypeSchema.Message.Fields.Role, message.Role.Value.ToFormula());
|
||||
yield return new NamedValue(TypeSchema.Message.Fields.Author, message.AuthorName.ToFormula());
|
||||
yield return new NamedValue(TypeSchema.Message.Fields.Content, FormulaValue.NewTable(s_contentRecordType, message.GetContentRecords()));
|
||||
yield return new NamedValue(TypeSchema.Message.Fields.Text, message.Text.ToFormula());
|
||||
yield return new NamedValue(TypeSchema.Message.Fields.Metadata, message.AdditionalProperties.ToRecord());
|
||||
}
|
||||
|
||||
private static IEnumerable<RecordValue> GetContentRecords(this ChatMessage message) =>
|
||||
message.Contents.Select(content => FormulaValue.NewRecordFromFields(content.GetContentFields()));
|
||||
|
||||
private static IEnumerable<NamedValue> GetContentFields(this AIContent content)
|
||||
{
|
||||
return
|
||||
content switch
|
||||
{
|
||||
UriContent uriContent => CreateContentRecord(TypeSchema.Message.ContentTypes.ImageUrl, uriContent.Uri.ToString()),
|
||||
HostedFileContent fileContent => CreateContentRecord(TypeSchema.Message.ContentTypes.ImageFile, fileContent.FileId),
|
||||
TextContent textContent => CreateContentRecord(TypeSchema.Message.ContentTypes.Text, textContent.Text),
|
||||
_ => []
|
||||
};
|
||||
|
||||
static IEnumerable<NamedValue> CreateContentRecord(string type, string value)
|
||||
{
|
||||
yield return new NamedValue(TypeSchema.Message.Fields.ContentType, type.ToFormula());
|
||||
yield return new NamedValue(TypeSchema.Message.Fields.ContentValue, value.ToFormula());
|
||||
}
|
||||
}
|
||||
private static RecordValue ToRecord(this AdditionalPropertiesDictionary? value)
|
||||
{
|
||||
return FormulaValue.NewRecordFromFields(GetFields());
|
||||
|
||||
IEnumerable<NamedValue> GetFields()
|
||||
{
|
||||
if (value is not null)
|
||||
{
|
||||
foreach (string key in value.Keys)
|
||||
{
|
||||
yield return new NamedValue(key, value[key].ToFormula());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly RecordType s_contentRecordType =
|
||||
RecordType.Empty()
|
||||
.Add(TypeSchema.Message.Fields.ContentType, FormulaType.String)
|
||||
.Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String);
|
||||
|
||||
private static readonly RecordType s_messageRecordType =
|
||||
RecordType.Empty()
|
||||
.Add(TypeSchema.Message.Fields.Id, FormulaType.String)
|
||||
.Add(TypeSchema.Message.Fields.Role, FormulaType.String)
|
||||
.Add(TypeSchema.Message.Fields.Author, FormulaType.String)
|
||||
.Add(TypeSchema.Message.Fields.Content, s_contentRecordType.ToTable())
|
||||
.Add(TypeSchema.Message.Fields.Text, FormulaType.String)
|
||||
.Add(TypeSchema.Message.Fields.Metadata, RecordType.Empty());
|
||||
}
|
||||
|
||||
+45
-3
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
@@ -9,11 +10,11 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class DataValueExtensions
|
||||
{
|
||||
public static FormulaValue ToFormulaValue(this DataValue? value) =>
|
||||
public static FormulaValue ToFormula(this DataValue? value) =>
|
||||
value switch
|
||||
{
|
||||
null => FormulaValue.NewBlank(),
|
||||
BlankDataValue blankValue => BlankValue.NewBlank(),
|
||||
BlankDataValue => BlankValue.NewBlank(),
|
||||
BooleanDataValue boolValue => FormulaValue.New(boolValue.Value),
|
||||
NumberDataValue numberValue => FormulaValue.New(numberValue.Value),
|
||||
FloatDataValue floatValue => FormulaValue.New(floatValue.Value),
|
||||
@@ -53,12 +54,30 @@ internal static class DataValueExtensions
|
||||
_ => FormulaType.Unknown,
|
||||
};
|
||||
|
||||
public static object? ToObject(this DataValue? value) =>
|
||||
value switch
|
||||
{
|
||||
null => null,
|
||||
BlankDataValue => null,
|
||||
BooleanDataValue boolValue => boolValue.Value,
|
||||
NumberDataValue numberValue => numberValue.Value,
|
||||
FloatDataValue floatValue => floatValue.Value,
|
||||
StringDataValue stringValue => stringValue.Value,
|
||||
DateTimeDataValue dateTimeValue => dateTimeValue.Value.DateTime,
|
||||
DateDataValue dateValue => dateValue.Value,
|
||||
TimeDataValue timeValue => timeValue.Value,
|
||||
TableDataValue tableValue => tableValue.Values.Select(value => value.ToObject()).ToArray(),
|
||||
RecordDataValue recordValue => recordValue.ToDictionary(),
|
||||
OptionDataValue optionValue => optionValue.Value.Value,
|
||||
_ => throw new DeclarativeModelException($"Unsupported {nameof(DataValue)} type: {value.GetType().Name}"),
|
||||
};
|
||||
|
||||
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())));
|
||||
property => new NamedValue(property.Key, property.Value.ToFormula())));
|
||||
|
||||
public static RecordType ToRecordType(this RecordDataType record)
|
||||
{
|
||||
@@ -70,6 +89,19 @@ internal static class DataValueExtensions
|
||||
return recordType;
|
||||
}
|
||||
|
||||
public static ExpandoObject ToObject(this RecordDataValue recordDataValue)
|
||||
{
|
||||
ExpandoObject expandoObject = new();
|
||||
|
||||
IDictionary<string, object?> dictionary = expandoObject;
|
||||
foreach (KeyValuePair<string, DataValue> field in recordDataValue.Properties)
|
||||
{
|
||||
dictionary[field.Key] = field.Value?.ToObject();
|
||||
}
|
||||
|
||||
return expandoObject;
|
||||
}
|
||||
|
||||
private static RecordType ParseRecordType(this RecordDataValue record)
|
||||
{
|
||||
RecordType recordType = RecordType.Empty();
|
||||
@@ -79,4 +111,14 @@ internal static class DataValueExtensions
|
||||
}
|
||||
return recordType;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> ToDictionary(this RecordDataValue record)
|
||||
{
|
||||
Dictionary<string, object?> result = [];
|
||||
foreach (KeyValuePair<string, DataValue> property in record.Properties)
|
||||
{
|
||||
result[property.Key] = property.Value.ToObject();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class DialogBaseExtensions
|
||||
{
|
||||
public static TDialog WrapWithBot<TDialog>(this TDialog dialog) where TDialog : DialogBase
|
||||
{
|
||||
BotDefinition bot
|
||||
= new BotDefinition.Builder
|
||||
{
|
||||
Components =
|
||||
{
|
||||
new DialogComponent.Builder
|
||||
{
|
||||
SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema",
|
||||
Dialog = dialog.ToBuilder(),
|
||||
}
|
||||
}
|
||||
}.Build();
|
||||
|
||||
return bot.Descendants().OfType<TDialog>().First();
|
||||
}
|
||||
}
|
||||
+28
-33
@@ -8,6 +8,7 @@ using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using BlankType = Microsoft.PowerFx.Types.BlankType;
|
||||
@@ -20,10 +21,11 @@ internal static class FormulaValueExtensions
|
||||
|
||||
public static FormulaValue NewBlank(this FormulaType? type) => FormulaValue.NewBlank(type ?? FormulaType.Blank);
|
||||
|
||||
public static FormulaValue ToFormulaValue(this object? value) =>
|
||||
public static FormulaValue ToFormula(this object? value) =>
|
||||
value switch
|
||||
{
|
||||
null => FormulaValue.NewBlank(),
|
||||
UnassignedValue => FormulaValue.NewBlank(),
|
||||
FormulaValue formulaValue => formulaValue,
|
||||
bool booleanValue => FormulaValue.New(booleanValue),
|
||||
int decimalValue => FormulaValue.New(decimalValue),
|
||||
@@ -35,8 +37,9 @@ internal static class FormulaValueExtensions
|
||||
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(),
|
||||
object when value is IDictionary dictionaryValue => dictionaryValue.ToRecord(),
|
||||
object when value is IEnumerable tableValue => tableValue.ToTable(),
|
||||
_ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"),
|
||||
};
|
||||
|
||||
@@ -141,7 +144,20 @@ internal static class FormulaValueExtensions
|
||||
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());
|
||||
RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()));
|
||||
|
||||
private static RecordValue ToRecord(this IDictionary value)
|
||||
{
|
||||
return FormulaValue.NewRecordFromFields(GetFields());
|
||||
|
||||
IEnumerable<NamedValue> GetFields()
|
||||
{
|
||||
foreach (string key in value.Keys)
|
||||
{
|
||||
yield return new NamedValue(key, value[key].ToFormula());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static RecordDataType ToDataType(this RecordType record)
|
||||
{
|
||||
@@ -176,48 +192,27 @@ internal static class FormulaValueExtensions
|
||||
private static RecordValue ToRecord(this ExpandoObject value) =>
|
||||
FormulaValue.NewRecordFromFields(
|
||||
value.Select(
|
||||
property => new NamedValue(property.Key, property.Value.ToFormulaValue())));
|
||||
property => new NamedValue(property.Key, property.Value.ToFormula())));
|
||||
|
||||
private static TableType ToTableType(this IEnumerable value)
|
||||
{
|
||||
Type valueType = value.GetType();
|
||||
Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault();
|
||||
|
||||
if (elementType is not null)
|
||||
foreach (object? element in value)
|
||||
{
|
||||
if (elementType != typeof(ExpandoObject))
|
||||
if (element is not ExpandoObject expandoElement)
|
||||
{
|
||||
throw new DeclarativeModelException($"Invalid table element: {elementType.Name}");
|
||||
throw new DeclarativeModelException($"Invalid table element: {element.GetType().Name}");
|
||||
}
|
||||
|
||||
foreach (ExpandoObject element in value)
|
||||
{
|
||||
return element.ToRecordType().ToTable();
|
||||
}
|
||||
return expandoElement.ToRecordType().ToTable(); // Return first element
|
||||
}
|
||||
|
||||
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 TableValue ToTable(this IEnumerable value) =>
|
||||
FormulaValue.NewTable(
|
||||
value.ToTableType().ToRecord(),
|
||||
[.. value.OfType<ExpandoObject>().Select(element => element.ToRecord())]);
|
||||
|
||||
private static KeyValuePair<string, DataValue> GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue());
|
||||
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
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.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class IWorkflowContextExtensions
|
||||
{
|
||||
public static ValueTask RaiseInvocationEventAsync(this IWorkflowContext context, DialogAction action, string? priorEventId = null) =>
|
||||
context.AddEventAsync(new DeclarativeActionInvokeEvent(action, priorEventId));
|
||||
|
||||
public static ValueTask RaiseCompletionEventAsync(this IWorkflowContext context, DialogAction action) =>
|
||||
context.AddEventAsync(new DeclarativeActionCompleteEvent(action));
|
||||
|
||||
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) =>
|
||||
context.SendMessageAsync(new ExecutorResultMessage(id, result));
|
||||
|
||||
public static ValueTask QueueStateUpdateAsync<TValue>(this IWorkflowContext context, PropertyPath variablePath, TValue? value) =>
|
||||
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.VariableScopeName));
|
||||
|
||||
public static async Task<WorkflowFormulaState> GetStateAsync(this IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context is DeclarativeWorkflowContext declarativeContext)
|
||||
{
|
||||
return declarativeContext.State;
|
||||
}
|
||||
|
||||
WorkflowFormulaState state = new(RecalcEngineFactory.Create());
|
||||
|
||||
await state.RestoreAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,6 @@ internal static class StringExtensions
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
public static FormulaValue ToFormulaValue(this string? value) =>
|
||||
public static FormulaValue ToFormula(this string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? FormulaValue.NewBlank() : FormulaValue.New(value);
|
||||
}
|
||||
|
||||
@@ -10,21 +10,21 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class TemplateExtensions
|
||||
{
|
||||
public static string? Format(this RecalcEngine engine, IEnumerable<TemplateLine> template)
|
||||
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)
|
||||
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)
|
||||
public static string Format(this RecalcEngine engine, TemplateSegment segment)
|
||||
{
|
||||
if (segment is TextSegment textSegment)
|
||||
{
|
||||
return textSegment.Value;
|
||||
return textSegment.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
if (segment is ExpressionSegment expressionSegment)
|
||||
|
||||
+40
-32
@@ -1,12 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Frozen;
|
||||
using System.Diagnostics;
|
||||
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.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -15,29 +15,24 @@ 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)
|
||||
internal abstract class DeclarativeActionExecutor<TAction>(TAction model, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor(model, state)
|
||||
where TAction : DialogAction
|
||||
{
|
||||
public new TAction Model => (TAction)base.Model;
|
||||
}
|
||||
|
||||
internal abstract class WorkflowActionExecutor : Executor<DeclarativeExecutorResult>
|
||||
internal abstract class DeclarativeActionExecutor : Executor<ExecutorResultMessage>
|
||||
{
|
||||
public const string RootActionId = "(root)";
|
||||
|
||||
private static readonly ImmutableHashSet<string> s_mutableScopes =
|
||||
new HashSet<string>
|
||||
{
|
||||
VariableScopeNames.Topic,
|
||||
VariableScopeNames.Global,
|
||||
}.ToImmutableHashSet();
|
||||
private static readonly FrozenSet<string> s_mutableScopes =
|
||||
[
|
||||
VariableScopeNames.Topic,
|
||||
VariableScopeNames.Global
|
||||
];
|
||||
|
||||
private string? _parentId;
|
||||
|
||||
protected WorkflowActionExecutor(DialogAction model, DeclarativeWorkflowState state)
|
||||
protected DeclarativeActionExecutor(DialogAction model, WorkflowFormulaState state)
|
||||
: base(model.Id.Value)
|
||||
{
|
||||
if (!model.HasRequiredProperties)
|
||||
@@ -51,16 +46,18 @@ internal abstract class WorkflowActionExecutor : Executor<DeclarativeExecutorRes
|
||||
|
||||
public DialogAction Model { get; }
|
||||
|
||||
public string ParentId => this._parentId ??= this.Model.GetParentId() ?? RootActionId;
|
||||
public string ParentId => this._parentId ??= this.Model.GetParentId() ?? WorkflowActionVisitor.Steps.Root();
|
||||
|
||||
internal ILogger Logger { get; set; } = NullLogger<WorkflowActionExecutor>.Instance;
|
||||
internal ILogger Logger { get; set; } = NullLogger<DeclarativeActionExecutor>.Instance;
|
||||
|
||||
protected DeclarativeWorkflowState State { get; }
|
||||
protected WorkflowFormulaState State { get; }
|
||||
|
||||
protected virtual bool IsDiscreteAction => true;
|
||||
|
||||
protected virtual bool EmitResultEvent => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context)
|
||||
public override async ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context)
|
||||
{
|
||||
if (this.Model.Disabled)
|
||||
{
|
||||
@@ -68,15 +65,18 @@ internal abstract class WorkflowActionExecutor : Executor<DeclarativeExecutorRes
|
||||
return;
|
||||
}
|
||||
|
||||
await this.RaiseInvocationEventAsync(context, message.ExecutorId).ConfigureAwait(false);
|
||||
await context.RaiseInvocationEventAsync(this.Model, message.ExecutorId).ConfigureAwait(false);
|
||||
|
||||
await this.State.RestoreAsync(context, default).ConfigureAwait(false);
|
||||
Debug.WriteLine($"RESULT #{this.Id} - {message.Result ?? "(null)"}");
|
||||
|
||||
try
|
||||
{
|
||||
object? result = await this.ExecuteAsync(context, cancellationToken: default).ConfigureAwait(false);
|
||||
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this.State), cancellationToken: default).ConfigureAwait(false);
|
||||
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id, result)).ConfigureAwait(false);
|
||||
if (this.EmitResultEvent)
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id, result).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (DeclarativeActionException exception)
|
||||
{
|
||||
@@ -92,21 +92,33 @@ internal abstract class WorkflowActionExecutor : Executor<DeclarativeExecutorRes
|
||||
{
|
||||
if (this.IsDiscreteAction)
|
||||
{
|
||||
await this.RaiseCompletionEventAsync(context).ConfigureAwait(false);
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default);
|
||||
|
||||
protected async ValueTask AssignAsync(PropertyPath targetPath, FormulaValue result, IWorkflowContext context)
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
|
||||
this.State.RestoreAsync(context, cancellation);
|
||||
|
||||
protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue result, IWorkflowContext context)
|
||||
{
|
||||
if (targetPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!s_mutableScopes.Contains(Throw.IfNull(targetPath.VariableScopeName)))
|
||||
{
|
||||
throw new DeclarativeModelException($"Invalid scope: {targetPath.VariableScopeName}");
|
||||
}
|
||||
|
||||
await this.State.SetAsync(targetPath, result, context).ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(targetPath, result).ConfigureAwait(false);
|
||||
|
||||
#if DEBUG
|
||||
string? resultValue = result.Format();
|
||||
@@ -114,7 +126,7 @@ internal abstract class WorkflowActionExecutor : Executor<DeclarativeExecutorRes
|
||||
Debug.WriteLine(
|
||||
$"""
|
||||
STATE: {this.GetType().Name} [{this.Id}]
|
||||
NAME: {targetPath.Format()}
|
||||
NAME: {targetPath}
|
||||
VALUE:{valuePosition}{result.Format()} ({result.GetType().Name})
|
||||
""");
|
||||
#endif
|
||||
@@ -125,8 +137,4 @@ internal abstract class WorkflowActionExecutor : Executor<DeclarativeExecutorRes
|
||||
string message = $"Unexpected workflow failure during {this.Model.GetType().Name} [{this.Id}]: {text}";
|
||||
return exception is null ? new(message) : new(message, exception);
|
||||
}
|
||||
|
||||
protected ValueTask RaiseInvocationEventAsync(IWorkflowContext context, string? priorEventId = null) => context.AddEventAsync(new DeclarativeActionInvokeEvent(this.Id, this.Model, priorEventId));
|
||||
|
||||
protected ValueTask RaiseCompletionEventAsync(IWorkflowContext context) => context.AddEventAsync(new DeclarativeActionCompleteEvent(this.Id, this.Model));
|
||||
}
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
public DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state)
|
||||
{
|
||||
this.Source = source;
|
||||
this.State = state;
|
||||
}
|
||||
|
||||
private IWorkflowContext Source { get; }
|
||||
public WorkflowFormulaState State { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => this.Source.AddEventAsync(workflowEvent);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask QueueClearScopeAsync(string? scopeName = null)
|
||||
{
|
||||
this.State.ResetAll(scopeName);
|
||||
return this.Source.QueueClearScopeAsync(scopeName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null)
|
||||
{
|
||||
ValueTask task = value switch
|
||||
{
|
||||
null => QueueEmptyStateAsync(),
|
||||
FormulaValue formulaValue => QueueFormulaStateAsync(formulaValue),
|
||||
DataValue dataValue => QueueDataValueStateAsync(dataValue),
|
||||
_ => QueueNativeStateAsync(value),
|
||||
};
|
||||
|
||||
await task.ConfigureAwait(false);
|
||||
|
||||
ValueTask QueueEmptyStateAsync()
|
||||
{
|
||||
this.State.Set(key, FormulaValue.NewBlank(), scopeName);
|
||||
return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName);
|
||||
}
|
||||
|
||||
ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
|
||||
{
|
||||
this.State.Set(key, formulaValue, scopeName);
|
||||
return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
|
||||
}
|
||||
|
||||
ValueTask QueueDataValueStateAsync(DataValue dataValue)
|
||||
{
|
||||
FormulaValue formulaValue = dataValue.ToFormula();
|
||||
this.State.Set(key, formulaValue, scopeName);
|
||||
return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
|
||||
}
|
||||
|
||||
ValueTask QueueNativeStateAsync(object? rawValue)
|
||||
{
|
||||
FormulaValue formulaValue = rawValue.ToFormula();
|
||||
this.State.Set(key, formulaValue, scopeName);
|
||||
return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null) => this.Source.ReadStateAsync<T>(key, scopeName);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null) => this.Source.ReadStateKeysAsync(scopeName);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId);
|
||||
}
|
||||
+7
-4
@@ -12,16 +12,19 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
/// </summary>
|
||||
internal sealed class DeclarativeWorkflowExecutor<TInput>(
|
||||
string workflowId,
|
||||
DeclarativeWorkflowState state,
|
||||
WorkflowFormulaState state,
|
||||
Func<TInput, ChatMessage> inputTransform) :
|
||||
Executor<TInput>(workflowId)
|
||||
where TInput : notnull
|
||||
{
|
||||
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context)
|
||||
{
|
||||
ChatMessage input = inputTransform.Invoke(message);
|
||||
await state.SetLastMessageAsync(context, input).ConfigureAwait(false);
|
||||
// No state to restore if we're starting from the beginning.
|
||||
state.SetInitialized();
|
||||
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
ChatMessage input = inputTransform.Invoke(message);
|
||||
state.SetLastMessage(input);
|
||||
|
||||
await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
+43
-9
@@ -2,8 +2,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
@@ -43,7 +43,19 @@ internal sealed class DeclarativeWorkflowModel
|
||||
throw new DeclarativeModelException($"Unresolved parent for {executor.Id}: {parentId}.");
|
||||
}
|
||||
|
||||
ModelNode stepNode = this.DefineNode(executor, parentNode, executor.GetType(), completionHandler);
|
||||
ModelNode stepNode = this.DefineNode(executor, parentNode, completionHandler);
|
||||
|
||||
parentNode.Children.Add(stepNode);
|
||||
}
|
||||
|
||||
public void AddPort(InputPort port, string parentId)
|
||||
{
|
||||
if (!this.Nodes.TryGetValue(parentId, out ModelNode? parentNode))
|
||||
{
|
||||
throw new DeclarativeModelException($"Unresolved parent for {port.Id}: {parentId}.");
|
||||
}
|
||||
|
||||
ModelNode stepNode = this.DefineNode(port, parentNode);
|
||||
|
||||
parentNode.Children.Add(stepNode);
|
||||
}
|
||||
@@ -77,7 +89,8 @@ internal sealed class DeclarativeWorkflowModel
|
||||
|
||||
public void ConnectNodes(WorkflowBuilder workflowBuilder)
|
||||
{
|
||||
foreach (ModelNode node in this.Nodes.Values.ToImmutableArray())
|
||||
// Push `Values` into array to avoid modification during iteration.
|
||||
foreach (ModelNode node in this.Nodes.Values.ToArray())
|
||||
{
|
||||
if (node.CompletionHandler is not null)
|
||||
{
|
||||
@@ -96,13 +109,32 @@ internal sealed class DeclarativeWorkflowModel
|
||||
|
||||
Debug.WriteLine($"> CONNECT: {link.Source.Id} => {link.TargetId}{(link.Condition is null ? string.Empty : " (?)")}");
|
||||
|
||||
workflowBuilder.AddEdge(link.Source.Executor, targetNode.Executor, link.Condition);
|
||||
workflowBuilder.AddEdge(GetExecutorIsh(link.Source), GetExecutorIsh(targetNode), link.Condition);
|
||||
}
|
||||
|
||||
ExecutorIsh GetExecutorIsh(ModelNode node)
|
||||
{
|
||||
if (node.Port is not null)
|
||||
{
|
||||
return node.Port;
|
||||
}
|
||||
|
||||
return node.Executor;
|
||||
}
|
||||
}
|
||||
|
||||
private ModelNode DefineNode(Executor executor, ModelNode? parentNode = null, Type? executorType = null, Action? completionHandler = null)
|
||||
private ModelNode DefineNode(Executor executor, ModelNode? parentNode = null, Action? completionHandler = null)
|
||||
{
|
||||
ModelNode stepNode = new(executor, parentNode, executorType, completionHandler);
|
||||
ModelNode stepNode = new(executor, port: null, parentNode, completionHandler);
|
||||
|
||||
this.Nodes.Add(stepNode.Id, stepNode);
|
||||
|
||||
return stepNode;
|
||||
}
|
||||
|
||||
private ModelNode DefineNode(InputPort port, ModelNode? parentNode = null)
|
||||
{
|
||||
ModelNode stepNode = new(executor: null!, port, parentNode);
|
||||
|
||||
this.Nodes.Add(stepNode.Id, stepNode);
|
||||
|
||||
@@ -134,13 +166,15 @@ internal sealed class DeclarativeWorkflowModel
|
||||
return null;
|
||||
}
|
||||
|
||||
private sealed class ModelNode(Executor executor, ModelNode? parent = null, Type? executorType = null, Action? completionHandler = null)
|
||||
private sealed class ModelNode(Executor executor, InputPort? port, ModelNode? parent = null, Action? completionHandler = null)
|
||||
{
|
||||
public string Id => executor.Id;
|
||||
public string Id => port?.Id ?? executor.Id;
|
||||
|
||||
public Executor Executor => executor;
|
||||
|
||||
public Type? ExecutorType => executorType;
|
||||
public InputPort? Port => port;
|
||||
|
||||
public Type? ExecutorType => this.Port?.GetType() ?? this.Executor.GetType();
|
||||
|
||||
public ModelNode? Parent { get; } = parent;
|
||||
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-7
@@ -1,29 +1,50 @@
|
||||
// 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.PowerFx;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal delegate ValueTask DelegateAction(IWorkflowContext context, CancellationToken cancellationToken);
|
||||
internal delegate ValueTask DelegateAction<TMessage>(IWorkflowContext context, TMessage message, CancellationToken cancellationToken) where TMessage : notnull;
|
||||
|
||||
internal sealed class DelegateActionExecutor : Executor<DeclarativeExecutorResult>
|
||||
internal sealed class DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction<ExecutorResultMessage>? action = null, bool emitResult = true)
|
||||
: DelegateActionExecutor<ExecutorResultMessage>(actionId, state, action, emitResult)
|
||||
{
|
||||
private readonly DelegateAction? _action;
|
||||
public override ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context)
|
||||
{
|
||||
Debug.WriteLine($"RESULT #{this.Id} - {message.Result ?? "(null)"}");
|
||||
|
||||
public DelegateActionExecutor(string actionId, DelegateAction? action = null)
|
||||
return base.HandleAsync(message, context);
|
||||
}
|
||||
}
|
||||
|
||||
internal class DelegateActionExecutor<TMessage> : Executor<TMessage> where TMessage : notnull
|
||||
{
|
||||
private readonly WorkflowFormulaState _state;
|
||||
private readonly DelegateAction<TMessage>? _action;
|
||||
private readonly bool _emitResult;
|
||||
|
||||
public DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction<TMessage>? action = null, bool emitResult = true)
|
||||
: base(actionId)
|
||||
{
|
||||
this._state = state;
|
||||
this._action = action;
|
||||
this._emitResult = emitResult;
|
||||
}
|
||||
|
||||
public override async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context)
|
||||
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context)
|
||||
{
|
||||
if (this._action is not null)
|
||||
{
|
||||
await this._action.Invoke(context, default).ConfigureAwait(false);
|
||||
await this._action.Invoke(new DeclarativeWorkflowContext(context, this._state), message, default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
if (this._emitResult)
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed class DurableProperty<TValue>(string name) where TValue : struct
|
||||
{
|
||||
public async ValueTask<TValue> ReadAsync(IWorkflowContext context)
|
||||
{
|
||||
TValue? storedValue = await context.ReadStateAsync<TValue>(name).ConfigureAwait(false);
|
||||
return storedValue ?? default;
|
||||
}
|
||||
|
||||
public ValueTask WriteAsync(IWorkflowContext context, TValue value) =>
|
||||
context.QueueStateUpdateAsync(name, value);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed record class ExecutorResultMessage(string ExecutorId, object? Result = null)
|
||||
{
|
||||
public static ExecutorResultMessage ThrowIfNot(object? message)
|
||||
{
|
||||
if (message is not ExecutorResultMessage executorMessage)
|
||||
{
|
||||
throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ExecutorResultMessage)})");
|
||||
}
|
||||
|
||||
return executorMessage;
|
||||
}
|
||||
}
|
||||
+119
-48
@@ -3,22 +3,33 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
{
|
||||
private const string DefaultWorkflowId = "workflow";
|
||||
|
||||
internal static class Steps
|
||||
{
|
||||
public static string Root(string? actionId = null) => $"{actionId ?? DefaultWorkflowId}_{nameof(Root)}";
|
||||
|
||||
public static string Post(string actionId) => $"{actionId}_{nameof(Post)}";
|
||||
}
|
||||
|
||||
private readonly WorkflowBuilder _workflowBuilder;
|
||||
private readonly DeclarativeWorkflowModel _workflowModel;
|
||||
private readonly DeclarativeWorkflowOptions _workflowOptions;
|
||||
private readonly DeclarativeWorkflowState _workflowState;
|
||||
private readonly WorkflowFormulaState _workflowState;
|
||||
|
||||
public WorkflowActionVisitor(
|
||||
Executor rootAction,
|
||||
DeclarativeWorkflowState state,
|
||||
WorkflowFormulaState state,
|
||||
DeclarativeWorkflowOptions options)
|
||||
{
|
||||
this._workflowBuilder = new WorkflowBuilder(rootAction);
|
||||
@@ -47,10 +58,10 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
// Handle case where root element is its own parent
|
||||
if (item.Id.Equals(parentId))
|
||||
{
|
||||
parentId = RootId(parentId);
|
||||
parentId = Steps.Root(parentId);
|
||||
}
|
||||
|
||||
this.ContinueWith(this.CreateStep(item.Id.Value), parentId, condition: null, CompletionHandler);
|
||||
this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId, condition: null, CompletionHandler);
|
||||
|
||||
// Complete the action scope.
|
||||
void CompletionHandler()
|
||||
@@ -59,7 +70,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
{
|
||||
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
|
||||
this._workflowModel.AddLink(completionId, Steps.Post(parentId)); // Merge with parent scope
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +84,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
{
|
||||
string stepId = ConditionGroupExecutor.Steps.Item(conditionGroup.Model, item);
|
||||
string parentId = GetParentId(item);
|
||||
this._workflowModel.AddNode(this.CreateStep(stepId), parentId, CompletionHandler);
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor(stepId, this._workflowState), parentId, CompletionHandler);
|
||||
|
||||
base.VisitConditionItem(item);
|
||||
|
||||
@@ -81,7 +92,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
void CompletionHandler()
|
||||
{
|
||||
string completionId = this.ContinuationFor(stepId, conditionGroup.DoneAsync); // End items
|
||||
this._workflowModel.AddLink(completionId, PostId(conditionGroup.Id)); // Merge with parent scope
|
||||
this._workflowModel.AddLink(completionId, Steps.Post(conditionGroup.Id)); // Merge with parent scope
|
||||
|
||||
// Merge link when no action group is defined
|
||||
if (!item.Actions.Any())
|
||||
@@ -117,6 +128,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
// 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));
|
||||
@@ -127,10 +139,10 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
{
|
||||
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);
|
||||
GotoExecutor action = new(item, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
this._workflowModel.AddLink(action.Id, item.ActionId.Value);
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(Foreach item)
|
||||
@@ -140,17 +152,18 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
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
|
||||
this.ContinueWith(new DelegateActionExecutor(loopId, this._workflowState, 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);
|
||||
|
||||
string startId = ForeachExecutor.Steps.Start(action.Id);
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor(startId, this._workflowState), action.Id);
|
||||
this._workflowModel.AddLink(loopId, startId, (_) => action.HasValue);
|
||||
|
||||
void CompletionHandler()
|
||||
{
|
||||
string endActionsId = ForeachExecutor.Steps.End(action.Id); // Loop continuation
|
||||
this.ContinueWith(this.CreateStep(endActionsId, action.ResetAsync), action.Id);
|
||||
this.ContinueWith(new DelegateActionExecutor(endActionsId, this._workflowState, action.ResetAsync), action.Id);
|
||||
this._workflowModel.AddLink(endActionsId, loopId);
|
||||
}
|
||||
}
|
||||
@@ -163,8 +176,8 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
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.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId);
|
||||
this._workflowModel.AddLink(item.Id.Value, Steps.Post(loopExecutor.Id));
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
}
|
||||
@@ -177,7 +190,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
if (loopExecutor is not null)
|
||||
{
|
||||
string parentId = GetParentId(item);
|
||||
this.ContinueWith(this.CreateStep(item.Id.Value), parentId);
|
||||
this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId);
|
||||
this._workflowModel.AddLink(item.Id.Value, ForeachExecutor.Steps.Next(loopExecutor.Id));
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
@@ -188,15 +201,77 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this.Trace(item);
|
||||
|
||||
string parentId = GetParentId(item);
|
||||
this.ContinueWith(this.CreateStep(item.Id.Value), parentId);
|
||||
this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId);
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
|
||||
protected override void Visit(AnswerQuestionWithAI item)
|
||||
protected override void Visit(Question item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new AnswerQuestionWithAIExecutor(item, this._workflowOptions.AgentProvider, this._workflowState));
|
||||
string parentId = GetParentId(item);
|
||||
string actionId = item.GetId();
|
||||
string postId = Steps.Post(actionId);
|
||||
|
||||
QuestionExecutor questionExecutor = new(item, this._workflowState);
|
||||
this.ContinueWith(questionExecutor);
|
||||
this._workflowModel.AddLink(actionId, postId, message => questionExecutor.IsComplete(message));
|
||||
|
||||
string prepareId = QuestionExecutor.Steps.Prepare(actionId);
|
||||
this.ContinueWith(new DelegateActionExecutor(prepareId, this._workflowState, questionExecutor.PrepareResponseAsync, emitResult: false), parentId, message => !questionExecutor.IsComplete(message));
|
||||
|
||||
string inputId = QuestionExecutor.Steps.Input(actionId);
|
||||
InputPort inputPort = InputPort.Create<InputRequest, InputResponse>(inputId);
|
||||
this._workflowModel.AddPort(inputPort, parentId);
|
||||
this._workflowModel.AddLinkFromPeer(parentId, inputId);
|
||||
|
||||
string captureId = QuestionExecutor.Steps.Capture(actionId);
|
||||
this.ContinueWith(new DelegateActionExecutor<InputResponse>(captureId, this._workflowState, questionExecutor.CaptureResponseAsync, emitResult: false), parentId);
|
||||
|
||||
this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, questionExecutor.CompleteAsync), parentId, message => questionExecutor.IsComplete(message));
|
||||
this._workflowModel.AddLink(captureId, prepareId, message => !questionExecutor.IsComplete(message));
|
||||
}
|
||||
|
||||
protected override void Visit(CreateConversation item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new CreateConversationExecutor(item, this._workflowOptions.AgentProvider, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(AddConversationMessage item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new AddConversationMessageExecutor(item, this._workflowOptions.AgentProvider, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(CopyConversationMessages item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new CopyConversationMessagesExecutor(item, this._workflowOptions.AgentProvider, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeAzureAgent item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new InvokeAzureAgentExecutor(item, this._workflowOptions.AgentProvider, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(RetrieveConversationMessage item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new RetrieveConversationMessageExecutor(item, this._workflowOptions.AgentProvider, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(RetrieveConversationMessages item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new RetrieveConversationMessagesExecutor(item, this._workflowOptions.AgentProvider, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(SetVariable item)
|
||||
@@ -206,6 +281,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this.ContinueWith(new SetVariableExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(SetMultipleVariables item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
this.ContinueWith(new SetMultipleVariablesExecutor(item, this._workflowState));
|
||||
}
|
||||
|
||||
protected override void Visit(SetTextVariable item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -257,6 +339,11 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
|
||||
#region Not supported
|
||||
|
||||
protected override void Visit(AnswerQuestionWithAI item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(DeleteActivity item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
@@ -317,11 +404,6 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(Question item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(CSATQuestion item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
@@ -435,7 +517,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
#endregion
|
||||
|
||||
private void ContinueWith(
|
||||
WorkflowActionExecutor executor,
|
||||
DeclarativeActionExecutor executor,
|
||||
Func<object?, bool>? condition = null,
|
||||
Action? completionHandler = null)
|
||||
{
|
||||
@@ -453,32 +535,21 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this._workflowModel.AddLinkFromPeer(parentId, executor.Id, condition);
|
||||
}
|
||||
|
||||
public static string RootId(string? actionId) => $"root_{actionId ?? "workflow"}";
|
||||
private string ContinuationFor(string parentId, DelegateAction<ExecutorResultMessage>? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction);
|
||||
|
||||
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, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction);
|
||||
|
||||
private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null)
|
||||
private string ContinuationFor(string actionId, string parentId, DelegateAction<ExecutorResultMessage>? stepAction = null)
|
||||
{
|
||||
actionId = PostId(actionId);
|
||||
this._workflowModel.AddNode(this.CreateStep(actionId, stepAction), parentId);
|
||||
actionId = Steps.Post(actionId);
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor(actionId, this._workflowState, stepAction), parentId);
|
||||
return actionId;
|
||||
}
|
||||
|
||||
private void RestartAfter(string actionId, string parentId) =>
|
||||
this._workflowModel.AddNode(this.CreateStep($"{actionId}_Continue"), parentId);
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor($"{actionId}_Continue", this._workflowState), parentId);
|
||||
|
||||
private DelegateActionExecutor CreateStep(string actionId, DelegateAction? stepAction = null)
|
||||
{
|
||||
DelegateActionExecutor stepExecutor = new(actionId, stepAction);
|
||||
|
||||
return stepExecutor;
|
||||
}
|
||||
private static string GetParentId(BotElement item) =>
|
||||
item.GetParentId() ??
|
||||
throw new DeclarativeModelException($"Missing parent ID for action element: {item.GetId()} [{item.GetType().Name}].");
|
||||
|
||||
private void NotSupported(DialogAction item)
|
||||
{
|
||||
@@ -496,7 +567,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
string? parentId = item.GetParentId();
|
||||
if (item.Id.Equals(parentId ?? string.Empty))
|
||||
{
|
||||
parentId = RootId(parentId);
|
||||
parentId = Steps.Root(parentId);
|
||||
}
|
||||
Debug.WriteLine($"> VISIT: {new string('\t', this._workflowModel.GetDepth(parentId))}{FormatItem(item)} => {FormatParent(item)}");
|
||||
}
|
||||
|
||||
+2
-5
@@ -6,16 +6,13 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed class WorkflowElementWalker : BotElementWalker
|
||||
{
|
||||
private readonly WorkflowActionVisitor _visitor;
|
||||
private readonly DialogActionVisitor _visitor;
|
||||
|
||||
public WorkflowElementWalker(BotElement rootElement, WorkflowActionVisitor visitor)
|
||||
public WorkflowElementWalker(DialogActionVisitor 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)
|
||||
|
||||
+1
@@ -27,6 +27,7 @@
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
|
||||
<PackageReference Include="Microsoft.PowerFx.Interpreter" />
|
||||
<PackageReference Include="System.Collections.Immutable" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
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.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class AddConversationMessageExecutor(AddConversationMessage model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<AddConversationMessage>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
StringExpression conversationExpression = Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}");
|
||||
string conversationId = this.State.Evaluator.GetValue(conversationExpression).Value;
|
||||
|
||||
ChatMessage newMessage = new(this.GetRole(), [.. this.GetContent()]) { AdditionalProperties = this.GetMetadata() };
|
||||
|
||||
await agentProvider.CreateMessageAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this.AssignAsync(this.Model.Message?.Path, newMessage.ToRecord(), context).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private IEnumerable<AIContent> GetContent()
|
||||
{
|
||||
foreach (AddConversationMessageContent content in this.Model.Content)
|
||||
{
|
||||
AIContent? messageContent = content.Type.Value.ToContent(this.State.Engine.Format(content.Value));
|
||||
if (messageContent is not null)
|
||||
{
|
||||
yield return messageContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ChatRole GetRole()
|
||||
{
|
||||
if (this.Model.Role is null)
|
||||
{
|
||||
return ChatRole.User;
|
||||
}
|
||||
|
||||
AgentMessageRoleWrapper roleWrapper = this.State.Evaluator.GetValue(this.Model.Role).Value;
|
||||
|
||||
return roleWrapper.Value.ToChatRole();
|
||||
}
|
||||
|
||||
private AdditionalPropertiesDictionary? GetMetadata()
|
||||
{
|
||||
if (this.Model.Metadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RecordDataValue? metadataValue = this.State.Evaluator.GetValue(this.Model.Metadata).Value;
|
||||
|
||||
return metadataValue.ToMetadata();
|
||||
}
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -10,19 +10,19 @@ using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, DeclarativeWorkflowState state)
|
||||
internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, WorkflowFormulaState 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);
|
||||
EvaluationResult<VariablesToClearWrapper> variablesResult = this.State.Evaluator.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
|
||||
private sealed class ScopeHandler(string executorId, WorkflowFormulaState state) : IEnumVariablesToClearHandler
|
||||
{
|
||||
public void HandleAllGlobalVariables()
|
||||
{
|
||||
@@ -36,7 +36,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Declara
|
||||
|
||||
public void HandleConversationScopedVariables()
|
||||
{
|
||||
this.ClearAll(WorkflowScopes.DefaultScopeName);
|
||||
this.ClearAll(WorkflowFormulaState.DefaultScopeName);
|
||||
}
|
||||
|
||||
public void HandleUnknownValue()
|
||||
@@ -51,7 +51,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Declara
|
||||
|
||||
private void ClearAll(string scope)
|
||||
{
|
||||
state.Reset(scope);
|
||||
state.ResetAll(scope);
|
||||
Debug.WriteLine(
|
||||
$"""
|
||||
STATE: {this.GetType().Name} [{executorId}]
|
||||
|
||||
+12
-18
@@ -3,7 +3,9 @@
|
||||
using System;
|
||||
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;
|
||||
|
||||
@@ -26,31 +28,23 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
|
||||
public static string Else(ConditionGroup model) => model.ElseActions.Id.Value ?? $"{model.Id}_Else";
|
||||
}
|
||||
|
||||
public ConditionGroupExecutor(ConditionGroup model, DeclarativeWorkflowState state)
|
||||
public ConditionGroupExecutor(ConditionGroup model, WorkflowFormulaState state)
|
||||
: base(model, state)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool IsDiscreteAction => false;
|
||||
|
||||
public bool IsMatch(ConditionItem conditionItem, object? result)
|
||||
public bool IsMatch(ConditionItem conditionItem, object? message)
|
||||
{
|
||||
if (result is not DeclarativeExecutorResult message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(Steps.Item(this.Model, conditionItem), message.Result as string, StringComparison.Ordinal);
|
||||
ExecutorResultMessage executorMessage = ExecutorResultMessage.ThrowIfNot(message);
|
||||
return string.Equals(Steps.Item(this.Model, conditionItem), executorMessage.Result as string, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public bool IsElse(object? result)
|
||||
public bool IsElse(object? message)
|
||||
{
|
||||
if (result is not DeclarativeExecutorResult message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(Steps.Else(this.Model), message.Result as string, StringComparison.Ordinal);
|
||||
ExecutorResultMessage executorMessage = ExecutorResultMessage.ThrowIfNot(message);
|
||||
return string.Equals(Steps.Else(this.Model), executorMessage.Result as string, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
@@ -65,7 +59,7 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
|
||||
continue; // Skip if no condition is defined
|
||||
}
|
||||
|
||||
EvaluationResult<bool> expressionResult = this.State.ExpressionEngine.GetValue(conditionItem.Condition);
|
||||
EvaluationResult<bool> expressionResult = this.State.Evaluator.GetValue(conditionItem.Condition);
|
||||
if (expressionResult.Value)
|
||||
{
|
||||
return Steps.Item(this.Model, conditionItem);
|
||||
@@ -75,8 +69,8 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
|
||||
return Steps.Else(this.Model);
|
||||
}
|
||||
|
||||
public async ValueTask DoneAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
public async ValueTask DoneAsync(IWorkflowContext context, ExecutorResultMessage _, CancellationToken cancellationToken)
|
||||
{
|
||||
await this.RaiseCompletionEventAsync(context).ConfigureAwait(false);
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// 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.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<CopyConversationMessages>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value;
|
||||
DataValue? inputMessages = this.GetInputMessages();
|
||||
|
||||
if (inputMessages is not null)
|
||||
{
|
||||
foreach (ChatMessage message in inputMessages.ToChatMessages())
|
||||
{
|
||||
await agentProvider.CreateMessageAsync(conversationId, message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private DataValue? GetInputMessages()
|
||||
{
|
||||
DataValue? messages = null;
|
||||
|
||||
if (this.Model.Messages is not null)
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(this.Model.Messages);
|
||||
messages = expressionResult.Value;
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
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.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class CreateConversationExecutor(CreateConversation model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<CreateConversation>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.ConversationId?.Path, FormulaValue.New(conversationId), context).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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.PowerFx.Types;
|
||||
@@ -13,7 +14,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class EditTableExecutor(EditTable model, DeclarativeWorkflowState state) : DeclarativeActionExecutor<EditTable>(model, state)
|
||||
internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState state) : DeclarativeActionExecutor<EditTable>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -22,7 +23,7 @@ internal sealed class EditTableExecutor(EditTable model, DeclarativeWorkflowStat
|
||||
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}'.");
|
||||
throw this.Exception($"Require '{variablePath}' to be a table, not: '{table.GetType().Name}'.");
|
||||
}
|
||||
|
||||
TableChangeType changeType = this.Model.ChangeType.Value;
|
||||
@@ -30,14 +31,14 @@ internal sealed class EditTableExecutor(EditTable model, DeclarativeWorkflowStat
|
||||
{
|
||||
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());
|
||||
EvaluationResult<DataValue> addResult = this.State.Evaluator.GetValue(addItemValue);
|
||||
RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), addResult.Value.ToFormula());
|
||||
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);
|
||||
EvaluationResult<DataValue> removeResult = this.State.Evaluator.GetValue(removeItemValue);
|
||||
if (removeResult.Value is TableDataValue removeItemTable)
|
||||
{
|
||||
await tableValue.RemoveAsync(removeItemTable?.Values.Select(row => row.ToRecordValue()), all: true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
+7
-6
@@ -6,6 +6,7 @@ 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.PowerFx.Types;
|
||||
@@ -13,7 +14,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflowState state) : DeclarativeActionExecutor<EditTableV2>(model, state)
|
||||
internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaState state) : DeclarativeActionExecutor<EditTableV2>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -22,15 +23,15 @@ internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflow
|
||||
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}'.");
|
||||
throw this.Exception($"Require '{variablePath}' 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());
|
||||
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(addItemValue);
|
||||
RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormula());
|
||||
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false);
|
||||
}
|
||||
@@ -42,8 +43,8 @@ internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflow
|
||||
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)
|
||||
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(removeItemValue);
|
||||
if (expressionResult.Value.ToFormula() 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);
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.PowerFx.Types;
|
||||
@@ -24,7 +25,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
private int _index;
|
||||
private FormulaValue[] _values;
|
||||
|
||||
public ForeachExecutor(Foreach model, DeclarativeWorkflowState state)
|
||||
public ForeachExecutor(Foreach model, WorkflowFormulaState state)
|
||||
: base(model, state)
|
||||
{
|
||||
this._values = [];
|
||||
@@ -45,40 +46,40 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
}
|
||||
else
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.GetValue(this.Model.Items);
|
||||
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(this.Model.Items);
|
||||
if (expressionResult.Value is TableDataValue tableValue)
|
||||
{
|
||||
this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormulaValue())];
|
||||
this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())];
|
||||
}
|
||||
else
|
||||
{
|
||||
this._values = [expressionResult.Value.ToFormulaValue()];
|
||||
this._values = [expressionResult.Value.ToFormula()];
|
||||
}
|
||||
}
|
||||
|
||||
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
await this.ResetAsync(context, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async ValueTask TakeNextAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, 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);
|
||||
await context.QueueStateUpdateAsync(Throw.IfNull(this.Model.Value), value).ConfigureAwait(false);
|
||||
|
||||
if (this.Model.Index is not null)
|
||||
{
|
||||
await this.State.SetAsync(this.Model.Index.Path, FormulaValue.New(this._index), context).ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(this.Model.Index.Path, FormulaValue.New(this._index)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._index++;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
public async ValueTask ResetAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -90,7 +91,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
}
|
||||
finally
|
||||
{
|
||||
await this.RaiseCompletionEventAsync(context).ConfigureAwait(false);
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class GotoExecutor(GotoAction model, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<GotoAction>(model, state)
|
||||
{
|
||||
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// No action needed - the edge will be followed automatically
|
||||
return default;
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// 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.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<InvokeAzureAgent>(model, state)
|
||||
{
|
||||
private AzureAgentUsage AgentUsage => Throw.IfNull(this.Model.Agent, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}");
|
||||
private AzureAgentInput? AgentInput => this.Model.Input;
|
||||
private AzureAgentOutput? AgentOutput => this.Model.Output;
|
||||
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string? conversationId = this.GetConversationId();
|
||||
string agentName = this.GetAgentName();
|
||||
string? additionalInstructions = this.GetAdditionalInstructions();
|
||||
bool autoSend = this.GetAutoSendValue();
|
||||
DataValue? inputMessages = this.GetInputMessages();
|
||||
|
||||
AgentRunResponse agentResponse = InvokeAgentAsync().ToEnumerable().ToAgentRunResponse();
|
||||
|
||||
if (autoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
ChatMessage response = agentResponse.Messages.Last();
|
||||
await this.AssignAsync(this.AgentOutput?.Messages?.Path, response.ToRecord(), context).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
|
||||
async IAsyncEnumerable<AgentRunResponseUpdate> InvokeAgentAsync()
|
||||
{
|
||||
AIAgent agent = await agentProvider.GetAgentAsync(agentName, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ChatClientAgentRunOptions options =
|
||||
new(
|
||||
new ChatOptions()
|
||||
{
|
||||
Instructions = additionalInstructions,
|
||||
});
|
||||
|
||||
AgentThread agentThread = new() { ConversationId = conversationId };
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates =
|
||||
inputMessages is not null ?
|
||||
agent.RunStreamingAsync([.. inputMessages.ToChatMessages()], agentThread, options, cancellationToken) :
|
||||
agent.RunStreamingAsync(agentThread, options, cancellationToken);
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false))
|
||||
{
|
||||
await AssignConversationId(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false);
|
||||
|
||||
if (autoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
async ValueTask AssignConversationId(string? assignValue)
|
||||
{
|
||||
if (assignValue is not null && conversationId is null)
|
||||
{
|
||||
conversationId = assignValue;
|
||||
this.State.SetConversationId(conversationId);
|
||||
await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DataValue? GetInputMessages()
|
||||
{
|
||||
DataValue? userInput = null;
|
||||
if (this.AgentInput?.Messages is not null)
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(this.AgentInput.Messages);
|
||||
userInput = expressionResult.Value;
|
||||
}
|
||||
|
||||
return userInput;
|
||||
}
|
||||
|
||||
private string? GetConversationId()
|
||||
{
|
||||
if (this.Model.ConversationId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
EvaluationResult<string> conversationIdResult = this.State.Evaluator.GetValue(this.Model.ConversationId);
|
||||
return conversationIdResult.Value.Length == 0 ? null : conversationIdResult.Value;
|
||||
}
|
||||
|
||||
private string GetAgentName() =>
|
||||
this.State.Evaluator.GetValue(
|
||||
Throw.IfNull(
|
||||
this.AgentUsage.Name,
|
||||
$"{nameof(this.Model)}.{nameof(this.Model.Agent)}.{nameof(this.Model.Agent.Name)}")).Value;
|
||||
|
||||
private string? GetAdditionalInstructions()
|
||||
{
|
||||
string? additionalInstructions = null;
|
||||
|
||||
if (this.AgentInput?.AdditionalInstructions is not null)
|
||||
{
|
||||
additionalInstructions = this.State.Engine.Format(this.AgentInput.AdditionalInstructions);
|
||||
}
|
||||
|
||||
return additionalInstructions;
|
||||
}
|
||||
|
||||
private bool GetAutoSendValue()
|
||||
{
|
||||
if (this.AgentOutput?.AutoSend is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
EvaluationResult<bool> autoSendResult = this.State.Evaluator.GetValue(this.AgentOutput.AutoSend);
|
||||
|
||||
return autoSendResult.Value;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ 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.PowerFx.Types;
|
||||
@@ -14,7 +15,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class ParseValueExecutor(ParseValue model, DeclarativeWorkflowState state) :
|
||||
internal sealed class ParseValueExecutor(ParseValue model, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<ParseValue>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
@@ -22,13 +23,13 @@ internal sealed class ParseValueExecutor(ParseValue model, DeclarativeWorkflowSt
|
||||
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);
|
||||
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(valueExpression);
|
||||
|
||||
FormulaValue? parsedResult = null;
|
||||
|
||||
if (expressionResult.Value is RecordDataValue recordValue)
|
||||
{
|
||||
parsedResult = recordValue.ToFormulaValue();
|
||||
parsedResult = recordValue.ToFormula();
|
||||
}
|
||||
else if (expressionResult.Value is StringDataValue stringValue)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Entities;
|
||||
using Microsoft.Agents.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class QuestionExecutor(Question model, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<Question>(model, state)
|
||||
{
|
||||
public static class Steps
|
||||
{
|
||||
public static string Prepare(string id) => $"{id}_{nameof(Prepare)}";
|
||||
public static string Input(string id) => $"{id}_{nameof(Input)}";
|
||||
public static string Capture(string id) => $"{id}_{nameof(Capture)}";
|
||||
}
|
||||
|
||||
private readonly DurableProperty<int> _promptCount = new(nameof(_promptCount));
|
||||
private readonly DurableProperty<bool> _hasExecuted = new(nameof(_hasExecuted));
|
||||
|
||||
protected override bool IsDiscreteAction => false;
|
||||
protected override bool EmitResultEvent => false;
|
||||
|
||||
public bool IsComplete(object? message)
|
||||
{
|
||||
ExecutorResultMessage executorMessage = ExecutorResultMessage.ThrowIfNot(message);
|
||||
return executorMessage.Result is null;
|
||||
}
|
||||
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await this._promptCount.WriteAsync(context, 0).ConfigureAwait(false);
|
||||
|
||||
InitializablePropertyPath variable = Throw.IfNull(this.Model.Variable);
|
||||
bool hasValue = this.State.Get(variable.Path) is BlankValue;
|
||||
bool alwaysPrompt = this.State.Evaluator.GetValue(this.Model.AlwaysPrompt).Value;
|
||||
|
||||
bool proceed = !alwaysPrompt || hasValue;
|
||||
if (proceed)
|
||||
{
|
||||
SkipQuestionMode mode = this.State.Evaluator.GetValue(this.Model.SkipQuestionMode).Value;
|
||||
proceed =
|
||||
mode switch
|
||||
{
|
||||
SkipQuestionMode.SkipOnFirstExecutionIfVariableHasValue => !await this._hasExecuted.ReadAsync(context).ConfigureAwait(false),
|
||||
SkipQuestionMode.AlwaysSkipIfVariableHasValue => hasValue,
|
||||
SkipQuestionMode.AlwaysAsk => true,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
if (proceed)
|
||||
{
|
||||
await this.PromptAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async ValueTask PrepareResponseAsync(IWorkflowContext context, ExecutorResultMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false);
|
||||
InputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt));
|
||||
await context.SendMessageAsync(inputRequest).ConfigureAwait(false);
|
||||
await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask CaptureResponseAsync(IWorkflowContext context, InputResponse message, CancellationToken cancellationToken)
|
||||
{
|
||||
FormulaValue? extractedValue = null;
|
||||
if (string.IsNullOrWhiteSpace(message.Value))
|
||||
{
|
||||
string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt);
|
||||
await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim())).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, message.Value);
|
||||
if (entityResult.IsValid)
|
||||
{
|
||||
extractedValue = entityResult.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
string invalidResponse = this.FormatPrompt(this.Model.InvalidPrompt);
|
||||
await context.AddEventAsync(new MessageActivityEvent(invalidResponse.Trim())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (extractedValue is null)
|
||||
{
|
||||
await this.PromptAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.AssignAsync(this.Model.Variable?.Path, extractedValue, context).ConfigureAwait(false);
|
||||
await this._hasExecuted.WriteAsync(context, true).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask CompleteAsync(IWorkflowContext context, ExecutorResultMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask PromptAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
long repeatCount = this.State.Evaluator.GetValue(this.Model.RepeatCount).Value;
|
||||
int actualCount = await this._promptCount.ReadAsync(context).ConfigureAwait(false);
|
||||
if (actualCount >= repeatCount)
|
||||
{
|
||||
ValueExpression defaultValueExpression = Throw.IfNull(this.Model.DefaultValue);
|
||||
DataValue defaultValue = this.State.Evaluator.GetValue(defaultValueExpression).Value;
|
||||
await this.AssignAsync(this.Model.Variable?.Path, defaultValue.ToFormula(), context).ConfigureAwait(false);
|
||||
string defaultValueResponse = this.FormatPrompt(this.Model.DefaultValueResponse);
|
||||
await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim())).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id, result: true, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatPrompt(ActivityTemplateBase? promptTemplate)
|
||||
{
|
||||
if (promptTemplate is not MessageActivityTemplate messageActivity)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return this.State.Engine.Format(messageActivity.Text).Trim();
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -3,14 +3,14 @@
|
||||
using System.Diagnostics;
|
||||
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.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class ResetVariableExecutor(ResetVariable model, DeclarativeWorkflowState state) :
|
||||
internal sealed class ResetVariableExecutor(ResetVariable model, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<ResetVariable>(model, state)
|
||||
{
|
||||
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
@@ -21,7 +21,7 @@ internal sealed class ResetVariableExecutor(ResetVariable model, DeclarativeWork
|
||||
Debug.WriteLine(
|
||||
$"""
|
||||
STATE: {this.GetType().Name} [{this.Id}]
|
||||
NAME: {this.Model.Variable!.Format()}
|
||||
NAME: {this.Model.Variable}
|
||||
""");
|
||||
|
||||
return default;
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// 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.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMessage model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<RetrieveConversationMessage>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value;
|
||||
string messageId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.MessageId, $"{nameof(this.Model)}.{nameof(this.Model.MessageId)}")).Value;
|
||||
|
||||
ChatMessage message = await agentProvider.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this.AssignAsync(this.Model.Message?.Path, message.ToRecord(), context).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
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.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationMessages model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<RetrieveConversationMessages>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value;
|
||||
|
||||
ChatMessage[] messages = await agentProvider.GetMessagesAsync(
|
||||
conversationId,
|
||||
limit: this.GetLimit(),
|
||||
after: this.GetMessage(this.Model.MessageAfter),
|
||||
before: this.GetMessage(this.Model.MessageBefore),
|
||||
newestFirst: this.IsDescending(),
|
||||
cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this.AssignAsync(this.Model.Messages?.Path, messages.ToTable(), context).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private int? GetLimit()
|
||||
{
|
||||
if (this.Model.Limit is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
long limit = this.State.Evaluator.GetValue(this.Model.Limit).Value;
|
||||
return Convert.ToInt32(Math.Min(limit, 100));
|
||||
}
|
||||
|
||||
private string? GetMessage(StringExpression? messagExpression)
|
||||
{
|
||||
if (messagExpression is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.State.Evaluator.GetValue(messagExpression).Value;
|
||||
}
|
||||
|
||||
private bool IsDescending()
|
||||
{
|
||||
if (this.Model.SortOrder is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AgentMessageSortOrderWrapper sortOrderWrapper = this.State.Evaluator.GetValue(this.Model.SortOrder).Value;
|
||||
|
||||
return sortOrderWrapper.Value == AgentMessageSortOrder.NewestFirst;
|
||||
}
|
||||
}
|
||||
+5
-14
@@ -1,33 +1,24 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
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.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class SendActivityExecutor(SendActivity model, DeclarativeWorkflowState state) :
|
||||
internal sealed class SendActivityExecutor(SendActivity model, WorkflowFormulaState 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.Engine.Format(messageActivity.Text).Trim();
|
||||
|
||||
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);
|
||||
await context.AddEventAsync(new MessageActivityEvent(activityText.Trim())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// 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.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class SetMultipleVariablesExecutor(SetMultipleVariables model, WorkflowFormulaState state)
|
||||
: DeclarativeActionExecutor<SetMultipleVariables>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (VariableAssignment assignment in this.Model.Assignments)
|
||||
{
|
||||
if (assignment.Variable is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assignment.Value is null)
|
||||
{
|
||||
await this.AssignAsync(assignment.Variable, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(assignment.Value);
|
||||
|
||||
await this.AssignAsync(assignment.Variable, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -2,14 +2,16 @@
|
||||
|
||||
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.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class SetTextVariableExecutor(SetTextVariable model, DeclarativeWorkflowState state)
|
||||
internal sealed class SetTextVariableExecutor(SetTextVariable model, WorkflowFormulaState state)
|
||||
: DeclarativeActionExecutor<SetTextVariable>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
@@ -22,7 +24,7 @@ internal sealed class SetTextVariableExecutor(SetTextVariable model, Declarative
|
||||
}
|
||||
else
|
||||
{
|
||||
FormulaValue expressionResult = FormulaValue.New(this.State.Format(this.Model.Value));
|
||||
FormulaValue expressionResult = FormulaValue.New(this.State.Engine.Format(this.Model.Value));
|
||||
|
||||
await this.AssignAsync(variablePath, expressionResult, context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
+4
-3
@@ -4,6 +4,7 @@ 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.PowerFx.Types;
|
||||
@@ -11,7 +12,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class SetVariableExecutor(SetVariable model, DeclarativeWorkflowState state)
|
||||
internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaState state)
|
||||
: DeclarativeActionExecutor<SetVariable>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
@@ -24,9 +25,9 @@ internal sealed class SetVariableExecutor(SetVariable model, DeclarativeWorkflow
|
||||
}
|
||||
else
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.GetValue(this.Model.Value);
|
||||
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(this.Model.Value);
|
||||
|
||||
await this.AssignAsync(variablePath, expressionResult.Value.ToFormulaValue(), context).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal sealed class UserMessage : ReflectionFunction
|
||||
{
|
||||
public const string FunctionName = nameof(UserMessage);
|
||||
|
||||
public UserMessage()
|
||||
: base(FunctionName, FormulaType.String, FormulaType.String)
|
||||
{ }
|
||||
|
||||
public static FormulaValue Execute(StringValue input) =>
|
||||
string.IsNullOrEmpty(input.Value) ?
|
||||
FormulaValue.NewBlank(RecordType.Empty()) :
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(ChatRole.User.Value)),
|
||||
new NamedValue(
|
||||
TypeSchema.Message.Fields.Content,
|
||||
FormulaValue.NewTable(
|
||||
RecordType.Empty()
|
||||
.Add(TypeSchema.Message.Fields.ContentType, FormulaType.String)
|
||||
.Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String),
|
||||
[
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.Message.Fields.ContentType, FormulaValue.New(TypeSchema.Message.ContentTypes.Text)),
|
||||
new NamedValue(TypeSchema.Message.Fields.ContentValue, input))
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
@@ -36,6 +37,7 @@ internal static class RecalcEngineFactory
|
||||
}
|
||||
|
||||
config.EnableSetFunction();
|
||||
config.AddFunction(new UserMessage());
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
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;
|
||||
@@ -16,6 +14,8 @@ namespace Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
|
||||
internal static class SystemScope
|
||||
{
|
||||
private static readonly RecordValue s_emptyMessage = new ChatMessage(ChatRole.User, string.Empty).ToRecord();
|
||||
|
||||
public static class Names
|
||||
{
|
||||
public const string Activity = nameof(Activity);
|
||||
@@ -31,7 +31,7 @@ internal static class SystemScope
|
||||
public const string UserLanguage = nameof(UserLanguage);
|
||||
}
|
||||
|
||||
public static ImmutableHashSet<string> AllNames { get; } = GetNames().ToImmutableHashSet();
|
||||
public static FrozenSet<string> AllNames { get; } = GetNames().ToFrozenSet();
|
||||
|
||||
public static IEnumerable<string> GetNames()
|
||||
{
|
||||
@@ -48,12 +48,12 @@ internal static class SystemScope
|
||||
yield return Names.UserLanguage;
|
||||
}
|
||||
|
||||
public static void InitializeSystem(this WorkflowScopes scopes)
|
||||
public static void InitializeSystem(this WorkflowFormulaState 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);
|
||||
scopes.Set(Names.LastMessage, s_emptyMessage, VariableScopeNames.System);
|
||||
Set(Names.LastMessageId);
|
||||
Set(Names.LastMessageText);
|
||||
|
||||
@@ -95,27 +95,21 @@ internal static class SystemScope
|
||||
}
|
||||
}
|
||||
|
||||
public static FormulaValue GetConversationId(this DeclarativeWorkflowState state) =>
|
||||
state.Get(VariableScopeNames.System, Names.ConversationId);
|
||||
public static FormulaValue GetConversationId(this WorkflowFormulaState state) =>
|
||||
state.Get(Names.ConversationId, VariableScopeNames.System);
|
||||
|
||||
public static async ValueTask SetConversationIdAsync(this DeclarativeWorkflowState state, IWorkflowContext context, string conversationId)
|
||||
public static void SetConversationId(this WorkflowFormulaState state, string conversationId)
|
||||
{
|
||||
RecordValue conversation = (RecordValue)state.Get(VariableScopeNames.System, Names.Conversation);
|
||||
RecordValue conversation = (RecordValue)state.Get(Names.Conversation, VariableScopeNames.System);
|
||||
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);
|
||||
state.Set(Names.Conversation, conversation, VariableScopeNames.System);
|
||||
state.Set(Names.ConversationId, FormulaValue.New(conversationId), VariableScopeNames.System);
|
||||
}
|
||||
|
||||
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)
|
||||
public static void SetLastMessage(this WorkflowFormulaState state, 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);
|
||||
state.Set(Names.LastMessage, message.ToRecord(), VariableScopeNames.System);
|
||||
state.Set(Names.LastMessageId, message.MessageId is null ? FormulaValue.NewBlank(FormulaType.String) : FormulaValue.New(message.MessageId), VariableScopeNames.System);
|
||||
state.Set(Names.LastMessageText, FormulaValue.New(message.Text), VariableScopeNames.System);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal static class TypeSchema
|
||||
{
|
||||
public static class Message
|
||||
{
|
||||
public static class Fields
|
||||
{
|
||||
public const string Id = nameof(Id);
|
||||
public const string ConversationId = nameof(ConversationId);
|
||||
public const string AgentId = nameof(AgentId);
|
||||
public const string RunId = nameof(RunId);
|
||||
public const string Role = nameof(Role);
|
||||
public const string Author = nameof(Author);
|
||||
public const string Text = nameof(Text);
|
||||
public const string Content = nameof(Content);
|
||||
public const string ContentType = nameof(ContentType);
|
||||
public const string ContentValue = nameof(ContentValue);
|
||||
public const string Metadata = nameof(Metadata);
|
||||
}
|
||||
|
||||
public static class ContentTypes
|
||||
{
|
||||
public const string Text = nameof(AgentMessageContentType.Text);
|
||||
public const string ImageUrl = nameof(AgentMessageContentType.ImageUrl);
|
||||
public const string ImageFile = nameof(AgentMessageContentType.ImageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user