Merge branch 'main' into python-middleware

This commit is contained in:
Dmytro Struk
2025-09-15 22:15:46 -07:00
Unverified
200 changed files with 6515 additions and 1786 deletions
+1 -1
View File
@@ -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>
+7 -4
View File
@@ -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" />
+4 -3
View File
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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)
@@ -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>
@@ -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>
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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.
@@ -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());
}
@@ -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();
}
}
@@ -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());
@@ -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)
@@ -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));
}
@@ -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);
}
@@ -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);
}
}
@@ -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;
@@ -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);
}
}
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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)}");
}
@@ -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)
@@ -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>
@@ -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();
}
}
@@ -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);
}
}
}
}
@@ -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}]
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
@@ -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;
}
}
@@ -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,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;
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
@@ -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;
}
}
@@ -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,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