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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user