mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflow - Integrated updated CPS Object Model (#681)
* Checkpoint * Update workflows/DeepResearch.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Comment * Fix comment * Update package version * Fix nuget haxx * Checkpoint * Code complete * Testing * Message content workaround * Add sequential flow * Checkpoint * Integration test project * Checkpoint * Checkpoint cleanup * Complete * Update package --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -86,14 +86,16 @@
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.74.1" />
|
||||
<!-- Workflows -->
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="2025.8.2-1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="2025.8.2-1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="2025.8.2-1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0-build.20250625-1002" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.915-1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.915-1" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.915-1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0-build.20250821-1001" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.6.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="Moq" Version="[4.18.4]" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.65.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.65.0-beta" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
|
||||
@@ -78,9 +78,9 @@
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/Examples/">
|
||||
<File Path="../workflows/DeepResearch.yaml" />
|
||||
<File Path="../workflows/HelloWorld.yaml" />
|
||||
<File Path="../workflows/HumanInLoop.yaml" />
|
||||
<File Path="../workflows/Marketing.yaml" />
|
||||
<File Path="../workflows/MathChat.yaml" />
|
||||
<File Path="../workflows/Question.yaml" />
|
||||
<File Path="../workflows/README.md" />
|
||||
<File Path="../workflows/wttr.json" />
|
||||
</Folder>
|
||||
@@ -96,8 +96,8 @@
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Checkpoint/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/HumanInTheLoop/">
|
||||
@@ -265,6 +265,7 @@
|
||||
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj" />
|
||||
<Project Path="tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj" />
|
||||
<Project Path="tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletion.IntegrationTests.csproj" />
|
||||
<Project Path="tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ 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>
|
||||
@@ -56,7 +57,7 @@ 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);
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that broadcasts the conversation identifier.
|
||||
/// </summary>
|
||||
public class ConversationUpdateEvent(string executorid, string conversationId) : ExecutorEvent(executorid, conversationId)
|
||||
{
|
||||
/// <summary>
|
||||
/// The conversation ID associated with the workflow.
|
||||
/// </summary>
|
||||
public string ConversationId { get; } = conversationId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has been invoked.
|
||||
/// </summary>
|
||||
public class DeclarativeActionInvokeEvent(string actionId, DialogAction action, string? priorActionId) : WorkflowEvent(action)
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action id.
|
||||
/// </summary>
|
||||
public string ActionId => actionId;
|
||||
|
||||
/// <summary>
|
||||
/// The declarative action type name.
|
||||
/// </summary>
|
||||
public string ActionType => action.GetType().Name;
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the parent action.
|
||||
/// </summary>
|
||||
public string? ParentActionId => action.GetParentId();
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the previous action.
|
||||
/// </summary>
|
||||
public string? PriorActionId => priorActionId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has completed.
|
||||
/// </summary>
|
||||
public class DeclarativeActionCompleteEvent(string actionId, DialogAction action) : WorkflowEvent(action)
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action identifier.
|
||||
/// </summary>
|
||||
public string ActionId => actionId;
|
||||
|
||||
/// <summary>
|
||||
/// The declarative action type name.
|
||||
/// </summary>
|
||||
public string ActionType => action.GetType().Name;
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the parent action.
|
||||
/// </summary>
|
||||
public string? ParentActionId => action.GetParentId();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Entities;
|
||||
|
||||
internal sealed record class EntityExtractionResult
|
||||
{
|
||||
public EntityExtractionResult(FormulaValue? value)
|
||||
{
|
||||
this.Value = value;
|
||||
this.ErrorMessage = null;
|
||||
}
|
||||
|
||||
public EntityExtractionResult(string errorMessage)
|
||||
{
|
||||
this.Value = null;
|
||||
this.ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public FormulaValue? Value { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
public bool IsValid => this.Value is not null;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Net.Mail;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Entities;
|
||||
|
||||
internal static partial class EntityExtractor
|
||||
{
|
||||
private const string NumberUnitRegExExpression = @"(?<value>[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?|\d*\.\d+)";
|
||||
|
||||
#if NET
|
||||
[GeneratedRegex(NumberUnitRegExExpression, RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NumberUnitRegex();
|
||||
#else
|
||||
private static Regex NumberUnitRegex() => s_numberUnitRegex;
|
||||
private static readonly Regex s_numberUnitRegex = new(NumberUnitRegExExpression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
#endif
|
||||
|
||||
public static EntityExtractionResult Parse(EntityReference? entity, string value) =>
|
||||
entity switch
|
||||
{
|
||||
null => UndefinedEntity(value),
|
||||
AgePrebuiltEntity => TryParseNumberUnit(value, "age"),
|
||||
BooleanPrebuiltEntity => TryParseBoolean(value),
|
||||
CityPrebuiltEntity => TryParseString(value),
|
||||
ColorPrebuiltEntity => TryParseString(value),
|
||||
ContinentPrebuiltEntity => TryParseString(value),
|
||||
CountryOrRegionPrebuiltEntity => TryParseString(value),
|
||||
DatePrebuiltEntity => TryParseDate(value),
|
||||
DateTimeNoTimeZonePrebuiltEntity => TryParseDateTimeNoTimeZone(value),
|
||||
DateTimePrebuiltEntity => TryParseDateTime(value),
|
||||
DurationPrebuiltEntity => TryParseDuration(value),
|
||||
EmailPrebuiltEntity => TryParseEmail(value),
|
||||
EventPrebuiltEntity => TryParseString(value),
|
||||
LanguagePrebuiltEntity => TryParseString(value),
|
||||
MoneyPrebuiltEntity => TryParseNumberUnit(value, "money"),
|
||||
NumberPrebuiltEntity => TryParseNumber(value),
|
||||
PercentagePrebuiltEntity => TryParseNumberUnit(value, "percentage"),
|
||||
PhoneNumberPrebuiltEntity => TryParseString(value),
|
||||
PointOfInterestPrebuiltEntity => TryParseString(value),
|
||||
SpeedPrebuiltEntity => TryParseNumberUnit(value, "speed"),
|
||||
StatePrebuiltEntity => TryParseString(value),
|
||||
StreetAddressPrebuiltEntity => TryParseString(value),
|
||||
StringPrebuiltEntity => TryParseString(value),
|
||||
TemperaturePrebuiltEntity => TryParseNumberUnit(value, "temperature"),
|
||||
URLPrebuiltEntity => TryParseURL(value),
|
||||
WeightPrebuiltEntity => TryParseNumberUnit(value, "weight"),
|
||||
_ => UnsupportedEntity(entity),
|
||||
};
|
||||
|
||||
private static EntityExtractionResult TryParseBoolean(string value)
|
||||
{
|
||||
if (bool.TryParse(value, out bool parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid boolean value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseDate(string value)
|
||||
{
|
||||
if (DateTime.TryParse(value, out DateTime parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue.Date));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid date value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseDateTimeNoTimeZone(string value)
|
||||
{
|
||||
if (DateTime.TryParse(value, out DateTime parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(
|
||||
FormulaValue.New(
|
||||
DateTime.SpecifyKind(parsedValue, DateTimeKind.Unspecified)));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid date value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseDateTime(string value)
|
||||
{
|
||||
if (DateTime.TryParse(value, out DateTime parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid date-time value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseDuration(string value)
|
||||
{
|
||||
if (TimeSpan.TryParse(value, out TimeSpan parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid duration value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseEmail(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
MailAddress parsedValue = new(value);
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue.Address));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new EntityExtractionResult($"Invalid email value: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseNumberUnit(string value, string type)
|
||||
{
|
||||
Match m = NumberUnitRegex().Match(value);
|
||||
if (m.Success)
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(m.Groups[0].Value));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid {type} value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseNumber(string value)
|
||||
{
|
||||
if (double.TryParse(value, out double parsedValue))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(parsedValue));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid double value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseString(string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(value));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult("Empty value");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult TryParseURL(string value)
|
||||
{
|
||||
if (Uri.TryCreate(value, UriKind.Absolute, out Uri? uriResult))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.New(uriResult.AbsoluteUri));
|
||||
}
|
||||
|
||||
return new EntityExtractionResult($"Invalid double value: {value}");
|
||||
}
|
||||
|
||||
private static EntityExtractionResult UndefinedEntity(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return new EntityExtractionResult(FormulaValue.NewBlank());
|
||||
}
|
||||
|
||||
return new EntityExtractionResult(FormulaValue.New(value));
|
||||
}
|
||||
|
||||
private static EntityExtractionResult UnsupportedEntity(EntityReference entity) =>
|
||||
new($"Unsupported entity: {entity.GetType().Name}");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that broadcasts the conversation identifier.
|
||||
/// </summary>
|
||||
public sealed class ConversationUpdateEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The conversation ID associated with the workflow.
|
||||
/// </summary>
|
||||
public string ConversationId { get; }
|
||||
|
||||
internal ConversationUpdateEvent(string conversationId)
|
||||
: base(conversationId)
|
||||
{
|
||||
this.ConversationId = conversationId;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has been invoked.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeActionInvokeEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action id.
|
||||
/// </summary>
|
||||
public string ActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The declarative action type name.
|
||||
/// </summary>
|
||||
public string ActionType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the parent action.
|
||||
/// </summary>
|
||||
public string? ParentActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the previous action.
|
||||
/// </summary>
|
||||
public string? PriorActionId { get; }
|
||||
|
||||
internal DeclarativeActionInvokeEvent(DialogAction action, string? priorActionId) : base(action)
|
||||
{
|
||||
this.ActionId = action.GetId();
|
||||
this.ActionType = action.GetType().Name;
|
||||
this.ParentActionId = action.GetParentId();
|
||||
this.PriorActionId = priorActionId;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has completed.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeActionCompleteEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action identifier.
|
||||
/// </summary>
|
||||
public string ActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The declarative action type name.
|
||||
/// </summary>
|
||||
public string ActionType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifier of the parent action.
|
||||
/// </summary>
|
||||
public string? ParentActionId { get; }
|
||||
|
||||
internal DeclarativeActionCompleteEvent(DialogAction action) : base(action)
|
||||
{
|
||||
this.ActionId = action.GetId();
|
||||
this.ActionType = action.GetType().Name;
|
||||
this.ParentActionId = action.GetParentId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for user input.
|
||||
/// </summary>
|
||||
public sealed class InputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The prompt message to display to the user.
|
||||
/// </summary>
|
||||
public string Prompt { get; }
|
||||
|
||||
internal InputRequest(string prompt)
|
||||
{
|
||||
this.Prompt = prompt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user input response.
|
||||
/// </summary>
|
||||
public sealed class InputResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The response value.
|
||||
/// </summary>
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
public InputResponse(string value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative;
|
||||
|
||||
/// <summary>
|
||||
/// Event that broadcasts the conversation identifier.
|
||||
/// </summary>
|
||||
public sealed class MessageActivityEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The conversation ID associated with the workflow.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
internal MessageActivityEvent(string message) : base(message)
|
||||
{
|
||||
this.Message = message;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
|
||||
/// <summary>
|
||||
/// Represents an exception that occurs when the declarative model is not supported.
|
||||
/// </summary>
|
||||
public class DeclarativeModelException : DeclarativeWorkflowException
|
||||
public sealed class DeclarativeModelException : DeclarativeWorkflowException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeclarativeModelException"/> class.
|
||||
|
||||
+227
-5
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
@@ -9,15 +12,234 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class ChatMessageExtensions
|
||||
{
|
||||
// ISSUE #485 - Align with message type updated OM is available.
|
||||
public static RecordValue ToRecord(this ChatMessage message) =>
|
||||
RecordValue.NewRecordFromFields(message.GetMessageFields());
|
||||
|
||||
public static TableValue ToTable(this IEnumerable<ChatMessage> messages) =>
|
||||
FormulaValue.NewTable(s_messageRecordType, messages.Select(message => message.ToRecord()));
|
||||
|
||||
public static IEnumerable<ChatMessage> ToChatMessages(this DataValue messages)
|
||||
{
|
||||
if (messages is TableDataValue table)
|
||||
{
|
||||
return table.ToChatMessages();
|
||||
}
|
||||
|
||||
if (messages is RecordDataValue record)
|
||||
{
|
||||
return [record.ToChatMessage()];
|
||||
}
|
||||
|
||||
if (messages is StringDataValue text)
|
||||
{
|
||||
return [text.ToChatMessage()];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public static IEnumerable<ChatMessage> ToChatMessages(this TableDataValue messages)
|
||||
{
|
||||
foreach (DataValue message in messages.Values)
|
||||
{
|
||||
if (message is RecordDataValue record)
|
||||
{
|
||||
if (record.Properties.Count == 1 && record.Properties.TryGetValue("Value", out DataValue? singleColumn))
|
||||
{
|
||||
record = singleColumn as RecordDataValue ?? record;
|
||||
}
|
||||
ChatMessage? convertedMessage = record.ToChatMessage();
|
||||
if (convertedMessage is not null)
|
||||
{
|
||||
yield return convertedMessage;
|
||||
}
|
||||
}
|
||||
else if (message is StringDataValue text)
|
||||
{
|
||||
yield return ToChatMessage(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ChatMessage? ToChatMessage(this DataValue message)
|
||||
{
|
||||
if (message is RecordDataValue record)
|
||||
{
|
||||
return record.ToChatMessage();
|
||||
}
|
||||
|
||||
if (message is StringDataValue text)
|
||||
{
|
||||
return text.ToChatMessage();
|
||||
}
|
||||
|
||||
if (message is BlankDataValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new DeclarativeActionException($"Unable to convert {message.GetDataType()} to {nameof(ChatMessage)}.");
|
||||
}
|
||||
|
||||
public static ChatMessage ToChatMessage(this RecordDataValue message) =>
|
||||
new(message.GetRole(), [.. message.GetContent()])
|
||||
{
|
||||
AdditionalProperties = message.GetProperty<RecordDataValue>("metadata").ToMetadata()
|
||||
};
|
||||
|
||||
public static ChatMessage ToChatMessage(this StringDataValue message) => new(ChatRole.User, message.Value);
|
||||
|
||||
public static AdditionalPropertiesDictionary? ToMetadata(this RecordDataValue? metadata)
|
||||
{
|
||||
if (metadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
AdditionalPropertiesDictionary properties = [];
|
||||
|
||||
foreach (KeyValuePair<string, DataValue> property in metadata.Properties)
|
||||
{
|
||||
properties[property.Key] = property.Value.ToObject();
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
public static ChatRole ToChatRole(this AgentMessageRole role) =>
|
||||
role switch
|
||||
{
|
||||
AgentMessageRole.Agent => ChatRole.Assistant,
|
||||
AgentMessageRole.User => ChatRole.User,
|
||||
_ => ChatRole.User
|
||||
};
|
||||
|
||||
public static ChatRole ToChatRole(this AgentMessageRole? role) => role?.ToChatRole() ?? ChatRole.User;
|
||||
|
||||
public static AIContent? ToContent(this AgentMessageContentType contentType, string? contentValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return
|
||||
contentType switch
|
||||
{
|
||||
AgentMessageContentType.ImageUrl => new UriContent(contentValue, "image/*"),
|
||||
AgentMessageContentType.ImageFile => new HostedFileContent(contentValue),
|
||||
_ => new TextContent(contentValue)
|
||||
};
|
||||
}
|
||||
|
||||
private static ChatRole GetRole(this RecordDataValue message)
|
||||
{
|
||||
StringDataValue? roleValue = message.GetProperty<StringDataValue>(TypeSchema.Message.Fields.Role);
|
||||
if (roleValue is null || string.IsNullOrWhiteSpace(roleValue.Value))
|
||||
{
|
||||
return ChatRole.User;
|
||||
}
|
||||
|
||||
AgentMessageRole? role = null;
|
||||
if (Enum.TryParse<AgentMessageRole>(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, TableValue.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 => RecordValue.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());
|
||||
}
|
||||
|
||||
+31
-3
@@ -9,11 +9,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 +53,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.ToRecordValue()).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)
|
||||
{
|
||||
@@ -79,4 +97,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;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-32
@@ -20,7 +20,7 @@ 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(),
|
||||
@@ -35,8 +35,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}"),
|
||||
};
|
||||
|
||||
@@ -143,6 +144,19 @@ internal static class FormulaValueExtensions
|
||||
public static RecordDataValue ToRecord(this RecordValue value) =>
|
||||
RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()).ToImmutableArray());
|
||||
|
||||
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)
|
||||
{
|
||||
RecordDataType recordType = new();
|
||||
@@ -176,48 +190,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());
|
||||
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+28
-19
@@ -15,19 +15,15 @@ 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)
|
||||
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>
|
||||
{
|
||||
@@ -37,7 +33,7 @@ internal abstract class WorkflowActionExecutor : Executor<DeclarativeExecutorRes
|
||||
|
||||
private string? _parentId;
|
||||
|
||||
protected WorkflowActionExecutor(DialogAction model, DeclarativeWorkflowState state)
|
||||
protected DeclarativeActionExecutor(DialogAction model, DeclarativeWorkflowState state)
|
||||
: base(model.Id.Value)
|
||||
{
|
||||
if (!model.HasRequiredProperties)
|
||||
@@ -51,16 +47,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 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 +66,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);
|
||||
|
||||
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,15 +93,27 @@ 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}");
|
||||
@@ -125,8 +138,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));
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,6 +22,6 @@ internal sealed class DeclarativeWorkflowExecutor<TInput>(
|
||||
ChatMessage input = inputTransform.Invoke(message);
|
||||
await state.SetLastMessageAsync(context, input).ConfigureAwait(false);
|
||||
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-7
@@ -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);
|
||||
}
|
||||
@@ -96,13 +108,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 +165,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;
|
||||
|
||||
|
||||
+3
-3
@@ -77,9 +77,9 @@ internal sealed class DeclarativeWorkflowState
|
||||
await context.QueueStateUpdateAsync(varName, value.ToObject(), scopeName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string? Format(IEnumerable<TemplateLine> template) => this._engine.Format(template);
|
||||
public string Format(IEnumerable<TemplateLine> template) => this._engine.Format(template);
|
||||
|
||||
public string? Format(TemplateLine? line) => this._engine.Format(line);
|
||||
public string Format(TemplateLine? line) => this._engine.Format(line);
|
||||
|
||||
public async ValueTask RestoreAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -96,7 +96,7 @@ internal sealed class DeclarativeWorkflowState
|
||||
foreach (string key in keys)
|
||||
{
|
||||
object? value = await context.ReadStateAsync<object>(key, scopeName).ConfigureAwait(false);
|
||||
this._scopes.Set(key, value.ToFormulaValue(), scopeName);
|
||||
this._scopes.Set(key, value.ToFormula(), scopeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-7
@@ -1,29 +1,47 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
|
||||
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, DelegateAction<ExecutorResultMessage>? action = null, bool emitResult = true)
|
||||
: DelegateActionExecutor<ExecutorResultMessage>(actionId, 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 DelegateAction<TMessage>? _action;
|
||||
private readonly bool _emitResult;
|
||||
|
||||
public DelegateActionExecutor(string actionId, DelegateAction<TMessage>? action = null, bool emitResult = true)
|
||||
: base(actionId)
|
||||
{
|
||||
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(context, message, default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
if (this._emitResult)
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed class DurableProperty<TValue>(string name) where TValue : struct
|
||||
{
|
||||
public async ValueTask<TValue> ReadAsync(IWorkflowContext context)
|
||||
{
|
||||
TValue? storedValue = await context.ReadStateAsync<TValue>(name).ConfigureAwait(false);
|
||||
return storedValue ?? default;
|
||||
}
|
||||
|
||||
public ValueTask WriteAsync(IWorkflowContext context, TValue value) =>
|
||||
context.QueueStateUpdateAsync(name, value);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
|
||||
internal sealed record class ExecutorResultMessage(string ExecutorId, object? Result = null)
|
||||
{
|
||||
public static ExecutorResultMessage ThrowIfNot(object? message)
|
||||
{
|
||||
if (message is not ExecutorResultMessage executorMessage)
|
||||
{
|
||||
throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ExecutorResultMessage)})");
|
||||
}
|
||||
|
||||
return executorMessage;
|
||||
}
|
||||
}
|
||||
+116
-46
@@ -3,6 +3,7 @@
|
||||
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.Bot.ObjectModel;
|
||||
@@ -11,6 +12,15 @@ 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;
|
||||
@@ -47,10 +57,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), parentId, condition: null, CompletionHandler);
|
||||
|
||||
// Complete the action scope.
|
||||
void CompletionHandler()
|
||||
@@ -59,7 +69,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 +83,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), parentId, CompletionHandler);
|
||||
|
||||
base.VisitConditionItem(item);
|
||||
|
||||
@@ -81,7 +91,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 +127,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 +138,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 +151,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, 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), 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, action.ResetAsync), action.Id);
|
||||
this._workflowModel.AddLink(endActionsId, loopId);
|
||||
}
|
||||
}
|
||||
@@ -163,8 +175,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), parentId);
|
||||
this._workflowModel.AddLink(item.Id.Value, Steps.Post(loopExecutor.Id));
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
}
|
||||
@@ -177,7 +189,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), parentId);
|
||||
this._workflowModel.AddLink(item.Id.Value, ForeachExecutor.Steps.Next(loopExecutor.Id));
|
||||
this.RestartAfter(item.Id.Value, parentId);
|
||||
}
|
||||
@@ -188,15 +200,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), 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, 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, questionExecutor.CaptureResponseAsync, emitResult: false), parentId);
|
||||
|
||||
this.ContinueWith(new DelegateActionExecutor(postId, 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 +280,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 +338,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 +403,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 +516,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
#endregion
|
||||
|
||||
private void ContinueWith(
|
||||
WorkflowActionExecutor executor,
|
||||
DeclarativeActionExecutor executor,
|
||||
Func<object?, bool>? condition = null,
|
||||
Action? completionHandler = null)
|
||||
{
|
||||
@@ -453,32 +534,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, 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"), 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 +566,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)}");
|
||||
}
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// 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.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class AddConversationMessageExecutor(AddConversationMessage model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState 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.ExpressionEngine.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.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.ExpressionEngine.GetValue(this.Model.Role).Value;
|
||||
|
||||
return roleWrapper.Value.ToChatRole();
|
||||
}
|
||||
|
||||
private AdditionalPropertiesDictionary? GetMetadata()
|
||||
{
|
||||
if (this.Model.Metadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RecordDataValue? metadataValue = this.State.ExpressionEngine.GetValue(this.Model.Metadata).Value;
|
||||
|
||||
return metadataValue.ToMetadata();
|
||||
}
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class AnswerQuestionWithAIExecutor(AnswerQuestionWithAI model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState state)
|
||||
: DeclarativeActionExecutor<AnswerQuestionWithAI>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
StringExpression userInputExpression = Throw.IfNull(this.Model.UserInput, $"{nameof(this.Model)}.{nameof(this.Model.UserInput)}");
|
||||
|
||||
string agentInstructions = this.State.Format(this.Model.AdditionalInstructions) ?? string.Empty;
|
||||
// ISSUE #485 - Agent identifier embedded in instructions until updated OM is available.
|
||||
string agentId;
|
||||
string? additionalInstructions = null;
|
||||
int delimiterIndex = agentInstructions.IndexOf(',');
|
||||
if (delimiterIndex < 0)
|
||||
{
|
||||
agentId = agentInstructions.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
agentId = agentInstructions.Substring(0, delimiterIndex).Trim();
|
||||
additionalInstructions = agentInstructions.Substring(delimiterIndex + 1).Trim();
|
||||
}
|
||||
|
||||
AIAgent agent = await agentProvider.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string? userInput = null;
|
||||
if (this.Model.UserInput is not null)
|
||||
{
|
||||
EvaluationResult<string> expressionResult = this.State.ExpressionEngine.GetValue(userInputExpression);
|
||||
userInput = expressionResult.Value;
|
||||
}
|
||||
|
||||
ChatClientAgentRunOptions options =
|
||||
new(
|
||||
new ChatOptions()
|
||||
{
|
||||
Instructions = additionalInstructions,
|
||||
});
|
||||
|
||||
FormulaValue conversationValue =
|
||||
this.Model.AutoSend ? // ISSUE #485: Conversation implicitly managed until updated OM is available.
|
||||
this.State.GetConversationId() :
|
||||
this.State.GetInternalConversationId();
|
||||
|
||||
string? conversationId = null;
|
||||
if (conversationValue is StringValue stringValue)
|
||||
{
|
||||
await AssignConversationId(stringValue.Value).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
AgentThread agentThread = new() { ConversationId = conversationId };
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates =
|
||||
!string.IsNullOrWhiteSpace(userInput) ?
|
||||
agent.RunStreamingAsync(userInput, agentThread, options, cancellationToken) :
|
||||
agent.RunStreamingAsync(agentThread, options, cancellationToken);
|
||||
|
||||
string? messageId = null;
|
||||
List<AgentRunResponseUpdate> agentResponseUpdates = new(0x400);
|
||||
await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false))
|
||||
{
|
||||
agentResponseUpdates.Add(update);
|
||||
messageId ??= update.MessageId;
|
||||
await AssignConversationId(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false);
|
||||
if (this.Model.AutoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
AgentRunResponse agentResponse = agentResponseUpdates.ToAgentRunResponse();
|
||||
|
||||
ChatMessage response = agentResponse.Messages.Last();
|
||||
await this.State.SetLastMessageAsync(context, response).ConfigureAwait(false);
|
||||
if (this.Model.AutoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Assign conversation ID if it wasn't already assigned.
|
||||
if (conversationValue is not StringValue && conversationId is not null)
|
||||
{
|
||||
if (this.Model.AutoSend) // ISSUE #485: Conversation implicitly managed until updated OM is available.
|
||||
{
|
||||
await this.State.SetConversationIdAsync(context, conversationId).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.State.SetInternalConversationIdAsync(context, conversationId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
PropertyPath? variablePath = this.Model.Variable?.Path;
|
||||
if (variablePath is not null)
|
||||
{
|
||||
await this.AssignAsync(variablePath, response.ToRecord(), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
async ValueTask AssignConversationId(string? assignValue)
|
||||
{
|
||||
if (assignValue != null && conversationId == null)
|
||||
{
|
||||
conversationId = assignValue;
|
||||
await context.AddEventAsync(new ConversationUpdateEvent(this.Id, conversationId)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-16
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
@@ -33,24 +34,16 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
|
||||
|
||||
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
|
||||
@@ -75,8 +68,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);
|
||||
}
|
||||
}
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState state) :
|
||||
DeclarativeActionExecutor<CopyConversationMessages>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = this.State.ExpressionEngine.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.ExpressionEngine.GetValue(this.Model.Messages);
|
||||
messages = expressionResult.Value;
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class CreateConversationExecutor(CreateConversation model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState 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;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ 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());
|
||||
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;
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflow
|
||||
{
|
||||
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());
|
||||
RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormula());
|
||||
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflow
|
||||
{
|
||||
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)
|
||||
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);
|
||||
|
||||
@@ -48,20 +48,20 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.GetValue(this.Model.Items);
|
||||
if (expressionResult.Value is TableDataValue tableValue)
|
||||
{
|
||||
this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormulaValue())];
|
||||
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)
|
||||
{
|
||||
@@ -78,7 +78,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
public async ValueTask ResetAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -90,7 +90,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
}
|
||||
finally
|
||||
{
|
||||
await this.RaiseCompletionEventAsync(context).ConfigureAwait(false);
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class GotoExecutor(GotoAction model, DeclarativeWorkflowState state) :
|
||||
DeclarativeActionExecutor<GotoAction>(model, state)
|
||||
{
|
||||
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// No action needed - the edge will be followed automatically
|
||||
return default;
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState 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;
|
||||
await this.State.SetConversationIdAsync(context, conversationId).ConfigureAwait(false);
|
||||
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.ExpressionEngine.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.ExpressionEngine.GetValue(this.Model.ConversationId);
|
||||
return conversationIdResult.Value.Length == 0 ? null : conversationIdResult.Value;
|
||||
}
|
||||
|
||||
private string GetAgentName() =>
|
||||
this.State.ExpressionEngine.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.Format(this.AgentInput.AdditionalInstructions);
|
||||
}
|
||||
|
||||
return additionalInstructions;
|
||||
}
|
||||
|
||||
private bool GetAutoSendValue()
|
||||
{
|
||||
if (this.AgentOutput?.AutoSend is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
EvaluationResult<bool> autoSendResult = this.State.ExpressionEngine.GetValue(this.AgentOutput.AutoSend);
|
||||
|
||||
return autoSendResult.Value;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ internal sealed class ParseValueExecutor(ParseValue model, DeclarativeWorkflowSt
|
||||
|
||||
if (expressionResult.Value is RecordDataValue recordValue)
|
||||
{
|
||||
parsedResult = recordValue.ToFormulaValue();
|
||||
parsedResult = recordValue.ToFormula();
|
||||
}
|
||||
else if (expressionResult.Value is StringDataValue stringValue)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class QuestionExecutor(Question model, DeclarativeWorkflowState 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.ExpressionEngine.GetValue(this.Model.AlwaysPrompt).Value;
|
||||
|
||||
bool proceed = !alwaysPrompt || hasValue;
|
||||
if (proceed)
|
||||
{
|
||||
SkipQuestionMode mode = this.State.ExpressionEngine.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.ExpressionEngine.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.ExpressionEngine.GetValue(defaultValueExpression).Value;
|
||||
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.Format(messageActivity.Text).Trim();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMessage model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState state) :
|
||||
DeclarativeActionExecutor<RetrieveConversationMessage>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = this.State.ExpressionEngine.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value;
|
||||
string messageId = this.State.ExpressionEngine.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;
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// 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.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationMessages model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState state) :
|
||||
DeclarativeActionExecutor<RetrieveConversationMessages>(model, state)
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string conversationId = this.State.ExpressionEngine.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.ExpressionEngine.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.ExpressionEngine.GetValue(messagExpression).Value;
|
||||
}
|
||||
|
||||
private bool IsDescending()
|
||||
{
|
||||
if (this.Model.SortOrder is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AgentMessageSortOrderWrapper sortOrderWrapper = this.State.ExpressionEngine.GetValue(this.Model.SortOrder).Value;
|
||||
|
||||
return sortOrderWrapper.Value == AgentMessageSortOrder.NewestFirst;
|
||||
}
|
||||
}
|
||||
+2
-13
@@ -1,12 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
@@ -17,17 +14,9 @@ internal sealed class SendActivityExecutor(SendActivity model, DeclarativeWorkfl
|
||||
{
|
||||
if (this.Model.Activity is MessageActivityTemplate messageActivity)
|
||||
{
|
||||
StringBuilder templateBuilder = new();
|
||||
if (!string.IsNullOrEmpty(messageActivity.Summary))
|
||||
{
|
||||
templateBuilder.AppendLine($"\t{messageActivity.Summary}");
|
||||
}
|
||||
string activityText = this.State.Format(messageActivity.Text).Trim();
|
||||
|
||||
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;
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class SetMultipleVariablesExecutor(SetMultipleVariables model, DeclarativeWorkflowState 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.ExpressionEngine.GetValue(assignment.Value);
|
||||
|
||||
await this.AssignAsync(assignment.Variable, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -26,7 +26,7 @@ internal sealed class SetVariableExecutor(SetVariable model, DeclarativeWorkflow
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.State.ExpressionEngine.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;
|
||||
}
|
||||
|
||||
@@ -106,12 +106,6 @@ internal static class SystemScope
|
||||
await state.SetAsync(VariableScopeNames.System, Names.ConversationId, FormulaValue.New(conversationId), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static FormulaValue GetInternalConversationId(this DeclarativeWorkflowState state) =>
|
||||
state.Get(VariableScopeNames.System, Names.InternalId);
|
||||
|
||||
public static ValueTask SetInternalConversationIdAsync(this DeclarativeWorkflowState state, IWorkflowContext context, string conversationId) =>
|
||||
state.SetAsync(VariableScopeNames.System, Names.InternalId, FormulaValue.New(conversationId), context);
|
||||
|
||||
public static async ValueTask SetLastMessageAsync(this DeclarativeWorkflowState state, IWorkflowContext context, ChatMessage message)
|
||||
{
|
||||
await state.SetAsync(VariableScopeNames.System, Names.LastMessage, message.ToRecord(), context).ConfigureAwait(false);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ internal static class WorkflowDiagnostics
|
||||
continue;
|
||||
}
|
||||
|
||||
FormulaValue defaultValue = variableDiagnostic.ConstantValue?.ToFormulaValue() ?? variableDiagnostic.Type.NewBlank();
|
||||
FormulaValue defaultValue = variableDiagnostic.ConstantValue?.ToFormula() ?? variableDiagnostic.Type.NewBlank();
|
||||
|
||||
if (variableDiagnostic.Path.VariableScopeName?.Equals(VariableScopeNames.System, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Collections.Generic;
|
||||
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;
|
||||
@@ -20,47 +18,48 @@ public abstract class WorkflowAgentProvider
|
||||
/// </summary>
|
||||
/// <param name="agentId">The unique identifier of the AI agent to retrieve. Cannot be null or empty.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="AIAgent"/> associated
|
||||
/// with the specified <paramref name="agentId"/>. Returns <see langword="null"/> if no agent is found.</returns>
|
||||
/// <returns>The task result contains the <see cref="AIAgent"/> associated.</returns>
|
||||
public abstract Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to interact with Foundry agents within a specified project context.
|
||||
/// </summary>
|
||||
/// <remarks>This class is used to retrieve and manage AI agents associated with a Foundry project. It requires a
|
||||
/// project endpoint and credentials to authenticate requests.</remarks>
|
||||
/// <param name="projectEndpoint">The endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project.</param>
|
||||
/// <param name="projectCredentials">The credentials used to authenticate with the Foundry project. This must be a valid instance of <see cref="TokenCredential"/>.</param>
|
||||
/// <param name="httpClient">An optional <see cref="HttpClient"/> instance to be used for making HTTP requests. If not provided, a default client will be used.</param>
|
||||
public sealed class FoundryAgentProvider(string projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider
|
||||
{
|
||||
private PersistentAgentsClient? _agentsClient;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AIAgent agent = await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private PersistentAgentsClient GetAgentsClient()
|
||||
{
|
||||
if (this._agentsClient is null)
|
||||
{
|
||||
PersistentAgentsAdministrationClientOptions clientOptions = new();
|
||||
|
||||
if (httpClient is not null)
|
||||
{
|
||||
clientOptions.Transport = new HttpClientTransport(httpClient);
|
||||
}
|
||||
|
||||
PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions);
|
||||
|
||||
Interlocked.CompareExchange(ref this._agentsClient, newClient, null);
|
||||
}
|
||||
|
||||
return this._agentsClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously creates a new conversation and returns its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>The conversation identifier</returns>
|
||||
public abstract Task<string> CreateConversationAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new message in the specified conversation.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The identifier of the target conversation.</param>
|
||||
/// <param name="conversationMessage">The message being added.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
public abstract Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a specific message from a conversation.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The identifier of the target conversation.</param>
|
||||
/// <param name="messageId">The identifier of the target message.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>The requested message</returns>
|
||||
public abstract Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a set of messages from a conversation.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The identifier of the target conversation.</param>
|
||||
/// <param name="limit">A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.</param>
|
||||
/// <param name="after">A cursor for use in pagination. after is an object ID that defines your place in the list.</param>
|
||||
/// <param name="before">A cursor for use in pagination. before is an object ID that defines your place in the list.</param>
|
||||
/// <param name="newestFirst">Provide records in descending order when true.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>The requested messages</returns>
|
||||
public abstract IAsyncEnumerable<ChatMessage> GetMessagesAsync(
|
||||
string conversationId,
|
||||
int? limit = null,
|
||||
string? after = null,
|
||||
string? before = null,
|
||||
bool newestFirst = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
type: foundry_agent
|
||||
name: BasicAgent
|
||||
description: Basic agent for integration tests
|
||||
model:
|
||||
id: ${AzureAI:ModelDeployment}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shared.IntegrationTests;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowBuilder"/>.
|
||||
/// </summary>
|
||||
[Collection("Global")]
|
||||
public sealed class DeclarativeWorkflowTest(ITestOutputHelper output, AgentFixture agentFixture) : WorkflowTest(output), IClassFixture<AgentFixture>
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("SendActivity.yaml", "SendActivity.json")]
|
||||
[InlineData("InvokeAgent.yaml", "InvokeAgent.json")]
|
||||
public Task Validate(string workflowFileName, string testcaseFileName) =>
|
||||
this.RunWorkflow(workflowFileName, testcaseFileName);
|
||||
|
||||
private Task RunWorkflow(string workflowFileName, string testcaseFileName)
|
||||
{
|
||||
this.Output.WriteLine($"WORKFLOW: {workflowFileName}");
|
||||
this.Output.WriteLine($"TESTCASE: {testcaseFileName}");
|
||||
|
||||
Testcase testcase = ReadTestcase(testcaseFileName);
|
||||
IConfiguration configuration = InitializeConfig();
|
||||
string workflowPath = Path.Combine("Workflows", workflowFileName);
|
||||
|
||||
this.Output.WriteLine($" {testcase.Description}");
|
||||
|
||||
return
|
||||
testcase.Setup.Input.Type switch
|
||||
{
|
||||
nameof(ChatMessage) => this.RunWorkflow<ChatMessage>(testcase, workflowPath, configuration),
|
||||
nameof(String) => this.RunWorkflow<string>(testcase, workflowPath, configuration),
|
||||
_ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."),
|
||||
};
|
||||
}
|
||||
|
||||
private async Task RunWorkflow<TInput>(
|
||||
Testcase testcase,
|
||||
string workflowPath,
|
||||
IConfiguration configuration) where TInput : notnull
|
||||
{
|
||||
this.Output.WriteLine($"INPUT: {testcase.Setup.Input.Value}");
|
||||
|
||||
AzureAIConfiguration? foundryConfig = configuration.GetSection("AzureAI").Get<AzureAIConfiguration>();
|
||||
Assert.NotNull(foundryConfig);
|
||||
|
||||
IDictionary<string, string?> agentMap = await agentFixture.GetAgentsAsync(foundryConfig);
|
||||
|
||||
IConfiguration workflowConfig =
|
||||
new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(agentMap)
|
||||
.Build();
|
||||
|
||||
DeclarativeWorkflowOptions workflowOptions =
|
||||
new(new AzureAgentProvider(foundryConfig.Endpoint, new AzureCliCredential()))
|
||||
{
|
||||
Configuration = workflowConfig,
|
||||
LoggerFactory = this.Output
|
||||
};
|
||||
Workflow<TInput> workflow = DeclarativeWorkflowBuilder.Build<TInput>(workflowPath, workflowOptions);
|
||||
|
||||
WorkflowEvents workflowEvents = await WorkflowHarness.RunAsync(workflow, (TInput)GetInput<TInput>(testcase));
|
||||
foreach (DeclarativeActionInvokeEvent actionInvokeEvent in workflowEvents.ActionInvokeEvents)
|
||||
{
|
||||
this.Output.WriteLine($"ACTION: {actionInvokeEvent.ActionId} [{actionInvokeEvent.ActionType}]");
|
||||
}
|
||||
|
||||
Assert.Equal(testcase.Validation.ActionCount, workflowEvents.ActionInvokeEvents.Count);
|
||||
Assert.Equal(testcase.Validation.ActionCount, workflowEvents.ActionCompleteEvents.Count);
|
||||
}
|
||||
|
||||
private static object GetInput<TInput>(Testcase testcase) where TInput : notnull =>
|
||||
testcase.Setup.Input.Type switch
|
||||
{
|
||||
nameof(ChatMessage) => new ChatMessage(ChatRole.User, testcase.Setup.Input.Value),
|
||||
nameof(String) => testcase.Setup.Input.Value,
|
||||
_ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."),
|
||||
};
|
||||
|
||||
private static Testcase ReadTestcase(string testcaseFileName)
|
||||
{
|
||||
using Stream testcaseStream = File.Open(Path.Combine("Testcases", testcaseFileName), FileMode.Open);
|
||||
Testcase? testcase = JsonSerializer.Deserialize<Testcase>(testcaseStream, s_jsonSerializerOptions);
|
||||
Assert.NotNull(testcase);
|
||||
return testcase;
|
||||
}
|
||||
|
||||
private static IConfigurationRoot InitializeConfig() =>
|
||||
new ConfigurationBuilder()
|
||||
.AddUserSecrets(Assembly.GetExecutingAssembly())
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = true,
|
||||
};
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.AzureAI;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
|
||||
|
||||
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
internal static class AgentFactory
|
||||
{
|
||||
public static async Task<ImmutableDictionary<string, string?>> CreateAsync(string agentsDirectory, AzureAIConfiguration config, CancellationToken cancellationToken)
|
||||
{
|
||||
PersistentAgentsClient clientAgents = new(config.Endpoint, new AzureCliCredential());
|
||||
|
||||
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
|
||||
kernelBuilder.Services.AddSingleton(clientAgents);
|
||||
Kernel kernel = kernelBuilder.Build();
|
||||
|
||||
AzureAIAgentFactory factory = new();
|
||||
|
||||
Dictionary<string, string?> agentMap = [];
|
||||
|
||||
foreach (string file in Directory.GetFiles(agentsDirectory, "*.yaml"))
|
||||
{
|
||||
Debug.WriteLine($"TEST AGENT: Creating - {file}");
|
||||
string agentText = File.ReadAllText(file);
|
||||
|
||||
Agent? agent = await factory.CreateAgentFromYamlAsync(agentText, new AgentCreationOptions() { Kernel = kernel }, configuration: null, cancellationToken);
|
||||
|
||||
Assert.NotNull(agent?.Name);
|
||||
|
||||
Debug.WriteLine($"TEST AGENT: {agent.Name} => {agent.Id}");
|
||||
agentMap[agent.Name] = agent.Id;
|
||||
}
|
||||
|
||||
return agentMap.ToImmutableDictionary();
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
|
||||
|
||||
public sealed class AgentFixture : IDisposable
|
||||
{
|
||||
private static ImmutableDictionary<string, string?>? s_agentMap;
|
||||
|
||||
internal async Task<ImmutableDictionary<string, string?>> GetAgentsAsync(AzureAIConfiguration config, CancellationToken cancellationToken = default)
|
||||
{
|
||||
s_agentMap ??= await AgentFactory.CreateAsync("Agents", config, cancellationToken);
|
||||
|
||||
return s_agentMap;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
|
||||
|
||||
public sealed class TestOutputAdapter(ITestOutputHelper output) : TextWriter, ILogger, ILoggerFactory
|
||||
{
|
||||
private readonly Stack<string> _scopes = [];
|
||||
|
||||
public override Encoding Encoding { get; } = Encoding.UTF8;
|
||||
|
||||
public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException();
|
||||
|
||||
public ILogger CreateLogger(string categoryName) => this;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public override void WriteLine(object? value = null) => this.SafeWrite($"{value}");
|
||||
|
||||
public override void WriteLine(string? format, params object?[] arg) => this.SafeWrite(string.Format(format ?? string.Empty, arg));
|
||||
|
||||
public override void WriteLine(string? value) => this.SafeWrite(value ?? string.Empty);
|
||||
|
||||
public override void Write(object? value = null) => this.SafeWrite($"{value}");
|
||||
|
||||
public override void Write(char[]? buffer) => this.SafeWrite(new string(buffer));
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull
|
||||
{
|
||||
this._scopes.Push($"{state}");
|
||||
return new LoggerScope(() => this._scopes.Pop());
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
string message = formatter(state, exception);
|
||||
string scope = this._scopes.Count > 0 ? $"[{this._scopes.Peek()}] " : string.Empty;
|
||||
output.WriteLine($"{scope}{message}");
|
||||
}
|
||||
|
||||
private void SafeWrite(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
output.WriteLine(value ?? string.Empty);
|
||||
}
|
||||
catch (InvalidOperationException exception) when (exception.Message == "There is no currently active test.")
|
||||
{
|
||||
// This exception is thrown when the test output is accessed outside of a test context.
|
||||
// We can ignore it since we are not in a test context.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoggerScope(Action action) : IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this._disposed)
|
||||
{
|
||||
action.Invoke();
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
|
||||
|
||||
public sealed class Testcase
|
||||
{
|
||||
[JsonConstructor]
|
||||
public Testcase(
|
||||
string description,
|
||||
TestcaseSetup setup,
|
||||
TestcaseValidation validation)
|
||||
{
|
||||
this.Description = description;
|
||||
this.Setup = setup;
|
||||
this.Validation = validation;
|
||||
}
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public TestcaseSetup Setup { get; }
|
||||
|
||||
public TestcaseValidation Validation { get; }
|
||||
}
|
||||
|
||||
public sealed class TestcaseSetup
|
||||
{
|
||||
[JsonConstructor]
|
||||
public TestcaseSetup(TestcaseInput input)
|
||||
{
|
||||
this.Input = input;
|
||||
}
|
||||
public TestcaseInput Input { get; }
|
||||
}
|
||||
|
||||
public sealed class TestcaseInput
|
||||
{
|
||||
[JsonConstructor]
|
||||
public TestcaseInput(string type, string value)
|
||||
{
|
||||
this.Type = type;
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public string Type { get; }
|
||||
public string Value { get; }
|
||||
}
|
||||
|
||||
public sealed class TestcaseValidation
|
||||
{
|
||||
[JsonConstructor]
|
||||
public TestcaseValidation(int actionCount)
|
||||
{
|
||||
this.ActionCount = actionCount;
|
||||
}
|
||||
|
||||
public int ActionCount { get; }
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
|
||||
|
||||
internal sealed class WorkflowEvents
|
||||
{
|
||||
public WorkflowEvents(ImmutableList<WorkflowEvent> workflowEvents)
|
||||
{
|
||||
this.Events = workflowEvents;
|
||||
this.EventCounts = workflowEvents.GroupBy(e => e.GetType()).ToImmutableDictionary(e => e.Key, e => e.Count());
|
||||
this.ActionInvokeEvents = workflowEvents.OfType<DeclarativeActionInvokeEvent>().ToImmutableList();
|
||||
this.ActionCompleteEvents = workflowEvents.OfType<DeclarativeActionCompleteEvent>().ToImmutableList();
|
||||
}
|
||||
|
||||
public ImmutableList<WorkflowEvent> Events { get; }
|
||||
public IImmutableDictionary<Type, int> EventCounts { get; }
|
||||
public ImmutableList<DeclarativeActionInvokeEvent> ActionInvokeEvents { get; }
|
||||
public ImmutableList<DeclarativeActionCompleteEvent> ActionCompleteEvents { get; private set; }
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
|
||||
|
||||
internal static class WorkflowHarness
|
||||
{
|
||||
public static async Task<WorkflowEvents> RunAsync<TInput>(Workflow<TInput> workflow, TInput input) where TInput : notnull
|
||||
{
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
|
||||
ImmutableList<WorkflowEvent> workflowEvents = run.WatchStreamAsync().ToEnumerable().ToImmutableList();
|
||||
return new WorkflowEvents(workflowEvents);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for workflow tests.
|
||||
/// </summary>
|
||||
public abstract class WorkflowTest : IDisposable
|
||||
{
|
||||
public TestOutputAdapter Output { get; }
|
||||
|
||||
protected WorkflowTest(ITestOutputHelper output)
|
||||
{
|
||||
this.Output = new TestOutputAdapter(output);
|
||||
Console.SetOut(this.Output);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(isDisposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool isDisposing)
|
||||
{
|
||||
if (isDisposing)
|
||||
{
|
||||
this.Output.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? VariableScopeNames.Topic}.{variableName}";
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);IDE1006</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedIntegrationTestCode>true</InjectSharedIntegrationTestCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.Workflows.Declarative\Microsoft.Agents.Workflows.Declarative.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.Abstractions" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.AzureAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.Yaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Agents\*.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Testcases\*.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Workflows\*.yaml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"description": "Produce a single response from an agent.",
|
||||
"setup": {
|
||||
"input": {
|
||||
"type": "String",
|
||||
"value": "Why is the sky blue?"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"action_count": 1
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"description": "Send an activity message .",
|
||||
"setup": {
|
||||
"input": {
|
||||
"type": "String",
|
||||
"value": "Why is the sky blue?"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"action_count": 3
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# This workflow demonstrates a conversation and message manipulation.
|
||||
#
|
||||
# Any Foundry Agent may be used to provide the response.
|
||||
# See: ./setup/QuestionAgent.yaml
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: CreateConversation
|
||||
id: conversation_create1
|
||||
conversationId: Topic.FirstConversationId
|
||||
|
||||
- kind: CreateConversation
|
||||
id: conversation_create2
|
||||
conversationId: Topic.SecondConversationId
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_conversation
|
||||
activity: |-
|
||||
Conversation 1: {Topic.FirstConversationId}
|
||||
Conversation 2: {Topic.SecondConversationId}
|
||||
|
||||
- kind: AddConversationMessage
|
||||
id: add_message
|
||||
message: Topic.MyMessage1
|
||||
role: User
|
||||
conversationId: =Topic.FirstConversationId
|
||||
content:
|
||||
- type: Text
|
||||
value: {System.LastMessage.Text}
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_message
|
||||
activity: |-
|
||||
Messsage 1: {Topic.MyMessage1}
|
||||
|
||||
- kind: CopyConversationMessages
|
||||
id: copy_messages
|
||||
conversationId: =Topic.SecondConversationId
|
||||
messages: =[Topic.MyMessage1]
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_copy
|
||||
activity: Done!
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#
|
||||
# This workflow demonstrates a conversation and message manipulation.
|
||||
#
|
||||
# Any Foundry Agent may be used to provide the response.
|
||||
# See: ./setup/QuestionAgent.yaml
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: RetrieveConversationMessage
|
||||
id: get_message
|
||||
message: Topic.MyMessage
|
||||
conversationId: thread_T8xIzNrNcPkUkoCEGzxg80Vt
|
||||
messageId: msg_J4x6YZTDUUWNs60FOUAucldy
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_message
|
||||
activity: |-
|
||||
{Topic.MyMessage}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# This workflow demonstrates a conversation and message manipulation.
|
||||
#
|
||||
# Any Foundry Agent may be used to provide the response.
|
||||
# See: ./setup/QuestionAgent.yaml
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: RetrieveConversationMessages
|
||||
id: get_message
|
||||
messages: Topic.MyMessages
|
||||
conversationId: thread_T8xIzNrNcPkUkoCEGzxg80Vt
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_message
|
||||
activity: |-
|
||||
{Topic.MyMessages}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_agent
|
||||
agent:
|
||||
name: =Env.BasicAgent
|
||||
input:
|
||||
messages: =[UserMessage(System.LastMessageText)]
|
||||
output:
|
||||
messages: Topic.Answer
|
||||
+3
-3
@@ -7,14 +7,14 @@ kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_demo
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
# Capture input
|
||||
- kind: SetVariable
|
||||
id: setvar_userinput
|
||||
variable: Topic.UserInput
|
||||
value: =System.LastMessageText
|
||||
value: =System.LastMessage.Text
|
||||
|
||||
# Capture environment variable
|
||||
- kind: SetVariable
|
||||
@@ -27,4 +27,4 @@ beginDialog:
|
||||
id: sendActivity_demo
|
||||
activity: |-
|
||||
Hello {Global.UserName},
|
||||
You said, "{Topic.UserInput}"
|
||||
You said, "{Topic.UserInput}"
|
||||
+4
-5
@@ -168,7 +168,6 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
[InlineData(typeof(InvokeSkillAction.Builder))]
|
||||
[InlineData(typeof(LogCustomTelemetryEvent.Builder))]
|
||||
[InlineData(typeof(OAuthInput.Builder))]
|
||||
[InlineData(typeof(Question.Builder))]
|
||||
[InlineData(typeof(RecognizeIntent.Builder))]
|
||||
[InlineData(typeof(RepeatDialog.Builder))]
|
||||
[InlineData(typeof(ReplaceDialog.Builder))]
|
||||
@@ -192,7 +191,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
BeginDialog =
|
||||
new OnActivity.Builder()
|
||||
{
|
||||
Id = "workflow",
|
||||
Id = "anything",
|
||||
Actions = [unsupportedAction]
|
||||
}
|
||||
};
|
||||
@@ -226,7 +225,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
|
||||
private void AssertMessage(string message)
|
||||
{
|
||||
Assert.Contains(this.WorkflowEvents.OfType<AgentRunResponseEvent>(), e => string.Equals(e.Response.Messages[0].Text.Trim(), message, StringComparison.Ordinal));
|
||||
Assert.Contains(this.WorkflowEvents.OfType<MessageActivityEvent>(), e => string.Equals(e.Message.Trim(), message, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private Task RunWorkflow(string workflowPath) => this.RunWorkflow<string>(workflowPath, string.Empty);
|
||||
@@ -246,7 +245,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
{
|
||||
if (workflowEvent is ExecutorInvokedEvent invokeEvent)
|
||||
{
|
||||
DeclarativeExecutorResult? message = invokeEvent.Data as DeclarativeExecutorResult;
|
||||
ExecutorResultMessage? message = invokeEvent.Data as ExecutorResultMessage;
|
||||
this.Output.WriteLine($"EXEC: {invokeEvent.ExecutorId} << {message?.ExecutorId ?? "?"} [{message?.Result ?? "-"}]");
|
||||
}
|
||||
else if (workflowEvent is AgentRunResponseEvent messageEvent)
|
||||
@@ -258,7 +257,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
}
|
||||
|
||||
private sealed class RootExecutor() :
|
||||
ReflectingExecutor<RootExecutor>(WorkflowActionVisitor.RootId("workflow")),
|
||||
ReflectingExecutor<RootExecutor>(WorkflowActionVisitor.Steps.Root("anything")),
|
||||
IMessageHandler<string>
|
||||
{
|
||||
public async ValueTask HandleAsync(string message, IWorkflowContext context)
|
||||
|
||||
+19
-9
@@ -19,7 +19,7 @@ public class FormulaValueExtensionsTests
|
||||
BooleanDataValue typedValue = Assert.IsType<BooleanDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
BooleanValue formulaCopy = Assert.IsType<BooleanValue>(dataValue.ToFormulaValue());
|
||||
BooleanValue formulaCopy = Assert.IsType<BooleanValue>(dataValue.ToFormula());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal(bool.TrueString, formulaValue.Format());
|
||||
@@ -35,7 +35,7 @@ public class FormulaValueExtensionsTests
|
||||
StringDataValue typedValue = Assert.IsType<StringDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
StringValue formulaCopy = Assert.IsType<StringValue>(typedValue.ToFormulaValue());
|
||||
StringValue formulaCopy = Assert.IsType<StringValue>(typedValue.ToFormula());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal(formulaValue.Value, formulaValue.Format());
|
||||
@@ -51,7 +51,7 @@ public class FormulaValueExtensionsTests
|
||||
NumberDataValue typedValue = Assert.IsType<NumberDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
DecimalValue formulaCopy = Assert.IsType<DecimalValue>(typedValue.ToFormulaValue());
|
||||
DecimalValue formulaCopy = Assert.IsType<DecimalValue>(typedValue.ToFormula());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal("45.3", formulaValue.Format());
|
||||
@@ -67,7 +67,7 @@ public class FormulaValueExtensionsTests
|
||||
FloatDataValue typedValue = Assert.IsType<FloatDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
NumberValue formulaCopy = Assert.IsType<NumberValue>(typedValue.ToFormulaValue());
|
||||
NumberValue formulaCopy = Assert.IsType<NumberValue>(typedValue.ToFormula());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal("3.1415926535897", formulaValue.Format());
|
||||
@@ -103,7 +103,7 @@ public class FormulaValueExtensionsTests
|
||||
DateDataValue typedValue = Assert.IsType<DateDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
|
||||
|
||||
DateValue formulaCopy = Assert.IsType<DateValue>(dataValue.ToFormulaValue());
|
||||
DateValue formulaCopy = Assert.IsType<DateValue>(dataValue.ToFormula());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
|
||||
|
||||
Assert.Equal($"{timestamp}", formulaValue.Format());
|
||||
@@ -120,7 +120,7 @@ public class FormulaValueExtensionsTests
|
||||
DateTimeDataValue typedValue = Assert.IsType<DateTimeDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
|
||||
|
||||
DateTimeValue formulaCopy = Assert.IsType<DateTimeValue>(typedValue.ToFormulaValue());
|
||||
DateTimeValue formulaCopy = Assert.IsType<DateTimeValue>(typedValue.ToFormula());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
|
||||
|
||||
Assert.Equal($"{timestamp}", formulaValue.Format());
|
||||
@@ -136,7 +136,7 @@ public class FormulaValueExtensionsTests
|
||||
TimeDataValue typedValue = Assert.IsType<TimeDataValue>(dataValue);
|
||||
Assert.Equal(formulaValue.Value, typedValue.Value);
|
||||
|
||||
TimeValue formulaCopy = Assert.IsType<TimeValue>(typedValue.ToFormulaValue());
|
||||
TimeValue formulaCopy = Assert.IsType<TimeValue>(typedValue.ToFormula());
|
||||
Assert.Equal(typedValue.Value, formulaCopy.Value);
|
||||
|
||||
Assert.Equal("10:35:00", formulaValue.Format());
|
||||
@@ -158,7 +158,7 @@ public class FormulaValueExtensionsTests
|
||||
Assert.Contains(property.Key, formulaValue.Fields.Select(field => field.Name));
|
||||
}
|
||||
|
||||
RecordValue formulaCopy = Assert.IsType<RecordValue>(dataValue.ToFormulaValue(), exactMatch: false);
|
||||
RecordValue formulaCopy = Assert.IsType<RecordValue>(dataValue.ToFormula(), exactMatch: false);
|
||||
Assert.Equal(formulaCopy.Fields.Count(), dataValue.Properties.Count);
|
||||
foreach (NamedValue field in formulaCopy.Fields)
|
||||
{
|
||||
@@ -174,6 +174,16 @@ public class FormulaValueExtensionsTests
|
||||
}
|
||||
""",
|
||||
formulaValue.Format().Replace(Environment.NewLine, "\n"));
|
||||
|
||||
Dictionary<string, int> source =
|
||||
new()
|
||||
{
|
||||
["FieldA"] = 1,
|
||||
["FieldB"] = 2,
|
||||
["FieldC"] = 3
|
||||
};
|
||||
FormulaValue formula = source.ToFormula();
|
||||
Assert.IsType<RecordValue>(formula, exactMatch: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -188,7 +198,7 @@ public class FormulaValueExtensionsTests
|
||||
TableDataValue dataValue = formulaValue.ToTable();
|
||||
Assert.Equal(formulaValue.Rows.Count(), dataValue.Values.Length);
|
||||
|
||||
TableValue formulaCopy = Assert.IsType<TableValue>(dataValue.ToFormulaValue(), exactMatch: false);
|
||||
TableValue formulaCopy = Assert.IsType<TableValue>(dataValue.ToFormula(), exactMatch: false);
|
||||
Assert.Equal(formulaCopy.Rows.Count(), dataValue.Values.Length);
|
||||
|
||||
Assert.Equal(
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ public sealed class SendActivityExecutorTest(ITestOutputHelper output) : Workflo
|
||||
|
||||
// Assert
|
||||
this.VerifyModel(model, action);
|
||||
Assert.Contains(events, e => e is AgentRunResponseEvent);
|
||||
Assert.Contains(events, e => e is MessageActivityEvent);
|
||||
}
|
||||
|
||||
private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null)
|
||||
|
||||
+4
-4
@@ -14,7 +14,7 @@ using Xunit.Abstractions;
|
||||
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Base test class for <see cref="WorkflowActionExecutor"/> implementations.
|
||||
/// Base test class for <see cref="DeclarativeActionExecutor"/> implementations.
|
||||
/// </summary>
|
||||
public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : WorkflowTest(output)
|
||||
{
|
||||
@@ -26,7 +26,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
|
||||
|
||||
protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}";
|
||||
|
||||
internal async Task<WorkflowEvent[]> Execute(WorkflowActionExecutor executor)
|
||||
internal async Task<WorkflowEvent[]> Execute(DeclarativeActionExecutor executor)
|
||||
{
|
||||
TestWorkflowExecutor workflowExecutor = new();
|
||||
WorkflowBuilder workflowBuilder = new(workflowExecutor);
|
||||
@@ -38,7 +38,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
|
||||
return events;
|
||||
}
|
||||
|
||||
internal void VerifyModel(DialogAction model, WorkflowActionExecutor action)
|
||||
internal void VerifyModel(DialogAction model, DeclarativeActionExecutor action)
|
||||
{
|
||||
Assert.Equal(model.Id, action.Id);
|
||||
Assert.Equal(model, action.Model);
|
||||
@@ -80,7 +80,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
|
||||
{
|
||||
public async ValueTask HandleAsync(WorkflowScopes message, IWorkflowContext context)
|
||||
{
|
||||
await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+140
-110
@@ -81,54 +81,64 @@ beginDialog:
|
||||
id: sendActivity_yFsbRy
|
||||
activity: Analyzing facts...
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
- kind: CreateConversation
|
||||
id: conversation_1a2b3c
|
||||
conversationId: Topic.InternalConversationId
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_UDoMUw
|
||||
displayName: Get Facts
|
||||
autoSend: false
|
||||
variable: Topic.TaskFacts
|
||||
userInput: =Topic.InputTask
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHANALYST},
|
||||
In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability.
|
||||
Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from.
|
||||
conversationId: =Topic.InternalConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_RESEARCHANALYST
|
||||
output:
|
||||
messages: Topic.TaskFacts
|
||||
input:
|
||||
messages: =[UserMessage(Topic.InputTask)]
|
||||
additionalInstructions: |-
|
||||
In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability.
|
||||
Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from.
|
||||
|
||||
Here is the pre-survey:
|
||||
Here is the pre-survey:
|
||||
|
||||
1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none.
|
||||
2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself.
|
||||
3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation)
|
||||
4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc.
|
||||
1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none.
|
||||
2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself.
|
||||
3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation)
|
||||
4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc.
|
||||
|
||||
When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings:
|
||||
When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings:
|
||||
|
||||
1. GIVEN OR VERIFIED FACTS
|
||||
2. FACTS TO LOOK UP
|
||||
3. FACTS TO DERIVE
|
||||
4. EDUCATED GUESSES
|
||||
1. GIVEN OR VERIFIED FACTS
|
||||
2. FACTS TO LOOK UP
|
||||
3. FACTS TO DERIVE
|
||||
4. EDUCATED GUESSES
|
||||
|
||||
DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so.
|
||||
DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so.
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_yFsbRz
|
||||
activity: Creating a plan...
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_DsBaJU
|
||||
displayName: Create a Plan
|
||||
autoSend: false
|
||||
variable: Topic.Plan
|
||||
userInput: =""
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHMANAGER},
|
||||
Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request.
|
||||
conversationId: =Topic.InternalConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER
|
||||
output:
|
||||
messages: Topic.Plan
|
||||
input:
|
||||
messages: =[UserMessage(Topic.InputTask)]
|
||||
additionalInstructions: |-
|
||||
Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request.
|
||||
|
||||
Only select the following team which is listed as "- [Name]: [Description]"
|
||||
Only select the following team which is listed as "- [Name]: [Description]"
|
||||
|
||||
{Topic.TeamDescription}
|
||||
{Topic.TeamDescription}
|
||||
|
||||
The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]"
|
||||
The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]"
|
||||
|
||||
Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task.
|
||||
Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task.
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_Kk2LDL
|
||||
@@ -162,54 +172,57 @@ beginDialog:
|
||||
id: sendActivity_bwNZiM
|
||||
activity: {Topic.TaskInstructions}
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_o3BQkf
|
||||
displayName: Progress Ledger Prompt
|
||||
autoSend: false
|
||||
variable: Topic.ProgressLedgerUpdate
|
||||
userInput: =Topic.AgentResponseText
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHMANAGER},
|
||||
Recall we are working on the following request:
|
||||
conversationId: =Topic.InternalConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER
|
||||
output:
|
||||
messages: Topic.ProgressLedgerUpdate
|
||||
input:
|
||||
messages: =[UserMessage(Topic.AgentResponseText)]
|
||||
additionalInstructions: |-
|
||||
Recall we are working on the following request:
|
||||
|
||||
{Topic.InputTask}
|
||||
{Topic.InputTask}
|
||||
|
||||
And we have assembled the following team:
|
||||
And we have assembled the following team:
|
||||
|
||||
{Topic.TeamDescription}
|
||||
{Topic.TeamDescription}
|
||||
|
||||
To make progress on the request, please answer the following questions, including necessary reasoning:
|
||||
To make progress on the request, please answer the following questions, including necessary reasoning:
|
||||
|
||||
- Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed)
|
||||
- Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times.
|
||||
- Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file)
|
||||
- Who should speak next? (select from: {Concat(Topic.AvailableAgents, name, ",")})
|
||||
- What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need)
|
||||
- Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed)
|
||||
- Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times.
|
||||
- Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file)
|
||||
- Who should speak next? (select from: {Concat(Topic.AvailableAgents, name, ",")})
|
||||
- What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need)
|
||||
|
||||
Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA:
|
||||
Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA:
|
||||
|
||||
{{
|
||||
"is_request_satisfied": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"is_in_loop": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"is_progress_being_made": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"next_speaker": {{
|
||||
"reason": string,
|
||||
"answer": string (select from: {Concat(Topic.AvailableAgents, name, ",")})
|
||||
}},
|
||||
"instruction_or_question": {{
|
||||
"reason": string,
|
||||
"answer": string
|
||||
}}
|
||||
}}
|
||||
{{
|
||||
"is_request_satisfied": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"is_in_loop": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"is_progress_being_made": {{
|
||||
"reason": string,
|
||||
"answer": boolean
|
||||
}},
|
||||
"next_speaker": {{
|
||||
"reason": string,
|
||||
"answer": string (select from: {Concat(Topic.AvailableAgents, name, ",")})
|
||||
}},
|
||||
"instruction_or_question": {{
|
||||
"reason": string,
|
||||
"answer": string
|
||||
}}
|
||||
}}
|
||||
|
||||
- kind: ParseValue
|
||||
id: parse_rNZtlV
|
||||
@@ -266,16 +279,20 @@ beginDialog:
|
||||
id: sendActivity_kdl3mC
|
||||
activity: Completed! {Topic.TypedProgressLedger.is_request_satisfied.reason}
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_Ke3l1d
|
||||
displayName: Generate Response
|
||||
variable: Topic.FinalResponse
|
||||
userInput: =Topic.SeedTask
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHMANAGER},
|
||||
We have completed the task.
|
||||
Based only on the conversation and without adding any new information, synthesize the result of the conversation as a complete response to the user task.
|
||||
The user will only every see this last response and not the entire conversation, so please ensure it is complete and self-contained.
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER
|
||||
output:
|
||||
messages: Topic.FinalResponse
|
||||
input:
|
||||
messages: =[UserMessage(Topic.SeedTask)]
|
||||
additionalInstructions: |-
|
||||
We have completed the task.
|
||||
Based only on the conversation and without adding any new information, synthesize the result of the conversation as a complete response to the user task.
|
||||
The user will only every see this last response and not the entire conversation, so please ensure it is complete and self-contained.
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_SVoNSV
|
||||
@@ -342,45 +359,53 @@ beginDialog:
|
||||
id: sendActivity_cwNZiM
|
||||
activity: Re-analyzing facts...
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_wFJ123
|
||||
displayName: Get New Facts Prompt
|
||||
autoSend: false
|
||||
variable: Topic.TaskFacts
|
||||
userInput: |-
|
||||
="As a reminder, we are working to solve the following task:
|
||||
conversationId: =Topic.InternalConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_RESEARCHANALYST
|
||||
output:
|
||||
messages: Topic.TaskFacts
|
||||
input:
|
||||
messages: |-
|
||||
=[
|
||||
UserMessage(
|
||||
"As a reminder, we are working to solve the following task:
|
||||
|
||||
" & Topic.InputTask
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHANALYST},
|
||||
It's clear we aren't making as much progress as we would like, but we may have learned something new.
|
||||
Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful.
|
||||
Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc.
|
||||
Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited.
|
||||
This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning.
|
||||
" & Topic.InputTask)
|
||||
]
|
||||
additionalInstructions: |-
|
||||
It's clear we aren't making as much progress as we would like, but we may have learned something new.
|
||||
Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful.
|
||||
Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc.
|
||||
Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited.
|
||||
This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning.
|
||||
|
||||
Here is the old fact sheet:
|
||||
Here is the old fact sheet:
|
||||
|
||||
{Topic.TaskFacts}
|
||||
{Topic.TaskFacts}
|
||||
|
||||
- kind: SendActivity
|
||||
id: sendActivity_dsBaJU
|
||||
activity: Re-analyzing plan...
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_uEJ456
|
||||
displayName: Create new Plan Prompt
|
||||
autoSend: false
|
||||
variable: Topic.Plan
|
||||
userInput: =""
|
||||
additionalInstructions: |-
|
||||
{Env.FOUNDRY_AGENT_RESEARCHMANAGER},
|
||||
Please briefly explain what went wrong on this last run (the root cause of the failure),
|
||||
and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes.
|
||||
As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition
|
||||
(do not involve any other outside people since we cannot contact anyone else):
|
||||
conversationId: =Topic.InternalConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER
|
||||
output:
|
||||
messages: Topic.Plan
|
||||
input:
|
||||
additionalInstructions: |-
|
||||
Please briefly explain what went wrong on this last run (the root cause of the failure),
|
||||
and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes.
|
||||
As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition
|
||||
(do not involve any other outside people since we cannot contact anyone else):
|
||||
|
||||
{Topic.TeamDescription}
|
||||
{Topic.TeamDescription}
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_jW7tmM
|
||||
@@ -452,13 +477,18 @@ beginDialog:
|
||||
displayName: If next Agent tool Exists
|
||||
actions:
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_orsBf06
|
||||
variable: Topic.AgentResponse
|
||||
userInput: =Topic.SeedTask
|
||||
additionalInstructions: |-
|
||||
{First(Topic.NextSpeaker).agentid},
|
||||
{Topic.TypedProgressLedger.instruction_or_question.answer}
|
||||
displayName: Progress Ledger Prompt
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =First(Topic.NextSpeaker).agentid
|
||||
output:
|
||||
messages: Topic.AgentResponse
|
||||
input:
|
||||
messages: =[UserMessage(Topic.SeedTask)]
|
||||
additionalInstructions: |-
|
||||
{Topic.TypedProgressLedger.instruction_or_question.answer}
|
||||
|
||||
- kind: SetVariable
|
||||
id: setVariable_XzNrdM
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#
|
||||
# This workflow demonstrates a single agent interaction based on user input.
|
||||
#
|
||||
# Any Foundry Agent may be used to provide the response.
|
||||
# See: ./setup/QuestionAgent.yaml
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
# Capture original input
|
||||
- kind: SetVariable
|
||||
id: set_project
|
||||
variable: Topic.OriginalInput
|
||||
value: =System.LastMessage.Text
|
||||
|
||||
# Request input from user
|
||||
- kind: Question
|
||||
id: question_confirm
|
||||
alwaysPrompt: false
|
||||
property: Topic.ConfirmedInput
|
||||
prompt:
|
||||
kind: Message
|
||||
text:
|
||||
- "CONFIRM:"
|
||||
entity:
|
||||
kind: StringPrebuiltEntity
|
||||
|
||||
# Respond with input
|
||||
- kind: SendActivity
|
||||
id: sendActivity_input
|
||||
activity: |-
|
||||
You entered:
|
||||
{Topic.OriginalInput}
|
||||
|
||||
Confirmed input:
|
||||
{Topic.ConfirmedInput}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#
|
||||
# This workflow demonstrates a single agent interaction based on user input.
|
||||
#
|
||||
# Any Foundry Agent may be used to provide the response.
|
||||
# See: ./setup/QuestionAgent.yaml
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_analyst
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_ANSWER
|
||||
input:
|
||||
messages: =[UserMessage(System.LastMessageText)]
|
||||
additionalInstructions: |-
|
||||
You are a marketing analyst. Given a product description, identify:
|
||||
- Key features
|
||||
- Target audience
|
||||
- Unique selling points
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_writer
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_ANSWER
|
||||
input:
|
||||
additionalInstructions: |-
|
||||
You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
|
||||
compose a compelling marketing copy (like a newsletter section) that highlights these points.
|
||||
Output should be short (around 150 words), output just the copy as a single text block.
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_editor
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_ANSWER
|
||||
input:
|
||||
additionalInstructions: |-
|
||||
You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
|
||||
give format and make it polished. Output the final improved copy as a single text block.
|
||||
+14
-8
@@ -34,21 +34,27 @@ beginDialog:
|
||||
- kind: SetVariable
|
||||
id: set_project
|
||||
variable: Topic.Project
|
||||
value: =System.LastMessage.Text
|
||||
value: =System.LastMessageText
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_student
|
||||
userInput: =Topic.Project
|
||||
additionalInstructions: {Env.FOUNDRY_AGENT_STUDENT}
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_STUDENT
|
||||
input:
|
||||
messages: =[UserMessage(Topic.Project)]
|
||||
output:
|
||||
messages: Topic.Answer
|
||||
|
||||
- kind: ResetVariable
|
||||
id: reset_project
|
||||
variable: Topic.Project
|
||||
|
||||
- kind: AnswerQuestionWithAI
|
||||
- kind: InvokeAzureAgent
|
||||
id: question_teacher
|
||||
userInput: =""
|
||||
additionalInstructions: {Env.FOUNDRY_AGENT_TEACHER}
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: =Env.FOUNDRY_AGENT_TEACHER
|
||||
|
||||
- kind: SetVariable
|
||||
id: set_count_increment
|
||||
@@ -59,7 +65,7 @@ beginDialog:
|
||||
id: check_completion
|
||||
conditions:
|
||||
|
||||
- condition: =!IsBlank(Find("congratulations", Lower(System.LastMessage.Text)))
|
||||
- condition: =!IsBlank(Find("congratulations", Lower(System.LastMessageText)))
|
||||
id: check_turn_done
|
||||
actions:
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#
|
||||
# This workflow demonstrates a single agent interaction based on user input.
|
||||
#
|
||||
# Any Foundry Agent may be used to provide the response.
|
||||
# See: ./setup/QuestionAgent.yaml
|
||||
#
|
||||
kind: AdaptiveDialog
|
||||
beginDialog:
|
||||
|
||||
kind: OnActivity
|
||||
id: workflow_demo
|
||||
actions:
|
||||
|
||||
# Respond with input
|
||||
- kind: SendActivity
|
||||
id: sendActivity_demo
|
||||
activity: "Working..."
|
||||
|
||||
# Use AI to answer the question
|
||||
- kind: AnswerQuestionWithAI
|
||||
id: question_demo
|
||||
variable: Topic.Answer
|
||||
userInput: =System.LastMessage.Text
|
||||
additionalInstructions: {Env.FOUNDRY_AGENT_ANSWER}
|
||||
Reference in New Issue
Block a user