mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflows - Support "structured inputs" feature for declarative workflows (#2053)
This commit is contained in:
@@ -7,10 +7,12 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents;
|
||||
using Azure.Core;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
@@ -32,6 +34,11 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
private AgentClient? _agentClient;
|
||||
private ConversationClient? _conversationClient;
|
||||
|
||||
/// <summary>
|
||||
/// Optional options used when creating the <see cref="AgentClient"/>.
|
||||
/// </summary>
|
||||
public AgentClientOptions? ClientOptions { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<string> CreateConversationAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -78,6 +85,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
string? agentVersion,
|
||||
string? conversationId,
|
||||
IEnumerable<ChatMessage>? messages,
|
||||
IDictionary<string, object?>? inputArguments,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentVersion agentVersionResult = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false);
|
||||
@@ -90,6 +98,14 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
AllowMultipleToolCalls = this.AllowMultipleToolCalls,
|
||||
};
|
||||
|
||||
if (inputArguments is not null)
|
||||
{
|
||||
JsonNode jsonNode = inputArguments.ToFormula().ToJson();
|
||||
ResponseCreationOptions responseCreationOptions = new();
|
||||
responseCreationOptions.SetStructuredInputs(BinaryData.FromString(jsonNode.ToJsonString()));
|
||||
chatOptions.RawRepresentationFactory = (_) => responseCreationOptions;
|
||||
}
|
||||
|
||||
ChatClientAgentRunOptions runOptions = new(chatOptions);
|
||||
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentResponse =
|
||||
@@ -206,7 +222,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj
|
||||
{
|
||||
if (this._agentClient is null)
|
||||
{
|
||||
AgentClientOptions clientOptions = new();
|
||||
AgentClientOptions clientOptions = this.ClientOptions ?? new();
|
||||
|
||||
if (httpClient is not null)
|
||||
{
|
||||
|
||||
+2
-1
@@ -17,9 +17,10 @@ internal static class AgentProviderExtensions
|
||||
string? conversationId,
|
||||
bool autoSend,
|
||||
IEnumerable<ChatMessage>? inputMessages = null,
|
||||
IDictionary<string, object?>? inputArguments = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, cancellationToken);
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, inputArguments, cancellationToken);
|
||||
|
||||
// Enable "autoSend" behavior if this is the workflow conversation.
|
||||
bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? workflowConversationId);
|
||||
|
||||
+18
-17
@@ -163,6 +163,24 @@ internal static class FormulaValueExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static JsonNode ToJson(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
BooleanValue booleanValue => JsonValue.Create(booleanValue.Value),
|
||||
DecimalValue decimalValue => JsonValue.Create(decimalValue.Value),
|
||||
NumberValue numberValue => JsonValue.Create(numberValue.Value),
|
||||
DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"),
|
||||
StringValue stringValue => JsonValue.Create(stringValue.Value),
|
||||
GuidValue guidValue => JsonValue.Create(guidValue.Value),
|
||||
RecordValue recordValue => recordValue.ToJson(),
|
||||
TableValue tableValue => tableValue.ToJson(),
|
||||
BlankValue => JsonValue.Create(string.Empty),
|
||||
_ => $"[{value.GetType().Name}]",
|
||||
};
|
||||
|
||||
public static RecordValue ToRecord(this Dictionary<string, PortableValue> value) =>
|
||||
FormulaValue.NewRecordFromFields(
|
||||
value.Select(
|
||||
@@ -256,23 +274,6 @@ internal static class FormulaValueExtensions
|
||||
|
||||
private static KeyValuePair<string, DataValue> GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue());
|
||||
|
||||
private static JsonNode ToJson(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
BooleanValue booleanValue => JsonValue.Create(booleanValue.Value),
|
||||
DecimalValue decimalValue => JsonValue.Create(decimalValue.Value),
|
||||
NumberValue numberValue => JsonValue.Create(numberValue.Value),
|
||||
DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
|
||||
TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"),
|
||||
StringValue stringValue => JsonValue.Create(stringValue.Value),
|
||||
GuidValue guidValue => JsonValue.Create(guidValue.Value),
|
||||
RecordValue recordValue => recordValue.ToJson(),
|
||||
TableValue tableValue => tableValue.ToJson(),
|
||||
BlankValue => JsonValue.Create(string.Empty),
|
||||
_ => $"[{value.GetType().Name}]",
|
||||
};
|
||||
|
||||
private static JsonArray ToJson(this TableValue value)
|
||||
{
|
||||
return new([.. GetJsonElements()]);
|
||||
|
||||
@@ -33,5 +33,5 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA
|
||||
bool autoSend,
|
||||
IEnumerable<ChatMessage>? inputMessages = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, cancellationToken);
|
||||
=> agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, inputArguments: null, cancellationToken);
|
||||
}
|
||||
|
||||
+19
-2
@@ -60,8 +60,8 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
string? conversationId = this.GetConversationId();
|
||||
string agentName = this.GetAgentName();
|
||||
bool autoSend = this.GetAutoSendValue();
|
||||
|
||||
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, cancellationToken).ConfigureAwait(false);
|
||||
Dictionary<string, object?>? inputParameters = this.GetStructuredInputs();
|
||||
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, inputParameters, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ChatMessage[] actionableMessages = FilterActionableContent(agentResponse).ToArray();
|
||||
if (actionableMessages.Length > 0)
|
||||
@@ -107,6 +107,23 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Dictionary<string, object?>? GetStructuredInputs()
|
||||
{
|
||||
Dictionary<string, object?>? inputs = null;
|
||||
|
||||
if (this.AgentInput?.Arguments is not null)
|
||||
{
|
||||
inputs = [];
|
||||
|
||||
foreach (KeyValuePair<string, ValueExpression> argument in this.AgentInput.Arguments)
|
||||
{
|
||||
inputs[argument.Key] = this.Evaluator.GetValue(argument.Value).Value.ToObject();
|
||||
}
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
private IEnumerable<ChatMessage>? GetInputMessages()
|
||||
{
|
||||
DataValue? userInput = null;
|
||||
|
||||
+2
-5
@@ -8,7 +8,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Bot.ObjectModel.Abstractions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
@@ -17,17 +16,15 @@ internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaStat
|
||||
{
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}");
|
||||
|
||||
if (this.Model.Value is null)
|
||||
{
|
||||
await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.Variable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(this.Model.Value);
|
||||
|
||||
await this.AssignAsync(variablePath, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.Variable?.Path, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal sealed class AgentMessage : MessageFunction
|
||||
{
|
||||
public const string FunctionName = nameof(AgentMessage);
|
||||
|
||||
public AgentMessage() : base(FunctionName) { }
|
||||
|
||||
public static FormulaValue Execute(StringValue input) => Create(ChatRole.Assistant, input);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal abstract class MessageFunction : ReflectionFunction
|
||||
{
|
||||
protected MessageFunction(string functionName)
|
||||
: base(functionName, FormulaType.String, FormulaType.String)
|
||||
{ }
|
||||
|
||||
protected static FormulaValue Create(ChatRole role, StringValue input) =>
|
||||
string.IsNullOrEmpty(input.Value) ?
|
||||
FormulaValue.NewBlank(RecordType.Empty()) :
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula()),
|
||||
new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(role.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))
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal static class MessageText
|
||||
{
|
||||
public const string FunctionName = nameof(MessageText);
|
||||
|
||||
public sealed class StringInput()
|
||||
: ReflectionFunction(FunctionName, FormulaType.String, FormulaType.String)
|
||||
{
|
||||
public static FormulaValue Execute(StringValue input) => input;
|
||||
}
|
||||
|
||||
public sealed class RecordInput() : ReflectionFunction(FunctionName, FormulaType.String, RecordType.Empty())
|
||||
{
|
||||
public static FormulaValue Execute(RecordValue input) => FormulaValue.New(GetTextFromRecord(input));
|
||||
}
|
||||
|
||||
public sealed class TableInput() : ReflectionFunction(FunctionName, FormulaType.String, TableType.Empty())
|
||||
{
|
||||
public static FormulaValue Execute(TableValue tableValue)
|
||||
{
|
||||
return FormulaValue.New(string.Join("\n", GetText()));
|
||||
|
||||
IEnumerable<string> GetText()
|
||||
{
|
||||
foreach (DValue<RecordValue> row in tableValue.Rows)
|
||||
{
|
||||
string text = GetTextFromRecord(row.Value);
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
yield return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTextFromRecord(RecordValue recordValue)
|
||||
{
|
||||
FormulaValue textValue = recordValue.GetField(TypeSchema.Message.Fields.Text);
|
||||
|
||||
return textValue switch
|
||||
{
|
||||
StringValue stringValue => stringValue.Value.Trim(),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
+3
-26
@@ -1,38 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
|
||||
|
||||
internal sealed class UserMessage : ReflectionFunction
|
||||
internal sealed class UserMessage : MessageFunction
|
||||
{
|
||||
public const string FunctionName = nameof(UserMessage);
|
||||
|
||||
public UserMessage()
|
||||
: base(FunctionName, FormulaType.String, FormulaType.String)
|
||||
{ }
|
||||
public UserMessage() : base(FunctionName) { }
|
||||
|
||||
public static FormulaValue Execute(StringValue input) =>
|
||||
string.IsNullOrEmpty(input.Value) ?
|
||||
FormulaValue.NewBlank(RecordType.Empty()) :
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula()),
|
||||
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))
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
public static FormulaValue Execute(StringValue input) => Create(ChatRole.User, input);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,11 @@ internal static class RecalcEngineFactory
|
||||
}
|
||||
|
||||
config.EnableSetFunction();
|
||||
config.AddFunction(new AgentMessage());
|
||||
config.AddFunction(new UserMessage());
|
||||
config.AddFunction(new MessageText.StringInput());
|
||||
config.AddFunction(new MessageText.RecordInput());
|
||||
config.AddFunction(new MessageText.TableInput());
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -87,9 +87,16 @@ public abstract class WorkflowAgentProvider
|
||||
/// <param name="agentVersion">An optional agent version.</param>
|
||||
/// <param name="conversationId">Optional identifier of the target conversation.</param>
|
||||
/// <param name="messages">The messages to include in the invocation.</param>
|
||||
/// <param name="inputArguments">Optional input arguments for agents that provide support.</param>
|
||||
/// <param name="cancellationToken">A token that propagates notification when operation should be canceled.</param>
|
||||
/// <returns>Asynchronous set of <see cref="AgentRunResponseUpdate"/>.</returns>
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> InvokeAgentAsync(string agentId, string? agentVersion, string? conversationId, IEnumerable<ChatMessage>? messages, CancellationToken cancellationToken = default);
|
||||
public abstract IAsyncEnumerable<AgentRunResponseUpdate> InvokeAgentAsync(
|
||||
string agentId,
|
||||
string? agentVersion,
|
||||
string? conversationId,
|
||||
IEnumerable<ChatMessage>? messages,
|
||||
IDictionary<string, object?>? inputArguments,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a set of messages from a conversation.
|
||||
|
||||
Reference in New Issue
Block a user