diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs index 2e2a6460af..10ac05f003 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs @@ -17,7 +17,7 @@ namespace Microsoft.Agents.Workflows.Declarative; public static class DeclarativeWorkflowBuilder { /// - /// Builds a workflow from the provided YAML definition. + /// Builder for converting a Foundry workflow object-model YAML definition into a process. /// /// The type of the input message /// The path to the workflow. @@ -55,18 +55,19 @@ public static class DeclarativeWorkflowBuilder throw new DeclarativeModelException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(Workflow)}."); } - string rootId = WorkflowActionVisitor.Steps.Root(workflowElement.BeginDialog?.Id.Value); + string rootId = WorkflowActionVisitor.Steps.Root(workflowElement); WorkflowFormulaState state = new(options.CreateRecalcEngine()); state.Initialize(workflowElement.WrapWithBot(), options.Configuration); DeclarativeWorkflowExecutor rootExecutor = new(rootId, + options.AgentProvider, state, message => inputTransform?.Invoke(message) ?? DefaultTransform(message)); WorkflowActionVisitor visitor = new(rootExecutor, state, options); WorkflowElementWalker walker = new(visitor); - walker.Visit(rootElement); + walker.Visit(workflowElement); return visitor.Complete(); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/AgentProviderExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/AgentProviderExtensions.cs new file mode 100644 index 0000000000..e5b8068b1b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/AgentProviderExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.Workflows.Declarative.Extensions; + +internal static class AgentProviderExtensions +{ + public static async IAsyncEnumerable InvokeAgentAsync( + this WorkflowAgentProvider agentProvider, + string executorId, + IWorkflowContext context, + string agentName, + string? conversationId, + bool autoSend, + string? additionalInstructions = null, + IEnumerable? inputMessages = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + AIAgent agent = await agentProvider.GetAgentAsync(agentName, cancellationToken).ConfigureAwait(false); + + ChatClientAgentRunOptions options = + new( + new ChatOptions() + { + Instructions = additionalInstructions, + }); + + AgentThread agentThread = conversationId is not null && agent is ChatClientAgent chatClientAgent ? chatClientAgent.GetNewThread(conversationId) : agent.GetNewThread(); + IAsyncEnumerable agentUpdates = + inputMessages is not null ? + agent.RunStreamingAsync([.. inputMessages], agentThread, options, cancellationToken) : + agent.RunStreamingAsync(agentThread, options, cancellationToken); + + await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false)) + { + await AssignConversationIdAsync(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false); + + if (autoSend) + { + await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update)).ConfigureAwait(false); + } + + yield return update; + } + + async ValueTask AssignConversationIdAsync(string? assignValue) + { + if (assignValue is not null && conversationId is null) + { + conversationId = assignValue; + + await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false); + } + } + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs index 66f7254d6f..9eec9796ef 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs @@ -16,10 +16,15 @@ internal static class ChatMessageExtensions FormulaValue.NewRecordFromFields(message.GetMessageFields()); public static TableValue ToTable(this IEnumerable messages) => - FormulaValue.NewTable(s_messageRecordType, messages.Select(message => message.ToRecord())); + FormulaValue.NewTable(TypeSchema.Message.MessageRecordType, messages.Select(message => message.ToRecord())); - public static IEnumerable ToChatMessages(this DataValue messages) + public static IEnumerable? ToChatMessages(this DataValue? messages) { + if (messages is null || messages is BlankDataValue) + { + return null; + } + if (messages is TableDataValue table) { return table.ToChatMessages(); @@ -35,7 +40,7 @@ internal static class ChatMessageExtensions return [text.ToChatMessage()]; } - return []; + return null; } public static IEnumerable ToChatMessages(this TableDataValue messages) @@ -185,10 +190,11 @@ internal static class ChatMessageExtensions private static IEnumerable GetMessageFields(this ChatMessage message) { + yield return new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula()); yield return new NamedValue(TypeSchema.Message.Fields.Id, message.MessageId.ToFormula()); yield return new NamedValue(TypeSchema.Message.Fields.Role, message.Role.Value.ToFormula()); yield return new NamedValue(TypeSchema.Message.Fields.Author, message.AuthorName.ToFormula()); - yield return new NamedValue(TypeSchema.Message.Fields.Content, FormulaValue.NewTable(s_contentRecordType, message.GetContentRecords())); + yield return new NamedValue(TypeSchema.Message.Fields.Content, FormulaValue.NewTable(TypeSchema.Message.ContentRecordType, message.GetContentRecords())); yield return new NamedValue(TypeSchema.Message.Fields.Text, message.Text.ToFormula()); yield return new NamedValue(TypeSchema.Message.Fields.Metadata, message.AdditionalProperties.ToRecord()); } @@ -228,18 +234,4 @@ internal static class ChatMessageExtensions } } } - - private static readonly RecordType s_contentRecordType = - RecordType.Empty() - .Add(TypeSchema.Message.Fields.ContentType, FormulaType.String) - .Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String); - - private static readonly RecordType s_messageRecordType = - RecordType.Empty() - .Add(TypeSchema.Message.Fields.Id, FormulaType.String) - .Add(TypeSchema.Message.Fields.Role, FormulaType.String) - .Add(TypeSchema.Message.Fields.Author, FormulaType.String) - .Add(TypeSchema.Message.Fields.Content, s_contentRecordType.ToTable()) - .Add(TypeSchema.Message.Fields.Text, FormulaType.String) - .Add(TypeSchema.Message.Fields.Metadata, RecordType.Empty()); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs index cc4761bf07..034f4bcbb7 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs @@ -9,7 +9,9 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; using Microsoft.PowerFx.Types; using BlankType = Microsoft.PowerFx.Types.BlankType; @@ -37,6 +39,7 @@ internal static class FormulaValueExtensions DateTime dateonlyValue when dateonlyValue.TimeOfDay == TimeSpan.Zero => FormulaValue.NewDateOnly(dateonlyValue), DateTime datetimeValue => FormulaValue.New(datetimeValue), TimeSpan timeValue => FormulaValue.New(timeValue), + ChatMessage chatMessage => chatMessage.ToRecord(), ExpandoObject expandoValue => expandoValue.ToRecord(), object when value is IDictionary dictionaryValue => dictionaryValue.ToRecord(), object when value is IEnumerable tableValue => tableValue.ToTable(), @@ -209,10 +212,40 @@ internal static class FormulaValueExtensions return TableType.Empty(); } - private static TableValue ToTable(this IEnumerable value) => - FormulaValue.NewTable( - value.ToTableType().ToRecord(), - [.. value.OfType().Select(element => element.ToRecord())]); + private static TableValue ToTable(this IEnumerable value) + { + Type? elementType = value.GetType().GetElementType(); + if (elementType is null || elementType == typeof(object)) + { + IEnumerator enumerator = value.GetEnumerator(); + if (enumerator.MoveNext()) + { + elementType = enumerator.Current?.GetType(); + } + } + + return + elementType switch + { + null => FormulaValue.NewTable(RecordType.EmptySealed(), []), + _ when elementType == typeof(ExpandoObject) => + FormulaValue.NewTable( + value.ToTableType().ToRecord(), + [.. value.OfType().Select(element => element.ToRecord())]), + _ when typeof(ChatMessage).IsAssignableFrom(elementType) => + FormulaValue.NewTable( + TypeSchema.Message.MessageRecordType, + [.. value.OfType().Select(message => message.ToRecord())]), + _ when typeof(IDictionary).IsAssignableFrom(elementType) => value.ToTableOfRecords(), + _ => throw new DeclarativeModelException($"Unsupported element type: {elementType.Name}"), + }; + } + + private static TableValue ToTableOfRecords(this IEnumerable list) + { + RecordValue[] elements = [.. list.OfType().Select(table => table.ToRecord())]; + return FormulaValue.NewTable(elements.First().Type, elements); + } private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue()); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs index ee1b5697c2..d85bf373a7 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Interpreter; @@ -20,7 +19,7 @@ internal static class IWorkflowContextExtensions context.AddEventAsync(new DeclarativeActionCompletedEvent(action)); public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) => - context.SendMessageAsync(new ExecutorResultMessage(id, result)); + context.SendMessageAsync(new ActionExecutorResult(id, result)); public static ValueTask QueueStateResetAsync(this IWorkflowContext context, PropertyPath variablePath) => context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.NamespaceAlias)); @@ -47,25 +46,6 @@ internal static class IWorkflowContextExtensions await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false); } - // Ensure "System.Conversation.Id" and "System.ConversationId" are properly initialized when referenced. - public static async ValueTask EnsureWorkflowConversationAsync(this IWorkflowContext context, WorkflowAgentProvider agentProvider, StringExpression expression, CancellationToken cancellationToken) - { - if (expression.IsVariableReference && - expression.VariableReference.IsVariableReferenceWithScope(VariableNamespace.System, out string? variableName)) - { - if (string.Equals(variableName, SystemScope.Names.Conversation, StringComparison.Ordinal) || - string.Equals(variableName, SystemScope.Names.ConversationId, StringComparison.Ordinal)) - { - FormulaValue variableValue = context.ReadState(SystemScope.Names.ConversationId, VariableScopeNames.System); - if (variableValue is BlankValue) - { - string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false); - await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false); - } - } - } - } - private static DeclarativeWorkflowContext DeclarativeContext(IWorkflowContext context) { if (context is not DeclarativeWorkflowContext declarativeContext) diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ActionExecutorResult.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ActionExecutorResult.cs new file mode 100644 index 0000000000..c75f82195c --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ActionExecutorResult.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.Workflows.Declarative.Interpreter; + +/// +/// Message sent to initiate a transition to another . +/// +public sealed record class ActionExecutorResult +{ + /// + /// The identifier of the that produced this message. + /// + public string ExecutorId { get; } + + /// + /// The result of the action, if any provided. + /// + public object? Result { get; } + + internal ActionExecutorResult(string executorId, object? result = null) + { + this.ExecutorId = executorId; + this.Result = result; + } + + internal static ActionExecutorResult ThrowIfNot(object? message) + { + if (message is not ActionExecutorResult executorMessage) + { + throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ActionExecutorResult)})"); + } + + return executorMessage; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs index c870b77138..f323f7cc57 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs @@ -21,7 +21,7 @@ internal abstract class DeclarativeActionExecutor(TAction model, Workfl public new TAction Model => (TAction)base.Model; } -internal abstract class DeclarativeActionExecutor : Executor +internal abstract class DeclarativeActionExecutor : Executor { private string? _parentId; private readonly WorkflowFormulaState _state; @@ -54,7 +54,7 @@ internal abstract class DeclarativeActionExecutor : Executor true; /// - public override async ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context) + public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context) { if (this.Model.Disabled) { @@ -64,11 +64,10 @@ internal abstract class DeclarativeActionExecutor : Executor - public ValueTask ReadStateAsync(string key, string? scopeName = null) => this.Source.ReadStateAsync(key, scopeName); + public async ValueTask ReadStateAsync(string key, string? scopeName = null) + { + bool isManagedScope = + scopeName is not null && // null scope cannot be managed + VariableScopeNames.IsValidName(scopeName); + + return typeof(TValue) switch + { + // 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 !isManagedScope => await this.Source.ReadStateAsync(key, scopeName).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. + _ => await this.Source.ReadStateAsync(key, scopeName).ConfigureAwait(false), + }; + } /// public ValueTask> ReadStateKeysAsync(string? scopeName = null) => this.Source.ReadStateKeysAsync(scopeName); @@ -86,9 +101,8 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext private ValueTask UpdateStateAsync(string key, T? value, string? scopeName, bool allowSystem = true) { bool isManagedScope = - scopeName != null && // null scope cannot be managed - (ManagedScopes.Contains(scopeName) || - (allowSystem && VariableScopeNames.System.Equals(scopeName, StringComparison.Ordinal))); + scopeName is not null && // null scope cannot be managed + VariableScopeNames.IsValidName(scopeName); if (!isManagedScope) { @@ -97,6 +111,11 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext return this.Source.QueueStateUpdateAsync(key, value, scopeName); } + if (!ManagedScopes.Contains(scopeName!) && !allowSystem) + { + throw new DeclarativeActionException($"Cannot manage variable definitions in scope: '{scopeName}'."); + } + return value switch { null => QueueEmptyStateAsync(), @@ -127,19 +146,19 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext ValueTask QueueDataValueStateAsync(DataValue dataValue) { - FormulaValue formulaValue = dataValue.ToFormula(); if (isManagedScope) { + FormulaValue formulaValue = dataValue.ToFormula(); this.State.Set(key, formulaValue, scopeName); } - return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName); + return this.Source.QueueStateUpdateAsync(key, dataValue.ToObject(), scopeName); } ValueTask QueueNativeStateAsync(object? rawValue) { - FormulaValue formulaValue = rawValue.ToFormula(); if (isManagedScope) { + FormulaValue formulaValue = rawValue.ToFormula(); this.State.Set(key, formulaValue, scopeName); } return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs index a912258157..b634dc21e3 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Extensions.AI; @@ -12,6 +13,7 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter; /// internal sealed class DeclarativeWorkflowExecutor( string workflowId, + WorkflowAgentProvider agentProvider, WorkflowFormulaState state, Func inputTransform) : Executor(workflowId) @@ -22,9 +24,15 @@ internal sealed class DeclarativeWorkflowExecutor( // No state to restore if we're starting from the beginning. state.SetInitialized(); + DeclarativeWorkflowContext declarativeContext = new(context, state); ChatMessage input = inputTransform.Invoke(message); - state.SetLastMessage(input); - await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false); + string conversationId = await agentProvider.CreateConversationAsync(cancellationToken: default).ConfigureAwait(false); + await declarativeContext.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false); + + await agentProvider.CreateMessageAsync(conversationId, input, cancellationToken: default).ConfigureAwait(false); + await declarativeContext.SetLastMessageAsync(input).ConfigureAwait(false); + + await context.SendResultMessageAsync(this.Id).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs index efa4f04643..8f52f53c53 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs @@ -10,10 +10,10 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter; internal delegate ValueTask DelegateAction(IWorkflowContext context, TMessage message, CancellationToken cancellationToken) where TMessage : notnull; -internal sealed class DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction? action = null, bool emitResult = true) - : DelegateActionExecutor(actionId, state, action, emitResult) +internal sealed class DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction? action = null, bool emitResult = true) + : DelegateActionExecutor(actionId, state, action, emitResult) { - public override ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context) + public override ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context) { Debug.WriteLine($"RESULT #{this.Id} - {message.Result ?? "(null)"}"); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs deleted file mode 100644 index 99d3a71984..0000000000 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.Workflows.Declarative.Interpreter; - -internal sealed record class ExecutorResultMessage(string ExecutorId, object? Result = null) -{ - public static ExecutorResultMessage ThrowIfNot(object? message) - { - if (message is not ExecutorResultMessage executorMessage) - { - throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ExecutorResultMessage)})"); - } - - return executorMessage; - } -} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs index c9ebdca9a6..c85d885fa1 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs @@ -17,9 +17,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor internal static class Steps { + public static string Root(AdaptiveDialog action) => $"{action.BeginDialog?.Id.Value ?? DefaultWorkflowId}_{nameof(Root)}"; + public static string Root(string? actionId = null) => $"{actionId ?? DefaultWorkflowId}_{nameof(Root)}"; public static string Post(string actionId) => $"{actionId}_{nameof(Post)}"; + + public static string Restart(string actionId) => $"{actionId}_{nameof(Restart)}"; } private readonly WorkflowBuilder _workflowBuilder; @@ -66,17 +70,21 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor // Complete the action scope. void CompletionHandler() { + // No completion for root scope if (this._workflowModel.GetDepth(item.Id.Value) > 1) { - DelegateAction? action = null; + DelegateAction? action = null; ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(parentId); if (conditionGroup is not null) { action = conditionGroup.DoneAsync; } - string completionId = this.ContinuationFor(item.Id.Value, action); // End scope - this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId); // Connect with final action - this._workflowModel.AddLink(completionId, Steps.Post(parentId)); // Merge with parent scope + + // Define post action for this scope + string completionId = this.ContinuationFor(item.Id.Value, action); + this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId); + // Transition to post action of parent scope + this._workflowModel.AddLink(completionId, Steps.Post(parentId)); } } } @@ -85,11 +93,11 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { this.Trace(item); - ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(item.GetParentId()); + string parentId = GetParentId(item); + ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(parentId); if (conditionGroup is not null) { string stepId = ConditionGroupExecutor.Steps.Item(conditionGroup.Model, item); - string parentId = GetParentId(item); this._workflowModel.AddNode(new DelegateActionExecutor(stepId, this._workflowState), parentId, CompletionHandler); base.VisitConditionItem(item); @@ -145,9 +153,12 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { this.Trace(item); - GotoExecutor action = new(item, this._workflowState); + // Represent action with default executor + DefaultActionExecutor action = new(item, this._workflowState); this.ContinueWith(action); + // Transition to target action this._workflowModel.AddLink(action.Id, item.ActionId.Value); + // Define a clean-start to ensure "goto" is not a source for any edge this.RestartAfter(action.Id, action.ParentId); } @@ -155,21 +166,28 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { this.Trace(item); + // Entry point for loop ForeachExecutor action = new(item, this._workflowState); string loopId = ForeachExecutor.Steps.Next(action.Id); - this.ContinueWith(action, condition: null, CompletionHandler); // Foreach - this.ContinueWith(new DelegateActionExecutor(loopId, this._workflowState, action.TakeNextAsync), action.Id); // Loop Increment - string continuationId = this.ContinuationFor(action.Id, action.ParentId); // Action continuation + this.ContinueWith(action, condition: null, CompletionHandler); + // Transition to select the next item + this.ContinueWith(new DelegateActionExecutor(loopId, this._workflowState, action.TakeNextAsync), action.Id); + + // Transition to post action if no more items + string continuationId = this.ContinuationFor(action.Id, action.ParentId); this._workflowModel.AddLink(loopId, continuationId, (_) => !action.HasValue); + // Transition to start of inner actions if there is a current item string startId = ForeachExecutor.Steps.Start(action.Id); this._workflowModel.AddNode(new DelegateActionExecutor(startId, this._workflowState), action.Id); this._workflowModel.AddLink(loopId, startId, (_) => action.HasValue); void CompletionHandler() { - string endActionsId = ForeachExecutor.Steps.End(action.Id); // Loop continuation + // Transition to end of inner actions + string endActionsId = ForeachExecutor.Steps.End(action.Id); this.ContinueWith(new DelegateActionExecutor(endActionsId, this._workflowState, action.ResetAsync), action.Id); + // Transition to select the next item this._workflowModel.AddLink(endActionsId, loopId); } } @@ -178,13 +196,18 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { this.Trace(item); - ForeachExecutor? loopExecutor = this._workflowModel.LocateParent(item.GetParentId()); - if (loopExecutor is not null) + // Locate the nearest "Foreach" loop that contains this action + ForeachExecutor? loopAction = this._workflowModel.LocateParent(item.GetParentId()); + // Skip action if its not contained a loop + if (loopAction is not null) { - DefaultActionExecutor breakLoopExecutor = new(item, this._workflowState); - this.ContinueWith(breakLoopExecutor); - this._workflowModel.AddLink(breakLoopExecutor.Id, Steps.Post(loopExecutor.Id)); - this.RestartAfter(breakLoopExecutor.Id, breakLoopExecutor.ParentId); + // Represent action with default executor + DefaultActionExecutor action = new(item, this._workflowState); + this.ContinueWith(action); + // Transition to post action + this._workflowModel.AddLink(action.Id, Steps.Post(loopAction.Id)); + // Define a clean-start to ensure "break" is not a source for any edge + this.RestartAfter(action.Id, action.ParentId); } } @@ -192,34 +215,21 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { this.Trace(item); - ForeachExecutor? loopExecutor = this._workflowModel.LocateParent(item.GetParentId()); - if (loopExecutor is not null) + // Locate the nearest "Foreach" loop that contains this action + ForeachExecutor? loopAction = this._workflowModel.LocateParent(item.GetParentId()); + // Skip action if its not contained a loop + if (loopAction is not null) { - DefaultActionExecutor continueLoopExecutor = new(item, this._workflowState); - this.ContinueWith(continueLoopExecutor); - this._workflowModel.AddLink(continueLoopExecutor.Id, ForeachExecutor.Steps.Next(loopExecutor.Id)); - this.RestartAfter(continueLoopExecutor.Id, continueLoopExecutor.ParentId); + // Represent action with default executor + DefaultActionExecutor action = new(item, this._workflowState); + this.ContinueWith(action); + // Transition to select the next item + this._workflowModel.AddLink(action.Id, ForeachExecutor.Steps.Next(loopAction.Id)); + // Define a clean-start to ensure "continue" is not a source for any edge + this.RestartAfter(action.Id, action.ParentId); } } - protected override void Visit(EndConversation item) - { - this.Trace(item); - - DefaultActionExecutor endExecutor = new(item, this._workflowState); - this.ContinueWith(endExecutor); - this.RestartAfter(item.Id.Value, endExecutor.ParentId); - } - - protected override void Visit(EndDialog item) - { - this.Trace(item); - - DefaultActionExecutor endExecutor = new(item, this._workflowState); - this.ContinueWith(endExecutor); - this.RestartAfter(item.Id.Value, endExecutor.ParentId); - } - protected override void Visit(Question item) { this.Trace(item); @@ -228,25 +238,55 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor string actionId = item.GetId(); string postId = Steps.Post(actionId); - QuestionExecutor questionExecutor = new(item, this._workflowState); - this.ContinueWith(questionExecutor); + // Entry point for question + QuestionExecutor action = new(item, this._workflowState); + this.ContinueWith(action); + // Transition to post action if complete this._workflowModel.AddLink(actionId, postId, QuestionExecutor.IsComplete); + // Perpare for input request if not complete string prepareId = QuestionExecutor.Steps.Prepare(actionId); - this.ContinueWith(new DelegateActionExecutor(prepareId, this._workflowState, questionExecutor.PrepareResponseAsync, emitResult: false), parentId, message => !QuestionExecutor.IsComplete(message)); + this.ContinueWith(new DelegateActionExecutor(prepareId, this._workflowState, action.PrepareResponseAsync, emitResult: false), parentId, message => !QuestionExecutor.IsComplete(message)); + // Define input action string inputId = QuestionExecutor.Steps.Input(actionId); + //ModeledPort inputPort = new(InputPort.Create(inputId)); // %%% MODELING InputPort inputPort = InputPort.Create(inputId); this._workflowModel.AddPort(inputPort, parentId); this._workflowModel.AddLinkFromPeer(parentId, inputId); + // Capture input response string captureId = QuestionExecutor.Steps.Capture(actionId); - this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, questionExecutor.CaptureResponseAsync, emitResult: false), parentId); + this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), parentId); - this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, questionExecutor.CompleteAsync), parentId, QuestionExecutor.IsComplete); + // Transition to post action if complete + this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), parentId, QuestionExecutor.IsComplete); + // Transition to prepare action if not complete this._workflowModel.AddLink(captureId, prepareId, message => !QuestionExecutor.IsComplete(message)); } + protected override void Visit(EndDialog 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(EndConversation 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); @@ -354,15 +394,9 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor #region Not supported - protected override void Visit(AnswerQuestionWithAI item) - { - this.NotSupported(item); - } + protected override void Visit(AnswerQuestionWithAI item) => this.NotSupported(item); - protected override void Visit(DeleteActivity item) - { - this.NotSupported(item); - } + protected override void Visit(DeleteActivity item) => this.NotSupported(item); protected override void Visit(GetActivityMembers item) => this.NotSupported(item); @@ -386,10 +420,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor protected override void Visit(AdaptiveCardPrompt item) => this.NotSupported(item); - protected override void Visit(CSATQuestion item) - { - this.NotSupported(item); - } + protected override void Visit(CSATQuestion item) => this.NotSupported(item); protected override void Visit(OAuthInput item) => this.NotSupported(item); @@ -452,9 +483,9 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this._workflowModel.AddLinkFromPeer(parentId, executor.Id, condition); } - private string ContinuationFor(string parentId, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction); + private string ContinuationFor(string parentId, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction); - private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null) + private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null) { actionId = Steps.Post(actionId); this._workflowModel.AddNode(new DelegateActionExecutor(actionId, this._workflowState, stepAction), parentId); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs index 4944d15552..1e0694a572 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs @@ -18,8 +18,6 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); - await context.EnsureWorkflowConversationAsync(agentProvider, this.Model.ConversationId, cancellationToken).ConfigureAwait(false); - string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; ChatMessage newMessage = new(this.Model.Role.Value.ToChatRole(), [.. this.GetContent()]) { AdditionalProperties = this.GetMetadata() }; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs index 7d1bb7c68a..76180e9cf5 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs @@ -37,13 +37,13 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor + public async ValueTask DoneAsync(IWorkflowContext context, ActionExecutorResult _, CancellationToken cancellationToken) => await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs index 997e9bf8fd..abf450050a 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; @@ -18,14 +19,13 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); - await context.EnsureWorkflowConversationAsync(agentProvider, this.Model.ConversationId, cancellationToken).ConfigureAwait(false); string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; - DataValue? inputMessages = this.GetInputMessages(); + IEnumerable? inputMessages = this.GetInputMessages(); if (inputMessages is not null) { - foreach (ChatMessage message in inputMessages.ToChatMessages()) + foreach (ChatMessage message in inputMessages) { await agentProvider.CreateMessageAsync(conversationId, message, cancellationToken).ConfigureAwait(false); } @@ -34,7 +34,7 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages return default; } - private DataValue? GetInputMessages() + private IEnumerable? GetInputMessages() { DataValue? messages = null; @@ -44,6 +44,6 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages messages = expressionResult.Value; } - return messages; + return messages?.ToChatMessages(); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/GotoExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/GotoExecutor.cs deleted file mode 100644 index a7ba9bb731..0000000000 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/GotoExecutor.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.Workflows.Declarative.Interpreter; -using Microsoft.Agents.Workflows.Declarative.PowerFx; -using Microsoft.Bot.ObjectModel; - -namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; - -internal sealed class GotoExecutor(GotoAction model, WorkflowFormulaState state) : - DeclarativeActionExecutor(model, state) -{ - protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - // No action needed - the edge will be followed automatically - return default; - } -} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs index cc122f7cb6..24093456a3 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs @@ -28,71 +28,31 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA string agentName = this.GetAgentName(); string? additionalInstructions = this.GetAdditionalInstructions(); bool autoSend = this.GetAutoSendValue(); - DataValue? inputMessages = this.GetInputMessages(); + IEnumerable? inputMessages = this.GetInputMessages(); - AgentRunResponse agentResponse = InvokeAgentAsync().ToEnumerable().ToAgentRunResponse(); + AgentRunResponse agentResponse = agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, inputMessages, cancellationToken).ToEnumerable().ToAgentRunResponse(); if (autoSend) { await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false); } - ChatMessage response = agentResponse.Messages[agentResponse.Messages.Count - 1]; - await this.AssignAsync(this.AgentOutput?.Messages?.Path, response.ToRecord(), context).ConfigureAwait(false); + await this.AssignAsync(this.AgentOutput?.Messages?.Path, agentResponse.Messages.ToTable(), context).ConfigureAwait(false); return default; - - async IAsyncEnumerable InvokeAgentAsync() - { - AIAgent agent = await agentProvider.GetAgentAsync(agentName, cancellationToken).ConfigureAwait(false); - - ChatClientAgentRunOptions options = - new( - new ChatOptions() - { - Instructions = additionalInstructions, - }); - - AgentThread agentThread = conversationId is not null && agent is ChatClientAgent chatClientAgent ? chatClientAgent.GetNewThread(conversationId) : agent.GetNewThread(); - IAsyncEnumerable agentUpdates = - inputMessages is not null ? - agent.RunStreamingAsync([.. inputMessages.ToChatMessages()], agentThread, options, cancellationToken) : - agent.RunStreamingAsync(agentThread, options, cancellationToken); - - await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false)) - { - await AssignConversationIdAsync(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false); - - if (autoSend) - { - await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false); - } - - yield return update; - } - } - - async ValueTask AssignConversationIdAsync(string? assignValue) - { - if (assignValue is not null && conversationId is null) - { - conversationId = assignValue; - - await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false); - } - } } - private DataValue? GetInputMessages() + private IEnumerable? GetInputMessages() { DataValue? userInput = null; + if (this.AgentInput?.Messages is not null) { EvaluationResult expressionResult = this.Evaluator.GetValue(this.AgentInput.Messages); userInput = expressionResult.Value; } - return userInput; + return userInput?.ToChatMessages(); } private string? GetConversationId() diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs index 5da764f76a..cb6d1c5a0d 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -29,9 +29,9 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat protected override bool IsDiscreteAction => false; protected override bool EmitResultEvent => false; - public static bool IsComplete(object? message) + public static bool IsComplete(object? message) // %%% BASE CLASS ??? { - ExecutorResultMessage executorMessage = ExecutorResultMessage.ThrowIfNot(message); + ActionExecutorResult executorMessage = ActionExecutorResult.ThrowIfNot(message); return executorMessage.Result is null; } @@ -69,7 +69,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat return default; } - public async ValueTask PrepareResponseAsync(IWorkflowContext context, ExecutorResultMessage message, CancellationToken cancellationToken) + public async ValueTask PrepareResponseAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken) { int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false); InputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt)); @@ -111,7 +111,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat } } - public async ValueTask CompleteAsync(IWorkflowContext context, ExecutorResultMessage message, CancellationToken cancellationToken) + public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken) { await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs index 0fe588faab..8fb0c98dc4 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs @@ -17,7 +17,6 @@ internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMe protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); - await context.EnsureWorkflowConversationAsync(agentProvider, this.Model.ConversationId, cancellationToken).ConfigureAwait(false); string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; string messageId = this.Evaluator.GetValue(Throw.IfNull(this.Model.MessageId, $"{nameof(this.Model)}.{nameof(this.Model.MessageId)}")).Value; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs index f9ccde6a4d..bb99d1b418 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs @@ -19,7 +19,6 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); - await context.EnsureWorkflowConversationAsync(agentProvider, this.Model.ConversationId, cancellationToken).ConfigureAwait(false); string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; ChatMessage[] messages = await agentProvider.GetMessagesAsync( diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/Functions/UserMessage.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/Functions/UserMessage.cs index fdd8048395..2165b5be58 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/Functions/UserMessage.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/Functions/UserMessage.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Extensions.AI; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; @@ -18,6 +19,7 @@ internal sealed class UserMessage : ReflectionFunction string.IsNullOrEmpty(input.Value) ? FormulaValue.NewBlank(RecordType.Empty()) : FormulaValue.NewRecordFromFields( + new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula()), new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(ChatRole.User.Value)), new NamedValue( TypeSchema.Message.Fields.Content, diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs index 94ce5215f9..a6d8d01138 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Frozen; using System.Globalization; +using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.SystemVariables; @@ -89,11 +90,10 @@ internal static class SystemScope } } - public static void SetLastMessage(this WorkflowFormulaState state, ChatMessage message) + public static async ValueTask SetLastMessageAsync(this IWorkflowContext context, ChatMessage message) { - state.Set(Names.LastMessage, message.ToRecord(), VariableScopeNames.System); - state.Set(Names.LastMessageId, message.MessageId is null ? FormulaValue.NewBlank(FormulaType.String) : FormulaValue.New(message.MessageId), VariableScopeNames.System); - state.Set(Names.LastMessageText, FormulaValue.New(message.Text), VariableScopeNames.System); - state.Bind(); + await context.QueueSystemUpdateAsync(Names.LastMessage, message.ToRecord()).ConfigureAwait(false); + await context.QueueSystemUpdateAsync(Names.LastMessageId, string.IsNullOrEmpty(message.MessageId) ? UnassignedValue.Instance : message.MessageId).ConfigureAwait(false); + await context.QueueSystemUpdateAsync(Names.LastMessageText, FormulaValue.New(message.Text)).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/TypeSchema.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/TypeSchema.cs index 21f6770d7b..de746e6d18 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/TypeSchema.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/TypeSchema.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; namespace Microsoft.Agents.Workflows.Declarative.PowerFx.Functions; internal static class TypeSchema { + public const string Discriminator = "__type__"; + public static class Message { public static class Fields @@ -29,5 +32,19 @@ internal static class TypeSchema public const string ImageUrl = nameof(AgentMessageContentType.ImageUrl); public const string ImageFile = nameof(AgentMessageContentType.ImageFile); } + + public static readonly RecordType ContentRecordType = + RecordType.Empty() + .Add(Fields.ContentType, FormulaType.String) + .Add(Fields.ContentValue, FormulaType.String); + + public static readonly RecordType MessageRecordType = + RecordType.Empty() + .Add(Fields.Id, FormulaType.String) + .Add(Fields.Role, FormulaType.String) + .Add(Fields.Author, FormulaType.String) + .Add(Fields.Content, ContentRecordType.ToTable()) + .Add(Fields.Text, FormulaType.String) + .Add(Fields.Metadata, RecordType.Empty()); } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/TestAgent.yaml similarity index 73% rename from dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml rename to dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/TestAgent.yaml index 2d41b2c5dd..01d9ad1554 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/TestAgent.yaml @@ -2,4 +2,4 @@ type: foundry_agent name: BasicAgent description: Basic agent for integration tests model: - id: ${AzureAI:ModelDeployment} + id: ${AzureAI:DeploymentMini} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs index a66240990b..c61c527976 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs @@ -15,7 +15,7 @@ using Xunit.Abstractions; namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests; -public sealed class AzureAgentProviderTest(ITestOutputHelper output) : WorkflowTest(output) +public sealed class AzureAgentProviderTest(ITestOutputHelper output) : IntegrationTest(output) { private AzureAIConfiguration? _configuration; diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs index c237dbbc38..a88e34cb82 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs @@ -1,16 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Linq; using System.Threading.Tasks; -using Azure.Identity; using Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Configuration; -using Shared.IntegrationTests; using Xunit.Abstractions; namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests; @@ -24,52 +17,20 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow [Theory] [InlineData("SendActivity.yaml", "SendActivity.json")] [InlineData("InvokeAgent.yaml", "InvokeAgent.json")] - public Task ValidateAsync(string workflowFileName, string testcaseFileName) => - this.RunWorkflowAsync(workflowFileName, testcaseFileName); + [InlineData("ConversationMessages.yaml", "ConversationMessages.json")] + public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName) => + this.RunWorkflowAsync(Path.Combine("Workflows", workflowFileName), testcaseFileName); - private Task RunWorkflowAsync(string workflowFileName, string testcaseFileName) + [Theory] + [InlineData("Marketing.yaml", "Marketing.json")] + [InlineData("MathChat.yaml", "MathChat.json")] + [InlineData("DeepResearch.yaml", "DeepResearch.json")] + [InlineData("HumanInLoop.yaml", "HumanInLoop.json", Skip = "TODO")] + public Task ValidateScenarioAsync(string workflowFileName, string testcaseFileName) => + this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName), testcaseFileName); + + protected override async Task RunAndVerifyAsync(Testcase testcase, string workflowPath, DeclarativeWorkflowOptions workflowOptions) { - this.Output.WriteLine($"WORKFLOW: {workflowFileName}"); - this.Output.WriteLine($"TESTCASE: {testcaseFileName}"); - - Testcase testcase = ReadTestcase(testcaseFileName); - IConfiguration configuration = InitializeConfig(); - string workflowPath = Path.Combine("Workflows", workflowFileName); - - this.Output.WriteLine($" {testcase.Description}"); - - return - testcase.Setup.Input.Type switch - { - nameof(ChatMessage) => this.RunWorkflowAsync(testcase, workflowPath, configuration), - nameof(String) => this.RunWorkflowAsync(testcase, workflowPath, configuration), - _ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."), - }; - } - - private async Task RunWorkflowAsync( - Testcase testcase, - string workflowPath, - IConfiguration configuration) where TInput : notnull - { - this.Output.WriteLine($"INPUT: {testcase.Setup.Input.Value}"); - - AzureAIConfiguration? foundryConfig = configuration.GetSection("AzureAI").Get(); - Assert.NotNull(foundryConfig); - - IReadOnlyDictionary agentMap = await AgentFixture.GetAgentsAsync(foundryConfig); - - IConfiguration workflowConfig = - new ConfigurationBuilder() - .AddInMemoryCollection(agentMap) - .Build(); - - DeclarativeWorkflowOptions workflowOptions = - new(new AzureAgentProvider(foundryConfig.Endpoint, new AzureCliCredential())) - { - Configuration = workflowConfig, - LoggerFactory = this.Output - }; Workflow workflow = DeclarativeWorkflowBuilder.Build(workflowPath, workflowOptions); WorkflowEvents workflowEvents = await WorkflowHarness.RunAsync(workflow, (TInput)GetInput(testcase)); @@ -78,30 +39,10 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow this.Output.WriteLine($"ACTION: {actionInvokeEvent.ActionId} [{actionInvokeEvent.ActionType}]"); } - Assert.Equal(testcase.Validation.ActionCount, workflowEvents.ActionInvokeEvents.Count); - Assert.Equal(testcase.Validation.ActionCount, workflowEvents.ActionCompleteEvents.Count); + Assert.NotEmpty(workflowEvents.ExecutorInvokeEvents); + Assert.NotEmpty(workflowEvents.ExecutorCompleteEvents); + AssertWorkflow.EventCounts(workflowEvents.ActionInvokeEvents.Count, testcase); + AssertWorkflow.EventCounts(workflowEvents.ActionCompleteEvents.Count, testcase); + AssertWorkflow.EventSequence(workflowEvents.ActionInvokeEvents.Select(e => e.ActionId), testcase); } - - private static object GetInput(Testcase testcase) where TInput : notnull => - testcase.Setup.Input.Type switch - { - nameof(ChatMessage) => new ChatMessage(ChatRole.User, testcase.Setup.Input.Value), - nameof(String) => testcase.Setup.Input.Value, - _ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."), - }; - - private static Testcase ReadTestcase(string testcaseFileName) - { - using Stream testcaseStream = File.Open(Path.Combine("Testcases", testcaseFileName), FileMode.Open); - Testcase? testcase = JsonSerializer.Deserialize(testcaseStream, s_jsonSerializerOptions); - Assert.NotNull(testcase); - return testcase; - } - - private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - WriteIndented = true, - }; } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs index d30ddd0341..0d43836bc6 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs @@ -1,12 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Azure.AI.Agents.Persistent; +using Azure.AI.Projects; using Azure.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; @@ -19,31 +24,67 @@ namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; internal static class AgentFactory { - public static async Task> CreateAsync(string agentsDirectory, AzureAIConfiguration config, CancellationToken cancellationToken) - { - PersistentAgentsClient clientAgents = new(config.Endpoint, new AzureCliCredential()); - - IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); - kernelBuilder.Services.AddSingleton(clientAgents); - Kernel kernel = kernelBuilder.Build(); - - AzureAIAgentFactory factory = new(); - - Dictionary agentMap = []; - - foreach (string file in Directory.GetFiles(agentsDirectory, "*.yaml")) + private static readonly Dictionary _agentDefinitions = + new() { - Debug.WriteLine($"TEST AGENT: Creating - {file}"); - string agentText = File.ReadAllText(file); + ["FOUNDRY_AGENT_TEST"] = "TestAgent.yaml", + ["FOUNDRY_AGENT_ANSWER"] = "QuestionAgent.yaml", + ["FOUNDRY_AGENT_STUDENT"] = "StudentAgent.yaml", + ["FOUNDRY_AGENT_TEACHER"] = "TeacherAgent.yaml", + ["FOUNDRY_AGENT_RESEARCHANALYST"] = "AnalystAgent.yaml", + ["FOUNDRY_AGENT_RESEARCHCODER"] = "CoderAgent.yaml", + ["FOUNDRY_AGENT_RESEARCHMANAGER"] = "ManagerAgent.yaml", + ["FOUNDRY_AGENT_RESEARCHWEATHER"] = "WeatherAgent.yaml", + ["FOUNDRY_AGENT_RESEARCHWEB"] = "WebAgent.yaml", + }; - Agent? agent = await factory.CreateAgentFromYamlAsync(agentText, new AgentCreationOptions() { Kernel = kernel }, configuration: null, cancellationToken); + private static FrozenDictionary? s_agentMap; - Assert.NotNull(agent?.Name); - - Debug.WriteLine($"TEST AGENT: {agent.Name} => {agent.Id}"); - agentMap[agent.Name] = agent.Id; + public static async Task> GetAgentsAsync(AzureAIConfiguration config, IConfiguration configuration, CancellationToken cancellationToken = default) + { + if (s_agentMap is not null) + { + return s_agentMap; } - return agentMap; + PersistentAgentsClient clientAgents = new(config.Endpoint, new AzureCliCredential()); + AIProjectClient clientProjects = new(new Uri(config.Endpoint), new AzureCliCredential()); + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + kernelBuilder.Services.AddSingleton(clientAgents); + kernelBuilder.Services.AddSingleton(clientProjects); + AgentCreationOptions creationOptions = new() { Kernel = kernelBuilder.Build() }; + AzureAIAgentFactory factory = new(); + string repoRoot = WorkflowTest.GetRepoFolder(); + + return s_agentMap = (await Task.WhenAll(_agentDefinitions.Select(kvp => CreateAgentAsync(kvp.Key, kvp.Value, cancellationToken)))).ToFrozenDictionary(t => t.Name, t => t.Id); + + async Task<(string Name, string? Id)> CreateAgentAsync(string id, string file, CancellationToken cancellationToken) + { + try + { + string filePath = Path.Combine("Agents", file); + if (!File.Exists(filePath)) + { + filePath = Path.Combine(repoRoot, "workflow-samples/setup", file); + } + Assert.True(File.Exists(filePath), $"Agent definition file not found: {file}"); + + Debug.WriteLine($"TEST AGENT: Creating - {file}"); + string agentText = File.ReadAllText(filePath); + + Agent? agent = await factory.CreateAgentFromYamlAsync(agentText, creationOptions, configuration, cancellationToken); + + Assert.NotNull(agent?.Name); + + Debug.WriteLine($"TEST AGENT: {agent.Name} => {agent.Id} [{id}]"); + + return (id, agent.Id); + } + catch (Exception exception) + { + Console.WriteLine($"FAILURE: Error creating agent {id} from file {file}: {exception.Message}"); + throw; + } + } } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs deleted file mode 100644 index d8836cfa94..0000000000 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Shared.IntegrationTests; - -namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; - -public static class AgentFixture -{ - private static IReadOnlyDictionary? s_agentMap; - - internal static async Task> GetAgentsAsync(AzureAIConfiguration config, CancellationToken cancellationToken = default) - { - s_agentMap ??= await AgentFactory.CreateAsync("Agents", config, cancellationToken); - - return s_agentMap; - } -} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs new file mode 100644 index 0000000000..cb54d5b410 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Reflection; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.Configuration; +using Xunit.Abstractions; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; + +/// +/// Base class for workflow tests. +/// +public abstract class IntegrationTest : IDisposable +{ + public TestOutputAdapter Output { get; } + + protected IntegrationTest(ITestOutputHelper output) + { + this.Output = new TestOutputAdapter(output); + Console.SetOut(this.Output); + SetProduct(); + } + + public void Dispose() + { + this.Dispose(isDisposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.Output.Dispose(); + } + } + + protected static void SetProduct() + { + if (!ProductContext.IsLocalScopeSupported()) + { + ProductContext.SetContext(Product.Foundry); + } + } + + internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? WorkflowFormulaState.DefaultScopeName}.{variableName}"; + + protected static IConfigurationRoot InitializeConfig() => + new ConfigurationBuilder() + .AddUserSecrets(Assembly.GetExecutingAssembly()) + .Build(); +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs index aa35ddf65c..1c4b277d7f 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using System.Text.Json.Serialization; namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; @@ -50,10 +51,34 @@ public sealed class TestcaseInput public sealed class TestcaseValidation { [JsonConstructor] - public TestcaseValidation(int actionCount) + public TestcaseValidation(int minActionCount, int? maxActionCount = null, TestcaseValidationActions? actions = null) { - this.ActionCount = actionCount; + this.MinActionCount = minActionCount; + this.MaxActionCount = maxActionCount; + this.Actions = actions ?? new TestcaseValidationActions([]); } - public int ActionCount { get; } + public TestcaseValidationActions Actions { get; } + public int MinActionCount { get; } + public int? MaxActionCount { get; } +} + +public sealed class TestcaseValidationActions +{ + [JsonConstructor] + public TestcaseValidationActions(IList start, IList? repeat = null, IList? final = null) + { + this.Start = start; + this.Repeat = repeat ?? []; + this.Final = final ?? []; + } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList Start { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList Repeat { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList Final { get; } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs index 42c4681ae1..a67d982d7a 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs @@ -14,10 +14,14 @@ internal sealed class WorkflowEvents this.EventCounts = workflowEvents.GroupBy(e => e.GetType()).ToDictionary(e => e.Key, e => e.Count()); this.ActionInvokeEvents = workflowEvents.OfType().ToList(); this.ActionCompleteEvents = workflowEvents.OfType().ToList(); + this.ExecutorInvokeEvents = workflowEvents.OfType().ToList(); + this.ExecutorCompleteEvents = workflowEvents.OfType().ToList(); } public IReadOnlyList Events { get; } public IReadOnlyDictionary EventCounts { get; } public IReadOnlyList ActionInvokeEvents { get; } public IReadOnlyList ActionCompleteEvents { get; } + public IReadOnlyList ExecutorInvokeEvents { get; } + public IReadOnlyList ExecutorCompleteEvents { get; } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs index b982c608df..94fc7700e9 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs @@ -1,45 +1,172 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Reflection; -using Microsoft.Agents.Workflows.Declarative.PowerFx; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Azure.Identity; +using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; +using Shared.IntegrationTests; using Xunit.Abstractions; +using Xunit.Sdk; namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; /// /// Base class for workflow tests. /// -public abstract class WorkflowTest : IDisposable +public abstract class WorkflowTest(ITestOutputHelper output) : IntegrationTest(output) { - public TestOutputAdapter Output { get; } + protected abstract Task RunAndVerifyAsync(Testcase testcase, string workflowPath, DeclarativeWorkflowOptions workflowOptions) where TInput : notnull; - protected WorkflowTest(ITestOutputHelper output) + protected Task RunWorkflowAsync(string workflowPath, string testcaseFileName) { - this.Output = new TestOutputAdapter(output); - Console.SetOut(this.Output); + this.Output.WriteLine($"WORKFLOW: {workflowPath}"); + this.Output.WriteLine($"TESTCASE: {testcaseFileName}"); + + Testcase testcase = ReadTestcase(testcaseFileName); + IConfiguration configuration = InitializeConfig(); + + this.Output.WriteLine($" {testcase.Description}"); + + return + testcase.Setup.Input.Type switch + { + nameof(ChatMessage) => this.TestWorkflowAsync(testcase, workflowPath, configuration), + nameof(String) => this.TestWorkflowAsync(testcase, workflowPath, configuration), + _ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."), + }; } - public void Dispose() + protected async Task TestWorkflowAsync( + Testcase testcase, + string workflowPath, + IConfiguration configuration) where TInput : notnull { - this.Dispose(isDisposing: true); - GC.SuppressFinalize(this); + this.Output.WriteLine($"INPUT: {testcase.Setup.Input.Value}"); + + AzureAIConfiguration? foundryConfig = configuration.GetSection("AzureAI").Get(); + Assert.NotNull(foundryConfig); + + FrozenDictionary agentMap = await AgentFactory.GetAgentsAsync(foundryConfig, configuration); + + IConfiguration workflowConfig = + new ConfigurationBuilder() + .AddInMemoryCollection(agentMap) + .Build(); + + DeclarativeWorkflowOptions workflowOptions = + new(new AzureAgentProvider(foundryConfig.Endpoint, new AzureCliCredential())) + { + Configuration = workflowConfig, + LoggerFactory = this.Output + }; + await this.RunAndVerifyAsync(testcase, workflowPath, workflowOptions); } - protected virtual void Dispose(bool isDisposing) - { - if (isDisposing) + protected static object GetInput(Testcase testcase) where TInput : notnull => + testcase.Setup.Input.Type switch { - this.Output.Dispose(); + nameof(ChatMessage) => new ChatMessage(ChatRole.User, testcase.Setup.Input.Value), + nameof(String) => testcase.Setup.Input.Value, + _ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."), + }; + + protected static Testcase ReadTestcase(string testcaseFileName) + { + using Stream testcaseStream = File.Open(Path.Combine("Testcases", testcaseFileName), FileMode.Open); + Testcase? testcase = JsonSerializer.Deserialize(testcaseStream, s_jsonSerializerOptions); + Assert.NotNull(testcase); + return testcase; + } + + internal 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; + } + + throw new XunitException("Unable to locate repository root folder."); + } + + protected static class AssertWorkflow + { + public static void EventCounts(int actualCount, Testcase testcase) + { + Assert.True(actualCount >= testcase.Validation.MinActionCount, $"Event count less than expected: {testcase.Validation.MinActionCount} ({actualCount})."); + Assert.True(actualCount <= (testcase.Validation.MaxActionCount ?? testcase.Validation.MinActionCount), $"Event count greater than expected: {testcase.Validation.MaxActionCount ?? testcase.Validation.MinActionCount} ({actualCount})."); + } + + internal static void EventSequence(IEnumerable sourceIds, Testcase testcase) + { + string lastId = string.Empty; + Queue startIds = []; + Queue repeatIds = []; + bool validateStart = false; + bool validateRepeat = false; + foreach (string sourceId in sourceIds) + { + if (!validateStart) + { + if (testcase.Validation.Actions.Start.Count > 0 && + startIds.Count == 0 && + sourceId.Equals(testcase.Validation.Actions.Start[0], StringComparison.Ordinal)) + { + // Initialize start sequence + startIds = new(testcase.Validation.Actions.Start); + } + + // Verify start sequence + if (startIds.Count > 0) + { + Assert.Equal(startIds.Dequeue(), sourceId); + validateStart = startIds.Count == 0; + } + } + else + { + if (testcase.Validation.Actions.Repeat.Count > 0 && + repeatIds.Count == 0 && + sourceId.Equals(testcase.Validation.Actions.Repeat[0], StringComparison.Ordinal)) + { + // Initialize repeat sequence + repeatIds = new(testcase.Validation.Actions.Repeat); + } + // Verify repeat sequence + if (repeatIds.Count > 0) + { + Assert.Equal(repeatIds.Dequeue(), sourceId); + validateRepeat = true; + } + } + lastId = sourceId; + } + + Assert.Equal(testcase.Validation.Actions.Start.Count > 0, validateStart); + Assert.Equal(testcase.Validation.Actions.Repeat.Count > 0, validateRepeat); + + Assert.NotEmpty(lastId); + HashSet finalIds = [.. testcase.Validation.Actions.Final]; + Assert.Contains(lastId, finalIds); } } - internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? WorkflowFormulaState.DefaultScopeName}.{variableName}"; - - protected static IConfigurationRoot InitializeConfig() => - new ConfigurationBuilder() - .AddUserSecrets(Assembly.GetExecutingAssembly()) - .AddEnvironmentVariables() - .Build(); + protected static readonly JsonSerializerOptions s_jsonSerializerOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + WriteIndented = true, + }; } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/ConversationMessages.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/ConversationMessages.json new file mode 100644 index 0000000000..4e4108fab3 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/ConversationMessages.json @@ -0,0 +1,26 @@ +{ + "description": "Create conversation and manipulate messages.", + "setup": { + "input": { + "type": "String", + "value": "Why is the sky blue?" + } + }, + "validation": { + "min_action_count": 7, + "actions": { + "start": [ + "conversation_create1", + "conversation_create2", + "sendActivity_conversation", + "add_message", + "sendActivity_message", + "copy_messages", + "sendActivity_copy" + ], + "final": [ + "sendActivity_copy" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/DeepResearch.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/DeepResearch.json new file mode 100644 index 0000000000..17c5806640 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/DeepResearch.json @@ -0,0 +1,40 @@ +{ + "description": "Planned orchestration sample - DeepResearch.yaml.", + "setup": { + "input": { + "type": "String", + "value": "What is the closest bus-stop that is next to ISHONI YAKINIKU in Seattle?" + } + }, + "validation": { + "min_action_count": 28, + "max_action_count": 56, + "actions": { + "start": [ + "setVariable_aASlmF", + "setVariable_V6yEbo", + "setVariable_NZ2u0l", + "setVariable_10u2ZN", + "sendActivity_yFsbRy", + "conversation_1a2b3c", + "question_UDoMUw", + "sendActivity_yFsbRz", + "question_DsBaJU", + "setVariable_Kk2LDL", + "sendActivity_bwNZiM", + "question_o3BQkf", + "parse_rNZtlV", + "conditionGroup_mVIecC" + ], + "repeat": [ + "question_o3BQkf", + "parse_rNZtlV", + "conditionGroup_mVIecC" + ], + "final": [ + "end_SVoNSV", + "end_GHVrFh" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/HumanInLoop.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/HumanInLoop.json new file mode 100644 index 0000000000..ea0a6ebdea --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/HumanInLoop.json @@ -0,0 +1,20 @@ +{ + "description": "Human in the loop sample - HumanInLoop.yaml.", + "setup": { + "input": { + "type": "String", + "value": "Iko" + } + }, + "validation": { + "min_action_count": 1, + "actions": { + "start": [ + "invoke_agent" + ], + "final": [ + "invoke_agent" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json index a48cc30b79..3984811290 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json @@ -7,6 +7,14 @@ } }, "validation": { - "action_count": 1 + "min_action_count": 1, + "actions": { + "start": [ + "invoke_agent" + ], + "final": [ + "invoke_agent" + ] + } } } \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json new file mode 100644 index 0000000000..cb6bfd8b66 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json @@ -0,0 +1,23 @@ +{ + "description": "Sequential agent invocation sample - Marketing.yaml.", + "setup": { + "input": { + "type": "String", + "value": "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours." + } + }, + "validation": { + "min_action_count": 4, + "actions": { + "start": [ + "add_input_message", + "invoke_analyst", + "invoke_writer", + "invoke_editor" + ], + "final": [ + "invoke_editor" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json new file mode 100644 index 0000000000..5e516f9361 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json @@ -0,0 +1,29 @@ +{ + "description": "Student/Teacher sample - MathChat.yaml.", + "setup": { + "input": { + "type": "String", + "value": "How could one compute the value of PI?" + } + }, + "validation": { + "min_action_count": 6, + "max_action_count": 25, + "actions": { + "start": [ + "set_project" + ], + "repeat": [ + "question_student", + "reset_project", + "question_teacher", + "set_count_increment", + "check_completion" + ], + "final": [ + "sendActivity_done", + "sendActivity_tired" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json index a0f6b74752..f5303d0676 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json @@ -1,5 +1,5 @@ { - "description": "Send an activity message .", + "description": "Send an activity message.", "setup": { "input": { "type": "String", @@ -7,6 +7,16 @@ } }, "validation": { - "action_count": 3 + "min_action_count": 3, + "actions": { + "start": [ + "set_user_input", + "set_user_name", + "send_result" + ], + "final": [ + "send_result" + ] + } } } \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/ConversationMessages.yaml similarity index 100% rename from dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml rename to dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/ConversationMessages.yaml diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml index 02397eb733..00539953d1 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml @@ -8,7 +8,7 @@ trigger: - kind: InvokeAzureAgent id: invoke_agent agent: - name: =Env.BasicAgent + name: =Env.FOUNDRY_AGENT_TEST input: messages: =[UserMessage(System.LastMessageText)] output: diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs index f30a2a4f3f..68504e2d65 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Interpreter; using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; using Moq; using Xunit.Abstractions; @@ -36,8 +38,10 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow public async Task LoopEachActionAsync() { await this.RunWorkflowAsync("LoopEach.yaml"); - this.AssertExecutionCount(expectedCount: 35); + this.AssertExecutionCount(expectedCount: 34); this.AssertExecuted("foreach_loop"); + this.AssertExecuted("set_variable_inner"); + this.AssertExecuted("send_activity_inner"); this.AssertExecuted("end_all"); } @@ -45,24 +49,24 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow public async Task LoopBreakActionAsync() { await this.RunWorkflowAsync("LoopBreak.yaml"); - this.AssertExecutionCount(expectedCount: 7); + this.AssertExecutionCount(expectedCount: 6); this.AssertExecuted("foreach_loop"); - this.AssertExecuted("breakLoop_now"); + this.AssertExecuted("break_loop_now"); this.AssertExecuted("end_all"); - this.AssertNotExecuted("setVariable_loop"); - this.AssertNotExecuted("sendActivity_loop"); + this.AssertNotExecuted("set_variable_inner"); + this.AssertNotExecuted("send_activity_inner"); } [Fact] public async Task LoopContinueActionAsync() { await this.RunWorkflowAsync("LoopContinue.yaml"); - this.AssertExecutionCount(expectedCount: 23); + this.AssertExecutionCount(expectedCount: 22); this.AssertExecuted("foreach_loop"); - this.AssertExecuted("continueLoop_now"); + this.AssertExecuted("continue_loop_now"); this.AssertExecuted("end_all"); - this.AssertNotExecuted("setVariable_loop"); - this.AssertNotExecuted("sendActivity_loop"); + this.AssertNotExecuted("set_variable_inner"); + this.AssertNotExecuted("send_activity_inner"); } [Fact] @@ -209,9 +213,9 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow AdaptiveDialog dialog = dialogBuilder.Build(); WorkflowFormulaState state = new(RecalcEngineFactory.Create()); - Mock mockAgentProvider = new(MockBehavior.Strict); + Mock mockAgentProvider = CreateMockProvider(); DeclarativeWorkflowOptions options = new(mockAgentProvider.Object); - WorkflowActionVisitor visitor = new(new DeclarativeWorkflowExecutor(WorkflowActionVisitor.Steps.Root("anything"), state, (message) => DeclarativeWorkflowBuilder.DefaultTransform(message)), state, options); + WorkflowActionVisitor visitor = new(new DeclarativeWorkflowExecutor(WorkflowActionVisitor.Steps.Root("anything"), mockAgentProvider.Object, state, (message) => DeclarativeWorkflowBuilder.DefaultTransform(message)), state, options); WorkflowElementWalker walker = new(visitor); walker.Visit(dialog); Assert.True(visitor.HasUnsupportedActions); @@ -249,7 +253,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow private async Task RunWorkflowAsync(string workflowPath, TInput workflowInput) where TInput : notnull { using StreamReader yamlReader = File.OpenText(Path.Combine("Workflows", workflowPath)); - Mock mockAgentProvider = new(MockBehavior.Strict); + Mock mockAgentProvider = CreateMockProvider(); DeclarativeWorkflowOptions workflowContext = new(mockAgentProvider.Object) { LoggerFactory = this.Output }; Workflow workflow = DeclarativeWorkflowBuilder.Build(yamlReader, workflowContext); @@ -261,7 +265,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow { if (workflowEvent is ExecutorInvokedEvent invokeEvent) { - ExecutorResultMessage? message = invokeEvent.Data as ExecutorResultMessage; + ActionExecutorResult? message = invokeEvent.Data as ActionExecutorResult; this.Output.WriteLine($"EXEC: {invokeEvent.ExecutorId} << {message?.ExecutorId ?? "?"} [{message?.Result ?? "-"}]"); } else if (workflowEvent is DeclarativeActionInvokedEvent actionInvokeEvent) @@ -283,4 +287,12 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow } this.WorkflowEventCounts = this.WorkflowEvents.GroupBy(e => e.GetType()).ToDictionary(e => e.Key, e => e.Count()); } + + private static Mock CreateMockProvider() + { + Mock mockAgentProvider = new(MockBehavior.Strict); + mockAgentProvider.Setup(provider => provider.CreateConversationAsync(It.IsAny())).Returns(() => Task.FromResult(Guid.NewGuid().ToString("N"))); + mockAgentProvider.Setup(provider => provider.CreateMessageAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + return mockAgentProvider; + } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs index 7bae493fde..f16249f70f 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs @@ -73,6 +73,6 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor IMessageHandler { public async ValueTask HandleAsync(WorkflowFormulaState message, IWorkflowContext context) => - await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false); + await context.SendMessageAsync(new ActionExecutorResult(this.Id)).ConfigureAwait(false); } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/WorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/WorkflowTest.cs index dde50db9b8..1c5cb31cfc 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/WorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/WorkflowTest.cs @@ -2,6 +2,7 @@ using System; using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; using Xunit.Abstractions; namespace Microsoft.Agents.Workflows.Declarative.UnitTests; @@ -17,6 +18,7 @@ public abstract class WorkflowTest : IDisposable { this.Output = new TestOutputAdapter(output); Console.SetOut(this.Output); + SetProduct(); } public void Dispose() @@ -33,6 +35,14 @@ public abstract class WorkflowTest : IDisposable } } + protected static void SetProduct() + { + if (!ProductContext.IsLocalScopeSupported()) + { + ProductContext.SetContext(Product.Foundry); + } + } + internal static string? FormatOptionalPath(string? variableName, string? scope = null) => variableName is null ? null : FormatVariablePath(variableName, scope); diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopBreak.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopBreak.yaml index 31a0cdcf78..bb11822314 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopBreak.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopBreak.yaml @@ -5,25 +5,23 @@ trigger: id: my_workflow actions: - - kind: SetVariable - id: setVariable_count - variable: Local.Count - value: =0 - - kind: Foreach id: foreach_loop items: =["a", "b", "c", "d", "e", "f"] index: Local.LoopIndex value: Local.LoopValue actions: + - kind: BreakLoop - id: breakLoop_now + id: break_loop_now + - kind: SetVariable - id: setVariable_loop + id: set_variable_inner variable: Local.Count value: =Local.Count + 1 + - kind: SendActivity - id: sendActivity_loop + id: send_activity_inner activity: x{Local.Count} - {Local.LoopIndex}:{Local.LoopValue} - kind: EndConversation diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopContinue.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopContinue.yaml index d45892348d..c2574a56b6 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopContinue.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopContinue.yaml @@ -3,12 +3,7 @@ trigger: kind: OnConversationStart id: my_workflow - actions: - - kind: SetVariable - id: setVariable_count - variable: Local.Count - value: =0 - kind: Foreach id: foreach_loop @@ -16,14 +11,17 @@ trigger: index: Local.LoopIndex value: Local.LoopValue actions: + - kind: ContinueLoop - id: continueLoop_now + id: continue_loop_now + - kind: SetVariable - id: setVariable_loop + id: set_variable_inner variable: Local.Count value: =Local.Count + 1 + - kind: SendActivity - id: sendActivity_loop + id: send_activity_inner activity: x{Local.Count} - {Local.LoopIndex}:{Local.LoopValue} - kind: EndConversation diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopEach.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopEach.yaml index 2dacc8eced..fe4919e614 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopEach.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/LoopEach.yaml @@ -3,12 +3,7 @@ trigger: kind: OnConversationStart id: my_workflow - actions: - - kind: SetVariable - id: setVariable_count - variable: Local.Count - value: =0 - kind: Foreach id: foreach_loop @@ -16,12 +11,14 @@ trigger: index: Local.LoopIndex value: Local.LoopValue actions: + - kind: SetVariable - id: setVariable_loop + id: set_variable_inner variable: Local.Count value: =Local.Count + 1 + - kind: SendActivity - id: sendActivity_loop + id: send_activity_inner activity: x{Local.Count} - {Local.LoopIndex}:{Local.LoopValue} - kind: EndConversation diff --git a/workflow-samples/DeepResearch.yaml b/workflow-samples/DeepResearch.yaml index 6b258d32b1..24113ffff8 100644 --- a/workflow-samples/DeepResearch.yaml +++ b/workflow-samples/DeepResearch.yaml @@ -75,7 +75,7 @@ trigger: id: setVariable_10u2ZN displayName: Set Task variable: Local.SeedTask - value: =Local.InputTask + value: =UserMessage(Local.InputTask) - kind: SendActivity id: sendActivity_yFsbRy @@ -94,7 +94,7 @@ trigger: output: messages: Local.TaskFacts input: - messages: =[UserMessage(Local.InputTask)] + messages: =UserMessage(Local.InputTask) additionalInstructions: |- In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability. Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from. @@ -128,7 +128,7 @@ trigger: output: messages: Local.Plan input: - messages: =[UserMessage(Local.InputTask)] + messages: =UserMessage(Local.InputTask) additionalInstructions: |- Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request. @@ -160,13 +160,13 @@ trigger: # FACTS Consider this initial fact sheet: - " & Trim(Local.TaskFacts.Text) & " + " & Trim(Last(Local.TaskFacts).Text) & " # PLAN Here is the plan to follow as best as possible: - - " & Local.Plan.Text + + " & Last(Local.Plan).Text - kind: SendActivity id: sendActivity_bwNZiM @@ -181,7 +181,7 @@ trigger: output: messages: Local.ProgressLedgerUpdate input: - messages: =[UserMessage(Local.AgentResponseText)] + messages: =UserMessage(Local.AgentResponseText) additionalInstructions: |- Recall we are working on the following request: @@ -228,7 +228,7 @@ trigger: id: parse_rNZtlV displayName: Parse ledger response variable: Local.TypedProgressLedger - value: =Local.ProgressLedgerUpdate.Text + value: =Last(Local.ProgressLedgerUpdate).Text valueType: kind: Record properties: @@ -288,7 +288,7 @@ trigger: output: messages: Local.FinalResponse input: - messages: =[UserMessage(Local.SeedTask)] + messages: =Local.SeedTask additionalInstructions: |- We have completed the task. Based only on the conversation and without adding any new information, synthesize the result of the conversation as a complete response to the user task. @@ -369,12 +369,10 @@ trigger: messages: Local.TaskFacts input: messages: |- - =[ - UserMessage( + =UserMessage( "As a reminder, we are working to solve the following task: " & Local.InputTask) - ] additionalInstructions: |- It's clear we aren't making as much progress as we would like, but we may have learned something new. Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful. @@ -486,14 +484,14 @@ trigger: output: messages: Local.AgentResponse input: - messages: =[UserMessage(Local.SeedTask)] + messages: =Local.SeedTask additionalInstructions: |- {Local.TypedProgressLedger.instruction_or_question.answer} - kind: SetVariable id: setVariable_XzNrdM variable: Local.AgentResponseText - value: =Local.AgentResponse.Text + value: =Last(Local.AgentResponse).Text - kind: ResetVariable id: setVariable_8eIx2A diff --git a/workflow-samples/MathChat.yaml b/workflow-samples/MathChat.yaml index 7d2ff07cf5..c6ac4d2ca9 100644 --- a/workflow-samples/MathChat.yaml +++ b/workflow-samples/MathChat.yaml @@ -33,8 +33,8 @@ trigger: - kind: SetVariable id: set_project - variable: Local.Project - value: =System.LastMessage.Text + variable: Local.InputTask + value: =UserMessage(System.LastMessageText) - kind: InvokeAzureAgent id: question_student @@ -42,11 +42,11 @@ trigger: agent: name: =Env.FOUNDRY_AGENT_STUDENT input: - messages: =[UserMessage(Local.Project)] + messages: =Local.InputTask - kind: ResetVariable id: reset_project - variable: Local.Project + variable: Local.InputTask - kind: InvokeAzureAgent id: question_teacher @@ -65,7 +65,7 @@ trigger: id: check_completion conditions: - - condition: =!IsBlank(Find("CONGRATULATIONS", Upper(Local.TeacherResponse.Text))) + - condition: =!IsBlank(Find("CONGRATULATIONS", Upper(Last(Local.TeacherResponse).Text))) id: check_turn_done actions: