.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:
Chris
2025-09-15 13:41:07 -07:00
committed by GitHub
Unverified
parent db58a10a37
commit 74879489a4
79 changed files with 2830 additions and 685 deletions
@@ -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;
}
}
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Workflows.Declarative.Extensions;
using Microsoft.Bot.ObjectModel;
namespace Microsoft.Agents.Workflows.Declarative;
/// <summary>
/// Event that indicates a declarative action has been invoked.
/// </summary>
public sealed class DeclarativeActionInvokeEvent : WorkflowEvent
{
/// <summary>
/// The declarative action id.
/// </summary>
public string ActionId { get; }
/// <summary>
/// The declarative action type name.
/// </summary>
public string ActionType { get; }
/// <summary>
/// Identifier of the parent action.
/// </summary>
public string? ParentActionId { get; }
/// <summary>
/// Identifier of the previous action.
/// </summary>
public string? PriorActionId { get; }
internal DeclarativeActionInvokeEvent(DialogAction action, string? priorActionId) : base(action)
{
this.ActionId = action.GetId();
this.ActionType = action.GetType().Name;
this.ParentActionId = action.GetParentId();
this.PriorActionId = priorActionId;
}
}
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Workflows.Declarative.Extensions;
using Microsoft.Bot.ObjectModel;
namespace Microsoft.Agents.Workflows.Declarative;
/// <summary>
/// Event that indicates a declarative action has completed.
/// </summary>
public sealed class DeclarativeActionCompleteEvent : WorkflowEvent
{
/// <summary>
/// The declarative action identifier.
/// </summary>
public string ActionId { get; }
/// <summary>
/// The declarative action type name.
/// </summary>
public string ActionType { get; }
/// <summary>
/// Identifier of the parent action.
/// </summary>
public string? ParentActionId { get; }
internal DeclarativeActionCompleteEvent(DialogAction action) : base(action)
{
this.ActionId = action.GetId();
this.ActionType = action.GetType().Name;
this.ParentActionId = action.GetParentId();
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.Workflows.Declarative.Events;
/// <summary>
/// Represents a request for user input.
/// </summary>
public sealed class InputRequest
{
/// <summary>
/// The prompt message to display to the user.
/// </summary>
public string Prompt { get; }
internal InputRequest(string prompt)
{
this.Prompt = prompt;
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.Workflows.Declarative.Events;
/// <summary>
/// Represents a user input response.
/// </summary>
public sealed class InputResponse
{
/// <summary>
/// The response value.
/// </summary>
public string Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="InputResponse"/> class.
/// </summary>
/// <param name="value">The response value.</param>
public InputResponse(string value)
{
this.Value = value;
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.Workflows.Declarative;
/// <summary>
/// Event that broadcasts the conversation identifier.
/// </summary>
public sealed class MessageActivityEvent : WorkflowEvent
{
/// <summary>
/// The conversation ID associated with the workflow.
/// </summary>
public string Message { get; }
internal MessageActivityEvent(string message) : base(message)
{
this.Message = message;
}
}
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
/// <summary>
/// Represents an exception that occurs when the declarative model is not supported.
/// </summary>
public class DeclarativeModelException : DeclarativeWorkflowException
public sealed class DeclarativeModelException : DeclarativeWorkflowException
{
/// <summary>
/// Initializes a new instance of the <see cref="DeclarativeModelException"/> class.
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
@@ -9,15 +12,234 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions;
internal static class ChatMessageExtensions
{
// ISSUE #485 - Align with message type updated OM is available.
public static RecordValue ToRecord(this ChatMessage message) =>
RecordValue.NewRecordFromFields(message.GetMessageFields());
public static TableValue ToTable(this IEnumerable<ChatMessage> messages) =>
FormulaValue.NewTable(s_messageRecordType, messages.Select(message => message.ToRecord()));
public static IEnumerable<ChatMessage> ToChatMessages(this DataValue messages)
{
if (messages is TableDataValue table)
{
return table.ToChatMessages();
}
if (messages is RecordDataValue record)
{
return [record.ToChatMessage()];
}
if (messages is StringDataValue text)
{
return [text.ToChatMessage()];
}
return [];
}
public static IEnumerable<ChatMessage> ToChatMessages(this TableDataValue messages)
{
foreach (DataValue message in messages.Values)
{
if (message is RecordDataValue record)
{
if (record.Properties.Count == 1 && record.Properties.TryGetValue("Value", out DataValue? singleColumn))
{
record = singleColumn as RecordDataValue ?? record;
}
ChatMessage? convertedMessage = record.ToChatMessage();
if (convertedMessage is not null)
{
yield return convertedMessage;
}
}
else if (message is StringDataValue text)
{
yield return ToChatMessage(text);
}
}
}
public static ChatMessage? ToChatMessage(this DataValue message)
{
if (message is RecordDataValue record)
{
return record.ToChatMessage();
}
if (message is StringDataValue text)
{
return text.ToChatMessage();
}
if (message is BlankDataValue)
{
return null;
}
throw new DeclarativeActionException($"Unable to convert {message.GetDataType()} to {nameof(ChatMessage)}.");
}
public static ChatMessage ToChatMessage(this RecordDataValue message) =>
new(message.GetRole(), [.. message.GetContent()])
{
AdditionalProperties = message.GetProperty<RecordDataValue>("metadata").ToMetadata()
};
public static ChatMessage ToChatMessage(this StringDataValue message) => new(ChatRole.User, message.Value);
public static AdditionalPropertiesDictionary? ToMetadata(this RecordDataValue? metadata)
{
if (metadata is null)
{
return null;
}
AdditionalPropertiesDictionary properties = [];
foreach (KeyValuePair<string, DataValue> property in metadata.Properties)
{
properties[property.Key] = property.Value.ToObject();
}
return properties;
}
public static ChatRole ToChatRole(this AgentMessageRole role) =>
role switch
{
AgentMessageRole.Agent => ChatRole.Assistant,
AgentMessageRole.User => ChatRole.User,
_ => ChatRole.User
};
public static ChatRole ToChatRole(this AgentMessageRole? role) => role?.ToChatRole() ?? ChatRole.User;
public static AIContent? ToContent(this AgentMessageContentType contentType, string? contentValue)
{
if (string.IsNullOrEmpty(contentValue))
{
return null;
}
return
contentType switch
{
AgentMessageContentType.ImageUrl => new UriContent(contentValue, "image/*"),
AgentMessageContentType.ImageFile => new HostedFileContent(contentValue),
_ => new TextContent(contentValue)
};
}
private static ChatRole GetRole(this RecordDataValue message)
{
StringDataValue? roleValue = message.GetProperty<StringDataValue>(TypeSchema.Message.Fields.Role);
if (roleValue is null || string.IsNullOrWhiteSpace(roleValue.Value))
{
return ChatRole.User;
}
AgentMessageRole? role = null;
if (Enum.TryParse<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());
}
@@ -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;
}
}
@@ -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());
@@ -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)
@@ -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));
}
@@ -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);
}
}
@@ -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;
@@ -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);
}
}
}
@@ -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);
}
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
internal sealed record class ExecutorResultMessage(string ExecutorId, object? Result = null)
{
public static ExecutorResultMessage ThrowIfNot(object? message)
{
if (message is not ExecutorResultMessage executorMessage)
{
throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ExecutorResultMessage)})");
}
return executorMessage;
}
}
@@ -3,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)}");
}
@@ -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();
}
}
@@ -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);
}
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
@@ -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;
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
@@ -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;
}
}
@@ -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);
}