mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflows - WIP Declarative action update (#1761)
* WIP * Fixed build errors (#1638) Comment and nullable type alignment * Sync to SDK update * Checkpoint * Checkpoint: Tests passing * Checkpoint: EndWorkflow * Add trace * .NET: Azure.AI.Agents Package Split + Initial Extensions (#1657) * Move packages * Update nuget.config * Address Xmldoc * Remove format from branches checks * Address Xmldocs * Add more details to the implementation * Moving Agent logic to ChatClient * Adding Name and Id overrides to AzureAIAgent * Updating extensions * Add GetAiAgent extensions * Adding support for version as name can conflict 409 using the Agents API with same name * Addressing more updates to the extensions * More improvements * Remove debugging code from sample * Address copilot feedback * Apply suggestions from co-pilot code review * Checkpoint * Update Directory.Packages.props Fix package version rollback: Azure.AI.Agents.Persistent (beta-6 => beta-7) * Add project reference * .NET: Add comprehensive unit tests for Microsoft.Agents.AI.AzureAIAgents extension methods (#1786) * Initial plan * Add comprehensive unit test project for Microsoft.Agents.AI.AzureAIAgents Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Add README documenting test project and package dependency requirements Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Fix documentation URL to use learn.microsoft.com Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Bump back AAAP 1.2.0-beta.7 * Address AI generated UT's * Remove UT Readme * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * .NET: Change model to be required just for prompt agent definition specific extensions (#1812) * Remove unneeded model from extensions * Add noop justification * Update Package Nameing: V1 -> AzureAI.Persistent / V2 -> AzureAI (#1829) * Checkpoint for merge * No build errors * .NET: Update Extensions for Strict Agent Definitions + Improvements (#1892) * Update Package Nameing: V1 -> AzureAI.Persistent / V2 -> AzureAI * Update agents and extensions to comply with strict agent definitions * More static updates * Address UT, and ResponseTool support * Improving reusability extensions * Addressing ResponseTools Unit Tests and extension setup * Adapted workaround on breaking AAA with OpenAI 2.6.0 * Small updates * Remove strictness when retrieving agents, improved XmlDocs * Improve sample comments * Update dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AgentsClientExtensionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address PR comments * Address UT failing * Address Copilot feedback * Address Copilot feedback * Address comment typo * Address PR feedback * Address typo * Add missing Extensions with ChatClientAgentOptions * Address comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updated package version (#1897) * Version update (#1901) * Checkpoint * Updated package version (#1906) * Checkpoint * Checkpoint * Checkpoint * Align with azure ai agent * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/MCPToolApproval/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactored external input * Update dotnet/samples/GettingStarted/Workflows/Declarative/MCPToolApproval/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Agent tools patch * Demos validated * Checkpoint * Hygiene * Checkpoint - Samples * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Checkpoint * Checkpoint - Deep Research * Update baseline * Update * Typo * Checkpoint * Typos * Sample cleanup * Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.AzureAI/AgentsClientExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Typo * Typo * Fix input loop * Sample - Function Calling / External Input * Typo * Finessed * Checkpoint * Fix feed * Checkpoint - so close * Ding dong! * "there" *** * Fixup comments * Fix sample * Code analysis * Header * Typo (variableName) * Remove dead code * Skip test (agent api ratchet) * Comment * Update dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Typo --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
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.AI.Agents;
|
||||
using Azure.Core;
|
||||
using Azure.Core.Pipeline;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative;
|
||||
|
||||
@@ -18,31 +21,23 @@ namespace Microsoft.Agents.AI.Workflows.Declarative;
|
||||
/// </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="projectEndpoint">A <see cref="Uri"/> instance representing 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
|
||||
public sealed class AzureAgentProvider(Uri 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 readonly Dictionary<string, AgentVersion> _versionCache = [];
|
||||
private readonly Dictionary<string, AIAgent> _agentCache = [];
|
||||
|
||||
private PersistentAgentsClient? _agentsClient;
|
||||
private AgentsClient? _agentsClient;
|
||||
private ConversationClient? _conversationClient;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<string> CreateConversationAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
PersistentAgentThread conversation =
|
||||
await this.GetAgentsClient().Threads.CreateThreadAsync(
|
||||
messages: null,
|
||||
toolResources: null,
|
||||
metadata: null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
AgentConversation conversation =
|
||||
await this.GetConversationClient()
|
||||
.CreateConversationAsync(options: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return conversation.Id;
|
||||
}
|
||||
@@ -50,61 +45,117 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatMessage> CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PersistentThreadMessage newMessage =
|
||||
await this.GetAgentsClient().Messages.CreateMessageAsync(
|
||||
ReadOnlyCollection<ResponseItem> newItems =
|
||||
await this.GetConversationClient().CreateConversationItemsAsync(
|
||||
conversationId,
|
||||
role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()],
|
||||
contentBlocks: GetContent(),
|
||||
attachments: null,
|
||||
metadata: GetMetadata(),
|
||||
items: GetResponseItems(),
|
||||
include: null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return ToChatMessage(newMessage);
|
||||
return newItems.AsChatMessages().Single();
|
||||
|
||||
Dictionary<string, string>? GetMetadata()
|
||||
IEnumerable<ResponseItem> GetResponseItems()
|
||||
{
|
||||
if (conversationMessage.AdditionalProperties is null)
|
||||
IEnumerable<ChatMessage> messages = [conversationMessage];
|
||||
|
||||
foreach (ResponseItem item in messages.AsOpenAIResponseItems())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return conversationMessage.AdditionalProperties.ToDictionary(prop => prop.Key, prop => prop.Value?.ToString() ?? string.Empty);
|
||||
}
|
||||
|
||||
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())),
|
||||
DataContent dataContent when dataContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(dataContent.Uri)),
|
||||
_ => null // Unsupported content type
|
||||
};
|
||||
|
||||
if (contentBlock is not null)
|
||||
if (string.IsNullOrEmpty(item.Id))
|
||||
{
|
||||
yield return contentBlock;
|
||||
yield return item;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new ReferenceResponseItem(item.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default)
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> InvokeAgentAsync(
|
||||
string agentId,
|
||||
string? agentVersion,
|
||||
string? conversationId,
|
||||
IEnumerable<ChatMessage>? messages,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChatClientAgent agent =
|
||||
await this.GetAgentsClient().GetAIAgentAsync(
|
||||
agentId,
|
||||
new ChatOptions()
|
||||
{
|
||||
AllowMultipleToolCalls = this.AllowMultipleToolCalls,
|
||||
},
|
||||
clientFactory: null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
AgentVersion agentDefinition = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false);
|
||||
AIAgent agent = await this.GetAgentAsync(agentDefinition, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ChatOptions chatOptions =
|
||||
new()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
AllowMultipleToolCalls = this.AllowMultipleToolCalls,
|
||||
};
|
||||
|
||||
ChatClientAgentRunOptions runOptions = new(chatOptions);
|
||||
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentResponse =
|
||||
messages is not null ?
|
||||
agent.RunStreamingAsync([.. messages], null, runOptions, cancellationToken) :
|
||||
agent.RunStreamingAsync([new ChatMessage(ChatRole.User, string.Empty)], null, runOptions, cancellationToken);
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in agentResponse.ConfigureAwait(false))
|
||||
{
|
||||
update.AuthorName = agentDefinition.Name;
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AgentVersion> QueryAgentAsync(string agentName, string? agentVersion, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string agentKey = $"{agentName}:{agentVersion}";
|
||||
if (this._versionCache.TryGetValue(agentKey, out AgentVersion? targetAgent))
|
||||
{
|
||||
return targetAgent;
|
||||
}
|
||||
|
||||
AgentsClient client = this.GetAgentsClient();
|
||||
|
||||
if (string.IsNullOrEmpty(agentVersion))
|
||||
{
|
||||
AgentRecord agentRecord =
|
||||
await client.GetAgentAsync(
|
||||
agentName,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
targetAgent = agentRecord.Versions.Latest;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAgent =
|
||||
await client.GetAgentVersionAsync(
|
||||
agentName,
|
||||
agentVersion,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._versionCache[agentKey] = targetAgent;
|
||||
|
||||
return targetAgent;
|
||||
}
|
||||
|
||||
private async Task<AIAgent> GetAgentAsync(AgentVersion agentDefinition, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._agentCache.TryGetValue(agentDefinition.Id, out AIAgent? agent))
|
||||
{
|
||||
return agent;
|
||||
}
|
||||
|
||||
AgentsClient client = this.GetAgentsClient();
|
||||
|
||||
IList<AITool>? tools = null;
|
||||
if (agentDefinition.Definition is PromptAgentDefinition promptAgent)
|
||||
{
|
||||
tools =
|
||||
promptAgent.Tools
|
||||
.Select(tool => tool.AsAITool())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
agent = client.GetAIAgent(agentDefinition, tools, clientFactory: null, openAIClientOptions: null, requireInvocableTools: false, cancellationToken);
|
||||
|
||||
FunctionInvokingChatClient? functionInvokingClient = agent.GetService<FunctionInvokingChatClient>();
|
||||
if (functionInvokingClient is not null)
|
||||
@@ -127,14 +178,17 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
}
|
||||
}
|
||||
|
||||
this._agentCache[agentDefinition.Id] = agent;
|
||||
|
||||
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);
|
||||
AgentResponseItem responseItem = await this.GetConversationClient().GetConversationItemAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false);
|
||||
ResponseItem[] items = [responseItem.AsOpenAIResponseItem()];
|
||||
return items.AsChatMessages().Single();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -146,25 +200,29 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
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))
|
||||
AgentsListOrder order = newestFirst ? AgentsListOrder.Asc : AgentsListOrder.Desc;
|
||||
await foreach (AgentResponseItem responseItem in this.GetConversationClient().GetConversationItemsAsync(conversationId, limit, order, after, before, itemType: null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return ToChatMessage(message);
|
||||
ResponseItem[] items = [responseItem.AsOpenAIResponseItem()];
|
||||
foreach (ChatMessage message in items.AsChatMessages())
|
||||
{
|
||||
yield return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PersistentAgentsClient GetAgentsClient()
|
||||
private AgentsClient GetAgentsClient()
|
||||
{
|
||||
if (this._agentsClient is null)
|
||||
{
|
||||
PersistentAgentsAdministrationClientOptions clientOptions = new();
|
||||
AgentsClientOptions clientOptions = new();
|
||||
|
||||
if (httpClient is not null)
|
||||
{
|
||||
clientOptions.Transport = new HttpClientTransport(httpClient);
|
||||
clientOptions.Transport = new HttpClientPipelineTransport(httpClient);
|
||||
}
|
||||
|
||||
PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions);
|
||||
AgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions);
|
||||
|
||||
Interlocked.CompareExchange(ref this._agentsClient, newClient, null);
|
||||
}
|
||||
@@ -172,43 +230,15 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
return this._agentsClient;
|
||||
}
|
||||
|
||||
private static ChatMessage ToChatMessage(PersistentThreadMessage message)
|
||||
private ConversationClient GetConversationClient()
|
||||
{
|
||||
return
|
||||
new ChatMessage(new ChatRole(message.Role.ToString()), [.. GetContent()])
|
||||
{
|
||||
MessageId = message.Id,
|
||||
CreatedAt = message.CreatedAt,
|
||||
AdditionalProperties = GetMetadata()
|
||||
};
|
||||
|
||||
IEnumerable<AIContent> GetContent()
|
||||
if (this._conversationClient is null)
|
||||
{
|
||||
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
|
||||
};
|
||||
ConversationClient conversationClient = this.GetAgentsClient().GetConversationClient();
|
||||
|
||||
if (content is not null)
|
||||
{
|
||||
yield return content;
|
||||
}
|
||||
}
|
||||
Interlocked.CompareExchange(ref this._conversationClient, conversationClient, null);
|
||||
}
|
||||
|
||||
AdditionalPropertiesDictionary? GetMetadata()
|
||||
{
|
||||
if (message.Metadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AdditionalPropertiesDictionary(message.Metadata.Select(m => new KeyValuePair<string, object?>(m.Key, m.Value)));
|
||||
}
|
||||
return this._conversationClient;
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -61,7 +61,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
|
||||
EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true);
|
||||
EvaluateBoolExpression(this.Model.Output?.AutoSend, "autoSend", defaultValue: true);
|
||||
EvaluateMessageTemplate(this.Model.Input?.AdditionalInstructions, "additionalInstructions");
|
||||
EvaluateListExpression<ChatMessage>(this.Model.Input?.Messages, "inputMessages");
|
||||
this.Write(@"
|
||||
|
||||
@@ -71,7 +70,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen
|
||||
agentName,
|
||||
conversationId,
|
||||
autoSend,
|
||||
additionalInstructions,
|
||||
inputMessages,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
-2
@@ -19,7 +19,6 @@ internal sealed class <#= this.Name #>Executor(FormulaSession session, WorkflowA
|
||||
<#
|
||||
EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true);
|
||||
EvaluateBoolExpression(this.Model.Output?.AutoSend, "autoSend", defaultValue: true);
|
||||
EvaluateMessageTemplate(this.Model.Input?.AdditionalInstructions, "additionalInstructions");
|
||||
EvaluateListExpression<ChatMessage>(this.Model.Input?.Messages, "inputMessages");#>
|
||||
|
||||
AgentRunResponse agentResponse =
|
||||
@@ -28,7 +27,6 @@ internal sealed class <#= this.Name #>Executor(FormulaSession session, WorkflowA
|
||||
agentName,
|
||||
conversationId,
|
||||
autoSend,
|
||||
additionalInstructions,
|
||||
inputMessages,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more function tool requests.
|
||||
/// </summary>
|
||||
public sealed class AgentFunctionToolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of function tool requests.
|
||||
/// </summary>
|
||||
public IList<FunctionCallContent> FunctionCalls { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal AgentFunctionToolRequest(string agentName, IList<FunctionCallContent> functionCalls)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.FunctionCalls = functionCalls;
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more function tool responses.
|
||||
/// </summary>
|
||||
public sealed class AgentFunctionToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool response.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of tool responses.
|
||||
/// </summary>
|
||||
public IList<FunctionResultContent> FunctionResults { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal AgentFunctionToolResponse(string agentName, IList<FunctionResultContent> functionResults)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.FunctionResults = functionResults;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create an <see cref="AgentFunctionToolResponse"/> from an <see cref="AgentFunctionToolRequest"/>
|
||||
/// Ensures that all function calls in the request have a corresponding result.
|
||||
/// </summary>
|
||||
/// <param name="toolRequest">The tool request.</param>
|
||||
/// <param name="functionResults">One or more function results</param>
|
||||
/// <returns>An <see cref="AgentFunctionToolResponse"/> that can be provided to the workflow.</returns>
|
||||
/// <exception cref="DeclarativeActionException">Not all <see cref="AgentFunctionToolRequest.FunctionCalls"/> have a corresponding <see cref="FunctionResultContent"/>.</exception>
|
||||
public static AgentFunctionToolResponse Create(AgentFunctionToolRequest toolRequest, params IEnumerable<FunctionResultContent> functionResults)
|
||||
{
|
||||
HashSet<string> callIds = [.. toolRequest.FunctionCalls.Select(call => call.CallId)];
|
||||
HashSet<string> resultIds = [.. functionResults.Select(call => call.CallId)];
|
||||
|
||||
if (!callIds.SetEquals(resultIds))
|
||||
{
|
||||
throw new DeclarativeActionException($"Missing results for: {string.Join(",", callIds.Except(resultIds))}");
|
||||
}
|
||||
|
||||
return new AgentFunctionToolResponse(toolRequest.AgentName, [.. functionResults]);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for user input in response to a `Question` action.
|
||||
/// </summary>
|
||||
public sealed class AnswerRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// An optional prompt for the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This prompt is utilized for the "Question" action type in the Declarative Workflow,
|
||||
/// but is redundant when the user is responding to an agent since the agent's message
|
||||
/// is the implicit prompt.
|
||||
/// </remarks>
|
||||
public string? Prompt { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal AnswerRequest(string? prompt = null)
|
||||
{
|
||||
this.Prompt = prompt;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user input response.
|
||||
/// </summary>
|
||||
public sealed class AnswerResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The response value.
|
||||
/// </summary>
|
||||
public ChatMessage Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AnswerResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
[JsonConstructor]
|
||||
public AnswerResponse(ChatMessage value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AnswerResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
public AnswerResponse(string value)
|
||||
{
|
||||
this.Value = new ChatMessage(ChatRole.User, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for external input.
|
||||
/// </summary>
|
||||
public sealed class ExternalInputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The source message that triggered the request for external input.
|
||||
/// </summary>
|
||||
public AgentRunResponse AgentResponse { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal ExternalInputRequest(AgentRunResponse agentResponse)
|
||||
{
|
||||
this.AgentResponse = agentResponse;
|
||||
}
|
||||
|
||||
internal ExternalInputRequest(ChatMessage message)
|
||||
{
|
||||
this.AgentResponse = new AgentRunResponse(message);
|
||||
}
|
||||
|
||||
internal ExternalInputRequest(string text)
|
||||
{
|
||||
this.AgentResponse = new AgentRunResponse(new ChatMessage(ChatRole.User, text));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response to a <see cref="ExternalInputRequest"/>.
|
||||
/// </summary>
|
||||
public sealed class ExternalInputResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The message being provided as external input to the workflow.
|
||||
/// </summary>
|
||||
public IList<ChatMessage> Messages { get; }
|
||||
|
||||
internal bool HasMessages => this.Messages?.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ExternalInputResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">The external input message being provided to the workflow.</param>
|
||||
public ExternalInputResponse(ChatMessage message)
|
||||
{
|
||||
this.Messages = [message];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ExternalInputResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="messages">The external input messages being provided to the workflow.</param>
|
||||
[JsonConstructor]
|
||||
public ExternalInputResponse(IList<ChatMessage> messages)
|
||||
{
|
||||
this.Messages = messages;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more user-input requests.
|
||||
/// </summary>
|
||||
public sealed class UserInputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of user input requests.
|
||||
/// </summary>
|
||||
public IList<AIContent> InputRequests { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal UserInputRequest(string agentName, IList<AIContent> inputRequests)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.InputRequests = inputRequests;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more user-input responses.
|
||||
/// </summary>
|
||||
public sealed class UserInputResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of approval responses.
|
||||
/// </summary>
|
||||
public IList<AIContent> InputResponses { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal UserInputResponse(string agentName, IList<AIContent> inputResponses)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.InputResponses = inputResponses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create an <see cref="UserInputResponse"/> from a <see cref="UserInputRequest"/>
|
||||
/// Ensures that all requests have a corresponding result.
|
||||
/// </summary>
|
||||
/// <param name="inputRequest">The input request.</param>
|
||||
/// <param name="inputResponses">One or more responses</param>
|
||||
/// <returns>An <see cref="UserInputResponse"/> that can be provided to the workflow.</returns>
|
||||
/// <exception cref="DeclarativeActionException">Not all <see cref="AgentFunctionToolRequest.FunctionCalls"/> have a corresponding <see cref="FunctionResultContent"/>.</exception>
|
||||
public static UserInputResponse Create(UserInputRequest inputRequest, params IEnumerable<UserInputResponseContent> inputResponses)
|
||||
{
|
||||
HashSet<string> callIds = [.. inputRequest.InputRequests.OfType<UserInputRequestContent>().Select(call => call.Id)];
|
||||
HashSet<string> resultIds = [.. inputResponses.Select(call => call.Id)];
|
||||
|
||||
if (!callIds.SetEquals(resultIds))
|
||||
{
|
||||
throw new DeclarativeActionException($"Missing responses for: {string.Join(",", callIds.Except(resultIds))}");
|
||||
}
|
||||
|
||||
return new UserInputResponse(inputRequest.AgentName, [.. inputResponses]);
|
||||
}
|
||||
}
|
||||
+3
-43
@@ -1,24 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class AgentProviderExtensions
|
||||
{
|
||||
private static readonly HashSet<Azure.AI.Agents.Persistent.RunStatus> s_failureStatus =
|
||||
[
|
||||
Azure.AI.Agents.Persistent.RunStatus.Failed,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Cancelled,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Cancelling,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Expired,
|
||||
];
|
||||
|
||||
public static async ValueTask<AgentRunResponse> InvokeAgentAsync(
|
||||
this WorkflowAgentProvider agentProvider,
|
||||
string executorId,
|
||||
@@ -26,27 +16,10 @@ internal static class AgentProviderExtensions
|
||||
string agentName,
|
||||
string? conversationId,
|
||||
bool autoSend,
|
||||
string? additionalInstructions = null,
|
||||
IEnumerable<ChatMessage>? inputMessages = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get the specified agent.
|
||||
AIAgent agent = await agentProvider.GetAgentAsync(agentName, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Prepare the run options.
|
||||
ChatClientAgentRunOptions options =
|
||||
new(
|
||||
new ChatOptions()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
Instructions = additionalInstructions,
|
||||
});
|
||||
|
||||
// Initialize the agent thread.
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates =
|
||||
inputMessages is not null ?
|
||||
agent.RunStreamingAsync([.. inputMessages], null, options, cancellationToken) :
|
||||
agent.RunStreamingAsync(null, options, cancellationToken);
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, cancellationToken);
|
||||
|
||||
// Enable "autoSend" behavior if this is the workflow conversation.
|
||||
bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? workflowConversationId);
|
||||
@@ -60,13 +33,6 @@ internal static class AgentProviderExtensions
|
||||
|
||||
updates.Add(update);
|
||||
|
||||
if (update.RawRepresentation is ChatResponseUpdate chatUpdate &&
|
||||
chatUpdate.RawRepresentation is RunUpdate runUpdate &&
|
||||
s_failureStatus.Contains(runUpdate.Value.Status))
|
||||
{
|
||||
throw new DeclarativeActionException($"Unexpected failure invoking agent, run {runUpdate.Value.Status}: {agent.Name ?? agent.Id} [{runUpdate.Value.Id}/{conversationId}]");
|
||||
}
|
||||
|
||||
if (autoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update), cancellationToken).ConfigureAwait(false);
|
||||
@@ -80,16 +46,10 @@ internal static class AgentProviderExtensions
|
||||
await context.AddEventAsync(new AgentRunResponseEvent(executorId, response), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// If autoSend is enabled and this is not the workflow conversation, copy messages to the workflow conversation.
|
||||
if (autoSend && !isWorkflowConversation && workflowConversationId is not null)
|
||||
{
|
||||
// Copy messages with content that aren't function calls or results.
|
||||
IEnumerable<ChatMessage> messages =
|
||||
response.Messages.Where(
|
||||
message =>
|
||||
!string.IsNullOrEmpty(message.Text) &&
|
||||
!message.Contents.OfType<FunctionCallContent>().Any() &&
|
||||
!message.Contents.OfType<FunctionResultContent>().Any());
|
||||
foreach (ChatMessage message in messages)
|
||||
foreach (ChatMessage message in response.Messages)
|
||||
{
|
||||
await agentProvider.CreateMessageAsync(workflowConversationId, message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
+59
-39
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -43,16 +44,28 @@ internal static class JsonDocumentExtensions
|
||||
|
||||
private static Dictionary<string, object?> ParseRecord(this JsonElement currentElement, VariableType targetType)
|
||||
{
|
||||
if (targetType.Schema is null)
|
||||
{
|
||||
throw new DeclarativeActionException($"Object schema not defined for. {targetType.Type.Name}.");
|
||||
}
|
||||
IEnumerable<KeyValuePair<string, object?>> keyValuePairs =
|
||||
targetType.Schema is null ?
|
||||
ParseValues() :
|
||||
ParseSchema(targetType.Schema);
|
||||
|
||||
return ParseValues().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
return keyValuePairs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
IEnumerable<KeyValuePair<string, object?>> ParseValues()
|
||||
{
|
||||
foreach (KeyValuePair<string, VariableType> property in targetType.Schema)
|
||||
foreach (JsonProperty objectProperty in currentElement.EnumerateObject())
|
||||
{
|
||||
if (!objectProperty.Value.TryParseValue(targetType: null, out object? parsedValue))
|
||||
{
|
||||
throw new DeclarativeActionException($"Unsupported data type '{objectProperty.Value.ValueKind}' for property '{objectProperty.Name}'");
|
||||
}
|
||||
yield return new KeyValuePair<string, object?>(objectProperty.Name, parsedValue);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<KeyValuePair<string, object?>> ParseSchema(FrozenDictionary<string, VariableType> schema)
|
||||
{
|
||||
foreach (KeyValuePair<string, VariableType> property in schema)
|
||||
{
|
||||
object? parsedValue = null;
|
||||
if (!currentElement.TryGetProperty(property.Key, out JsonElement propertyElement))
|
||||
@@ -131,22 +144,22 @@ internal static class JsonDocumentExtensions
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool TryParseValue(this JsonElement propertyElement, VariableType targetType, out object? value) =>
|
||||
private static bool TryParseValue(this JsonElement propertyElement, VariableType? targetType, out object? value) =>
|
||||
propertyElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => TryParseString(propertyElement, targetType.Type, out value),
|
||||
JsonValueKind.Number => TryParseNumber(propertyElement, targetType.Type, out value),
|
||||
JsonValueKind.String => TryParseString(propertyElement, targetType?.Type, out value),
|
||||
JsonValueKind.Number => TryParseNumber(propertyElement, targetType?.Type, out value),
|
||||
JsonValueKind.True or JsonValueKind.False => TryParseBoolean(propertyElement, out value),
|
||||
JsonValueKind.Object => TryParseObject(propertyElement, targetType, out value),
|
||||
JsonValueKind.Array => TryParseList(propertyElement, targetType, out value),
|
||||
JsonValueKind.Null => TryParseNull(targetType.Type, out value),
|
||||
JsonValueKind.Null => TryParseNull(targetType?.Type, out value),
|
||||
_ => throw new DeclarativeActionException($"JSON element of type {propertyElement.ValueKind} is not supported."),
|
||||
};
|
||||
|
||||
private static bool TryParseNull(Type valueType, out object? value)
|
||||
private static bool TryParseNull(Type? valueType, out object? value)
|
||||
{
|
||||
// If the target type is not nullable, we cannot assign null to it
|
||||
if (!valueType.IsNullable())
|
||||
if (valueType?.IsNullable() == false)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
@@ -170,7 +183,7 @@ internal static class JsonDocumentExtensions
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseString(JsonElement propertyElement, Type valueType, out object? value)
|
||||
private static bool TryParseString(JsonElement propertyElement, Type? valueType, out object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -178,23 +191,30 @@ internal static class JsonDocumentExtensions
|
||||
if (propertyValue is null)
|
||||
{
|
||||
value = null;
|
||||
return valueType.IsNullable(); // Parse fails if value is null and requested type is not.
|
||||
return valueType?.IsNullable() ?? false; // Parse fails if value is null and requested type is not.
|
||||
}
|
||||
|
||||
switch (valueType)
|
||||
if (valueType is null)
|
||||
{
|
||||
case Type targetType when targetType == typeof(string):
|
||||
value = propertyValue;
|
||||
break;
|
||||
case Type targetType when targetType == typeof(DateTime):
|
||||
value = DateTime.Parse(propertyValue, provider: null, styles: DateTimeStyles.RoundtripKind);
|
||||
break;
|
||||
case Type targetType when targetType == typeof(TimeSpan):
|
||||
value = TimeSpan.Parse(propertyValue);
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
value = propertyValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (valueType)
|
||||
{
|
||||
case Type targetType when targetType == typeof(string):
|
||||
value = propertyValue;
|
||||
break;
|
||||
case Type targetType when targetType == typeof(DateTime):
|
||||
value = DateTime.Parse(propertyValue, provider: null, styles: DateTimeStyles.RoundtripKind);
|
||||
break;
|
||||
case Type targetType when targetType == typeof(TimeSpan):
|
||||
value = TimeSpan.Parse(propertyValue);
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -206,7 +226,7 @@ internal static class JsonDocumentExtensions
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNumber(JsonElement element, Type valueType, out object? value)
|
||||
private static bool TryParseNumber(JsonElement element, Type? valueType, out object? value)
|
||||
{
|
||||
// Try parsing as integer types first (most precise representation)
|
||||
if (element.TryGetInt32(out int intValue))
|
||||
@@ -234,8 +254,14 @@ internal static class JsonDocumentExtensions
|
||||
value = null;
|
||||
return false;
|
||||
|
||||
static bool ConvertToExpectedType(Type valueType, object sourceValue, out object? value)
|
||||
static bool ConvertToExpectedType(Type? valueType, object sourceValue, out object? value)
|
||||
{
|
||||
if (valueType is null)
|
||||
{
|
||||
value = sourceValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
value = Convert.ChangeType(sourceValue, valueType);
|
||||
@@ -249,23 +275,17 @@ internal static class JsonDocumentExtensions
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseObject(JsonElement propertyElement, VariableType targetType, out object? value)
|
||||
private static bool TryParseObject(JsonElement propertyElement, VariableType? targetType, out object? value)
|
||||
{
|
||||
if (!targetType.HasSchema)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = propertyElement.ParseRecord(targetType);
|
||||
value = propertyElement.ParseRecord(targetType ?? VariableType.RecordType);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseList(JsonElement propertyElement, VariableType targetType, out object? value)
|
||||
private static bool TryParseList(JsonElement propertyElement, VariableType? targetType, out object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = ParseTable(propertyElement, targetType);
|
||||
value = ParseTable(propertyElement, targetType ?? VariableType.ListType);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
|
||||
+3
-3
@@ -77,7 +77,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
this.State.Bind();
|
||||
}
|
||||
|
||||
private bool IsManagedScope(string? scopeName) => scopeName is not null && VariableScopeNames.IsValidName(scopeName);
|
||||
private static bool IsManagedScope(string? scopeName) => scopeName is not null && VariableScopeNames.IsValidName(scopeName);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<TValue?> ReadStateAsync<TValue>(string key, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
@@ -86,7 +86,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
// Not a managed scope, just pass through. This is valid when a declarative
|
||||
// workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
|
||||
_ when !this.IsManagedScope(scopeName) => await this.Source.ReadStateAsync<TValue>(key, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
_ when !IsManagedScope(scopeName) => await this.Source.ReadStateAsync<TValue>(key, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
// Retrieve formula values directly from the managed state to avoid conversion.
|
||||
_ when typeof(TValue) == typeof(FormulaValue) => (TValue?)(object?)this.State.Get(key, scopeName),
|
||||
// Retrieve native types from the source context to avoid conversion.
|
||||
@@ -100,7 +100,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
// Not a managed scope, just pass through. This is valid when a declarative
|
||||
// workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
|
||||
_ when !this.IsManagedScope(scopeName) => await this.Source.ReadOrInitStateAsync(key, initialStateFactory, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
_ when !IsManagedScope(scopeName) => await this.Source.ReadOrInitStateAsync(key, initialStateFactory, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
// Retrieve formula values directly from the managed state to avoid conversion.
|
||||
_ when typeof(TValue) == typeof(FormulaValue) => await EnsureFormulaValueAsync().ConfigureAwait(false),
|
||||
// Retrieve native types from the source context to avoid conversion.
|
||||
|
||||
+56
-23
@@ -239,6 +239,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
// Entry point for question
|
||||
QuestionExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
|
||||
// Transition to post action if complete
|
||||
string postId = Steps.Post(action.Id);
|
||||
this._workflowModel.AddLink(action.Id, postId, QuestionExecutor.IsComplete);
|
||||
@@ -249,13 +250,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
|
||||
// Define input action
|
||||
string inputId = QuestionExecutor.Steps.Input(action.Id);
|
||||
RequestPortAction inputPort = new(RequestPort.Create<AnswerRequest, AnswerResponse>(inputId));
|
||||
RequestPortAction inputPort = new(RequestPort.Create<ExternalInputRequest, ExternalInputResponse>(inputId));
|
||||
this._workflowModel.AddNode(inputPort, action.ParentId);
|
||||
this._workflowModel.AddLinkFromPeer(action.ParentId, inputId);
|
||||
|
||||
// Capture input response
|
||||
string captureId = QuestionExecutor.Steps.Capture(action.Id);
|
||||
this.ContinueWith(new DelegateActionExecutor<AnswerResponse>(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId);
|
||||
this.ContinueWith(new DelegateActionExecutor<ExternalInputResponse>(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId);
|
||||
|
||||
// Transition to post action if complete
|
||||
this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId, QuestionExecutor.IsComplete);
|
||||
@@ -263,6 +264,24 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this._workflowModel.AddLink(captureId, prepareId, message => !QuestionExecutor.IsComplete(message));
|
||||
}
|
||||
|
||||
protected override void Visit(RequestExternalInput item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
RequestExternalInputExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
|
||||
// Define input action
|
||||
string inputId = RequestExternalInputExecutor.Steps.Input(action.Id);
|
||||
RequestPortAction inputPort = new(RequestPort.Create<ExternalInputRequest, ExternalInputResponse>(inputId));
|
||||
this._workflowModel.AddNode(inputPort, action.ParentId);
|
||||
this._workflowModel.AddLinkFromPeer(action.ParentId, inputId);
|
||||
|
||||
// Capture input response
|
||||
string captureId = RequestExternalInputExecutor.Steps.Capture(action.Id);
|
||||
this.ContinueWith(new DelegateActionExecutor<ExternalInputResponse>(captureId, this._workflowState, action.CaptureResponseAsync), action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(EndDialog item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -285,6 +304,28 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CancelAllDialogs item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
// Represent action with default executor
|
||||
DefaultActionExecutor action = new(item, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
// Define a clean-start to ensure "end" is not a source for any edge
|
||||
this.RestartAfter(item.Id.Value, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CancelDialog item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
// Represent action with default executor
|
||||
DefaultActionExecutor action = new(item, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
// Define a clean-start to ensure "end" is not a source for any edge
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CreateConversation item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -318,33 +359,29 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this._workflowModel.AddLink(action.Id, postId, InvokeAzureAgentExecutor.RequiresNothing);
|
||||
|
||||
// Define request-port for function calling action
|
||||
string functionCallingPortId = InvokeAzureAgentExecutor.Steps.FunctionTool(action.Id);
|
||||
RequestPortAction functionCallingPort = new(RequestPort.Create<AgentFunctionToolRequest, AgentFunctionToolResponse>(functionCallingPortId));
|
||||
this._workflowModel.AddNode(functionCallingPort, action.ParentId);
|
||||
this._workflowModel.AddLink(action.Id, functionCallingPort.Id, InvokeAzureAgentExecutor.RequiresFunctionCall);
|
||||
|
||||
// Define request-port for user input, such as: mcp tool & function tool approval
|
||||
string userInputPortId = InvokeAzureAgentExecutor.Steps.UserInput(action.Id);
|
||||
RequestPortAction userInputPort = new(RequestPort.Create<UserInputRequest, UserInputResponse>(userInputPortId));
|
||||
this._workflowModel.AddNode(userInputPort, action.ParentId);
|
||||
this._workflowModel.AddLink(action.Id, userInputPortId, InvokeAzureAgentExecutor.RequiresUserInput);
|
||||
string externalInputPortId = InvokeAzureAgentExecutor.Steps.ExternalInput(action.Id);
|
||||
RequestPortAction externalInputPort = new(RequestPort.Create<ExternalInputRequest, ExternalInputResponse>(externalInputPortId));
|
||||
this._workflowModel.AddNode(externalInputPort, action.ParentId);
|
||||
this._workflowModel.AddLink(action.Id, externalInputPortId, InvokeAzureAgentExecutor.RequiresInput);
|
||||
|
||||
// Request ports always transitions to resume
|
||||
string resumeId = InvokeAzureAgentExecutor.Steps.Resume(action.Id);
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor<AgentFunctionToolResponse>(resumeId, this._workflowState, action.ResumeAsync), action.ParentId);
|
||||
this._workflowModel.AddLink(functionCallingPortId, resumeId);
|
||||
this._workflowModel.AddLink(userInputPortId, resumeId);
|
||||
// Transition to appropriate request port if more function calling is requested
|
||||
this._workflowModel.AddLink(resumeId, functionCallingPortId, InvokeAzureAgentExecutor.RequiresFunctionCall);
|
||||
// Transition to appropriate request port if more user input is requested
|
||||
this._workflowModel.AddLink(resumeId, userInputPortId, InvokeAzureAgentExecutor.RequiresUserInput);
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor<ExternalInputResponse>(resumeId, this._workflowState, action.ResumeAsync, emitResult: false), action.ParentId);
|
||||
this._workflowModel.AddLink(externalInputPortId, resumeId);
|
||||
// Transition to post action if complete
|
||||
this._workflowModel.AddLink(resumeId, postId, InvokeAzureAgentExecutor.RequiresNothing);
|
||||
// Transition to request port if more input is required
|
||||
this._workflowModel.AddLink(resumeId, externalInputPortId, InvokeAzureAgentExecutor.RequiresInput);
|
||||
|
||||
// Define post action
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeAzureResponse item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(RetrieveConversationMessage item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -462,10 +499,6 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
|
||||
protected override void Visit(ReplaceDialog item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(CancelAllDialogs item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(CancelDialog item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(EmitEvent item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(GetConversationMembers item) => this.NotSupported(item);
|
||||
|
||||
+27
-12
@@ -210,9 +210,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
protected override void Visit(Question item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
//this.Trace(item);
|
||||
}
|
||||
|
||||
//this.ContinueWith(new QuestionTemplate(item));
|
||||
protected override void Visit(RequestExternalInput item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(EndDialog item)
|
||||
@@ -237,6 +239,24 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CancelAllDialogs item)
|
||||
{
|
||||
// Represent action with default executor
|
||||
DefaultTemplate action = new(item, this._rootId);
|
||||
this.ContinueWith(action);
|
||||
// Define a clean-start to ensure "end" is not a source for any edge
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CancelDialog item)
|
||||
{
|
||||
// Represent action with default executor
|
||||
DefaultTemplate action = new(item, this._rootId);
|
||||
this.ContinueWith(action);
|
||||
// Define a clean-start to ensure "end" is not a source for any edge
|
||||
this.RestartAfter(action.Id, action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(CreateConversation item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -265,6 +285,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
this.ContinueWith(new InvokeAzureAgentTemplate(item));
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeAzureResponse item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
}
|
||||
|
||||
protected override void Visit(RetrieveConversationMessage item)
|
||||
{
|
||||
this.Trace(item);
|
||||
@@ -317,17 +342,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
protected override void Visit(EditTable item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
//this.Trace(item);
|
||||
|
||||
//this.ContinueWith(new EditTableTemplate(item));
|
||||
}
|
||||
|
||||
protected override void Visit(EditTableV2 item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
//this.Trace(item);
|
||||
|
||||
//this.ContinueWith(new EditTableV2Template(item));
|
||||
}
|
||||
|
||||
protected override void Visit(ParseValue item)
|
||||
@@ -384,10 +403,6 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
|
||||
protected override void Visit(ReplaceDialog item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(CancelAllDialogs item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(CancelDialog item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(EmitEvent item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(GetConversationMembers item) => this.NotSupported(item);
|
||||
|
||||
@@ -23,7 +23,6 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA
|
||||
/// <param name="agentName">The name or identifier of the agent.</param>
|
||||
/// <param name="conversationId">The identifier of the conversation.</param>
|
||||
/// <param name="autoSend">Send the agent's response as workflow output. (default: true).</param>
|
||||
/// <param name="additionalInstructions">Optional additional instructions to the agent.</param>
|
||||
/// <param name="inputMessages">Optional messages to add to the conversation prior to invocation.</param>
|
||||
/// <param name="cancellationToken">A token that can be used to observe cancellation.</param>
|
||||
/// <returns></returns>
|
||||
@@ -32,8 +31,7 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA
|
||||
string agentName,
|
||||
string? conversationId,
|
||||
bool autoSend,
|
||||
string? additionalInstructions = null,
|
||||
IEnumerable<ChatMessage>? inputMessages = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, inputMessages, cancellationToken);
|
||||
=> agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, cancellationToken);
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<NoWarn>$(NoWarn);MEAI001;OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -34,8 +34,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+57
-56
@@ -2,6 +2,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
@@ -21,14 +22,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
{
|
||||
public static class Steps
|
||||
{
|
||||
public static string UserInput(string id) => $"{id}_{nameof(UserInput)}";
|
||||
public static string FunctionTool(string id) => $"{id}_{nameof(FunctionTool)}";
|
||||
public static string ExternalInput(string id) => $"{id}_{nameof(ExternalInput)}";
|
||||
public static string Resume(string id) => $"{id}_{nameof(Resume)}";
|
||||
}
|
||||
|
||||
public static bool RequiresFunctionCall(object? message) => message is AgentFunctionToolRequest;
|
||||
|
||||
public static bool RequiresUserInput(object? message) => message is UserInputRequest;
|
||||
public static bool RequiresInput(object? message) => message is ExternalInputRequest;
|
||||
|
||||
public static bool RequiresNothing(object? message) => message is ActionExecutorResult;
|
||||
|
||||
@@ -46,8 +44,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask ResumeAsync(IWorkflowContext context, AgentFunctionToolResponse message, CancellationToken cancellationToken) =>
|
||||
this.InvokeAgentAsync(context, [message.FunctionResults.ToChatMessage()], cancellationToken);
|
||||
public async ValueTask ResumeAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false);
|
||||
await this.InvokeAgentAsync(context, response.Messages, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -58,40 +59,52 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
{
|
||||
string? conversationId = this.GetConversationId();
|
||||
string agentName = this.GetAgentName();
|
||||
string? additionalInstructions = this.GetAdditionalInstructions();
|
||||
bool autoSend = this.GetAutoSendValue();
|
||||
|
||||
bool isComplete = true;
|
||||
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(agentResponse.Text))
|
||||
ChatMessage[] actionableMessages = FilterActionableContent(agentResponse).ToArray();
|
||||
if (actionableMessages.Length > 0)
|
||||
{
|
||||
// Identify function calls that have no associated result.
|
||||
List<UserInputRequestContent> inputRequests = GetUserInputRequests(agentResponse);
|
||||
if (inputRequests.Count > 0)
|
||||
{
|
||||
isComplete = false;
|
||||
UserInputRequest approvalRequest = new(agentName, inputRequests.OfType<AIContent>().ToArray());
|
||||
await context.SendMessageAsync(approvalRequest, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Identify function calls that have no associated result.
|
||||
List<FunctionCallContent> functionCalls = GetOrphanedFunctionCalls(agentResponse);
|
||||
if (functionCalls.Count > 0)
|
||||
{
|
||||
isComplete = false;
|
||||
AgentFunctionToolRequest toolRequest = new(agentName, functionCalls);
|
||||
await context.SendMessageAsync(toolRequest, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isComplete)
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
AgentRunResponse filteredResponse =
|
||||
new(actionableMessages)
|
||||
{
|
||||
AdditionalProperties = agentResponse.AdditionalProperties,
|
||||
AgentId = agentResponse.AgentId,
|
||||
CreatedAt = agentResponse.CreatedAt,
|
||||
ResponseId = agentResponse.ResponseId,
|
||||
Usage = agentResponse.Usage,
|
||||
};
|
||||
await context.SendMessageAsync(new ExternalInputRequest(filteredResponse), cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.AssignAsync(this.AgentOutput?.Messages?.Path, agentResponse.Messages.ToTable(), context).ConfigureAwait(false);
|
||||
|
||||
// Attempt to parse the last message as JSON and assign to the response object variable.
|
||||
try
|
||||
{
|
||||
JsonDocument jsonDocument = JsonDocument.Parse(agentResponse.Messages.Last().Text);
|
||||
Dictionary<string, object?> objectProperties = jsonDocument.ParseRecord(VariableType.RecordType);
|
||||
await this.AssignAsync(this.AgentOutput?.ResponseObject?.Path, objectProperties.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Not valid json, skip assignment.
|
||||
}
|
||||
|
||||
if (this.Model.Input?.ExternalLoop?.When is not null)
|
||||
{
|
||||
bool requestInput = this.Evaluator.GetValue(this.Model.Input.ExternalLoop.When).Value;
|
||||
if (requestInput)
|
||||
{
|
||||
ExternalInputRequest inputRequest = new(agentResponse);
|
||||
await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IEnumerable<ChatMessage>? GetInputMessages()
|
||||
@@ -107,7 +120,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
return userInput?.ToChatMessages();
|
||||
}
|
||||
|
||||
private static List<FunctionCallContent> GetOrphanedFunctionCalls(AgentRunResponse agentResponse)
|
||||
private static IEnumerable<ChatMessage> FilterActionableContent(AgentRunResponse agentResponse)
|
||||
{
|
||||
HashSet<string> functionResultIds =
|
||||
[.. agentResponse.Messages
|
||||
@@ -117,21 +130,21 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
.OfType<FunctionResultContent>()
|
||||
.Select(functionCall => functionCall.CallId))];
|
||||
|
||||
List<FunctionCallContent> functionCalls = [];
|
||||
foreach (FunctionCallContent functionCall in agentResponse.Messages.SelectMany(m => m.Contents.OfType<FunctionCallContent>()))
|
||||
foreach (ChatMessage responseMessage in agentResponse.Messages)
|
||||
{
|
||||
if (!functionResultIds.Contains(functionCall.CallId))
|
||||
if (responseMessage.Contents.Any(content => content is UserInputRequestContent))
|
||||
{
|
||||
functionCalls.Add(functionCall);
|
||||
yield return responseMessage;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (responseMessage.Contents.OfType<FunctionCallContent>().Any(functionCall => !functionResultIds.Contains(functionCall.CallId)))
|
||||
{
|
||||
yield return responseMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return functionCalls;
|
||||
}
|
||||
|
||||
private static List<UserInputRequestContent> GetUserInputRequests(AgentRunResponse agentResponse) =>
|
||||
agentResponse.Messages.SelectMany(m => m.Contents.OfType<UserInputRequestContent>()).ToList();
|
||||
|
||||
private string? GetConversationId()
|
||||
{
|
||||
if (this.Model.ConversationId is null)
|
||||
@@ -149,18 +162,6 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
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.Engine.Format(this.AgentInput.AdditionalInstructions);
|
||||
}
|
||||
|
||||
return additionalInstructions;
|
||||
}
|
||||
|
||||
private bool GetAutoSendValue()
|
||||
{
|
||||
if (this.AgentOutput?.AutoSend is null)
|
||||
|
||||
+6
-5
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Entities;
|
||||
@@ -75,22 +76,22 @@ internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider age
|
||||
public async ValueTask PrepareResponseAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false);
|
||||
AnswerRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt));
|
||||
ExternalInputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt));
|
||||
await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
|
||||
await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask CaptureResponseAsync(IWorkflowContext context, AnswerResponse message, CancellationToken cancellationToken)
|
||||
public async ValueTask CaptureResponseAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
FormulaValue? extractedValue = null;
|
||||
if (message.Value is null)
|
||||
if (!response.HasMessages)
|
||||
{
|
||||
string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt);
|
||||
await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim()), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, message.Value.Text);
|
||||
EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, string.Concat(response.Messages.Select(message => message.Text)));
|
||||
if (entityResult.IsValid)
|
||||
{
|
||||
extractedValue = entityResult.Value;
|
||||
@@ -121,7 +122,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider age
|
||||
if (workflowConversationId is not null)
|
||||
{
|
||||
// Input message always defined if values has been extracted.
|
||||
ChatMessage input = message.Value!;
|
||||
ChatMessage input = response.Messages.Last();
|
||||
await agentProvider.CreateMessageAsync(workflowConversationId, input, cancellationToken).ConfigureAwait(false);
|
||||
await context.SetLastMessageAsync(input).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
internal sealed class RequestExternalInputExecutor(RequestExternalInput model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state)
|
||||
: DeclarativeActionExecutor<RequestExternalInput>(model, state)
|
||||
{
|
||||
public static class Steps
|
||||
{
|
||||
public static string Input(string id) => $"{id}_{nameof(Input)}";
|
||||
public static string Capture(string id) => $"{id}_{nameof(Capture)}";
|
||||
}
|
||||
|
||||
protected override bool IsDiscreteAction => false;
|
||||
protected override bool EmitResultEvent => false;
|
||||
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ExternalInputRequest inputRequest = new(new AgentRunResponse());
|
||||
|
||||
await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async ValueTask CaptureResponseAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
string? workflowConversationId = context.GetWorkflowConversation();
|
||||
if (workflowConversationId is not null)
|
||||
{
|
||||
foreach (ChatMessage inputMessage in response.Messages)
|
||||
{
|
||||
await agentProvider.CreateMessageAsync(workflowConversationId, inputMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.Variable?.Path, response.Messages.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ Please refer to the [README](../../samples/GettingStarted/Workflows/Declarative/
|
||||
|**ConditionItem**|Represents a single conditional statement within a group. It evaluates a specific logical condition and determines the next step in the flow.
|
||||
|**ContinueLoop**|Skips the remaining steps in the current iteration and continues with the next loop cycle. Commonly used to bypass specific cases without exiting the loop entirely.
|
||||
|**EndConversation**|Terminates the current conversation session. It ensures any necessary cleanup or final actions are performed before closing.
|
||||
|**EndDialog**|Ends the current dialog or sub-dialog within a broader conversation flow. This helps modularize complex interactions.
|
||||
|**EndWorkflow**|Ends the current workflow or sub-workflow within a broader conversation flow. This helps modularize complex interactions.
|
||||
|**Foreach**|Iterates through a collection of items, executing a set of actions for each. Ideal for processing lists or batch operations.
|
||||
|**GotoAction**|Jumps directly to a specified action within the workflow. Enables non-linear navigation in the logic flow.
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ public abstract class WorkflowAgentProvider
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of additional tools an agent is able to automatically invoke.
|
||||
/// If an agent is configured with a function tool that is not available, a <see cref="RequestPort"/> is executed
|
||||
/// that provides an <see cref="AgentFunctionToolRequest"/> that describes the function calls requested. The caller may
|
||||
/// then respond with a corrsponding <see cref="AgentFunctionToolResponse"/> that includes the results of the function calls.
|
||||
/// that provides an <see cref="ExternalInputRequest"/> that describes the function calls requested. The caller may
|
||||
/// then respond with a corrsponding <see cref="ExternalInputResponse"/> that includes the results of the function calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These will not impact the requests sent to the model by the <see cref="FunctionInvokingChatClient"/>.
|
||||
@@ -56,14 +56,6 @@ public abstract class WorkflowAgentProvider
|
||||
/// </remarks>
|
||||
public bool AllowMultipleToolCalls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves an AI agent by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The unique identifier of the AI agent to retrieve. Cannot be null or empty.</param>
|
||||
/// <param name="cancellationToken">A token that propagates notification when operation should be canceled.</param>
|
||||
/// <returns>The task result contains the <see cref="AIAgent"/> associated.</returns>
|
||||
public abstract Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously creates a new conversation and returns its unique identifier.
|
||||
/// </summary>
|
||||
@@ -88,6 +80,17 @@ public abstract class WorkflowAgentProvider
|
||||
/// <returns>The requested message</returns>
|
||||
public abstract Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves an AI agent by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The unique identifier of the AI agent to retrieve. Cannot be null or empty.</param>
|
||||
/// <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="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);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a set of messages from a conversation.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Build Code
|
||||
|
||||
Re-usable utility for building C# code in tests.
|
||||
|
||||
To use this in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedBuildTestCode>true</InjectSharedBuildTestCode>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable IDE0005
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents;
|
||||
|
||||
namespace Shared.Foundry;
|
||||
|
||||
internal static class AgentFactory
|
||||
{
|
||||
public static async ValueTask<AgentVersion> CreateAgentAsync(
|
||||
this AgentsClient agentsClient,
|
||||
string agentName,
|
||||
PromptAgentDefinition agentDefinition,
|
||||
string agentDescription)
|
||||
{
|
||||
AgentVersionCreationOptions options =
|
||||
new()
|
||||
{
|
||||
Description = agentDescription,
|
||||
Metadata =
|
||||
{
|
||||
{ "deleteme", bool.TrueString },
|
||||
{ "test", bool.TrueString },
|
||||
},
|
||||
};
|
||||
|
||||
AgentVersion agentVersion = await agentsClient.CreateAgentVersionAsync(agentName, agentDefinition, options).ConfigureAwait(false);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"PROMPT AGENT: {agentVersion.Name}:{agentVersion.Version}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
return agentVersion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Foundry Agents
|
||||
|
||||
Shared patterns for creating and utilizing Foundry agents.
|
||||
|
||||
To use this in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
# Workflow Execution
|
||||
|
||||
Common support for workflow execution.
|
||||
|
||||
To use this in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Shared.Workflows;
|
||||
|
||||
internal sealed class WorkflowFactory(string workflowFile, Uri foundryEndpoint)
|
||||
{
|
||||
public IList<AIFunction> Functions { get; init; } = [];
|
||||
|
||||
public IConfiguration? Configuration { get; init; }
|
||||
|
||||
// Assign to continue an existing conversation
|
||||
public string? ConversationId { get; init; }
|
||||
|
||||
// Assign to enable logging
|
||||
public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Create the workflow from the declarative YAML. Includes definition of the
|
||||
/// <see cref="DeclarativeWorkflowOptions" /> and the associated <see cref="WorkflowAgentProvider"/>.
|
||||
/// </summary>
|
||||
public Workflow CreateWorkflow()
|
||||
{
|
||||
// Create the agent provider that will service agent requests within the workflow.
|
||||
AzureAgentProvider agentProvider = new(foundryEndpoint, new AzureCliCredential())
|
||||
{
|
||||
// Functions included here will be auto-executed by the framework.
|
||||
Functions = this.Functions
|
||||
};
|
||||
|
||||
// Define the workflow options.
|
||||
DeclarativeWorkflowOptions options =
|
||||
new(agentProvider)
|
||||
{
|
||||
Configuration = this.Configuration,
|
||||
ConversationId = this.ConversationId,
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
};
|
||||
|
||||
string workflowPath = Path.Combine(AppContext.BaseDirectory, workflowFile);
|
||||
|
||||
// Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
|
||||
return DeclarativeWorkflowBuilder.Build<string>(workflowPath, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Uncomment to output unknown content types for debugging.
|
||||
//#define DEBUG_OUTPUT
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Shared.Workflows;
|
||||
|
||||
// Types are for evaluation purposes only and is subject to change or removal in future updates.
|
||||
#pragma warning disable OPENAI001
|
||||
#pragma warning disable OPENAICUA001
|
||||
#pragma warning disable MEAI001
|
||||
|
||||
internal sealed class WorkflowRunner
|
||||
{
|
||||
private Dictionary<string, AIFunction> FunctionMap { get; }
|
||||
private CheckpointInfo? LastCheckpoint { get; set; }
|
||||
|
||||
public static void Notify(string message, ConsoleColor? color = null)
|
||||
{
|
||||
Console.ForegroundColor = color ?? ConsoleColor.Cyan;
|
||||
try
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, checkpoints will be persisted to disk as JSON files.
|
||||
/// Otherwise an in-memory checkpoint store that will not persist checkpoints
|
||||
/// beyond the lifetime of the process.
|
||||
/// </summary>
|
||||
public bool UseJsonCheckpoints { get; init; }
|
||||
|
||||
public WorkflowRunner(params IEnumerable<AIFunction> functions)
|
||||
{
|
||||
this.FunctionMap = functions.ToDictionary(f => f.Name);
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(Func<Workflow> workflowProvider, string input)
|
||||
{
|
||||
Workflow workflow = workflowProvider.Invoke();
|
||||
|
||||
CheckpointManager checkpointManager;
|
||||
|
||||
if (this.UseJsonCheckpoints)
|
||||
{
|
||||
// Use a file-system based JSON checkpoint store to persist checkpoints to disk.
|
||||
DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:yyMMdd-hhmmss-ff}"));
|
||||
checkpointManager = CheckpointManager.CreateJson(new FileSystemJsonCheckpointStore(checkpointFolder));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process.
|
||||
checkpointManager = CheckpointManager.CreateInMemory();
|
||||
}
|
||||
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager).ConfigureAwait(false);
|
||||
|
||||
bool isComplete = false;
|
||||
ExternalResponse? requestResponse = null;
|
||||
do
|
||||
{
|
||||
ExternalRequest? externalRequest = await this.MonitorAndDisposeWorkflowRunAsync(run, requestResponse).ConfigureAwait(false);
|
||||
if (externalRequest is not null)
|
||||
{
|
||||
Notify("\nWORKFLOW: Yield\n", ConsoleColor.DarkYellow);
|
||||
|
||||
if (this.LastCheckpoint is null)
|
||||
{
|
||||
throw new InvalidOperationException("Checkpoint information missing after external request.");
|
||||
}
|
||||
|
||||
// Process the external request.
|
||||
object response = await this.HandleExternalRequestAsync(externalRequest).ConfigureAwait(false);
|
||||
requestResponse = externalRequest.CreateResponse(response);
|
||||
|
||||
// Let's resume on an entirely new workflow instance to demonstrate checkpoint portability.
|
||||
workflow = workflowProvider.Invoke();
|
||||
|
||||
// Restore the latest checkpoint.
|
||||
Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}");
|
||||
Notify("WORKFLOW: Restore", ConsoleColor.DarkYellow);
|
||||
|
||||
run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager, run.Run.RunId).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
isComplete = true;
|
||||
}
|
||||
}
|
||||
while (!isComplete);
|
||||
|
||||
Notify("\nWORKFLOW: Done!\n");
|
||||
}
|
||||
|
||||
public async Task<ExternalRequest?> MonitorAndDisposeWorkflowRunAsync(Checkpointed<StreamingRun> run, ExternalResponse? response = null)
|
||||
{
|
||||
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
|
||||
await using IAsyncDisposable disposeRun = run;
|
||||
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
|
||||
|
||||
bool hasStreamed = false;
|
||||
string? messageId = null;
|
||||
|
||||
bool shouldExit = false;
|
||||
ExternalRequest? externalResponse = null;
|
||||
|
||||
if (response is not null)
|
||||
{
|
||||
await run.Run.SendResponseAsync(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
switch (workflowEvent)
|
||||
{
|
||||
case ExecutorInvokedEvent executorInvoked:
|
||||
Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
|
||||
break;
|
||||
|
||||
case ExecutorCompletedEvent executorCompleted:
|
||||
Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
|
||||
break;
|
||||
|
||||
case DeclarativeActionInvokedEvent actionInvoked:
|
||||
Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]");
|
||||
break;
|
||||
|
||||
case DeclarativeActionCompletedEvent actionComplete:
|
||||
Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]");
|
||||
break;
|
||||
|
||||
case ExecutorFailedEvent executorFailure:
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure...");
|
||||
|
||||
case SuperStepCompletedEvent checkpointCompleted:
|
||||
this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint;
|
||||
Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]");
|
||||
if (externalResponse is not null)
|
||||
{
|
||||
shouldExit = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case RequestInfoEvent requestInfo:
|
||||
Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
|
||||
externalResponse = requestInfo.Request;
|
||||
break;
|
||||
|
||||
case ConversationUpdateEvent invokeEvent:
|
||||
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
|
||||
break;
|
||||
|
||||
case MessageActivityEvent activityEvent:
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("\nACTIVITY:");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(activityEvent.Message.Trim());
|
||||
break;
|
||||
|
||||
case AgentRunUpdateEvent streamEvent:
|
||||
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
|
||||
{
|
||||
hasStreamed = false;
|
||||
messageId = streamEvent.Update.MessageId;
|
||||
|
||||
if (messageId is not null)
|
||||
{
|
||||
string? agentName = streamEvent.Update.AuthorName ?? streamEvent.Update.AgentId ?? nameof(ChatRole.Assistant);
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write($"\n{agentName.ToUpperInvariant()}:");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{messageId}]");
|
||||
}
|
||||
}
|
||||
|
||||
ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
|
||||
switch (chatUpdate?.RawRepresentation)
|
||||
{
|
||||
case ImageGenerationCallResponseItem messageUpdate:
|
||||
await DownloadFileContentAsync(Path.GetFileName("response.png"), messageUpdate.ImageResultBytes).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case FunctionCallResponseItem actionUpdate:
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.Write($"Calling tool: {actionUpdate.FunctionName}");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{actionUpdate.CallId}]");
|
||||
break;
|
||||
|
||||
case McpToolCallItem actionUpdate:
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.Write($"Calling tool: {actionUpdate.ToolName}");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" [{actionUpdate.Id}]");
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Console.ResetColor();
|
||||
Console.Write(streamEvent.Update.Text);
|
||||
hasStreamed |= !string.IsNullOrEmpty(streamEvent.Update.Text);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
|
||||
case AgentRunResponseEvent messageEvent:
|
||||
try
|
||||
{
|
||||
if (hasStreamed)
|
||||
{
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
if (messageEvent.Response.Usage is not null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
#if DEBUG_OUTPUT
|
||||
Debug.WriteLine($"UNHANDLED: {workflowEvent.GetType().Name}");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldExit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return externalResponse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle request for external input.
|
||||
/// </summary>
|
||||
private async ValueTask<ExternalInputResponse> HandleExternalRequestAsync(ExternalRequest request)
|
||||
{
|
||||
ExternalInputRequest inputRequest =
|
||||
request.DataAs<ExternalInputRequest>() ??
|
||||
throw new InvalidOperationException($"Expected external request type: {request.GetType().Name}.");
|
||||
|
||||
List<ChatMessage> responseMessages = [];
|
||||
|
||||
foreach (ChatMessage message in inputRequest.AgentResponse.Messages)
|
||||
{
|
||||
await foreach (ChatMessage responseMessage in this.ProcessInputMessageAsync(message).ConfigureAwait(false))
|
||||
{
|
||||
responseMessages.Add(responseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (responseMessages.Count == 0)
|
||||
{
|
||||
// Must be request for user input.
|
||||
responseMessages.Add(HandleUserInputRequest(inputRequest));
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
return new ExternalInputResponse(responseMessages);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<ChatMessage> ProcessInputMessageAsync(ChatMessage message)
|
||||
{
|
||||
foreach (AIContent requestItem in message.Contents)
|
||||
{
|
||||
ChatMessage? responseMessage =
|
||||
requestItem switch
|
||||
{
|
||||
FunctionCallContent functionCall => await InvokeFunctionAsync(functionCall).ConfigureAwait(false),
|
||||
FunctionApprovalRequestContent functionApprovalRequest => ApproveFunction(functionApprovalRequest),
|
||||
McpServerToolApprovalRequestContent mcpApprovalRequest => ApproveMCP(mcpApprovalRequest),
|
||||
_ => HandleUnknown(requestItem),
|
||||
};
|
||||
|
||||
if (responseMessage is not null)
|
||||
{
|
||||
yield return responseMessage;
|
||||
}
|
||||
}
|
||||
|
||||
ChatMessage? HandleUnknown(AIContent request)
|
||||
{
|
||||
#if DEBUG_OUTPUT
|
||||
Notify($"INPUT - Unknown: {request.GetType().Name} [{request.RawRepresentation?.GetType().Name ?? "*"}]");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
ChatMessage ApproveFunction(FunctionApprovalRequestContent functionApprovalRequest)
|
||||
{
|
||||
Notify($"INPUT - Approving Function: {functionApprovalRequest.FunctionCall.Name}");
|
||||
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]);
|
||||
}
|
||||
|
||||
ChatMessage ApproveMCP(McpServerToolApprovalRequestContent mcpApprovalRequest)
|
||||
{
|
||||
Notify($"INPUT - Approving MCP: {mcpApprovalRequest.ToolCall.ToolName}");
|
||||
return new ChatMessage(ChatRole.User, [mcpApprovalRequest.CreateResponse(approved: true)]);
|
||||
}
|
||||
|
||||
async Task<ChatMessage> InvokeFunctionAsync(FunctionCallContent functionCall)
|
||||
{
|
||||
Notify($"INPUT - Executing Function: {functionCall.Name}");
|
||||
AIFunction functionTool = this.FunctionMap[functionCall.Name];
|
||||
AIFunctionArguments? functionArguments = functionCall.Arguments is null ? null : new(functionCall.Arguments.NormalizePortableValues());
|
||||
object? result = await functionTool.InvokeAsync(functionArguments).ConfigureAwait(false);
|
||||
return new ChatMessage(ChatRole.Tool, [new FunctionResultContent(functionCall.CallId, JsonSerializer.Serialize(result))]);
|
||||
}
|
||||
}
|
||||
|
||||
private static ChatMessage HandleUserInputRequest(ExternalInputRequest request)
|
||||
{
|
||||
string prompt =
|
||||
string.IsNullOrWhiteSpace(request.AgentResponse.Text) || request.AgentResponse.ResponseId is not null ?
|
||||
"INPUT:" :
|
||||
request.AgentResponse.Text;
|
||||
|
||||
string? userInput;
|
||||
do
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGreen;
|
||||
Console.Write($"{prompt} ");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
userInput = Console.ReadLine();
|
||||
}
|
||||
while (string.IsNullOrWhiteSpace(userInput));
|
||||
|
||||
return new ChatMessage(ChatRole.User, userInput);
|
||||
}
|
||||
|
||||
private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content)
|
||||
{
|
||||
string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename));
|
||||
filePath = Path.ChangeExtension(filePath, ".png");
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, content.ToArray()).ConfigureAwait(false);
|
||||
|
||||
Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/C start {filePath}"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Shared.Workflows;
|
||||
|
||||
internal static class Application
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration key used to identify the Foundry project endpoint.
|
||||
/// </summary>
|
||||
public static class Settings
|
||||
{
|
||||
public const string FoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
|
||||
public const string FoundryModelMini = "FOUNDRY_MODEL_DEPLOYMENT_NAME";
|
||||
public const string FoundryModelFull = "FOUNDRY_MEDIA_DEPLOYMENT_NAME";
|
||||
public const string FoundryGroundingTool = "FOUNDRY_CONNECTION_GROUNDING_TOOL";
|
||||
}
|
||||
|
||||
public static string GetInput(string[] args)
|
||||
{
|
||||
string? input = args.FirstOrDefault();
|
||||
|
||||
try
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGreen;
|
||||
|
||||
Console.Write("\nINPUT: ");
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
Console.WriteLine(input);
|
||||
return input;
|
||||
}
|
||||
while (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
input = Console.ReadLine();
|
||||
}
|
||||
|
||||
return input.Trim();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
public static string? GetRepoFolder()
|
||||
{
|
||||
DirectoryInfo? current = new(Directory.GetCurrentDirectory());
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(current.FullName, ".git")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetValue(this IConfiguration configuration, string settingName) =>
|
||||
configuration[settingName] ??
|
||||
throw new InvalidOperationException($"Undefined configuration setting: {settingName}");
|
||||
|
||||
/// <summary>
|
||||
/// Initialize configuration and environment
|
||||
/// </summary>
|
||||
public static IConfigurationRoot InitializeConfig() =>
|
||||
new ConfigurationBuilder()
|
||||
.AddUserSecrets(Assembly.GetExecutingAssembly())
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Workflow Settings
|
||||
|
||||
Common support configuration and environment used in workflow samples.
|
||||
|
||||
To use this in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
|
||||
</PropertyGroup>
|
||||
```
|
||||
Reference in New Issue
Block a user