diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 7b40e9aabf..5ac641004d 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -94,10 +94,10 @@ - - - - + + + + @@ -105,8 +105,6 @@ - - diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index e90cb8a1a5..499d375c3c 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -107,13 +107,19 @@ + + + + + + + - @@ -284,8 +290,8 @@ - + diff --git a/dotnet/eng/MSBuild/Shared.props b/dotnet/eng/MSBuild/Shared.props index 54f93699ad..da8806a1f3 100644 --- a/dotnet/eng/MSBuild/Shared.props +++ b/dotnet/eng/MSBuild/Shared.props @@ -11,4 +11,13 @@ + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/ConfirmInput.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/ConfirmInput.csproj new file mode 100644 index 0000000000..a78fbddbd7 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/ConfirmInput.csproj @@ -0,0 +1,39 @@ + + + + Exe + net9.0 + net9.0 + $(ProjectsDebugTargetFrameworks) + enable + enable + + + + true + true + true + true + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/Program.cs new file mode 100644 index 0000000000..5ecda10049 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/Program.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Configuration; +using Shared.Workflows; + +namespace Demo.Workflows.Declarative.ConfirmInput; + +/// +/// Demonstrate how to use the question action to request user input +/// and confirm it matches the original input. +/// +/// +/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// information the configuration required to run this sample. +/// +internal sealed class Program +{ + public static async Task Main(string[] args) + { + // Initialize configuration + IConfiguration configuration = Application.InitializeConfig(); + Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint)); + + // Get input from command line or console + string workflowInput = Application.GetInput(args); + + // Create the workflow factory. This class demonstrates how to initialize a + // declarative workflow from a YAML file. Once the workflow is created, it + // can be executed just like any regular workflow. + WorkflowFactory workflowFactory = new("ConfirmInput.yaml", foundryEndpoint); + + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + WorkflowRunner runner = new(); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput); + } +} diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/DeepResearch.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/DeepResearch.csproj new file mode 100644 index 0000000000..c4d542056d --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/DeepResearch.csproj @@ -0,0 +1,42 @@ + + + + Exe + net9.0 + net9.0 + $(ProjectsDebugTargetFrameworks) + enable + enable + + + + true + true + true + true + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs new file mode 100644 index 0000000000..e23c700637 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using OpenAI.Responses; +using Shared.Foundry; +using Shared.Workflows; + +namespace Demo.Workflows.Declarative.DeepResearch; + +/// +/// Demonstrate a declarative workflow that accomplishes a task +/// using the Magentic orchestration pattern developed by AutoGen. +/// +/// +/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// information the configuration required to run this sample. +/// +internal sealed class Program +{ + public static async Task Main(string[] args) + { + // Initialize configuration + IConfiguration configuration = Application.InitializeConfig(); + Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint)); + + // Ensure sample agents exist in Foundry. + await CreateAgentsAsync(foundryEndpoint, configuration); + + // Get input from command line or console + string workflowInput = Application.GetInput(args); + + // Create the workflow factory. This class demonstrates how to initialize a + // declarative workflow from a YAML file. Once the workflow is created, it + // can be executed just like any regular workflow. + WorkflowFactory workflowFactory = new("DeepResearch.yaml", foundryEndpoint); + + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + WorkflowRunner runner = new(); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput); + } + + private static async Task CreateAgentsAsync(Uri foundryEndpoint, IConfiguration configuration) + { + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + await agentsClient.CreateAgentAsync( + agentName: "ResearchAgent", + agentDefinition: DefineResearchAgent(configuration), + agentDescription: "Planner agent for DeepResearch workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "PlannerAgent", + agentDefinition: DefinePlannerAgent(configuration), + agentDescription: "Planner agent for DeepResearch workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "ManagerAgent", + agentDefinition: DefineManagerAgent(configuration), + agentDescription: "Manager agent for DeepResearch workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "SummaryAgent", + agentDefinition: DefineSummaryAgent(configuration), + agentDescription: "Summary agent for DeepResearch workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "KnowledgeAgent", + agentDefinition: DefineKnowledgeAgent(configuration), + agentDescription: "Research agent for DeepResearch workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "CoderAgent", + agentDefinition: DefineCoderAgent(configuration), + agentDescription: "Coder agent for DeepResearch workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "WeatherAgent", + agentDefinition: DefineWeatherAgent(configuration), + agentDescription: "Weather agent for DeepResearch workflow"); + } + + private static PromptAgentDefinition DefineResearchAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelFull)) + { + Instructions = + """ + 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. + + Here is the pre-survey: + + 1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none. + 2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself. + 3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation) + 4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc. + + When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings: + + 1. GIVEN OR VERIFIED FACTS + 2. FACTS TO LOOK UP + 3. FACTS TO DERIVE + 4. EDUCATED GUESSES + + DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so. + """, + Tools = + { + //AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available + // new BingGroundingSearchToolParameters( + // [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))])) + } + }; + + private static PromptAgentDefinition DefinePlannerAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = // TODO: Use Structured Inputs / Prompt Template + """ + Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request. + + Only select the following team which is listed as "- [Name]: [Description]" + + - WeatherAgent: Able to retrieve weather information + - CoderAgent: Able to write and execute Python code + - KnowledgeAgent: Able to perform generic websearches + + The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]" + + Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task. + """ + }; + + private static PromptAgentDefinition DefineManagerAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = // TODO: Use Structured Inputs / Prompt Template + """ + Recall we have assembled the following team: + + - KnowledgeAgent: Able to perform generic websearches + - CoderAgent: Able to write and execute Python code + - WeatherAgent: Able to retrieve weather information + + To make progress on the request, please answer the following questions, including necessary reasoning: + - Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed) + - Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times. + - Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file) + - Who should speak next? (select from: KnowledgeAgent, CoderAgent, WeatherAgent) + - What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need) + """, + TextOptions = + new ResponseTextOptions + { + TextFormat = + ResponseTextFormat.CreateJsonSchemaFormat( + "TaskEvaluation", + BinaryData.FromString( + """ + { + "type": "object", + "properties": { + "is_request_satisfied": { + "type": "object", + "properties": { + "reason": { "type": "string" }, + "answer": { "type": "boolean" } + }, + "required": ["reason", "answer"], + "additionalProperties": false + }, + "is_in_loop": { + "type": "object", + "properties": { + "reason": { "type": "string" }, + "answer": { "type": "boolean" } + }, + "required": ["reason", "answer"], + "additionalProperties": false + }, + "is_progress_being_made": { + "type": "object", + "properties": { + "reason": { "type": "string" }, + "answer": { "type": "boolean" } + }, + "required": ["reason", "answer"], + "additionalProperties": false + }, + "next_speaker": { + "type": "object", + "properties": { + "reason": { "type": "string" }, + "answer": { + "type": "string" + } + }, + "required": ["reason", "answer"], + "additionalProperties": false + }, + "instruction_or_question": { + "type": "object", + "properties": { + "reason": { "type": "string" }, + "answer": { "type": "string" } + }, + "required": ["reason", "answer"], + "additionalProperties": false + } + }, + "required": ["is_request_satisfied", "is_in_loop", "is_progress_being_made", "next_speaker", "instruction_or_question"], + "additionalProperties": false + } + """), + jsonSchemaFormatDescription: null, + jsonSchemaIsStrict: true), + } + }; + + private static PromptAgentDefinition DefineSummaryAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + 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. + + The user will only ever see this last response and not the entire conversation, + so please ensure it is complete and self-contained. + """ + }; + + private static PromptAgentDefinition DefineKnowledgeAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Tools = + { + //AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available + // new BingGroundingSearchToolParameters( + // [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))])) + } + }; + + private static PromptAgentDefinition DefineCoderAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + You solve problem by writing and executing code. + """, + Tools = + { + ResponseTool.CreateCodeInterpreterTool( + new(CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration())) + } + }; + + private static PromptAgentDefinition DefineWeatherAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + You are a weather expert. + """, + Tools = + { + AgentTool.CreateOpenApiTool( + new OpenApiFunctionDefinition( + "weather-forecast", + BinaryData.FromString(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "wttr.json"))), + new OpenApiAnonymousAuthDetails())) + } + }; +} diff --git a/workflow-samples/wttr.json b/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/wttr.json similarity index 100% rename from workflow-samples/wttr.json rename to dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/wttr.json diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/ExecuteCode.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/ExecuteCode.csproj index 72afa29cda..838f0f1e4f 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/ExecuteCode.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/ExecuteCode.csproj @@ -12,7 +12,9 @@ - true + true + true + true diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/Generated.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/Generated.cs index d1c8d45082..6fc508064e 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/Generated.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/Generated.cs @@ -5,6 +5,7 @@ // ------------------------------------------------------------------------------ #nullable enable +#pragma warning disable IDE0005 // Extra using directive is ok. using System; using System.Collections; @@ -18,7 +19,7 @@ using Microsoft.Agents.AI.Workflows.Declarative; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Extensions.AI; -namespace Test.WorkflowProviders; +namespace Demo.DeclarativeCode; /// /// This class provides a factory method to create a instance. @@ -29,7 +30,7 @@ namespace Test.WorkflowProviders; /// To learn more about Power FX, see: /// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio /// -public static class TestWorkflowProvider +public static class SampleWorkflowProvider { /// /// The root executor for a declarative workflow. @@ -42,845 +43,26 @@ public static class TestWorkflowProvider { protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken) { - // Set environment variables - await this.InitializeEnvironmentAsync( - context, - "FOUNDRY_AGENT_RESEARCHWEB", - "FOUNDRY_AGENT_RESEARCHANALYST", - "FOUNDRY_AGENT_RESEARCHCODER", - "FOUNDRY_AGENT_RESEARCHMANAGER", - "FOUNDRY_AGENT_RESEARCHWEATHER").ConfigureAwait(false); - - // Initialize variables - await context.QueueStateUpdateAsync("AgentResponse", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("AgentResponseText", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("AvailableAgents", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("FinalResponse", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("InputTask", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("InternalConversationId", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("NextSpeaker", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("Plan", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("ProgressLedgerUpdate", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("RestartCount", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("SeedTask", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("StallCount", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("TaskFacts", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("TaskInstructions", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("TeamDescription", UnassignedValue.Instance, "Local"); - await context.QueueStateUpdateAsync("TypedProgressLedger", UnassignedValue.Instance, "Local"); - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.AvailableAgents" variable. - /// - internal sealed class SetvariableAaslmfExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_aASlmF", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync(""" - [ - { - name: "WeatherAgent", - description: "Able to retrieve weather information", - agentid: Env.FOUNDRY_AGENT_RESEARCHWEATHER - }, - { - name: "CoderAgent", - description: "Able to write and execute Python code", - agentid: Env.FOUNDRY_AGENT_RESEARCHCODER - }, - { - name: "WebAgent", - description: "Able to perform generic websearches", - agentid: Env.FOUNDRY_AGENT_RESEARCHWEB - } - ] - """); - await context.QueueStateUpdateAsync(key: "AvailableAgents", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.TeamDescription" variable. - /// - internal sealed class SetvariableV6yeboExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_V6yEbo", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync(""" - Concat(ForAll(Local.AvailableAgents, $"- " & name & $": " & description), Value, " - ") - """); - await context.QueueStateUpdateAsync(key: "TeamDescription", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.InputTask" variable. - /// - internal sealed class SetvariableNz2u0lExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_NZ2u0l", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync("System.LastMessage.Text"); - await context.QueueStateUpdateAsync(key: "InputTask", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.SeedTask" variable. - /// - internal sealed class Setvariable10U2znExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_10u2ZN", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync("UserMessage(Local.InputTask)"); - await context.QueueStateUpdateAsync(key: "SeedTask", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityYfsbryExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_yFsbRy", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - Analyzing facts... - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Creates a new conversation and stores the identifier value to the "Local.InternalConversationId" variable. - /// - internal sealed class Conversation1A2b3cExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : ActionExecutor(id: "conversation_1a2b3c", session) - { - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string conversationId = await agentProvider.CreateConversationAsync(cancellationToken); - await context.QueueStateUpdateAsync(key: "InternalConversationId", value: conversationId, scopeName: "Local"); - - return default; } } /// /// Invokes an agent to process messages and return a response within a conversation context. /// - internal sealed class QuestionUdomuwExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_UDoMUw", session, agentProvider) + internal sealed class QuestionStudentExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_student", session, agentProvider) { // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - string? agentName = await context.ReadStateAsync(key: "FOUNDRY_AGENT_RESEARCHANALYST", scopeName: "Env"); + string? agentName = "StudentAgent"; if (string.IsNullOrWhiteSpace(agentName)) { - throw new InvalidOperationException($"Agent name must be defined: {this.Id}"); + throw new DeclarativeActionException($"Agent name must be defined: {this.Id}"); } - string? conversationId = await context.ReadStateAsync(key: "InternalConversationId", scopeName: "Local"); + string? conversationId = await context.ReadStateAsync(key: "ConversationId", scopeName: "System").ConfigureAwait(false); bool autoSend = true; - string additionalInstructions = - await context.FormatTemplateAsync( - """ - 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. - - Here is the pre-survey: - - 1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none. - 2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself. - 3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation) - 4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc. - - When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings: - - 1. GIVEN OR VERIFIED FACTS - 2. FACTS TO LOOK UP - 3. FACTS TO DERIVE - 4. EDUCATED GUESSES - - DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so. - """); - IList? inputMessages = await context.EvaluateListAsync("UserMessage(Local.InputTask)"); - - AgentRunResponse agentResponse = - await InvokeAgentAsync( - context, - agentName, - conversationId, - autoSend, - additionalInstructions, - inputMessages, - cancellationToken); - - if (autoSend) - { - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)); - } - - await context.QueueStateUpdateAsync(key: "TaskFacts", value: agentResponse.Messages, scopeName: "Local"); - - return default; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityYfsbrzExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_yFsbRz", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - Creating a plan... - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Invokes an agent to process messages and return a response within a conversation context. - /// - internal sealed class QuestionDsbajuExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_DsBaJU", session, agentProvider) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string? agentName = await context.ReadStateAsync(key: "FOUNDRY_AGENT_RESEARCHMANAGER", scopeName: "Env"); - - if (string.IsNullOrWhiteSpace(agentName)) - { - throw new InvalidOperationException($"Agent name must be defined: {this.Id}"); - } - - string? conversationId = await context.ReadStateAsync(key: "InternalConversationId", scopeName: "Local"); - bool autoSend = true; - string additionalInstructions = - await context.FormatTemplateAsync( - """ - Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request. - - Only select the following team which is listed as "- [Name]: [Description]" - - {Local.TeamDescription} - - The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]" - - Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task. - """); - IList? inputMessages = await context.EvaluateListAsync("UserMessage(Local.InputTask)"); - - AgentRunResponse agentResponse = - await InvokeAgentAsync( - context, - agentName, - conversationId, - autoSend, - additionalInstructions, - inputMessages, - cancellationToken); - - if (autoSend) - { - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)); - } - - await context.QueueStateUpdateAsync(key: "Plan", value: agentResponse.Messages, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.TaskInstructions" variable. - /// - internal sealed class SetvariableKk2ldlExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_Kk2LDL", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync(""" - "# TASK - Address the following user request: - - " & Local.InputTask & " - - - # TEAM - Use the following team to answer this request: - - " & Local.TeamDescription & " - - - # FACTS - Consider this initial fact sheet: - - " & Trim(Last(Local.TaskFacts).Text) & " - - - # PLAN - Here is the plan to follow as best as possible: - - " & Last(Local.Plan).Text - """); - await context.QueueStateUpdateAsync(key: "TaskInstructions", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityBwnzimExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_bwNZiM", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - {Local.TaskInstructions} - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Invokes an agent to process messages and return a response within a conversation context. - /// - internal sealed class QuestionO3bqkfExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_o3BQkf", session, agentProvider) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string? agentName = await context.ReadStateAsync(key: "FOUNDRY_AGENT_RESEARCHMANAGER", scopeName: "Env"); - - if (string.IsNullOrWhiteSpace(agentName)) - { - throw new InvalidOperationException($"Agent name must be defined: {this.Id}"); - } - - string? conversationId = await context.ReadStateAsync(key: "InternalConversationId", scopeName: "Local"); - bool autoSend = true; - string additionalInstructions = - await context.FormatTemplateAsync( - """ - Recall we are working on the following request: - - {Local.InputTask} - - And we have assembled the following team: - - {Local.TeamDescription} - - To make progress on the request, please answer the following questions, including necessary reasoning: - - - Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed) - - Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times. - - Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file) - - Who should speak next? (select from: {Concat(Local.AvailableAgents, name, ",")}) - - What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need) - - Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA: - - {{ - "is_request_satisfied": {{ - "reason": string, - "answer": boolean - }}, - "is_in_loop": {{ - "reason": string, - "answer": boolean - }}, - "is_progress_being_made": {{ - "reason": string, - "answer": boolean - }}, - "next_speaker": {{ - "reason": string, - "answer": string (select from: {Concat(Local.AvailableAgents, name, ",")}) - }}, - "instruction_or_question": {{ - "reason": string, - "answer": string - }} - }} - """); - IList? inputMessages = await context.EvaluateListAsync("UserMessage(Local.AgentResponseText)"); - - AgentRunResponse agentResponse = - await InvokeAgentAsync( - context, - agentName, - conversationId, - autoSend, - additionalInstructions, - inputMessages, - cancellationToken); - - if (autoSend) - { - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)); - } - - await context.QueueStateUpdateAsync(key: "ProgressLedgerUpdate", value: agentResponse.Messages, scopeName: "Local"); - - return default; - } - } - - /// - /// Parses a string or untyped value to the provided data type. When the input is a string, it will be treated as JSON. - /// - internal sealed class ParseRnztlvExecutor(FormulaSession session) : ActionExecutor(id: "parse_rNZtlV", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - VariableType targetType = - VariableType.Record( - ("is_progress_being_made", - VariableType.Record( - ("reason", typeof(string)), - ("answer", typeof(bool)))), - ("is_request_satisfied", - VariableType.Record( - ("reason", typeof(string)), - ("answer", typeof(bool)))), - ("is_in_loop", - VariableType.Record( - ("reason", typeof(string)), - ("answer", typeof(bool)))), - ("next_speaker", - VariableType.Record( - ("reason", typeof(string)), - ("answer", typeof(string)))), - ("instruction_or_question", - VariableType.Record( - ("reason", typeof(string)), - ("answer", typeof(string))))); - object? parsedValue = await context.ConvertValueAsync(targetType, "Last(Local.ProgressLedgerUpdate).Text", cancellationToken); - await context.QueueStateUpdateAsync(key: "TypedProgressLedger", value: parsedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Conditional branching similar to an if / elseif / elseif / else chain. - /// - internal sealed class ConditiongroupMvieccExecutor(FormulaSession session) : ActionExecutor(id: "conditionGroup_mVIecC", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - bool condition0 = await context.EvaluateValueAsync("Local.TypedProgressLedger.is_request_satisfied.answer"); - if (condition0) - { - return "conditionItem_fj432c"; - } - - bool condition1 = await context.EvaluateValueAsync("Local.TypedProgressLedger.is_in_loop.answer || Not(Local.TypedProgressLedger.is_progress_being_made.answer)"); - if (condition1) - { - return "conditionItem_yiqund"; - } - - return "conditionGroup_mVIecCElseActions"; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityKdl3mcExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_kdl3mC", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - Completed! {Local.TypedProgressLedger.is_request_satisfied.reason} - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Invokes an agent to process messages and return a response within a conversation context. - /// - internal sealed class QuestionKe3l1dExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_Ke3l1d", session, agentProvider) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string? agentName = await context.ReadStateAsync(key: "FOUNDRY_AGENT_RESEARCHMANAGER", scopeName: "Env"); - - if (string.IsNullOrWhiteSpace(agentName)) - { - throw new InvalidOperationException($"Agent name must be defined: {this.Id}"); - } - - string? conversationId = await context.ReadStateAsync(key: "ConversationId", scopeName: "System"); - bool autoSend = true; - string additionalInstructions = - await context.FormatTemplateAsync( - """ - 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. - The user will only every see this last response and not the entire conversation, so please ensure it is complete and self-contained. - """); - IList? inputMessages = await context.ReadListAsync(key: "SeedTask", scopeName: "Local"); - - AgentRunResponse agentResponse = - await InvokeAgentAsync( - context, - agentName, - conversationId, - autoSend, - additionalInstructions, - inputMessages, - cancellationToken); - - if (autoSend) - { - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)); - } - - await context.QueueStateUpdateAsync(key: "FinalResponse", value: agentResponse.Messages, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.StallCount" variable. - /// - internal sealed class SetvariableH5lxddExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_H5lXdD", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync("Local.StallCount + 1"); - await context.QueueStateUpdateAsync(key: "StallCount", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Conditional branching similar to an if / elseif / elseif / else chain. - /// - internal sealed class ConditiongroupVbtqd3Executor(FormulaSession session) : ActionExecutor(id: "conditionGroup_vBTQd3", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - bool condition0 = await context.EvaluateValueAsync(".TypedProgressLedger.is_in_loop.answer"); - if (condition0) - { - return "conditionItem_fpaNL9"; - } - - bool condition1 = await context.EvaluateValueAsync("Not(Local.TypedProgressLedger.is_progress_being_made.answer)"); - if (condition1) - { - return "conditionItem_NnqvXh"; - } - - return "conditionGroup_vBTQd3ElseActions"; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityFpanl9Executor(FormulaSession session) : ActionExecutor(id: "sendActivity_fpaNL9", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - {Local.TypedProgressLedger.is_in_loop.reason} - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityNnqvxhExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_NnqvXh", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - {Local.TypedProgressLedger.is_progress_being_made.reason} - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Conditional branching similar to an if / elseif / elseif / else chain. - /// - internal sealed class ConditiongroupXznrdmExecutor(FormulaSession session) : ActionExecutor(id: "conditionGroup_xzNrdM", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - bool condition0 = await context.EvaluateValueAsync("Local.StallCount > 2"); - if (condition0) - { - return "conditionItem_NlQTBv"; - } - - return "conditionGroup_xzNrdMElseActions"; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityH5lxddExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_H5lXdD", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - Unable to make sufficient progress... - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Conditional branching similar to an if / elseif / elseif / else chain. - /// - internal sealed class Conditiongroup4S1z27Executor(FormulaSession session) : ActionExecutor(id: "conditionGroup_4s1Z27", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - bool condition0 = await context.EvaluateValueAsync("Local.RestartCount > 2"); - if (condition0) - { - return "conditionItem_EXAlhZ"; - } - - return "conditionGroup_4s1Z27ElseActions"; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityXkxfuuExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_xKxFUU", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - Stopping after attempting {Local.RestartCount} restarts... - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityCwnzimExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_cwNZiM", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - Re-analyzing facts... - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Invokes an agent to process messages and return a response within a conversation context. - /// - internal sealed class QuestionWfj123Executor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_wFJ123", session, agentProvider) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string? agentName = await context.ReadStateAsync(key: "FOUNDRY_AGENT_RESEARCHANALYST", scopeName: "Env"); - - if (string.IsNullOrWhiteSpace(agentName)) - { - throw new InvalidOperationException($"Agent name must be defined: {this.Id}"); - } - - string? conversationId = await context.ReadStateAsync(key: "InternalConversationId", scopeName: "Local"); - bool autoSend = true; - string additionalInstructions = - await context.FormatTemplateAsync( - """ - 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. - Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc. - Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited. - This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning. - - Here is the old fact sheet: - - {Local.TaskFacts} - """); - IList? inputMessages = await context.EvaluateListAsync(""" - UserMessage( - "As a reminder, we are working to solve the following task: - - " & Local.InputTask) - """); - - AgentRunResponse agentResponse = - await InvokeAgentAsync( - context, - agentName, - conversationId, - autoSend, - additionalInstructions, - inputMessages, - cancellationToken); - - if (autoSend) - { - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)); - } - - await context.QueueStateUpdateAsync(key: "TaskFacts", value: agentResponse.Messages, scopeName: "Local"); - - return default; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityDsbajuExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_dsBaJU", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - Re-analyzing plan... - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Invokes an agent to process messages and return a response within a conversation context. - /// - internal sealed class QuestionUej456Executor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_uEJ456", session, agentProvider) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string? agentName = await context.ReadStateAsync(key: "FOUNDRY_AGENT_RESEARCHMANAGER", scopeName: "Env"); - - if (string.IsNullOrWhiteSpace(agentName)) - { - throw new InvalidOperationException($"Agent name must be defined: {this.Id}"); - } - - string? conversationId = await context.ReadStateAsync(key: "InternalConversationId", scopeName: "Local"); - bool autoSend = true; - string additionalInstructions = - await context.FormatTemplateAsync( - """ - Please briefly explain what went wrong on this last run (the root cause of the failure), - and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes. - As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition - (do not involve any other outside people since we cannot contact anyone else): - - {Local.TeamDescription} - """); IList? inputMessages = null; AgentRunResponse agentResponse = @@ -889,137 +71,67 @@ public static class TestWorkflowProvider agentName, conversationId, autoSend, - additionalInstructions, inputMessages, - cancellationToken); + cancellationToken).ConfigureAwait(false); if (autoSend) { - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)); + await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false); } - await context.QueueStateUpdateAsync(key: "Plan", value: agentResponse.Messages, scopeName: "Local"); + return default; + } + } + + /// + /// Invokes an agent to process messages and return a response within a conversation context. + /// + internal sealed class QuestionTeacherExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_teacher", session, agentProvider) + { + // + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + string? agentName = "TeacherAgent"; + + if (string.IsNullOrWhiteSpace(agentName)) + { + throw new DeclarativeActionException($"Agent name must be defined: {this.Id}"); + } + + string? conversationId = await context.ReadStateAsync(key: "ConversationId", scopeName: "System").ConfigureAwait(false); + bool autoSend = false; + IList? inputMessages = null; + + AgentRunResponse agentResponse = + await InvokeAgentAsync( + context, + agentName, + conversationId, + autoSend, + inputMessages, + cancellationToken).ConfigureAwait(false); + + if (autoSend) + { + await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false); + } + + await context.QueueStateUpdateAsync(key: "TeacherResponse", value: agentResponse.Messages, scopeName: "Local").ConfigureAwait(false); return default; } } /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.TaskInstructions" variable. + /// Assigns an evaluated expression, other variable, or literal value to the "Local.TurnCount" variable. /// - internal sealed class SetvariableJw7tmmExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_jW7tmM", session) + internal sealed class SetCountIncrementExecutor(FormulaSession session) : ActionExecutor(id: "set_count_increment", session) { // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - object? evaluatedValue = await context.EvaluateValueAsync(""" - "# TASK - Address the following user request: - - " & Local.InputTask & " - - - # TEAM - Use the following team to answer this request: - - " & Local.TeamDescription & " - - - # FACTS - Consider this initial fact sheet: - - " & Local.TaskFacts.Text & " - - - # PLAN - Here is the plan to follow as best as possible: - - " & Local.Plan.Text - """); - await context.QueueStateUpdateAsync(key: "TaskInstructions", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.StallCount" variable. - /// - internal sealed class Setvariable6J2snpExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_6J2snP", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = 0; - await context.QueueStateUpdateAsync(key: "StallCount", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.RestartCount" variable. - /// - internal sealed class SetvariableS6hcghExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_S6HCgh", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync("Local.RestartCount + 1"); - await context.QueueStateUpdateAsync(key: "RestartCount", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Formats a message template and sends an activity event. - /// - internal sealed class SendactivityL7ooqoExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_L7ooQO", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - string activityText = - await context.FormatTemplateAsync( - """ - ({Local.TypedProgressLedger.next_speaker.reason}) - - {Local.TypedProgressLedger.next_speaker.answer} - {Local.TypedProgressLedger.instruction_or_question.answer} - """ - ); - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.StallCount" variable. - /// - internal sealed class SetvariableL7ooqoExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_L7ooQO", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = 0; - await context.QueueStateUpdateAsync(key: "StallCount", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.NextSpeaker" variable. - /// - internal sealed class SetvariableNxn1meExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_nxN1mE", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync("Search(Local.AvailableAgents, Local.TypedProgressLedger.next_speaker.answer, name)"); - await context.QueueStateUpdateAsync(key: "NextSpeaker", value: evaluatedValue, scopeName: "Local"); + object? evaluatedValue = await context.EvaluateValueAsync("Local.TurnCount + 1").ConfigureAwait(false); + await context.QueueStateUpdateAsync(key: "TurnCount", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false); return default; } @@ -1028,90 +140,43 @@ public static class TestWorkflowProvider /// /// Conditional branching similar to an if / elseif / elseif / else chain. /// - internal sealed class ConditiongroupQfpif5Executor(FormulaSession session) : ActionExecutor(id: "conditionGroup_QFPiF5", session) + internal sealed class CheckCompletionExecutor(FormulaSession session) : ActionExecutor(id: "check_completion", session) { // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - bool condition0 = await context.EvaluateValueAsync("CountRows(Local.NextSpeaker) = 1"); + bool condition0 = await context.EvaluateValueAsync("""!IsBlank(Find("CONGRATULATIONS", Upper(Last(Local.TeacherResponse).Text)))""").ConfigureAwait(false); if (condition0) { - return "conditionItem_GmigcU"; + return "check_turn_done"; } - return "conditionGroup_QFPiF5ElseActions"; + bool condition1 = await context.EvaluateValueAsync("Local.TurnCount < 4").ConfigureAwait(false); + if (condition1) + { + return "check_turn_count"; + } + + return "check_completionElseActions"; } } /// - /// Invokes an agent to process messages and return a response within a conversation context. + /// Formats a message template and sends an activity event. /// - internal sealed class QuestionOrsbf06Executor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "question_orsBf06", session, agentProvider) + internal sealed class SendactivityDoneExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_done", session) { // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - string? agentName = await context.EvaluateValueAsync("First(Local.NextSpeaker).agentid"); - - if (string.IsNullOrWhiteSpace(agentName)) - { - throw new InvalidOperationException($"Agent name must be defined: {this.Id}"); - } - - string? conversationId = await context.ReadStateAsync(key: "ConversationId", scopeName: "System"); - bool autoSend = true; - string additionalInstructions = + string activityText = await context.FormatTemplateAsync( """ - {Local.TypedProgressLedger.instruction_or_question.answer} - """); - IList? inputMessages = await context.ReadListAsync(key: "SeedTask", scopeName: "Local"); - - AgentRunResponse agentResponse = - await InvokeAgentAsync( - context, - agentName, - conversationId, - autoSend, - additionalInstructions, - inputMessages, - cancellationToken); - - if (autoSend) - { - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)); - } - - await context.QueueStateUpdateAsync(key: "AgentResponse", value: agentResponse.Messages, scopeName: "Local"); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.AgentResponseText" variable. - /// - internal sealed class SetvariableXznrdmExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_XzNrdM", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync("Last(Local.AgentResponse).Text"); - await context.QueueStateUpdateAsync(key: "AgentResponseText", value: evaluatedValue, scopeName: "Local"); - - return default; - } - } - - /// - /// Resets the value of the "Local.SeedTask" variable, potentially causing re-evaluation - /// of the default value, question or action that provides the value to this variable. - /// - internal sealed class Setvariable8Eix2aExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_8eIx2A", session) - { - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - await context.QueueStateUpdateAsync(key: "SeedTask", value: UnassignedValue.Instance, scopeName: "Local"); + GOLD STAR! + """ + ); + AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); + await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)).ConfigureAwait(false); return default; } @@ -1120,7 +185,7 @@ public static class TestWorkflowProvider /// /// Formats a message template and sends an activity event. /// - internal sealed class SendactivityBhcsi7Executor(FormulaSession session) : ActionExecutor(id: "sendActivity_BhcsI7", session) + internal sealed class SendactivityTiredExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_tired", session) { // protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) @@ -1128,26 +193,11 @@ public static class TestWorkflowProvider string activityText = await context.FormatTemplateAsync( """ - Unable to choose next agent... + Let's try again later... """ ); AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)); - - return default; - } - } - - /// - /// Assigns an evaluated expression, other variable, or literal value to the "Local.StallCount" variable. - /// - internal sealed class SetvariableBhcsi7Executor(FormulaSession session) : ActionExecutor(id: "setVariable_BhcsI7", session) - { - // - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - object? evaluatedValue = await context.EvaluateValueAsync("Local.StallCount + 1"); - await context.QueueStateUpdateAsync(key: "StallCount", value: evaluatedValue, scopeName: "Local"); + await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)).ConfigureAwait(false); return default; } @@ -1162,189 +212,56 @@ public static class TestWorkflowProvider inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message); WorkflowDemoRootExecutor workflowDemoRoot = new(options, inputTransform); DelegateExecutor workflowDemo = new(id: "workflow_demo", workflowDemoRoot.Session); - SetvariableAaslmfExecutor setVariableAaslmf = new(workflowDemoRoot.Session); - SetvariableV6yeboExecutor setVariableV6yebo = new(workflowDemoRoot.Session); - SetvariableNz2u0lExecutor setVariableNz2u0l = new(workflowDemoRoot.Session); - Setvariable10U2znExecutor setVariable10U2zn = new(workflowDemoRoot.Session); - SendactivityYfsbryExecutor sendActivityYfsbry = new(workflowDemoRoot.Session); - Conversation1A2b3cExecutor conversation1A2b3c = new(workflowDemoRoot.Session, options.AgentProvider); - QuestionUdomuwExecutor questionUdomuw = new(workflowDemoRoot.Session, options.AgentProvider); - SendactivityYfsbrzExecutor sendActivityYfsbrz = new(workflowDemoRoot.Session); - QuestionDsbajuExecutor questionDsbaju = new(workflowDemoRoot.Session, options.AgentProvider); - SetvariableKk2ldlExecutor setVariableKk2ldl = new(workflowDemoRoot.Session); - SendactivityBwnzimExecutor sendActivityBwnzim = new(workflowDemoRoot.Session); - QuestionO3bqkfExecutor questionO3bqkf = new(workflowDemoRoot.Session, options.AgentProvider); - ParseRnztlvExecutor parseRnztlv = new(workflowDemoRoot.Session); - ConditiongroupMvieccExecutor conditionGroupMviecc = new(workflowDemoRoot.Session); - DelegateExecutor conditionItemFj432c = new(id: "conditionItem_fj432c", workflowDemoRoot.Session); - DelegateExecutor conditionItemYiqund = new(id: "conditionItem_yiqund", workflowDemoRoot.Session); - DelegateExecutor conditionGroupMvieccelseactions = new(id: "conditionGroup_mVIecCElseActions", workflowDemoRoot.Session); - DelegateExecutor conditionItemFj432cactions = new(id: "conditionItem_fj432cActions", workflowDemoRoot.Session); - SendactivityKdl3mcExecutor sendActivityKdl3mc = new(workflowDemoRoot.Session); - QuestionKe3l1dExecutor questionKe3l1d = new(workflowDemoRoot.Session, options.AgentProvider); - DelegateExecutor endSvonsv = new(id: "end_SVoNSV", workflowDemoRoot.Session); - DelegateExecutor conditionItemYiqundactions = new(id: "conditionItem_yiqundActions", workflowDemoRoot.Session); - SetvariableH5lxddExecutor setVariableH5lxdd = new(workflowDemoRoot.Session); - ConditiongroupVbtqd3Executor conditionGroupVbtqd3 = new(workflowDemoRoot.Session); - DelegateExecutor conditionItemFpanl9 = new(id: "conditionItem_fpaNL9", workflowDemoRoot.Session); - DelegateExecutor conditionItemNnqvxh = new(id: "conditionItem_NnqvXh", workflowDemoRoot.Session); - DelegateExecutor conditionItemFpanl9actions = new(id: "conditionItem_fpaNL9Actions", workflowDemoRoot.Session); - SendactivityFpanl9Executor sendActivityFpanl9 = new(workflowDemoRoot.Session); - DelegateExecutor conditionItemNnqvxhactions = new(id: "conditionItem_NnqvXhActions", workflowDemoRoot.Session); - SendactivityNnqvxhExecutor sendActivityNnqvxh = new(workflowDemoRoot.Session); - DelegateExecutor conditionGroupVbtqd3Post = new(id: "conditionGroup_vBTQd3_Post", workflowDemoRoot.Session); - ConditiongroupXznrdmExecutor conditionGroupXznrdm = new(workflowDemoRoot.Session); - DelegateExecutor conditionItemNlqtbv = new(id: "conditionItem_NlQTBv", workflowDemoRoot.Session); - DelegateExecutor conditionItemNlqtbvactions = new(id: "conditionItem_NlQTBvActions", workflowDemoRoot.Session); - SendactivityH5lxddExecutor sendActivityH5lxdd = new(workflowDemoRoot.Session); - Conditiongroup4S1z27Executor conditionGroup4S1z27 = new(workflowDemoRoot.Session); - DelegateExecutor conditionItemExalhz = new(id: "conditionItem_EXAlhZ", workflowDemoRoot.Session); - DelegateExecutor conditionItemExalhzactions = new(id: "conditionItem_EXAlhZActions", workflowDemoRoot.Session); - SendactivityXkxfuuExecutor sendActivityXkxfuu = new(workflowDemoRoot.Session); - DelegateExecutor endGhvrfh = new(id: "end_GHVrFh", workflowDemoRoot.Session); - DelegateExecutor conditionGroup4S1z27Post = new(id: "conditionGroup_4s1Z27_Post", workflowDemoRoot.Session); - SendactivityCwnzimExecutor sendActivityCwnzim = new(workflowDemoRoot.Session); - QuestionWfj123Executor questionWfj123 = new(workflowDemoRoot.Session, options.AgentProvider); - SendactivityDsbajuExecutor sendActivityDsbaju = new(workflowDemoRoot.Session); - QuestionUej456Executor questionUej456 = new(workflowDemoRoot.Session, options.AgentProvider); - SetvariableJw7tmmExecutor setVariableJw7tmm = new(workflowDemoRoot.Session); - Setvariable6J2snpExecutor setVariable6J2snp = new(workflowDemoRoot.Session); - SetvariableS6hcghExecutor setVariableS6hcgh = new(workflowDemoRoot.Session); - DelegateExecutor gotoLzfj8u = new(id: "goto_LzfJ8u", workflowDemoRoot.Session); - DelegateExecutor conditionItemYiqundRestart = new(id: "conditionItem_yiqund_Restart", workflowDemoRoot.Session); - SendactivityL7ooqoExecutor sendActivityL7ooqo = new(workflowDemoRoot.Session); - SetvariableL7ooqoExecutor setVariableL7ooqo = new(workflowDemoRoot.Session); - DelegateExecutor conditionGroupMvieccPost = new(id: "conditionGroup_mVIecC_Post", workflowDemoRoot.Session); - SetvariableNxn1meExecutor setVariableNxn1me = new(workflowDemoRoot.Session); - ConditiongroupQfpif5Executor conditionGroupQfpif5 = new(workflowDemoRoot.Session); - DelegateExecutor conditionItemGmigcu = new(id: "conditionItem_GmigcU", workflowDemoRoot.Session); - DelegateExecutor conditionGroupQfpif5elseactions = new(id: "conditionGroup_QFPiF5ElseActions", workflowDemoRoot.Session); - DelegateExecutor conditionItemGmigcuactions = new(id: "conditionItem_GmigcUActions", workflowDemoRoot.Session); - QuestionOrsbf06Executor questionOrsbf06 = new(workflowDemoRoot.Session, options.AgentProvider); - SetvariableXznrdmExecutor setVariableXznrdm = new(workflowDemoRoot.Session); - Setvariable8Eix2aExecutor setVariable8Eix2a = new(workflowDemoRoot.Session); - DelegateExecutor conditionItemGmigcuRestart = new(id: "conditionItem_GmigcU_Restart", workflowDemoRoot.Session); - SendactivityBhcsi7Executor sendActivityBhcsi7 = new(workflowDemoRoot.Session); - SetvariableBhcsi7Executor setVariableBhcsi7 = new(workflowDemoRoot.Session); - DelegateExecutor conditionGroupQfpif5Post = new(id: "conditionGroup_QFPiF5_Post", workflowDemoRoot.Session); - DelegateExecutor goto76Hne8 = new(id: "goto_76Hne8", workflowDemoRoot.Session); - DelegateExecutor conditionItemFj432cPost = new(id: "conditionItem_fj432c_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemYiqundPost = new(id: "conditionItem_yiqund_Post", workflowDemoRoot.Session); - DelegateExecutor endSvonsvRestart = new(id: "end_SVoNSV_Restart", workflowDemoRoot.Session); - DelegateExecutor conditionItemFj432cactionsPost = new(id: "conditionItem_fj432cActions_Post", workflowDemoRoot.Session); - DelegateExecutor conditionGroupXznrdmPost = new(id: "conditionGroup_xzNrdM_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemYiqundactionsPost = new(id: "conditionItem_yiqundActions_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemFpanl9Post = new(id: "conditionItem_fpaNL9_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemNnqvxhPost = new(id: "conditionItem_NnqvXh_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemFpanl9actionsPost = new(id: "conditionItem_fpaNL9Actions_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemNnqvxhactionsPost = new(id: "conditionItem_NnqvXhActions_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemNlqtbvPost = new(id: "conditionItem_NlQTBv_Post", workflowDemoRoot.Session); - DelegateExecutor gotoLzfj8uRestart = new(id: "goto_LzfJ8u_Restart", workflowDemoRoot.Session); - DelegateExecutor conditionItemNlqtbvactionsPost = new(id: "conditionItem_NlQTBvActions_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemExalhzPost = new(id: "conditionItem_EXAlhZ_Post", workflowDemoRoot.Session); - DelegateExecutor endGhvrfhRestart = new(id: "end_GHVrFh_Restart", workflowDemoRoot.Session); - DelegateExecutor conditionItemExalhzactionsPost = new(id: "conditionItem_EXAlhZActions_Post", workflowDemoRoot.Session); - DelegateExecutor conditionGroupMvieccelseactionsPost = new(id: "conditionGroup_mVIecCElseActions_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemGmigcuPost = new(id: "conditionItem_GmigcU_Post", workflowDemoRoot.Session); - DelegateExecutor conditionItemGmigcuactionsPost = new(id: "conditionItem_GmigcUActions_Post", workflowDemoRoot.Session); - DelegateExecutor conditionGroupQfpif5elseactionsPost = new(id: "conditionGroup_QFPiF5ElseActions_Post", workflowDemoRoot.Session); + QuestionStudentExecutor questionStudent = new(workflowDemoRoot.Session, options.AgentProvider); + QuestionTeacherExecutor questionTeacher = new(workflowDemoRoot.Session, options.AgentProvider); + SetCountIncrementExecutor setCountIncrement = new(workflowDemoRoot.Session); + CheckCompletionExecutor checkCompletion = new(workflowDemoRoot.Session); + DelegateExecutor checkTurnDone = new(id: "check_turn_done", workflowDemoRoot.Session); + DelegateExecutor checkTurnCount = new(id: "check_turn_count", workflowDemoRoot.Session); + DelegateExecutor checkCompletionelseactions = new(id: "check_completionElseActions", workflowDemoRoot.Session); + DelegateExecutor checkTurnDoneactions = new(id: "check_turn_doneActions", workflowDemoRoot.Session); + SendactivityDoneExecutor sendActivityDone = new(workflowDemoRoot.Session); + DelegateExecutor checkTurnCountactions = new(id: "check_turn_countActions", workflowDemoRoot.Session); + DelegateExecutor gotoStudentAgent = new(id: "goto_student_agent", workflowDemoRoot.Session); + DelegateExecutor checkTurnCountRestart = new(id: "check_turn_count_Restart", workflowDemoRoot.Session); + SendactivityTiredExecutor sendActivityTired = new(workflowDemoRoot.Session); + DelegateExecutor checkTurnDonePost = new(id: "check_turn_done_Post", workflowDemoRoot.Session); + DelegateExecutor checkCompletionPost = new(id: "check_completion_Post", workflowDemoRoot.Session); + DelegateExecutor checkTurnCountPost = new(id: "check_turn_count_Post", workflowDemoRoot.Session); + DelegateExecutor checkTurnDoneactionsPost = new(id: "check_turn_doneActions_Post", workflowDemoRoot.Session); + DelegateExecutor gotoStudentAgentRestart = new(id: "goto_student_agent_Restart", workflowDemoRoot.Session); + DelegateExecutor checkTurnCountactionsPost = new(id: "check_turn_countActions_Post", workflowDemoRoot.Session); + DelegateExecutor checkCompletionelseactionsPost = new(id: "check_completionElseActions_Post", workflowDemoRoot.Session); // Define the workflow builder WorkflowBuilder builder = new(workflowDemoRoot); // Connect executors builder.AddEdge(workflowDemoRoot, workflowDemo); - builder.AddEdge(workflowDemo, setVariableAaslmf); - builder.AddEdge(setVariableAaslmf, setVariableV6yebo); - builder.AddEdge(setVariableV6yebo, setVariableNz2u0l); - builder.AddEdge(setVariableNz2u0l, setVariable10U2zn); - builder.AddEdge(setVariable10U2zn, sendActivityYfsbry); - builder.AddEdge(sendActivityYfsbry, conversation1A2b3c); - builder.AddEdge(conversation1A2b3c, questionUdomuw); - builder.AddEdge(questionUdomuw, sendActivityYfsbrz); - builder.AddEdge(sendActivityYfsbrz, questionDsbaju); - builder.AddEdge(questionDsbaju, setVariableKk2ldl); - builder.AddEdge(setVariableKk2ldl, sendActivityBwnzim); - builder.AddEdge(sendActivityBwnzim, questionO3bqkf); - builder.AddEdge(questionO3bqkf, parseRnztlv); - builder.AddEdge(parseRnztlv, conditionGroupMviecc); - builder.AddEdge(conditionGroupMviecc, conditionItemFj432c, (object? result) => ActionExecutor.IsMatch("conditionItem_fj432c", result)); - builder.AddEdge(conditionGroupMviecc, conditionItemYiqund, (object? result) => ActionExecutor.IsMatch("conditionItem_yiqund", result)); - builder.AddEdge(conditionGroupMviecc, conditionGroupMvieccelseactions, (object? result) => ActionExecutor.IsMatch("conditionGroup_mVIecCElseActions", result)); - builder.AddEdge(conditionItemFj432c, conditionItemFj432cactions); - builder.AddEdge(conditionItemFj432cactions, sendActivityKdl3mc); - builder.AddEdge(sendActivityKdl3mc, questionKe3l1d); - builder.AddEdge(questionKe3l1d, endSvonsv); - builder.AddEdge(conditionItemYiqund, conditionItemYiqundactions); - builder.AddEdge(conditionItemYiqundactions, setVariableH5lxdd); - builder.AddEdge(setVariableH5lxdd, conditionGroupVbtqd3); - builder.AddEdge(conditionGroupVbtqd3, conditionItemFpanl9, (object? result) => ActionExecutor.IsMatch("conditionItem_fpaNL9", result)); - builder.AddEdge(conditionGroupVbtqd3, conditionItemNnqvxh, (object? result) => ActionExecutor.IsMatch("conditionItem_NnqvXh", result)); - builder.AddEdge(conditionItemFpanl9, conditionItemFpanl9actions); - builder.AddEdge(conditionItemFpanl9actions, sendActivityFpanl9); - builder.AddEdge(conditionItemNnqvxh, conditionItemNnqvxhactions); - builder.AddEdge(conditionItemNnqvxhactions, sendActivityNnqvxh); - builder.AddEdge(conditionGroupVbtqd3Post, conditionGroupXznrdm); - builder.AddEdge(conditionGroupXznrdm, conditionItemNlqtbv, (object? result) => ActionExecutor.IsMatch("conditionItem_NlQTBv", result)); - builder.AddEdge(conditionItemNlqtbv, conditionItemNlqtbvactions); - builder.AddEdge(conditionItemNlqtbvactions, sendActivityH5lxdd); - builder.AddEdge(sendActivityH5lxdd, conditionGroup4S1z27); - builder.AddEdge(conditionGroup4S1z27, conditionItemExalhz, (object? result) => ActionExecutor.IsMatch("conditionItem_EXAlhZ", result)); - builder.AddEdge(conditionItemExalhz, conditionItemExalhzactions); - builder.AddEdge(conditionItemExalhzactions, sendActivityXkxfuu); - builder.AddEdge(sendActivityXkxfuu, endGhvrfh); - builder.AddEdge(conditionGroup4S1z27Post, sendActivityCwnzim); - builder.AddEdge(sendActivityCwnzim, questionWfj123); - builder.AddEdge(questionWfj123, sendActivityDsbaju); - builder.AddEdge(sendActivityDsbaju, questionUej456); - builder.AddEdge(questionUej456, setVariableJw7tmm); - builder.AddEdge(setVariableJw7tmm, setVariable6J2snp); - builder.AddEdge(setVariable6J2snp, setVariableS6hcgh); - builder.AddEdge(setVariableS6hcgh, gotoLzfj8u); - builder.AddEdge(gotoLzfj8u, questionO3bqkf); - builder.AddEdge(conditionItemYiqundRestart, conditionGroupMvieccelseactions); - builder.AddEdge(conditionGroupMvieccelseactions, sendActivityL7ooqo); - builder.AddEdge(sendActivityL7ooqo, setVariableL7ooqo); - builder.AddEdge(conditionGroupMvieccPost, setVariableNxn1me); - builder.AddEdge(setVariableNxn1me, conditionGroupQfpif5); - builder.AddEdge(conditionGroupQfpif5, conditionItemGmigcu, (object? result) => ActionExecutor.IsMatch("conditionItem_GmigcU", result)); - builder.AddEdge(conditionGroupQfpif5, conditionGroupQfpif5elseactions, (object? result) => ActionExecutor.IsMatch("conditionGroup_QFPiF5ElseActions", result)); - builder.AddEdge(conditionItemGmigcu, conditionItemGmigcuactions); - builder.AddEdge(conditionItemGmigcuactions, questionOrsbf06); - builder.AddEdge(questionOrsbf06, setVariableXznrdm); - builder.AddEdge(setVariableXznrdm, setVariable8Eix2a); - builder.AddEdge(conditionItemGmigcuRestart, conditionGroupQfpif5elseactions); - builder.AddEdge(conditionGroupQfpif5elseactions, sendActivityBhcsi7); - builder.AddEdge(sendActivityBhcsi7, setVariableBhcsi7); - builder.AddEdge(conditionGroupQfpif5Post, goto76Hne8); - builder.AddEdge(goto76Hne8, questionO3bqkf); - builder.AddEdge(conditionItemFj432cPost, conditionGroupMvieccPost); - builder.AddEdge(conditionItemYiqundPost, conditionGroupMvieccPost); - builder.AddEdge(endSvonsvRestart, conditionItemFj432cactionsPost); - builder.AddEdge(conditionItemFj432cactionsPost, conditionItemFj432cPost); - builder.AddEdge(conditionGroupXznrdmPost, conditionItemYiqundactionsPost); - builder.AddEdge(conditionItemYiqundactionsPost, conditionItemYiqundPost); - builder.AddEdge(conditionItemFpanl9Post, conditionGroupVbtqd3Post); - builder.AddEdge(conditionItemNnqvxhPost, conditionGroupVbtqd3Post); - builder.AddEdge(sendActivityFpanl9, conditionItemFpanl9actionsPost); - builder.AddEdge(conditionItemFpanl9actionsPost, conditionItemFpanl9Post); - builder.AddEdge(sendActivityNnqvxh, conditionItemNnqvxhactionsPost); - builder.AddEdge(conditionItemNnqvxhactionsPost, conditionItemNnqvxhPost); - builder.AddEdge(conditionItemNlqtbvPost, conditionGroupXznrdmPost); - builder.AddEdge(gotoLzfj8uRestart, conditionItemNlqtbvactionsPost); - builder.AddEdge(conditionItemNlqtbvactionsPost, conditionItemNlqtbvPost); - builder.AddEdge(conditionItemExalhzPost, conditionGroup4S1z27Post); - builder.AddEdge(endGhvrfhRestart, conditionItemExalhzactionsPost); - builder.AddEdge(conditionItemExalhzactionsPost, conditionItemExalhzPost); - builder.AddEdge(setVariableL7ooqo, conditionGroupMvieccelseactionsPost); - builder.AddEdge(conditionGroupMvieccelseactionsPost, conditionGroupMvieccPost); - builder.AddEdge(conditionItemGmigcuPost, conditionGroupQfpif5Post); - builder.AddEdge(setVariable8Eix2a, conditionItemGmigcuactionsPost); - builder.AddEdge(conditionItemGmigcuactionsPost, conditionItemGmigcuPost); - builder.AddEdge(setVariableBhcsi7, conditionGroupQfpif5elseactionsPost); - builder.AddEdge(conditionGroupQfpif5elseactionsPost, conditionGroupQfpif5Post); + builder.AddEdge(workflowDemo, questionStudent); + builder.AddEdge(questionStudent, questionTeacher); + builder.AddEdge(questionTeacher, setCountIncrement); + builder.AddEdge(setCountIncrement, checkCompletion); + builder.AddEdge(checkCompletion, checkTurnDone, (object? result) => ActionExecutor.IsMatch("check_turn_done", result)); + builder.AddEdge(checkCompletion, checkTurnCount, (object? result) => ActionExecutor.IsMatch("check_turn_count", result)); + builder.AddEdge(checkCompletion, checkCompletionelseactions, (object? result) => ActionExecutor.IsMatch("check_completionElseActions", result)); + builder.AddEdge(checkTurnDone, checkTurnDoneactions); + builder.AddEdge(checkTurnDoneactions, sendActivityDone); + builder.AddEdge(checkTurnCount, checkTurnCountactions); + builder.AddEdge(checkTurnCountactions, gotoStudentAgent); + builder.AddEdge(gotoStudentAgent, questionStudent); + builder.AddEdge(checkTurnCountRestart, checkCompletionelseactions); + builder.AddEdge(checkCompletionelseactions, sendActivityTired); + builder.AddEdge(checkTurnDonePost, checkCompletionPost); + builder.AddEdge(checkTurnCountPost, checkCompletionPost); + builder.AddEdge(sendActivityDone, checkTurnDoneactionsPost); + builder.AddEdge(checkTurnDoneactionsPost, checkTurnDonePost); + builder.AddEdge(gotoStudentAgentRestart, checkTurnCountactionsPost); + builder.AddEdge(checkTurnCountactionsPost, checkTurnCountPost); + builder.AddEdge(sendActivityTired, checkCompletionelseactionsPost); + builder.AddEdge(checkCompletionelseactionsPost, checkCompletionPost); // Build the workflow - return builder.Build(); + return builder.Build(validateOrphans: false); } } diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/Program.cs index c1846dac5e..bfc738c336 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteCode/Program.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; +// Uncomment this to enable JSON checkpointing to the local file system. +//#define CHECKPOINT_JSON + using System.Reflection; -using Azure.AI.Agents.Persistent; using Azure.Identity; using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Declarative; -using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; -using Test.WorkflowProviders; +using Shared.Workflows; namespace Demo.DeclarativeCode; @@ -24,157 +24,64 @@ internal sealed class Program { public static async Task Main(string[] args) { - Program program = new(args); + string? workflowInput = ParseWorkflowInput(args); + + Program program = new(workflowInput); await program.ExecuteAsync(); } private async Task ExecuteAsync() + { + Notify("\nWORKFLOW: Starting..."); + + string input = this.GetWorkflowInput(); + + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + await this.Runner.ExecuteAsync(this.CreateWorkflow, input); + + Notify("\nWORKFLOW: Done!\n"); + } + + private Workflow CreateWorkflow() { // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file. DeclarativeWorkflowOptions options = - new(new AzureAgentProvider(this.FoundryEndpoint, new AzureCliCredential())) + new(new AzureAgentProvider(new Uri(this.FoundryEndpoint), new AzureCliCredential())) { Configuration = this.Configuration }; // Use the generated provider to create a workflow instance. - Workflow workflow = TestWorkflowProvider.CreateWorkflow(options); - - Notify("\nWORKFLOW: Starting..."); - - // Run the workflow, just like any other workflow - string input = this.GetWorkflowInput(); - StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: input); - await this.MonitorAndDisposeWorkflowRunAsync(run); - - Notify("\nWORKFLOW: Done!"); + return SampleWorkflowProvider.CreateWorkflow(options); } - private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT"; - - private static readonly Dictionary s_nameCache = []; - private static readonly HashSet s_fileCache = []; - private string? WorkflowInput { get; } private string FoundryEndpoint { get; } - private PersistentAgentsClient FoundryClient { get; } private IConfiguration Configuration { get; } + private WorkflowRunner Runner { get; } - private Program(string[] args) + private Program(string? workflowInput) { - this.WorkflowInput = ParseWorkflowInput(args); + this.WorkflowInput = workflowInput; this.Configuration = InitializeConfig(); - this.FoundryEndpoint = this.Configuration[ConfigKeyFoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {ConfigKeyFoundryEndpoint}"); - this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential()); - } + this.FoundryEndpoint = this.Configuration[Application.Settings.FoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {Application.Settings.FoundryEndpoint}"); - private async Task MonitorAndDisposeWorkflowRunAsync(StreamingRun run) - { - await using IAsyncDisposable disposeRun = run; - - string? messageId = null; - - await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync()) - { - switch (workflowEvent) + this.Runner = + new() { - case ExecutorInvokedEvent executorInvoked: - Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}"); - break; - - case ExecutorCompletedEvent executorComplete: - Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}"); - break; - - case ExecutorFailedEvent executorFailure: - Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}"); - break; - - case WorkflowErrorEvent workflowError: - throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure..."); - - case ConversationUpdateEvent invokeEvent: - Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}"); - break; - - case AgentRunUpdateEvent streamEvent: - if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal)) - { - messageId = streamEvent.Update.MessageId; - - if (messageId is not null) - { - string? agentId = streamEvent.Update.AuthorName; - if (agentId is not null) - { - if (!s_nameCache.TryGetValue(agentId, out string? realName)) - { - PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId); - s_nameCache[agentId] = agent.Name; - realName = agent.Name; - } - agentId = realName; - } - agentId ??= nameof(ChatRole.Assistant); - Console.ForegroundColor = ConsoleColor.Cyan; - Console.Write($"\n{agentId.ToUpperInvariant()}:"); - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine($" [{messageId}]"); - } - } - - ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate; - switch (chatUpdate?.RawRepresentation) - { - case MessageContentUpdate messageUpdate: - string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId; - if (fileId is not null && s_fileCache.Add(fileId)) - { - BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId); - await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content); - } - break; - } - try - { - Console.ResetColor(); - Console.Write(streamEvent.Data); - } - finally - { - Console.ResetColor(); - } - break; - - case AgentRunResponseEvent messageEvent: - try - { - Console.WriteLine(); - if (messageEvent.Response.AgentId is null) - { - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine("ACTIVITY:"); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine(messageEvent.Response?.Text.Trim()); - } - else - { - if (messageEvent.Response.Usage is not null) - { - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]"); - } - } - } - finally - { - Console.ResetColor(); - } - break; - } - } +#if CHECKPOINT_JSON + // Use an json file checkpoint store that will persist checkpoints to the local file system. + UseJsonCheckpoints = true +#else + // Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process. + UseJsonCheckpoints = false +#endif + }; } private string GetWorkflowInput() @@ -231,19 +138,4 @@ internal sealed class Program Console.ResetColor(); } } - - private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content) - { - string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename)); - filePath = Path.ChangeExtension(filePath, ".png"); - - await File.WriteAllBytesAsync(filePath, content.ToArray()); - - Process.Start( - new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/C start {filePath}" - }); - } } diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj index b885a71c3c..f5475b66f4 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj @@ -11,7 +11,9 @@ - true + true + true + true diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/Program.cs index ce6a19b0d3..1a761be436 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/Program.cs @@ -5,18 +5,12 @@ using System.Diagnostics; using System.Reflection; -using System.Text.Json; -using Azure.AI.Agents.Persistent; using Azure.Identity; using Microsoft.Agents.AI.Workflows; -#if CHECKPOINT_JSON -using Microsoft.Agents.AI.Workflows.Checkpointing; -#endif using Microsoft.Agents.AI.Workflows.Declarative; -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; +using Shared.Workflows; namespace Demo.DeclarativeWorkflow; @@ -62,52 +56,13 @@ internal sealed class Program Notify("\nWORKFLOW: Starting..."); - // Run the workflow, just like any other workflow string input = this.GetWorkflowInput(); -#if CHECKPOINT_JSON - // Use a file-system based JSON checkpoint store to persist checkpoints to disk. - DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:yyMMdd-hhmmss-ff}")); - CheckpointManager checkpointManager = CheckpointManager.CreateJson(new FileSystemJsonCheckpointStore(checkpointFolder)); -#else - // Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process. - CheckpointManager checkpointManager = CheckpointManager.CreateInMemory(); -#endif - - Checkpointed run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager); - - bool isComplete = false; - object? response = null; - do - { - ExternalRequest? externalRequest = await this.MonitorAndDisposeWorkflowRunAsync(run, response); - if (externalRequest is not null) - { - Notify("\nWORKFLOW: Yield"); - - if (this.LastCheckpoint is null) - { - throw new InvalidOperationException("Checkpoint information missing after external request."); - } - - // Process the external request. - response = await this.HandleExternalRequestAsync(externalRequest); - - // Let's resume on an entirely new workflow instance to demonstrate checkpoint portability. - workflow = this.CreateWorkflow(); - - // Restore the latest checkpoint. - Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}"); - Notify("\nWORKFLOW: Restore"); - - run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager, run.Run.RunId); - } - else - { - isComplete = true; - } - } - while (!isComplete); + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + await this.Runner.ExecuteAsync(this.CreateWorkflow, input); Notify("\nWORKFLOW: Done!\n"); } @@ -116,19 +71,16 @@ internal sealed class Program /// Create the workflow from the declarative YAML. Includes definition of the /// and the associated . /// - /// - /// The value assigned to controls on whether the function - /// tools () initialized in the constructor are included for auto-invocation. - /// private Workflow CreateWorkflow() { - // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file. - AzureAgentProvider agentProvider = new(this.FoundryEndpoint, new AzureCliCredential()) + // Create the agent provider that will service agent requests within the workflow. + AzureAgentProvider agentProvider = new(new Uri(this.FoundryEndpoint), new AzureCliCredential()) { // Functions included here will be auto-executed by the framework. - Functions = IncludeFunctions ? this.FunctionMap.Values : null, + Functions = this.Functions }; + // Define the workflow options. DeclarativeWorkflowOptions options = new(agentProvider) { @@ -137,31 +89,16 @@ internal sealed class Program //LoggerFactory = null, // Assign to enable logging }; + // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file. return DeclarativeWorkflowBuilder.Build(this.WorkflowFile, options); } - /// - /// Configuration key used to identify the Foundry project endpoint. - /// - private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT"; - - /// - /// Controls on whether the function tools () initialized - /// in the constructor are included for auto-invocation. - /// NOTE: By default, no functions exist as part of this sample. - /// - private const bool IncludeFunctions = true; - - private static Dictionary NameCache { get; } = []; - private static HashSet FileCache { get; } = []; - private string WorkflowFile { get; } private string? WorkflowInput { get; } private string FoundryEndpoint { get; } - private PersistentAgentsClient FoundryClient { get; } private IConfiguration Configuration { get; } - private CheckpointInfo? LastCheckpoint { get; set; } - private Dictionary FunctionMap { get; } + private WorkflowRunner Runner { get; } + private IList Functions { get; } private Program(string workflowFile, string? workflowInput) { @@ -170,246 +107,26 @@ internal sealed class Program this.Configuration = InitializeConfig(); - this.FoundryEndpoint = this.Configuration[ConfigKeyFoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {ConfigKeyFoundryEndpoint}"); - this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential()); + this.FoundryEndpoint = this.Configuration[Application.Settings.FoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {Application.Settings.FoundryEndpoint}"); - List functions = + this.Functions = [ // Manually define any custom functions that may be required by agents within the workflow. // By default, this sample does not include any functions. //AIFunctionFactory.Create(), ]; - this.FunctionMap = functions.ToDictionary(f => f.Name); - } - private async Task MonitorAndDisposeWorkflowRunAsync(Checkpointed run, object? response = null) - { - // Always dispose the run when done. - await using IAsyncDisposable disposeRun = run; - - bool hasStreamed = false; - string? messageId = null; - - await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync()) - { - switch (workflowEvent) + this.Runner = + new(this.Functions) { - case ExecutorInvokedEvent executorInvoked: - Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}"); - break; - - case ExecutorCompletedEvent executorCompleted: - Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}"); - break; - - case DeclarativeActionInvokedEvent actionInvoked: - Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]"); - break; - - case DeclarativeActionCompletedEvent actionComplete: - Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]"); - break; - - case ExecutorFailedEvent executorFailure: - Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}"); - break; - - case WorkflowErrorEvent workflowError: - throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure..."); - - case SuperStepCompletedEvent checkpointCompleted: - this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint; - Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]"); - break; - - case RequestInfoEvent requestInfo: - Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}"); - if (response is not null) - { - ExternalResponse requestResponse = requestInfo.Request.CreateResponse(response); - await run.Run.SendResponseAsync(requestResponse); - response = null; - } - else - { - // Yield to handle the external request - return requestInfo.Request; - } - break; - - case ConversationUpdateEvent invokeEvent: - Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}"); - break; - - case MessageActivityEvent activityEvent: - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine("\nACTIVITY:"); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine(activityEvent.Message.Trim()); - break; - - case AgentRunUpdateEvent streamEvent: - if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal)) - { - hasStreamed = false; - messageId = streamEvent.Update.MessageId; - - if (messageId is not null) - { - string? agentId = streamEvent.Update.AgentId; - if (agentId is not null) - { - if (!NameCache.TryGetValue(agentId, out string? realName)) - { - PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId); - NameCache[agentId] = agent.Name; - realName = agent.Name; - } - agentId = realName; - } - agentId ??= nameof(ChatRole.Assistant); - Console.ForegroundColor = ConsoleColor.Cyan; - Console.Write($"\n{agentId.ToUpperInvariant()}:"); - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine($" [{messageId}]"); - } - } - - ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate; - switch (chatUpdate?.RawRepresentation) - { - case MessageContentUpdate messageUpdate: - string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId; - if (fileId is not null && FileCache.Add(fileId)) - { - BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId); - await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content); - } - break; - case RequiredActionUpdate actionUpdate: - Console.ForegroundColor = ConsoleColor.White; - Console.Write($"Calling tool: {actionUpdate.FunctionName}"); - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine($" [{actionUpdate.ToolCallId}]"); - break; - } - try - { - Console.ResetColor(); - Console.Write(streamEvent.Update.Text); - hasStreamed |= !string.IsNullOrEmpty(streamEvent.Update.Text); - } - finally - { - Console.ResetColor(); - } - break; - - case AgentRunResponseEvent messageEvent: - try - { - if (hasStreamed) - { - Console.WriteLine(); - } - - if (messageEvent.Response.Usage is not null) - { - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]"); - } - } - finally - { - Console.ResetColor(); - } - break; - } - } - - return null; // No request to handle - } - - /// - /// Handle request for external input, either from a human or a function tool invocation. - /// - private async ValueTask HandleExternalRequestAsync(ExternalRequest request) => - request.Data.TypeId.TypeName switch - { - // Request for human input - _ when request.Data.TypeId.IsMatch() => HandleUserMessageRequest(request.DataAs()!), - // Request for function tool invocation. (Only active when functions are defined and IncludeFunctions is true.) - _ when request.Data.TypeId.IsMatch() => await this.HandleToolRequestAsync(request.DataAs()!), - // Request for user input, such as function or mcp tool approval - _ when request.Data.TypeId.IsMatch() => HandleUserInputRequest(request.DataAs()!), - // Unknown request type. - _ => throw new InvalidOperationException($"Unsupported external request type: {request.GetType().Name}."), - }; - - /// - /// Handle request for human input. - /// - private static AnswerResponse HandleUserMessageRequest(AnswerRequest request) - { - string? userInput; - do - { - Console.ForegroundColor = ConsoleColor.DarkGreen; - Console.Write($"\n{request.Prompt ?? "INPUT:"} "); - Console.ForegroundColor = ConsoleColor.White; - userInput = Console.ReadLine(); - } - while (string.IsNullOrWhiteSpace(userInput)); - - return new AnswerResponse(userInput); - } - - /// - /// Handle a function tool request by invoking the specified tools and returning the results. - /// - /// - /// This handler is only active when is set to true and - /// one or more instances are defined in the constructor. - /// - private async ValueTask HandleToolRequestAsync(AgentFunctionToolRequest request) - { - Task[] functionTasks = request.FunctionCalls.Select(functionCall => InvokesToolAsync(functionCall)).ToArray(); - - await Task.WhenAll(functionTasks); - - return AgentFunctionToolResponse.Create(request, functionTasks.Select(task => task.Result)); - - async Task InvokesToolAsync(FunctionCallContent functionCall) - { - AIFunction functionTool = this.FunctionMap[functionCall.Name]; - AIFunctionArguments? functionArguments = functionCall.Arguments is null ? null : new(functionCall.Arguments.NormalizePortableValues()); - object? result = await functionTool.InvokeAsync(functionArguments); - return new FunctionResultContent(functionCall.CallId, JsonSerializer.Serialize(result)); - } - } - - /// - /// Handle request for user input for mcp and function tool approval. - /// - private static UserInputResponse HandleUserInputRequest(UserInputRequest request) - { - return UserInputResponse.Create(request, ProcessRequests()); - - IEnumerable ProcessRequests() - { - foreach (UserInputRequestContent approvalRequest in request.InputRequests) - { - // Here we are explicitly approving all requests. - // In a real-world scenario, you would replace this logic to either solicit user approval or implement a more complex approval process. - yield return - approvalRequest switch - { - McpServerToolApprovalRequestContent mcpApprovalRequest => mcpApprovalRequest.CreateResponse(approved: true), - FunctionApprovalRequestContent functionApprovalRequest => functionApprovalRequest.CreateResponse(approved: true), - _ => throw new NotSupportedException($"Unsupported request of type {approvalRequest.GetType().Name}"), - }; - } - } +#if CHECKPOINT_JSON + // Use an json file checkpoint store that will persist checkpoints to the local file system. + UseJsonCheckpoints = true +#else + // Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process. + UseJsonCheckpoints = false +#endif + }; } private static string? ParseWorkflowFile(string[] args) @@ -516,19 +233,4 @@ internal sealed class Program Console.ResetColor(); } } - - private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content) - { - string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename)); - filePath = Path.ChangeExtension(filePath, ".png"); - - await File.WriteAllBytesAsync(filePath, content.ToArray()); - - Process.Start( - new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/C start {filePath}" - }); - } } diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.csproj new file mode 100644 index 0000000000..239df72098 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.csproj @@ -0,0 +1,39 @@ + + + + Exe + net9.0 + net9.0 + $(ProjectsDebugTargetFrameworks) + enable + enable + + + + true + true + true + true + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.yaml b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.yaml new file mode 100644 index 0000000000..0135111de5 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/FunctionTools.yaml @@ -0,0 +1,22 @@ +# +# This workflow demonstrates an agent that requires tool approval +# in a loop responding to user input. +# +# Example input: +# What is the soup of the day? +# +kind: Workflow +trigger: + + kind: OnConversationStart + id: workflow_demo + actions: + + - kind: InvokeAzureAgent + id: invoke_search + conversationId: =System.ConversationId + agent: + name: MenuAgent + input: + externalLoop: + when: =Upper(System.LastMessage.Text) <> "EXIT" diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/MenuPlugin.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/MenuPlugin.cs new file mode 100644 index 0000000000..efe2a1284e --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/MenuPlugin.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; + +namespace Demo.Workflows.Declarative.FunctionTools; + +#pragma warning disable CA1822 // Mark members as static + +public sealed class MenuPlugin +{ + [Description("Provides a list items on the menu.")] + public MenuItem[] GetMenu() + { + return s_menuItems; + } + + [Description("Provides a list of specials from the menu.")] + public MenuItem[] GetSpecials() + { + return [.. s_menuItems.Where(i => i.IsSpecial)]; + } + + [Description("Provides the price of the requested menu item.")] + public float? GetItemPrice( + [Description("The name of the menu item.")] + string name) + { + return s_menuItems.FirstOrDefault(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase))?.Price; + } + + private static readonly MenuItem[] s_menuItems = + [ + new() + { + Category = "Soup", + Name = "Clam Chowder", + Price = 4.95f, + IsSpecial = true, + }, + new() + { + Category = "Soup", + Name = "Tomato Soup", + Price = 4.95f, + IsSpecial = false, + }, + new() + { + Category = "Salad", + Name = "Cobb Salad", + Price = 9.99f, + }, + new() + { + Category = "Salad", + Name = "House Salad", + Price = 4.95f, + }, + new() + { + Category = "Drink", + Name = "Chai Tea", + Price = 2.95f, + IsSpecial = true, + }, + new() + { + Category = "Drink", + Name = "Soda", + Price = 1.95f, + }, + ]; + + public sealed class MenuItem + { + public string Category { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public float Price { get; init; } + public bool IsSpecial { get; init; } + } +} diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs new file mode 100644 index 0000000000..56c11cf0cf --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using OpenAI.Responses; +using Shared.Foundry; +using Shared.Workflows; + +namespace Demo.Workflows.Declarative.FunctionTools; + +/// +/// Demonstrate a workflow that responds to user input using an agent who +/// with function tools assigned. Exits the loop when the user enters "exit". +/// +/// +/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// information the configuration required to run this sample. +/// +internal sealed class Program +{ + public static async Task Main(string[] args) + { + // Initialize configuration + IConfiguration configuration = Application.InitializeConfig(); + Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint)); + + // Ensure sample agents exist in Foundry. + MenuPlugin menuPlugin = new(); + AIFunction[] functions = + [ + AIFunctionFactory.Create(menuPlugin.GetMenu), + AIFunctionFactory.Create(menuPlugin.GetSpecials), + AIFunctionFactory.Create(menuPlugin.GetItemPrice), + ]; + + await CreateAgentAsync(foundryEndpoint, configuration, functions); + + // Get input from command line or console + string workflowInput = Application.GetInput(args); + + // Create the workflow factory. This class demonstrates how to initialize a + // declarative workflow from a YAML file. Once the workflow is created, it + // can be executed just like any regular workflow. + WorkflowFactory workflowFactory = new("FunctionTools.yaml", foundryEndpoint); + + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + WorkflowRunner runner = new(functions) { UseJsonCheckpoints = true }; + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput); + } + + private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration, AIFunction[] functions) + { + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + await agentsClient.CreateAgentAsync( + agentName: "MenuAgent", + agentDefinition: DefineMenuAgent(configuration, functions), + agentDescription: "Provides information about the restaurant menu"); + } + + private static PromptAgentDefinition DefineMenuAgent(IConfiguration configuration, AIFunction[] functions) + { + PromptAgentDefinition agentDefinition = + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + Answer the users questions on the menu. + For questions or input that do not require searching the documentation, inform the + user that you can only answer questions what's on the menu. + """ + }; + + foreach (AIFunction function in functions) + { + agentDefinition.Tools.Add(function.AsOpenAIResponseTool()); + } + + return agentDefinition; + } +} diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/GenerateCode/GenerateCode.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/GenerateCode/GenerateCode.csproj index 72afa29cda..b10f7c5e95 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/GenerateCode/GenerateCode.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/GenerateCode/GenerateCode.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net9.0 net9.0 $(ProjectsDebugTargetFrameworks) enable diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Marketing.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Marketing.csproj new file mode 100644 index 0000000000..76792809a9 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Marketing.csproj @@ -0,0 +1,39 @@ + + + + Exe + net9.0 + net9.0 + $(ProjectsDebugTargetFrameworks) + enable + enable + + + + true + true + true + true + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Program.cs new file mode 100644 index 0000000000..95f4c92a3d --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Program.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Shared.Foundry; +using Shared.Workflows; + +namespace Demo.Workflows.Declarative.Marketing; + +/// +/// Demonstrate a declarative workflow with three agents (Analyst, Writer, Editor) +/// sequentially engaging in a task. +/// +/// +/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// information the configuration required to run this sample. +/// +internal sealed class Program +{ + public static async Task Main(string[] args) + { + // Initialize configuration + IConfiguration configuration = Application.InitializeConfig(); + Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint)); + + // Ensure sample agents exist in Foundry. + await CreateAgentsAsync(foundryEndpoint, configuration); + + // Get input from command line or console + string workflowInput = Application.GetInput(args); + + // Create the workflow factory. This class demonstrates how to initialize a + // declarative workflow from a YAML file. Once the workflow is created, it + // can be executed just like any regular workflow. + WorkflowFactory workflowFactory = new("Marketing.yaml", foundryEndpoint); + + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + WorkflowRunner runner = new(); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput); + } + + private static async Task CreateAgentsAsync(Uri foundryEndpoint, IConfiguration configuration) + { + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + await agentsClient.CreateAgentAsync( + agentName: "AnalystAgent", + agentDefinition: DefineAnalystAgent(configuration), + agentDescription: "Analyst agent for Marketing workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "WriterAgent", + agentDefinition: DefineWriterAgent(configuration), + agentDescription: "Writer agent for Marketing workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "EditorAgent", + agentDefinition: DefineEditorAgent(configuration), + agentDescription: "Editor agent for Marketing workflow"); + } + + private static PromptAgentDefinition DefineAnalystAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelFull)) + { + Instructions = + """ + You are a marketing analyst. Given a product description, identify: + - Key features + - Target audience + - Unique selling points + """, + Tools = + { + //AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available + // new BingGroundingSearchToolParameters( + // [new BingGroundingSearchConfiguration(configuration[Application.Settings.FoundryGroundingTool])])) + } + }; + + private static PromptAgentDefinition DefineWriterAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelFull)) + { + Instructions = + """ + You are a marketing copywriter. Given a block of text describing features, audience, and USPs, + compose a compelling marketing copy (like a newsletter section) that highlights these points. + Output should be short (around 150 words), output just the copy as a single text block. + """ + }; + + private static PromptAgentDefinition DefineEditorAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelFull)) + { + Instructions = + """ + You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone, + give format and make it polished. Output the final improved copy as a single text block. + """ + }; +} diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/README.md b/dotnet/samples/GettingStarted/Workflows/Declarative/README.md index 03023ea847..0418cfe8da 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/README.md +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/README.md @@ -1,26 +1,26 @@ # Summary -This demo showcases the ability to parse a declarative Foundry Workflow file (YAML) to build a `Workflow<>` -be executed using the same pattern as any code-based workflow. +These samples showcases the ability to parse a declarative Foundry Workflow file (YAML) +to build a `Workflow` that may be executed using the same pattern as any code-based workflow. ## Configuration -This demo requires configuration to access agents an [Azure Foundry Project](https://learn.microsoft.com/azure/ai-foundry). +These samples must be configured to create and use agents your +[Azure Foundry Project](https://learn.microsoft.com/azure/ai-foundry). -#### Settings +### Settings We suggest using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) to avoid the risk of leaking secrets into the repository, branches and pull requests. You can also use environment variables if you prefer. -To set your secrets as an environment variable (PowerShell): - -```pwsh -$env:FOUNDRY_PROJECT_ENDPOINT="https://..." -``` - -etc... +The configuraton required by the samples is: +|Setting Name| Description| +|:--|:--| +|FOUNDRY_PROJECT_ENDPOINT| The endpoint URL of your Azure Foundry Project.| +|FOUNDRY_MODEL_DEPLOYMENT_NAME| The name of the model deployment to use +|FOUNDRY_CONNECTION_GROUNDING_TOOL| The name of the Bing Grounding connection configured in your Azure Foundry Project.| To set your secrets with .NET Secret Manager: @@ -51,7 +51,7 @@ To set your secrets with .NET Secret Manager: 5. Define setting that identifies your Azure Foundry Model Deployment (endpoint): ``` - dotnet user-secrets set "FOUNDRY_MODEL_DEPLOYMENT_NAME" "gpt-4.1" + dotnet user-secrets set "FOUNDRY_MODEL_DEPLOYMENT_NAME" "gpt-5" ``` 6. Define setting that identifies your Bing Grounding connection: @@ -60,7 +60,15 @@ To set your secrets with .NET Secret Manager: dotnet user-secrets set "FOUNDRY_CONNECTION_GROUNDING_TOOL" "mybinggrounding" ``` -#### Authorization +You may alternatively set your secrets as an environment variable (PowerShell): + +```pwsh +$env:FOUNDRY_PROJECT_ENDPOINT="https://..." +$env:FOUNDRY_MODEL_DEPLOYMENT_NAME="gpt-5" +$env:FOUNDRY_CONNECTION_GROUNDING_TOOL="mybinggrounding" +``` + +### Authorization Use [_Azure CLI_](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) to authorize access to your Azure Foundry Project: @@ -69,34 +77,21 @@ az login az account get-access-token ``` -#### Agents - -The sample workflows rely on agents defined in your Azure Foundry Project. - -To create agents, run the [`Create.ps1`](../../../../../workflow-samples/setup/) script. -This will create the agents used in the sample workflows in your Azure Foundry Project and format a script you can copy and use to configure your environment. - -> Note: `Create.ps1` relies upon the `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_DEPLOYMENT_NAME`, and `FOUNDRY_CONNECTION_GROUNDING_TOOL` settings. - ## Execution -Run the demo from the console by specifying a path to a declarative (YAML) workflow file. -The repository has example workflows available in the root [`/workflow-samples`](../../../../../workflow-samples) folder. +The samples may be executed within _Visual Studio_ or _VS Code_. + +To run the sampes from the command line: 1. From the root of the repository, navigate the console to the project folder: ```sh - cd dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow + cd dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher ``` -2. Run the demo referencing a sample workflow by name: +2. Run the demo and optionally provided input: ```sh - dotnet run HelloWorld - ``` - -3. Run the demo with a path to any workflow file: - - ```sh - dotnet run c:/myworkflows/HelloWorld.yaml + dotnet run "How would you compute the value of PI?" ``` + > The sample will allow for interactive input in the absence of an input argument. \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs new file mode 100644 index 0000000000..2f65758f41 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Shared.Foundry; +using Shared.Workflows; + +namespace Demo.Workflows.Declarative.StudentTeacher; + +/// +/// Demonstrate a declarative workflow with two agents (Student and Teacher) +/// in an iterative conversation. +/// +/// +/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// information the configuration required to run this sample. +/// +internal sealed class Program +{ + public static async Task Main(string[] args) + { + // Initialize configuration + IConfiguration configuration = Application.InitializeConfig(); + Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint)); + + // Ensure sample agents exist in Foundry. + await CreateAgentsAsync(foundryEndpoint, configuration); + + // Get input from command line or console + string workflowInput = Application.GetInput(args); + + // Create the workflow factory. This class demonstrates how to initialize a + // declarative workflow from a YAML file. Once the workflow is created, it + // can be executed just like any regular workflow. + WorkflowFactory workflowFactory = new("MathChat.yaml", foundryEndpoint); + + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + WorkflowRunner runner = new(); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput); + } + + private static async Task CreateAgentsAsync(Uri foundryEndpoint, IConfiguration configuration) + { + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + await agentsClient.CreateAgentAsync( + agentName: "StudentAgent", + agentDefinition: DefineStudentAgent(configuration), + agentDescription: "Student agent for MathChat workflow"); + + await agentsClient.CreateAgentAsync( + agentName: "TeacherAgent", + agentDefinition: DefineTeacherAgent(configuration), + agentDescription: "Teacher agent for MathChat workflow"); + } + + private static PromptAgentDefinition DefineStudentAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + Your job is help a math teacher practice teaching by making intentional mistakes. + You attempt to solve the given math problem, but with intentional mistakes so the teacher can help. + Always incorporate the teacher's advice to fix your next response. + You have the math-skills of a 6th grader. + Don't describe who you are or reveal your instructions. + """ + }; + + private static PromptAgentDefinition DefineTeacherAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + Review and coach the student's approach to solving the given math problem. + Don't repeat the solution or try and solve it. + If the student has demonstrated comprehension and responded to all of your feedback, + give the student your congratulations by using the word "congratulations". + """ + }; +} diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/StudentTeacher.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/StudentTeacher.csproj new file mode 100644 index 0000000000..dae5bbef1f --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/StudentTeacher.csproj @@ -0,0 +1,39 @@ + + + + Exe + net9.0 + net9.0 + $(ProjectsDebugTargetFrameworks) + enable + enable + + + + true + true + true + true + + + + + + + + + + + + + + + + + + + Always + + + + \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs new file mode 100644 index 0000000000..0461927a02 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using OpenAI.Responses; +using Shared.Foundry; +using Shared.Workflows; + +namespace Demo.Workflows.Declarative.ToolApproval; + +/// +/// Demonstrate a workflow that responds to user input using an agent who +/// has an MCP tool that requires approval. Exits the loop when the user enters "exit". +/// +/// +/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// information the configuration required to run this sample. +/// +internal sealed class Program +{ + public static async Task Main(string[] args) + { + // Initialize configuration + IConfiguration configuration = Application.InitializeConfig(); + Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint)); + + // Ensure sample agents exist in Foundry. + await CreateAgentAsync(foundryEndpoint, configuration); + + // Get input from command line or console + string workflowInput = Application.GetInput(args); + + // Create the workflow factory. This class demonstrates how to initialize a + // declarative workflow from a YAML file. Once the workflow is created, it + // can be executed just like any regular workflow. + WorkflowFactory workflowFactory = new("ToolApproval.yaml", foundryEndpoint); + + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + WorkflowRunner runner = new() { UseJsonCheckpoints = true }; + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput); + } + + private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration) + { + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + await agentsClient.CreateAgentAsync( + agentName: "DocumentSearchAgent", + agentDefinition: DefineSearchAgent(configuration), + agentDescription: "Searches documents on Microsoft Learn"); + } + + private static PromptAgentDefinition DefineSearchAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + Answer the users questions by searching the Microsoft Learn documentation. + For questions or input that do not require searching the documentation, inform the + user that you can only answer questions related to Microsoft Learn documentation. + """, + Tools = + { + ResponseTool.CreateMcpTool( + serverLabel: "microsoft_docs", + serverUri: new Uri("https://learn.microsoft.com/api/mcp"), + toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval)) + } + }; +} diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.csproj new file mode 100644 index 0000000000..5d5a013a33 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.csproj @@ -0,0 +1,39 @@ + + + + Exe + net9.0 + net9.0 + $(ProjectsDebugTargetFrameworks) + enable + enable + + + + true + true + true + true + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.yaml b/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.yaml new file mode 100644 index 0000000000..9383a60fce --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.yaml @@ -0,0 +1,38 @@ +# +# This workflow demonstrates an agent that requires tool approval +# in a loop responding to user input. +# +# Example input: +# What is Microsoft Graph API used for? +# +kind: Workflow +trigger: + + kind: OnConversationStart + id: workflow_demo + actions: + + - kind: InvokeAzureAgent + id: invoke_search + conversationId: =System.ConversationId + agent: + name: DocumentSearchAgent + + - kind: RequestExternalInput + id: request_requirements + + - kind: ConditionGroup + id: check_completion + conditions: + + - condition: =Upper(System.LastMessage.Text) = "EXIT" + id: check_done + actions: + + - kind: EndWorkflow + id: all_done + + elseActions: + - kind: GotoAction + id: goto_search + actionId: invoke_search diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs index ac44890c1c..857989b21c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs @@ -1,15 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.ClientModel.Primitives; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Azure.AI.Agents.Persistent; +using Azure.AI.Agents; using Azure.Core; -using Azure.Core.Pipeline; using Microsoft.Extensions.AI; +using OpenAI.Responses; namespace Microsoft.Agents.AI.Workflows.Declarative; @@ -18,31 +21,23 @@ namespace Microsoft.Agents.AI.Workflows.Declarative; /// /// This class is used to retrieve and manage AI agents associated with a Foundry project. It requires a /// project endpoint and credentials to authenticate requests. -/// The endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project. +/// A instance representing the endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project. /// The credentials used to authenticate with the Foundry project. This must be a valid instance of . /// An optional instance to be used for making HTTP requests. If not provided, a default client will be used. -public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider +public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider { - private static readonly Dictionary s_roleMap = - new() - { - [ChatRole.User.Value.ToUpperInvariant()] = MessageRole.User, - [ChatRole.Assistant.Value.ToUpperInvariant()] = MessageRole.Agent, - [ChatRole.System.Value.ToUpperInvariant()] = new MessageRole(ChatRole.System.Value), - [ChatRole.Tool.Value.ToUpperInvariant()] = new MessageRole(ChatRole.Tool.Value), - }; + private readonly Dictionary _versionCache = []; + private readonly Dictionary _agentCache = []; - private PersistentAgentsClient? _agentsClient; + private AgentsClient? _agentsClient; + private ConversationClient? _conversationClient; /// public override async Task CreateConversationAsync(CancellationToken cancellationToken = default) { - PersistentAgentThread conversation = - await this.GetAgentsClient().Threads.CreateThreadAsync( - messages: null, - toolResources: null, - metadata: null, - cancellationToken).ConfigureAwait(false); + AgentConversation conversation = + await this.GetConversationClient() + .CreateConversationAsync(options: null, cancellationToken).ConfigureAwait(false); return conversation.Id; } @@ -50,61 +45,117 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p /// public override async Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default) { - PersistentThreadMessage newMessage = - await this.GetAgentsClient().Messages.CreateMessageAsync( + ReadOnlyCollection newItems = + await this.GetConversationClient().CreateConversationItemsAsync( conversationId, - role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()], - contentBlocks: GetContent(), - attachments: null, - metadata: GetMetadata(), + items: GetResponseItems(), + include: null, cancellationToken).ConfigureAwait(false); - return ToChatMessage(newMessage); + return newItems.AsChatMessages().Single(); - Dictionary? GetMetadata() + IEnumerable GetResponseItems() { - if (conversationMessage.AdditionalProperties is null) + IEnumerable messages = [conversationMessage]; + + foreach (ResponseItem item in messages.AsOpenAIResponseItems()) { - return null; - } - - return conversationMessage.AdditionalProperties.ToDictionary(prop => prop.Key, prop => prop.Value?.ToString() ?? string.Empty); - } - - IEnumerable GetContent() - { - foreach (AIContent content in conversationMessage.Contents) - { - MessageInputContentBlock? contentBlock = - content switch - { - TextContent textContent => new MessageInputTextBlock(textContent.Text), - HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)), - UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())), - DataContent dataContent when dataContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(dataContent.Uri)), - _ => null // Unsupported content type - }; - - if (contentBlock is not null) + if (string.IsNullOrEmpty(item.Id)) { - yield return contentBlock; + yield return item; + } + else + { + yield return new ReferenceResponseItem(item.Id); } } } } /// - public override async Task GetAgentAsync(string agentId, CancellationToken cancellationToken = default) + public override async IAsyncEnumerable InvokeAgentAsync( + string agentId, + string? agentVersion, + string? conversationId, + IEnumerable? messages, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - ChatClientAgent agent = - await this.GetAgentsClient().GetAIAgentAsync( - agentId, - new ChatOptions() - { - AllowMultipleToolCalls = this.AllowMultipleToolCalls, - }, - clientFactory: null, - cancellationToken).ConfigureAwait(false); + AgentVersion agentDefinition = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false); + AIAgent agent = await this.GetAgentAsync(agentDefinition, cancellationToken).ConfigureAwait(false); + + ChatOptions chatOptions = + new() + { + ConversationId = conversationId, + AllowMultipleToolCalls = this.AllowMultipleToolCalls, + }; + + ChatClientAgentRunOptions runOptions = new(chatOptions); + + IAsyncEnumerable agentResponse = + messages is not null ? + agent.RunStreamingAsync([.. messages], null, runOptions, cancellationToken) : + agent.RunStreamingAsync([new ChatMessage(ChatRole.User, string.Empty)], null, runOptions, cancellationToken); + + await foreach (AgentRunResponseUpdate update in agentResponse.ConfigureAwait(false)) + { + update.AuthorName = agentDefinition.Name; + yield return update; + } + } + + private async Task QueryAgentAsync(string agentName, string? agentVersion, CancellationToken cancellationToken = default) + { + string agentKey = $"{agentName}:{agentVersion}"; + if (this._versionCache.TryGetValue(agentKey, out AgentVersion? targetAgent)) + { + return targetAgent; + } + + AgentsClient client = this.GetAgentsClient(); + + if (string.IsNullOrEmpty(agentVersion)) + { + AgentRecord agentRecord = + await client.GetAgentAsync( + agentName, + cancellationToken).ConfigureAwait(false); + + targetAgent = agentRecord.Versions.Latest; + } + else + { + targetAgent = + await client.GetAgentVersionAsync( + agentName, + agentVersion, + cancellationToken).ConfigureAwait(false); + } + + this._versionCache[agentKey] = targetAgent; + + return targetAgent; + } + + private async Task GetAgentAsync(AgentVersion agentDefinition, CancellationToken cancellationToken = default) + { + if (this._agentCache.TryGetValue(agentDefinition.Id, out AIAgent? agent)) + { + return agent; + } + + AgentsClient client = this.GetAgentsClient(); + + IList? tools = null; + if (agentDefinition.Definition is PromptAgentDefinition promptAgent) + { + tools = + promptAgent.Tools + .Select(tool => tool.AsAITool()) + .ToArray(); + } + + agent = client.GetAIAgent(agentDefinition, tools, clientFactory: null, openAIClientOptions: null, requireInvocableTools: false, cancellationToken); FunctionInvokingChatClient? functionInvokingClient = agent.GetService(); if (functionInvokingClient is not null) @@ -127,14 +178,17 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p } } + this._agentCache[agentDefinition.Id] = agent; + return agent; } /// public override async Task GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default) { - PersistentThreadMessage message = await this.GetAgentsClient().Messages.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false); - return ToChatMessage(message); + AgentResponseItem responseItem = await this.GetConversationClient().GetConversationItemAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false); + ResponseItem[] items = [responseItem.AsOpenAIResponseItem()]; + return items.AsChatMessages().Single(); } /// @@ -146,25 +200,29 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p bool newestFirst = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - ListSortOrder order = newestFirst ? ListSortOrder.Ascending : ListSortOrder.Descending; - await foreach (PersistentThreadMessage message in this.GetAgentsClient().Messages.GetMessagesAsync(conversationId, runId: null, limit, order, after, before, cancellationToken).ConfigureAwait(false)) + AgentsListOrder order = newestFirst ? AgentsListOrder.Asc : AgentsListOrder.Desc; + await foreach (AgentResponseItem responseItem in this.GetConversationClient().GetConversationItemsAsync(conversationId, limit, order, after, before, itemType: null, cancellationToken).ConfigureAwait(false)) { - yield return ToChatMessage(message); + ResponseItem[] items = [responseItem.AsOpenAIResponseItem()]; + foreach (ChatMessage message in items.AsChatMessages()) + { + yield return message; + } } } - private PersistentAgentsClient GetAgentsClient() + private AgentsClient GetAgentsClient() { if (this._agentsClient is null) { - PersistentAgentsAdministrationClientOptions clientOptions = new(); + AgentsClientOptions clientOptions = new(); if (httpClient is not null) { - clientOptions.Transport = new HttpClientTransport(httpClient); + clientOptions.Transport = new HttpClientPipelineTransport(httpClient); } - PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions); + AgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions); Interlocked.CompareExchange(ref this._agentsClient, newClient, null); } @@ -172,43 +230,15 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p return this._agentsClient; } - private static ChatMessage ToChatMessage(PersistentThreadMessage message) + private ConversationClient GetConversationClient() { - return - new ChatMessage(new ChatRole(message.Role.ToString()), [.. GetContent()]) - { - MessageId = message.Id, - CreatedAt = message.CreatedAt, - AdditionalProperties = GetMetadata() - }; - - IEnumerable GetContent() + if (this._conversationClient is null) { - foreach (MessageContent contentItem in message.ContentItems) - { - AIContent? content = - contentItem switch - { - MessageTextContent textContent => new TextContent(textContent.Text), - MessageImageFileContent imageContent => new HostedFileContent(imageContent.FileId), - _ => null // Unsupported content type - }; + ConversationClient conversationClient = this.GetAgentsClient().GetConversationClient(); - if (content is not null) - { - yield return content; - } - } + Interlocked.CompareExchange(ref this._conversationClient, conversationClient, null); } - AdditionalPropertiesDictionary? GetMetadata() - { - if (message.Metadata is null) - { - return null; - } - - return new AdditionalPropertiesDictionary(message.Metadata.Select(m => new KeyValuePair(m.Key, m.Value))); - } + return this._conversationClient; } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.cs index 2fe387a5f6..3704d38b21 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.cs @@ -61,7 +61,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true); EvaluateBoolExpression(this.Model.Output?.AutoSend, "autoSend", defaultValue: true); - EvaluateMessageTemplate(this.Model.Input?.AdditionalInstructions, "additionalInstructions"); EvaluateListExpression(this.Model.Input?.Messages, "inputMessages"); this.Write(@" @@ -71,7 +70,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen agentName, conversationId, autoSend, - additionalInstructions, inputMessages, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.tt index b46e88588a..48c4acb859 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.tt @@ -19,7 +19,6 @@ internal sealed class <#= this.Name #>Executor(FormulaSession session, WorkflowA <# EvaluateStringExpression(this.Model.ConversationId, "conversationId", isNullable: true); EvaluateBoolExpression(this.Model.Output?.AutoSend, "autoSend", defaultValue: true); - EvaluateMessageTemplate(this.Model.Input?.AdditionalInstructions, "additionalInstructions"); EvaluateListExpression(this.Model.Input?.Messages, "inputMessages");#> AgentRunResponse agentResponse = @@ -28,7 +27,6 @@ internal sealed class <#= this.Name #>Executor(FormulaSession session, WorkflowA agentName, conversationId, autoSend, - additionalInstructions, inputMessages, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolRequest.cs deleted file mode 100644 index 5f9f878e79..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolRequest.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Workflows.Declarative.Events; - -/// -/// Represents one or more function tool requests. -/// -public sealed class AgentFunctionToolRequest -{ - /// - /// The name of the agent associated with the tool request. - /// - public string AgentName { get; } - - /// - /// A list of function tool requests. - /// - public IList FunctionCalls { get; } - - [JsonConstructor] - internal AgentFunctionToolRequest(string agentName, IList functionCalls) - { - this.AgentName = agentName; - this.FunctionCalls = functionCalls; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolResponse.cs deleted file mode 100644 index e03414bd2e..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolResponse.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Workflows.Declarative.Events; - -/// -/// Represents one or more function tool responses. -/// -public sealed class AgentFunctionToolResponse -{ - /// - /// The name of the agent associated with the tool response. - /// - public string AgentName { get; } - - /// - /// A list of tool responses. - /// - public IList FunctionResults { get; } - - [JsonConstructor] - internal AgentFunctionToolResponse(string agentName, IList functionResults) - { - this.AgentName = agentName; - this.FunctionResults = functionResults; - } - - /// - /// Factory method to create an from an - /// Ensures that all function calls in the request have a corresponding result. - /// - /// The tool request. - /// One or more function results - /// An that can be provided to the workflow. - /// Not all have a corresponding . - public static AgentFunctionToolResponse Create(AgentFunctionToolRequest toolRequest, params IEnumerable functionResults) - { - HashSet callIds = [.. toolRequest.FunctionCalls.Select(call => call.CallId)]; - HashSet resultIds = [.. functionResults.Select(call => call.CallId)]; - - if (!callIds.SetEquals(resultIds)) - { - throw new DeclarativeActionException($"Missing results for: {string.Join(",", callIds.Except(resultIds))}"); - } - - return new AgentFunctionToolResponse(toolRequest.AgentName, [.. functionResults]); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerRequest.cs deleted file mode 100644 index 845696a180..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerRequest.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Workflows.Declarative.Events; - -/// -/// Represents a request for user input in response to a `Question` action. -/// -public sealed class AnswerRequest -{ - /// - /// An optional prompt for the user. - /// - /// - /// This prompt is utilized for the "Question" action type in the Declarative Workflow, - /// but is redundant when the user is responding to an agent since the agent's message - /// is the implicit prompt. - /// - public string? Prompt { get; } - - [JsonConstructor] - internal AnswerRequest(string? prompt = null) - { - this.Prompt = prompt; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerResponse.cs deleted file mode 100644 index 00903f43f0..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerResponse.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Workflows.Declarative.Events; - -/// -/// Represents a user input response. -/// -public sealed class AnswerResponse -{ - /// - /// The response value. - /// - public ChatMessage Value { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The response value. - [JsonConstructor] - public AnswerResponse(ChatMessage value) - { - this.Value = value; - } - - /// - /// Initializes a new instance of the class. - /// - /// The response value. - public AnswerResponse(string value) - { - this.Value = new ChatMessage(ChatRole.User, value); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/ExternalInputRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/ExternalInputRequest.cs new file mode 100644 index 0000000000..8caf374b70 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/ExternalInputRequest.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.Declarative.Events; + +/// +/// Represents a request for external input. +/// +public sealed class ExternalInputRequest +{ + /// + /// The source message that triggered the request for external input. + /// + public AgentRunResponse AgentResponse { get; } + + [JsonConstructor] + internal ExternalInputRequest(AgentRunResponse agentResponse) + { + this.AgentResponse = agentResponse; + } + + internal ExternalInputRequest(ChatMessage message) + { + this.AgentResponse = new AgentRunResponse(message); + } + + internal ExternalInputRequest(string text) + { + this.AgentResponse = new AgentRunResponse(new ChatMessage(ChatRole.User, text)); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/ExternalInputResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/ExternalInputResponse.cs new file mode 100644 index 0000000000..0653a12ce5 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/ExternalInputResponse.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.Declarative.Events; + +/// +/// Represents the response to a . +/// +public sealed class ExternalInputResponse +{ + /// + /// The message being provided as external input to the workflow. + /// + public IList Messages { get; } + + internal bool HasMessages => this.Messages?.Count > 0; + + /// + /// Initializes a new instance of . + /// + /// The external input message being provided to the workflow. + public ExternalInputResponse(ChatMessage message) + { + this.Messages = [message]; + } + + /// + /// Initializes a new instance of . + /// + /// The external input messages being provided to the workflow. + [JsonConstructor] + public ExternalInputResponse(IList messages) + { + this.Messages = messages; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputRequest.cs deleted file mode 100644 index 1426025d74..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputRequest.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Workflows.Declarative.Events; - -/// -/// Represents one or more user-input requests. -/// -public sealed class UserInputRequest -{ - /// - /// The name of the agent associated with the tool request. - /// - public string AgentName { get; } - - /// - /// A list of user input requests. - /// - public IList InputRequests { get; } - - [JsonConstructor] - internal UserInputRequest(string agentName, IList inputRequests) - { - this.AgentName = agentName; - this.InputRequests = inputRequests; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputResponse.cs deleted file mode 100644 index edb9f3b7cc..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputResponse.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Workflows.Declarative.Events; - -/// -/// Represents one or more user-input responses. -/// -public sealed class UserInputResponse -{ - /// - /// The name of the agent associated with the tool request. - /// - public string AgentName { get; } - - /// - /// A list of approval responses. - /// - public IList InputResponses { get; } - - [JsonConstructor] - internal UserInputResponse(string agentName, IList inputResponses) - { - this.AgentName = agentName; - this.InputResponses = inputResponses; - } - - /// - /// Factory method to create an from a - /// Ensures that all requests have a corresponding result. - /// - /// The input request. - /// One or more responses - /// An that can be provided to the workflow. - /// Not all have a corresponding . - public static UserInputResponse Create(UserInputRequest inputRequest, params IEnumerable inputResponses) - { - HashSet callIds = [.. inputRequest.InputRequests.OfType().Select(call => call.Id)]; - HashSet resultIds = [.. inputResponses.Select(call => call.Id)]; - - if (!callIds.SetEquals(resultIds)) - { - throw new DeclarativeActionException($"Missing responses for: {string.Join(",", callIds.Except(resultIds))}"); - } - - return new UserInputResponse(inputRequest.AgentName, [.. inputResponses]); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/AgentProviderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/AgentProviderExtensions.cs index 5b6bbbc297..170d5a52a9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/AgentProviderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/AgentProviderExtensions.cs @@ -1,24 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Azure.AI.Agents.Persistent; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions; internal static class AgentProviderExtensions { - private static readonly HashSet s_failureStatus = - [ - Azure.AI.Agents.Persistent.RunStatus.Failed, - Azure.AI.Agents.Persistent.RunStatus.Cancelled, - Azure.AI.Agents.Persistent.RunStatus.Cancelling, - Azure.AI.Agents.Persistent.RunStatus.Expired, - ]; - public static async ValueTask InvokeAgentAsync( this WorkflowAgentProvider agentProvider, string executorId, @@ -26,27 +16,10 @@ internal static class AgentProviderExtensions string agentName, string? conversationId, bool autoSend, - string? additionalInstructions = null, IEnumerable? inputMessages = null, CancellationToken cancellationToken = default) { - // Get the specified agent. - AIAgent agent = await agentProvider.GetAgentAsync(agentName, cancellationToken).ConfigureAwait(false); - - // Prepare the run options. - ChatClientAgentRunOptions options = - new( - new ChatOptions() - { - ConversationId = conversationId, - Instructions = additionalInstructions, - }); - - // Initialize the agent thread. - IAsyncEnumerable agentUpdates = - inputMessages is not null ? - agent.RunStreamingAsync([.. inputMessages], null, options, cancellationToken) : - agent.RunStreamingAsync(null, options, cancellationToken); + IAsyncEnumerable agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, cancellationToken); // Enable "autoSend" behavior if this is the workflow conversation. bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? workflowConversationId); @@ -60,13 +33,6 @@ internal static class AgentProviderExtensions updates.Add(update); - if (update.RawRepresentation is ChatResponseUpdate chatUpdate && - chatUpdate.RawRepresentation is RunUpdate runUpdate && - s_failureStatus.Contains(runUpdate.Value.Status)) - { - throw new DeclarativeActionException($"Unexpected failure invoking agent, run {runUpdate.Value.Status}: {agent.Name ?? agent.Id} [{runUpdate.Value.Id}/{conversationId}]"); - } - if (autoSend) { await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update), cancellationToken).ConfigureAwait(false); @@ -80,16 +46,10 @@ internal static class AgentProviderExtensions await context.AddEventAsync(new AgentRunResponseEvent(executorId, response), cancellationToken).ConfigureAwait(false); } + // If autoSend is enabled and this is not the workflow conversation, copy messages to the workflow conversation. if (autoSend && !isWorkflowConversation && workflowConversationId is not null) { - // Copy messages with content that aren't function calls or results. - IEnumerable messages = - response.Messages.Where( - message => - !string.IsNullOrEmpty(message.Text) && - !message.Contents.OfType().Any() && - !message.Contents.OfType().Any()); - foreach (ChatMessage message in messages) + foreach (ChatMessage message in response.Messages) { await agentProvider.CreateMessageAsync(workflowConversationId, message, cancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/JsonDocumentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/JsonDocumentExtensions.cs index d3a4ef9cbc..9d3c5e335d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/JsonDocumentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/JsonDocumentExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -43,16 +44,28 @@ internal static class JsonDocumentExtensions private static Dictionary ParseRecord(this JsonElement currentElement, VariableType targetType) { - if (targetType.Schema is null) - { - throw new DeclarativeActionException($"Object schema not defined for. {targetType.Type.Name}."); - } + IEnumerable> keyValuePairs = + targetType.Schema is null ? + ParseValues() : + ParseSchema(targetType.Schema); - return ParseValues().ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + return keyValuePairs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); IEnumerable> ParseValues() { - foreach (KeyValuePair property in targetType.Schema) + foreach (JsonProperty objectProperty in currentElement.EnumerateObject()) + { + if (!objectProperty.Value.TryParseValue(targetType: null, out object? parsedValue)) + { + throw new DeclarativeActionException($"Unsupported data type '{objectProperty.Value.ValueKind}' for property '{objectProperty.Name}'"); + } + yield return new KeyValuePair(objectProperty.Name, parsedValue); + } + } + + IEnumerable> ParseSchema(FrozenDictionary schema) + { + foreach (KeyValuePair property in schema) { object? parsedValue = null; if (!currentElement.TryGetProperty(property.Key, out JsonElement propertyElement)) @@ -131,22 +144,22 @@ internal static class JsonDocumentExtensions return value; } - private static bool TryParseValue(this JsonElement propertyElement, VariableType targetType, out object? value) => + private static bool TryParseValue(this JsonElement propertyElement, VariableType? targetType, out object? value) => propertyElement.ValueKind switch { - JsonValueKind.String => TryParseString(propertyElement, targetType.Type, out value), - JsonValueKind.Number => TryParseNumber(propertyElement, targetType.Type, out value), + JsonValueKind.String => TryParseString(propertyElement, targetType?.Type, out value), + JsonValueKind.Number => TryParseNumber(propertyElement, targetType?.Type, out value), JsonValueKind.True or JsonValueKind.False => TryParseBoolean(propertyElement, out value), JsonValueKind.Object => TryParseObject(propertyElement, targetType, out value), JsonValueKind.Array => TryParseList(propertyElement, targetType, out value), - JsonValueKind.Null => TryParseNull(targetType.Type, out value), + JsonValueKind.Null => TryParseNull(targetType?.Type, out value), _ => throw new DeclarativeActionException($"JSON element of type {propertyElement.ValueKind} is not supported."), }; - private static bool TryParseNull(Type valueType, out object? value) + private static bool TryParseNull(Type? valueType, out object? value) { // If the target type is not nullable, we cannot assign null to it - if (!valueType.IsNullable()) + if (valueType?.IsNullable() == false) { value = null; return false; @@ -170,7 +183,7 @@ internal static class JsonDocumentExtensions } } - private static bool TryParseString(JsonElement propertyElement, Type valueType, out object? value) + private static bool TryParseString(JsonElement propertyElement, Type? valueType, out object? value) { try { @@ -178,23 +191,30 @@ internal static class JsonDocumentExtensions if (propertyValue is null) { value = null; - return valueType.IsNullable(); // Parse fails if value is null and requested type is not. + return valueType?.IsNullable() ?? false; // Parse fails if value is null and requested type is not. } - switch (valueType) + if (valueType is null) { - case Type targetType when targetType == typeof(string): - value = propertyValue; - break; - case Type targetType when targetType == typeof(DateTime): - value = DateTime.Parse(propertyValue, provider: null, styles: DateTimeStyles.RoundtripKind); - break; - case Type targetType when targetType == typeof(TimeSpan): - value = TimeSpan.Parse(propertyValue); - break; - default: - value = null; - return false; + value = propertyValue; + } + else + { + switch (valueType) + { + case Type targetType when targetType == typeof(string): + value = propertyValue; + break; + case Type targetType when targetType == typeof(DateTime): + value = DateTime.Parse(propertyValue, provider: null, styles: DateTimeStyles.RoundtripKind); + break; + case Type targetType when targetType == typeof(TimeSpan): + value = TimeSpan.Parse(propertyValue); + break; + default: + value = null; + return false; + } } return true; @@ -206,7 +226,7 @@ internal static class JsonDocumentExtensions } } - private static bool TryParseNumber(JsonElement element, Type valueType, out object? value) + private static bool TryParseNumber(JsonElement element, Type? valueType, out object? value) { // Try parsing as integer types first (most precise representation) if (element.TryGetInt32(out int intValue)) @@ -234,8 +254,14 @@ internal static class JsonDocumentExtensions value = null; return false; - static bool ConvertToExpectedType(Type valueType, object sourceValue, out object? value) + static bool ConvertToExpectedType(Type? valueType, object sourceValue, out object? value) { + if (valueType is null) + { + value = sourceValue; + return true; + } + try { value = Convert.ChangeType(sourceValue, valueType); @@ -249,23 +275,17 @@ internal static class JsonDocumentExtensions } } - private static bool TryParseObject(JsonElement propertyElement, VariableType targetType, out object? value) + private static bool TryParseObject(JsonElement propertyElement, VariableType? targetType, out object? value) { - if (!targetType.HasSchema) - { - value = null; - return false; - } - - value = propertyElement.ParseRecord(targetType); + value = propertyElement.ParseRecord(targetType ?? VariableType.RecordType); return true; } - private static bool TryParseList(JsonElement propertyElement, VariableType targetType, out object? value) + private static bool TryParseList(JsonElement propertyElement, VariableType? targetType, out object? value) { try { - value = ParseTable(propertyElement, targetType); + value = ParseTable(propertyElement, targetType ?? VariableType.ListType); return true; } catch diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs index 60e50e6abe..9a8d9da707 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs @@ -77,7 +77,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext this.State.Bind(); } - private bool IsManagedScope(string? scopeName) => scopeName is not null && VariableScopeNames.IsValidName(scopeName); + private static bool IsManagedScope(string? scopeName) => scopeName is not null && VariableScopeNames.IsValidName(scopeName); /// public async ValueTask ReadStateAsync(string key, string? scopeName = null, CancellationToken cancellationToken = default) @@ -86,7 +86,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext { // Not a managed scope, just pass through. This is valid when a declarative // workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized). - _ when !this.IsManagedScope(scopeName) => await this.Source.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false), + _ when !IsManagedScope(scopeName) => await this.Source.ReadStateAsync(key, scopeName, cancellationToken).ConfigureAwait(false), // Retrieve formula values directly from the managed state to avoid conversion. _ when typeof(TValue) == typeof(FormulaValue) => (TValue?)(object?)this.State.Get(key, scopeName), // Retrieve native types from the source context to avoid conversion. @@ -100,7 +100,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext { // Not a managed scope, just pass through. This is valid when a declarative // workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized). - _ when !this.IsManagedScope(scopeName) => await this.Source.ReadOrInitStateAsync(key, initialStateFactory, scopeName, cancellationToken).ConfigureAwait(false), + _ when !IsManagedScope(scopeName) => await this.Source.ReadOrInitStateAsync(key, initialStateFactory, scopeName, cancellationToken).ConfigureAwait(false), // Retrieve formula values directly from the managed state to avoid conversion. _ when typeof(TValue) == typeof(FormulaValue) => await EnsureFormulaValueAsync().ConfigureAwait(false), // Retrieve native types from the source context to avoid conversion. diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs index de90787cfd..715dfcd5b3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs @@ -239,6 +239,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor // Entry point for question QuestionExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState); this.ContinueWith(action); + // Transition to post action if complete string postId = Steps.Post(action.Id); this._workflowModel.AddLink(action.Id, postId, QuestionExecutor.IsComplete); @@ -249,13 +250,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor // Define input action string inputId = QuestionExecutor.Steps.Input(action.Id); - RequestPortAction inputPort = new(RequestPort.Create(inputId)); + RequestPortAction inputPort = new(RequestPort.Create(inputId)); this._workflowModel.AddNode(inputPort, action.ParentId); this._workflowModel.AddLinkFromPeer(action.ParentId, inputId); // Capture input response string captureId = QuestionExecutor.Steps.Capture(action.Id); - this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId); + this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId); // Transition to post action if complete this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId, QuestionExecutor.IsComplete); @@ -263,6 +264,24 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this._workflowModel.AddLink(captureId, prepareId, message => !QuestionExecutor.IsComplete(message)); } + protected override void Visit(RequestExternalInput item) + { + this.Trace(item); + + RequestExternalInputExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState); + this.ContinueWith(action); + + // Define input action + string inputId = RequestExternalInputExecutor.Steps.Input(action.Id); + RequestPortAction inputPort = new(RequestPort.Create(inputId)); + this._workflowModel.AddNode(inputPort, action.ParentId); + this._workflowModel.AddLinkFromPeer(action.ParentId, inputId); + + // Capture input response + string captureId = RequestExternalInputExecutor.Steps.Capture(action.Id); + this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync), action.ParentId); + } + protected override void Visit(EndDialog item) { this.Trace(item); @@ -285,6 +304,28 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this.RestartAfter(action.Id, action.ParentId); } + protected override void Visit(CancelAllDialogs item) + { + this.Trace(item); + + // Represent action with default executor + DefaultActionExecutor action = new(item, this._workflowState); + this.ContinueWith(action); + // Define a clean-start to ensure "end" is not a source for any edge + this.RestartAfter(item.Id.Value, action.ParentId); + } + + protected override void Visit(CancelDialog item) + { + this.Trace(item); + + // Represent action with default executor + DefaultActionExecutor action = new(item, this._workflowState); + this.ContinueWith(action); + // Define a clean-start to ensure "end" is not a source for any edge + this.RestartAfter(action.Id, action.ParentId); + } + protected override void Visit(CreateConversation item) { this.Trace(item); @@ -318,33 +359,29 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this._workflowModel.AddLink(action.Id, postId, InvokeAzureAgentExecutor.RequiresNothing); // Define request-port for function calling action - string functionCallingPortId = InvokeAzureAgentExecutor.Steps.FunctionTool(action.Id); - RequestPortAction functionCallingPort = new(RequestPort.Create(functionCallingPortId)); - this._workflowModel.AddNode(functionCallingPort, action.ParentId); - this._workflowModel.AddLink(action.Id, functionCallingPort.Id, InvokeAzureAgentExecutor.RequiresFunctionCall); - - // Define request-port for user input, such as: mcp tool & function tool approval - string userInputPortId = InvokeAzureAgentExecutor.Steps.UserInput(action.Id); - RequestPortAction userInputPort = new(RequestPort.Create(userInputPortId)); - this._workflowModel.AddNode(userInputPort, action.ParentId); - this._workflowModel.AddLink(action.Id, userInputPortId, InvokeAzureAgentExecutor.RequiresUserInput); + string externalInputPortId = InvokeAzureAgentExecutor.Steps.ExternalInput(action.Id); + RequestPortAction externalInputPort = new(RequestPort.Create(externalInputPortId)); + this._workflowModel.AddNode(externalInputPort, action.ParentId); + this._workflowModel.AddLink(action.Id, externalInputPortId, InvokeAzureAgentExecutor.RequiresInput); // Request ports always transitions to resume string resumeId = InvokeAzureAgentExecutor.Steps.Resume(action.Id); - this._workflowModel.AddNode(new DelegateActionExecutor(resumeId, this._workflowState, action.ResumeAsync), action.ParentId); - this._workflowModel.AddLink(functionCallingPortId, resumeId); - this._workflowModel.AddLink(userInputPortId, resumeId); - // Transition to appropriate request port if more function calling is requested - this._workflowModel.AddLink(resumeId, functionCallingPortId, InvokeAzureAgentExecutor.RequiresFunctionCall); - // Transition to appropriate request port if more user input is requested - this._workflowModel.AddLink(resumeId, userInputPortId, InvokeAzureAgentExecutor.RequiresUserInput); + this._workflowModel.AddNode(new DelegateActionExecutor(resumeId, this._workflowState, action.ResumeAsync, emitResult: false), action.ParentId); + this._workflowModel.AddLink(externalInputPortId, resumeId); // Transition to post action if complete this._workflowModel.AddLink(resumeId, postId, InvokeAzureAgentExecutor.RequiresNothing); + // Transition to request port if more input is required + this._workflowModel.AddLink(resumeId, externalInputPortId, InvokeAzureAgentExecutor.RequiresInput); // Define post action this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId); } + protected override void Visit(InvokeAzureResponse item) + { + this.NotSupported(item); + } + protected override void Visit(RetrieveConversationMessage item) { this.Trace(item); @@ -462,10 +499,6 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor protected override void Visit(ReplaceDialog item) => this.NotSupported(item); - protected override void Visit(CancelAllDialogs item) => this.NotSupported(item); - - protected override void Visit(CancelDialog item) => this.NotSupported(item); - protected override void Visit(EmitEvent item) => this.NotSupported(item); protected override void Visit(GetConversationMembers item) => this.NotSupported(item); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs index 4822a70024..5d6a7b3384 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs @@ -210,9 +210,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor protected override void Visit(Question item) { this.NotSupported(item); - //this.Trace(item); + } - //this.ContinueWith(new QuestionTemplate(item)); + protected override void Visit(RequestExternalInput item) + { + this.NotSupported(item); } protected override void Visit(EndDialog item) @@ -237,6 +239,24 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor this.RestartAfter(action.Id, action.ParentId); } + protected override void Visit(CancelAllDialogs item) + { + // Represent action with default executor + DefaultTemplate action = new(item, this._rootId); + this.ContinueWith(action); + // Define a clean-start to ensure "end" is not a source for any edge + this.RestartAfter(action.Id, action.ParentId); + } + + protected override void Visit(CancelDialog item) + { + // Represent action with default executor + DefaultTemplate action = new(item, this._rootId); + this.ContinueWith(action); + // Define a clean-start to ensure "end" is not a source for any edge + this.RestartAfter(action.Id, action.ParentId); + } + protected override void Visit(CreateConversation item) { this.Trace(item); @@ -265,6 +285,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor this.ContinueWith(new InvokeAzureAgentTemplate(item)); } + protected override void Visit(InvokeAzureResponse item) + { + this.NotSupported(item); + } + protected override void Visit(RetrieveConversationMessage item) { this.Trace(item); @@ -317,17 +342,11 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor protected override void Visit(EditTable item) { this.NotSupported(item); - //this.Trace(item); - - //this.ContinueWith(new EditTableTemplate(item)); } protected override void Visit(EditTableV2 item) { this.NotSupported(item); - //this.Trace(item); - - //this.ContinueWith(new EditTableV2Template(item)); } protected override void Visit(ParseValue item) @@ -384,10 +403,6 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor protected override void Visit(ReplaceDialog item) => this.NotSupported(item); - protected override void Visit(CancelAllDialogs item) => this.NotSupported(item); - - protected override void Visit(CancelDialog item) => this.NotSupported(item); - protected override void Visit(EmitEvent item) => this.NotSupported(item); protected override void Visit(GetConversationMembers item) => this.NotSupported(item); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/AgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/AgentExecutor.cs index 8189e9b8aa..9e6a6884b6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/AgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/AgentExecutor.cs @@ -23,7 +23,6 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA /// The name or identifier of the agent. /// The identifier of the conversation. /// Send the agent's response as workflow output. (default: true). - /// Optional additional instructions to the agent. /// Optional messages to add to the conversation prior to invocation. /// A token that can be used to observe cancellation. /// @@ -32,8 +31,7 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA string agentName, string? conversationId, bool autoSend, - string? additionalInstructions = null, IEnumerable? inputMessages = null, CancellationToken cancellationToken = default) - => agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, inputMessages, cancellationToken); + => agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, cancellationToken); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj index 7db0b0d941..17ce9f6c4f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj @@ -4,7 +4,7 @@ $(ProjectsTargetFrameworks) $(ProjectsDebugTargetFrameworks) preview - $(NoWarn);MEAI001 + $(NoWarn);MEAI001;OPENAI001 @@ -34,8 +34,8 @@ + - diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs index 8c4c9eee61..cafece9a57 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.Events; @@ -21,14 +22,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA { public static class Steps { - public static string UserInput(string id) => $"{id}_{nameof(UserInput)}"; - public static string FunctionTool(string id) => $"{id}_{nameof(FunctionTool)}"; + public static string ExternalInput(string id) => $"{id}_{nameof(ExternalInput)}"; public static string Resume(string id) => $"{id}_{nameof(Resume)}"; } - public static bool RequiresFunctionCall(object? message) => message is AgentFunctionToolRequest; - - public static bool RequiresUserInput(object? message) => message is UserInputRequest; + public static bool RequiresInput(object? message) => message is ExternalInputRequest; public static bool RequiresNothing(object? message) => message is ActionExecutorResult; @@ -46,8 +44,11 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA return default; } - public ValueTask ResumeAsync(IWorkflowContext context, AgentFunctionToolResponse message, CancellationToken cancellationToken) => - this.InvokeAgentAsync(context, [message.FunctionResults.ToChatMessage()], cancellationToken); + public async ValueTask ResumeAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken) + { + await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false); + await this.InvokeAgentAsync(context, response.Messages, cancellationToken).ConfigureAwait(false); + } public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken) { @@ -58,40 +59,52 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA { string? conversationId = this.GetConversationId(); string agentName = this.GetAgentName(); - string? additionalInstructions = this.GetAdditionalInstructions(); bool autoSend = this.GetAutoSendValue(); - bool isComplete = true; + AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, cancellationToken).ConfigureAwait(false); - AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, additionalInstructions, messages, cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(agentResponse.Text)) + ChatMessage[] actionableMessages = FilterActionableContent(agentResponse).ToArray(); + if (actionableMessages.Length > 0) { - // Identify function calls that have no associated result. - List inputRequests = GetUserInputRequests(agentResponse); - if (inputRequests.Count > 0) - { - isComplete = false; - UserInputRequest approvalRequest = new(agentName, inputRequests.OfType().ToArray()); - await context.SendMessageAsync(approvalRequest, cancellationToken).ConfigureAwait(false); - } - - // Identify function calls that have no associated result. - List functionCalls = GetOrphanedFunctionCalls(agentResponse); - if (functionCalls.Count > 0) - { - isComplete = false; - AgentFunctionToolRequest toolRequest = new(agentName, functionCalls); - await context.SendMessageAsync(toolRequest, cancellationToken).ConfigureAwait(false); - } - } - - if (isComplete) - { - await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false); + AgentRunResponse filteredResponse = + new(actionableMessages) + { + AdditionalProperties = agentResponse.AdditionalProperties, + AgentId = agentResponse.AgentId, + CreatedAt = agentResponse.CreatedAt, + ResponseId = agentResponse.ResponseId, + Usage = agentResponse.Usage, + }; + await context.SendMessageAsync(new ExternalInputRequest(filteredResponse), cancellationToken).ConfigureAwait(false); + return; } await this.AssignAsync(this.AgentOutput?.Messages?.Path, agentResponse.Messages.ToTable(), context).ConfigureAwait(false); + + // Attempt to parse the last message as JSON and assign to the response object variable. + try + { + JsonDocument jsonDocument = JsonDocument.Parse(agentResponse.Messages.Last().Text); + Dictionary objectProperties = jsonDocument.ParseRecord(VariableType.RecordType); + await this.AssignAsync(this.AgentOutput?.ResponseObject?.Path, objectProperties.ToFormula(), context).ConfigureAwait(false); + } + catch + { + // Not valid json, skip assignment. + } + + if (this.Model.Input?.ExternalLoop?.When is not null) + { + bool requestInput = this.Evaluator.GetValue(this.Model.Input.ExternalLoop.When).Value; + if (requestInput) + { + ExternalInputRequest inputRequest = new(agentResponse); + await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false); + return; + } + } + + await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false); } private IEnumerable? GetInputMessages() @@ -107,7 +120,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA return userInput?.ToChatMessages(); } - private static List GetOrphanedFunctionCalls(AgentRunResponse agentResponse) + private static IEnumerable FilterActionableContent(AgentRunResponse agentResponse) { HashSet functionResultIds = [.. agentResponse.Messages @@ -117,21 +130,21 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA .OfType() .Select(functionCall => functionCall.CallId))]; - List functionCalls = []; - foreach (FunctionCallContent functionCall in agentResponse.Messages.SelectMany(m => m.Contents.OfType())) + foreach (ChatMessage responseMessage in agentResponse.Messages) { - if (!functionResultIds.Contains(functionCall.CallId)) + if (responseMessage.Contents.Any(content => content is UserInputRequestContent)) { - functionCalls.Add(functionCall); + yield return responseMessage; + continue; + } + + if (responseMessage.Contents.OfType().Any(functionCall => !functionResultIds.Contains(functionCall.CallId))) + { + yield return responseMessage; } } - - return functionCalls; } - private static List GetUserInputRequests(AgentRunResponse agentResponse) => - agentResponse.Messages.SelectMany(m => m.Contents.OfType()).ToList(); - private string? GetConversationId() { if (this.Model.ConversationId is null) @@ -149,18 +162,6 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA this.AgentUsage.Name, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}.{nameof(this.Model.Agent.Name)}")).Value; - private string? GetAdditionalInstructions() - { - string? additionalInstructions = null; - - if (this.AgentInput?.AdditionalInstructions is not null) - { - additionalInstructions = this.Engine.Format(this.AgentInput.AdditionalInstructions); - } - - return additionalInstructions; - } - private bool GetAutoSendValue() { if (this.AgentOutput?.AutoSend is null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs index 145567f2ce..40dc5ce31e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.Entities; @@ -75,22 +76,22 @@ internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider age public async ValueTask PrepareResponseAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken) { int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false); - AnswerRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt)); + ExternalInputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt)); await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false); await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false); } - public async ValueTask CaptureResponseAsync(IWorkflowContext context, AnswerResponse message, CancellationToken cancellationToken) + public async ValueTask CaptureResponseAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken) { FormulaValue? extractedValue = null; - if (message.Value is null) + if (!response.HasMessages) { string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt); await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim()), cancellationToken).ConfigureAwait(false); } else { - EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, message.Value.Text); + EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, string.Concat(response.Messages.Select(message => message.Text))); if (entityResult.IsValid) { extractedValue = entityResult.Value; @@ -121,7 +122,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider age if (workflowConversationId is not null) { // Input message always defined if values has been extracted. - ChatMessage input = message.Value!; + ChatMessage input = response.Messages.Last(); await agentProvider.CreateMessageAsync(workflowConversationId, input, cancellationToken).ConfigureAwait(false); await context.SetLastMessageAsync(input).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RequestExternalInputExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RequestExternalInputExecutor.cs new file mode 100644 index 0000000000..2c35dc18e9 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RequestExternalInputExecutor.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Agents.AI.Workflows.Declarative.Extensions; +using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; +using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; + +internal sealed class RequestExternalInputExecutor(RequestExternalInput model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) + : DeclarativeActionExecutor(model, state) +{ + public static class Steps + { + public static string Input(string id) => $"{id}_{nameof(Input)}"; + public static string Capture(string id) => $"{id}_{nameof(Capture)}"; + } + + protected override bool IsDiscreteAction => false; + protected override bool EmitResultEvent => false; + + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + ExternalInputRequest inputRequest = new(new AgentRunResponse()); + + await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false); + + return default; + } + + public async ValueTask CaptureResponseAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken) + { + string? workflowConversationId = context.GetWorkflowConversation(); + if (workflowConversationId is not null) + { + foreach (ChatMessage inputMessage in response.Messages) + { + await agentProvider.CreateMessageAsync(workflowConversationId, inputMessage, cancellationToken).ConfigureAwait(false); + } + } + await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false); + await this.AssignAsync(this.Model.Variable?.Path, response.Messages.ToFormula(), context).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/README.md b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/README.md index 3a8fa6031d..4202b89b82 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/README.md +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/README.md @@ -55,7 +55,7 @@ Please refer to the [README](../../samples/GettingStarted/Workflows/Declarative/ |**ConditionItem**|Represents a single conditional statement within a group. It evaluates a specific logical condition and determines the next step in the flow. |**ContinueLoop**|Skips the remaining steps in the current iteration and continues with the next loop cycle. Commonly used to bypass specific cases without exiting the loop entirely. |**EndConversation**|Terminates the current conversation session. It ensures any necessary cleanup or final actions are performed before closing. -|**EndDialog**|Ends the current dialog or sub-dialog within a broader conversation flow. This helps modularize complex interactions. +|**EndWorkflow**|Ends the current workflow or sub-workflow within a broader conversation flow. This helps modularize complex interactions. |**Foreach**|Iterates through a collection of items, executing a set of actions for each. Ideal for processing lists or batch operations. |**GotoAction**|Jumps directly to a specified action within the workflow. Enables non-linear navigation in the logic flow. diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs index 1cb79db9f7..7afaf4fa04 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs @@ -16,8 +16,8 @@ public abstract class WorkflowAgentProvider /// /// Gets or sets a collection of additional tools an agent is able to automatically invoke. /// If an agent is configured with a function tool that is not available, a is executed - /// that provides an that describes the function calls requested. The caller may - /// then respond with a corrsponding that includes the results of the function calls. + /// that provides an that describes the function calls requested. The caller may + /// then respond with a corrsponding that includes the results of the function calls. /// /// /// These will not impact the requests sent to the model by the . @@ -56,14 +56,6 @@ public abstract class WorkflowAgentProvider /// public bool AllowMultipleToolCalls { get; init; } - /// - /// Asynchronously retrieves an AI agent by its unique identifier. - /// - /// The unique identifier of the AI agent to retrieve. Cannot be null or empty. - /// A token that propagates notification when operation should be canceled. - /// The task result contains the associated. - public abstract Task GetAgentAsync(string agentId, CancellationToken cancellationToken = default); - /// /// Asynchronously creates a new conversation and returns its unique identifier. /// @@ -88,6 +80,17 @@ public abstract class WorkflowAgentProvider /// The requested message public abstract Task GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default); + /// + /// Asynchronously retrieves an AI agent by its unique identifier. + /// + /// The unique identifier of the AI agent to retrieve. Cannot be null or empty. + /// An optional agent version. + /// Optional identifier of the target conversation. + /// The messages to include in the invocation. + /// A token that propagates notification when operation should be canceled. + /// Asynchronous set of . + public abstract IAsyncEnumerable InvokeAgentAsync(string agentId, string? agentVersion, string? conversationId, IEnumerable? messages, CancellationToken cancellationToken = default); + /// /// Retrieves a set of messages from a conversation. /// diff --git a/dotnet/src/Shared/CodeTests/README.md b/dotnet/src/Shared/CodeTests/README.md new file mode 100644 index 0000000000..e1282f1778 --- /dev/null +++ b/dotnet/src/Shared/CodeTests/README.md @@ -0,0 +1,11 @@ +# Build Code + +Re-usable utility for building C# code in tests. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/dotnet/src/Shared/Foundry/Agents/AgentFactory.cs b/dotnet/src/Shared/Foundry/Agents/AgentFactory.cs new file mode 100644 index 0000000000..1302ade588 --- /dev/null +++ b/dotnet/src/Shared/Foundry/Agents/AgentFactory.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0005 + +using System; +using System.Threading.Tasks; +using Azure.AI.Agents; + +namespace Shared.Foundry; + +internal static class AgentFactory +{ + public static async ValueTask CreateAgentAsync( + this AgentsClient agentsClient, + string agentName, + PromptAgentDefinition agentDefinition, + string agentDescription) + { + AgentVersionCreationOptions options = + new() + { + Description = agentDescription, + Metadata = + { + { "deleteme", bool.TrueString }, + { "test", bool.TrueString }, + }, + }; + + AgentVersion agentVersion = await agentsClient.CreateAgentVersionAsync(agentName, agentDefinition, options).ConfigureAwait(false); + + Console.ForegroundColor = ConsoleColor.Cyan; + try + { + Console.WriteLine($"PROMPT AGENT: {agentVersion.Name}:{agentVersion.Version}"); + } + finally + { + Console.ResetColor(); + } + + return agentVersion; + } +} diff --git a/dotnet/src/Shared/Foundry/Agents/README.md b/dotnet/src/Shared/Foundry/Agents/README.md new file mode 100644 index 0000000000..370068c555 --- /dev/null +++ b/dotnet/src/Shared/Foundry/Agents/README.md @@ -0,0 +1,11 @@ +# Foundry Agents + +Shared patterns for creating and utilizing Foundry agents. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/dotnet/src/Shared/Workflows/Execution/README.md b/dotnet/src/Shared/Workflows/Execution/README.md new file mode 100644 index 0000000000..4a885ae651 --- /dev/null +++ b/dotnet/src/Shared/Workflows/Execution/README.md @@ -0,0 +1,11 @@ +# Workflow Execution + +Common support for workflow execution. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/dotnet/src/Shared/Workflows/Execution/WorkflowFactory.cs b/dotnet/src/Shared/Workflows/Execution/WorkflowFactory.cs new file mode 100644 index 0000000000..08a39afe5e --- /dev/null +++ b/dotnet/src/Shared/Workflows/Execution/WorkflowFactory.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.Identity; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Agents.AI.Workflows.Declarative; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Shared.Workflows; + +internal sealed class WorkflowFactory(string workflowFile, Uri foundryEndpoint) +{ + public IList Functions { get; init; } = []; + + public IConfiguration? Configuration { get; init; } + + // Assign to continue an existing conversation + public string? ConversationId { get; init; } + + // Assign to enable logging + public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance; + + /// + /// Create the workflow from the declarative YAML. Includes definition of the + /// and the associated . + /// + public Workflow CreateWorkflow() + { + // Create the agent provider that will service agent requests within the workflow. + AzureAgentProvider agentProvider = new(foundryEndpoint, new AzureCliCredential()) + { + // Functions included here will be auto-executed by the framework. + Functions = this.Functions + }; + + // Define the workflow options. + DeclarativeWorkflowOptions options = + new(agentProvider) + { + Configuration = this.Configuration, + ConversationId = this.ConversationId, + LoggerFactory = this.LoggerFactory, + }; + + string workflowPath = Path.Combine(AppContext.BaseDirectory, workflowFile); + + // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file. + return DeclarativeWorkflowBuilder.Build(workflowPath, options); + } +} diff --git a/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs b/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs new file mode 100644 index 0000000000..75f451bf8e --- /dev/null +++ b/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs @@ -0,0 +1,378 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Uncomment to output unknown content types for debugging. +//#define DEBUG_OUTPUT + +using System.Diagnostics; +using System.Text.Json; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Agents.AI.Workflows.Checkpointing; +using Microsoft.Agents.AI.Workflows.Declarative; +using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Agents.AI.Workflows.Declarative.Kit; +using Microsoft.Extensions.AI; +using OpenAI.Responses; + +namespace Shared.Workflows; + +// Types are for evaluation purposes only and is subject to change or removal in future updates. +#pragma warning disable OPENAI001 +#pragma warning disable OPENAICUA001 +#pragma warning disable MEAI001 + +internal sealed class WorkflowRunner +{ + private Dictionary FunctionMap { get; } + private CheckpointInfo? LastCheckpoint { get; set; } + + public static void Notify(string message, ConsoleColor? color = null) + { + Console.ForegroundColor = color ?? ConsoleColor.Cyan; + try + { + Console.WriteLine(message); + } + finally + { + Console.ResetColor(); + } + } + + /// + /// When enabled, checkpoints will be persisted to disk as JSON files. + /// Otherwise an in-memory checkpoint store that will not persist checkpoints + /// beyond the lifetime of the process. + /// + public bool UseJsonCheckpoints { get; init; } + + public WorkflowRunner(params IEnumerable functions) + { + this.FunctionMap = functions.ToDictionary(f => f.Name); + } + + public async Task ExecuteAsync(Func workflowProvider, string input) + { + Workflow workflow = workflowProvider.Invoke(); + + CheckpointManager checkpointManager; + + if (this.UseJsonCheckpoints) + { + // Use a file-system based JSON checkpoint store to persist checkpoints to disk. + DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:yyMMdd-hhmmss-ff}")); + checkpointManager = CheckpointManager.CreateJson(new FileSystemJsonCheckpointStore(checkpointFolder)); + } + else + { + // Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process. + checkpointManager = CheckpointManager.CreateInMemory(); + } + + Checkpointed run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager).ConfigureAwait(false); + + bool isComplete = false; + ExternalResponse? requestResponse = null; + do + { + ExternalRequest? externalRequest = await this.MonitorAndDisposeWorkflowRunAsync(run, requestResponse).ConfigureAwait(false); + if (externalRequest is not null) + { + Notify("\nWORKFLOW: Yield\n", ConsoleColor.DarkYellow); + + if (this.LastCheckpoint is null) + { + throw new InvalidOperationException("Checkpoint information missing after external request."); + } + + // Process the external request. + object response = await this.HandleExternalRequestAsync(externalRequest).ConfigureAwait(false); + requestResponse = externalRequest.CreateResponse(response); + + // Let's resume on an entirely new workflow instance to demonstrate checkpoint portability. + workflow = workflowProvider.Invoke(); + + // Restore the latest checkpoint. + Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}"); + Notify("WORKFLOW: Restore", ConsoleColor.DarkYellow); + + run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager, run.Run.RunId).ConfigureAwait(false); + } + else + { + isComplete = true; + } + } + while (!isComplete); + + Notify("\nWORKFLOW: Done!\n"); + } + + public async Task MonitorAndDisposeWorkflowRunAsync(Checkpointed run, ExternalResponse? response = null) + { +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using IAsyncDisposable disposeRun = run; +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + + bool hasStreamed = false; + string? messageId = null; + + bool shouldExit = false; + ExternalRequest? externalResponse = null; + + if (response is not null) + { + await run.Run.SendResponseAsync(response).ConfigureAwait(false); + } + + await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false)) + { + switch (workflowEvent) + { + case ExecutorInvokedEvent executorInvoked: + Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}"); + break; + + case ExecutorCompletedEvent executorCompleted: + Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}"); + break; + + case DeclarativeActionInvokedEvent actionInvoked: + Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]"); + break; + + case DeclarativeActionCompletedEvent actionComplete: + Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]"); + break; + + case ExecutorFailedEvent executorFailure: + Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}"); + break; + + case WorkflowErrorEvent workflowError: + throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure..."); + + case SuperStepCompletedEvent checkpointCompleted: + this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint; + Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]"); + if (externalResponse is not null) + { + shouldExit = true; + } + break; + + case RequestInfoEvent requestInfo: + Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}"); + externalResponse = requestInfo.Request; + break; + + case ConversationUpdateEvent invokeEvent: + Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}"); + break; + + case MessageActivityEvent activityEvent: + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("\nACTIVITY:"); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(activityEvent.Message.Trim()); + break; + + case AgentRunUpdateEvent streamEvent: + if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal)) + { + hasStreamed = false; + messageId = streamEvent.Update.MessageId; + + if (messageId is not null) + { + string? agentName = streamEvent.Update.AuthorName ?? streamEvent.Update.AgentId ?? nameof(ChatRole.Assistant); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write($"\n{agentName.ToUpperInvariant()}:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" [{messageId}]"); + } + } + + ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate; + switch (chatUpdate?.RawRepresentation) + { + case ImageGenerationCallResponseItem messageUpdate: + await DownloadFileContentAsync(Path.GetFileName("response.png"), messageUpdate.ImageResultBytes).ConfigureAwait(false); + break; + + case FunctionCallResponseItem actionUpdate: + Console.ForegroundColor = ConsoleColor.White; + Console.Write($"Calling tool: {actionUpdate.FunctionName}"); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" [{actionUpdate.CallId}]"); + break; + + case McpToolCallItem actionUpdate: + Console.ForegroundColor = ConsoleColor.White; + Console.Write($"Calling tool: {actionUpdate.ToolName}"); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" [{actionUpdate.Id}]"); + break; + } + + try + { + Console.ResetColor(); + Console.Write(streamEvent.Update.Text); + hasStreamed |= !string.IsNullOrEmpty(streamEvent.Update.Text); + } + finally + { + Console.ResetColor(); + } + break; + + case AgentRunResponseEvent messageEvent: + try + { + if (hasStreamed) + { + Console.WriteLine(); + } + + if (messageEvent.Response.Usage is not null) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]"); + } + } + finally + { + Console.ResetColor(); + } + break; + + default: +#if DEBUG_OUTPUT + Debug.WriteLine($"UNHANDLED: {workflowEvent.GetType().Name}"); +#endif + break; + } + + if (shouldExit) + { + break; + } + } + + return externalResponse; + } + + /// + /// Handle request for external input. + /// + private async ValueTask HandleExternalRequestAsync(ExternalRequest request) + { + ExternalInputRequest inputRequest = + request.DataAs() ?? + throw new InvalidOperationException($"Expected external request type: {request.GetType().Name}."); + + List responseMessages = []; + + foreach (ChatMessage message in inputRequest.AgentResponse.Messages) + { + await foreach (ChatMessage responseMessage in this.ProcessInputMessageAsync(message).ConfigureAwait(false)) + { + responseMessages.Add(responseMessage); + } + } + + if (responseMessages.Count == 0) + { + // Must be request for user input. + responseMessages.Add(HandleUserInputRequest(inputRequest)); + } + + Console.WriteLine(); + + return new ExternalInputResponse(responseMessages); + } + + private async IAsyncEnumerable ProcessInputMessageAsync(ChatMessage message) + { + foreach (AIContent requestItem in message.Contents) + { + ChatMessage? responseMessage = + requestItem switch + { + FunctionCallContent functionCall => await InvokeFunctionAsync(functionCall).ConfigureAwait(false), + FunctionApprovalRequestContent functionApprovalRequest => ApproveFunction(functionApprovalRequest), + McpServerToolApprovalRequestContent mcpApprovalRequest => ApproveMCP(mcpApprovalRequest), + _ => HandleUnknown(requestItem), + }; + + if (responseMessage is not null) + { + yield return responseMessage; + } + } + + ChatMessage? HandleUnknown(AIContent request) + { +#if DEBUG_OUTPUT + Notify($"INPUT - Unknown: {request.GetType().Name} [{request.RawRepresentation?.GetType().Name ?? "*"}]"); +#endif + return null; + } + + ChatMessage ApproveFunction(FunctionApprovalRequestContent functionApprovalRequest) + { + Notify($"INPUT - Approving Function: {functionApprovalRequest.FunctionCall.Name}"); + return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved: true)]); + } + + ChatMessage ApproveMCP(McpServerToolApprovalRequestContent mcpApprovalRequest) + { + Notify($"INPUT - Approving MCP: {mcpApprovalRequest.ToolCall.ToolName}"); + return new ChatMessage(ChatRole.User, [mcpApprovalRequest.CreateResponse(approved: true)]); + } + + async Task InvokeFunctionAsync(FunctionCallContent functionCall) + { + Notify($"INPUT - Executing Function: {functionCall.Name}"); + AIFunction functionTool = this.FunctionMap[functionCall.Name]; + AIFunctionArguments? functionArguments = functionCall.Arguments is null ? null : new(functionCall.Arguments.NormalizePortableValues()); + object? result = await functionTool.InvokeAsync(functionArguments).ConfigureAwait(false); + return new ChatMessage(ChatRole.Tool, [new FunctionResultContent(functionCall.CallId, JsonSerializer.Serialize(result))]); + } + } + + private static ChatMessage HandleUserInputRequest(ExternalInputRequest request) + { + string prompt = + string.IsNullOrWhiteSpace(request.AgentResponse.Text) || request.AgentResponse.ResponseId is not null ? + "INPUT:" : + request.AgentResponse.Text; + + string? userInput; + do + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.Write($"{prompt} "); + Console.ForegroundColor = ConsoleColor.White; + userInput = Console.ReadLine(); + } + while (string.IsNullOrWhiteSpace(userInput)); + + return new ChatMessage(ChatRole.User, userInput); + } + + private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content) + { + string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename)); + filePath = Path.ChangeExtension(filePath, ".png"); + + await File.WriteAllBytesAsync(filePath, content.ToArray()).ConfigureAwait(false); + + Process.Start( + new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/C start {filePath}" + }); + } +} diff --git a/dotnet/src/Shared/Workflows/Settings/Application.cs b/dotnet/src/Shared/Workflows/Settings/Application.cs new file mode 100644 index 0000000000..de8eb51534 --- /dev/null +++ b/dotnet/src/Shared/Workflows/Settings/Application.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Reflection; +using Microsoft.Extensions.Configuration; + +namespace Shared.Workflows; + +internal static class Application +{ + /// + /// Configuration key used to identify the Foundry project endpoint. + /// + public static class Settings + { + public const string FoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT"; + public const string FoundryModelMini = "FOUNDRY_MODEL_DEPLOYMENT_NAME"; + public const string FoundryModelFull = "FOUNDRY_MEDIA_DEPLOYMENT_NAME"; + public const string FoundryGroundingTool = "FOUNDRY_CONNECTION_GROUNDING_TOOL"; + } + + public static string GetInput(string[] args) + { + string? input = args.FirstOrDefault(); + + try + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + + Console.Write("\nINPUT: "); + + Console.ForegroundColor = ConsoleColor.White; + + if (!string.IsNullOrWhiteSpace(input)) + { + Console.WriteLine(input); + return input; + } + while (string.IsNullOrWhiteSpace(input)) + { + input = Console.ReadLine(); + } + + return input.Trim(); + } + finally + { + Console.ResetColor(); + } + } + + public static string? GetRepoFolder() + { + DirectoryInfo? current = new(Directory.GetCurrentDirectory()); + + while (current is not null) + { + if (Directory.Exists(Path.Combine(current.FullName, ".git"))) + { + return current.FullName; + } + + current = current.Parent; + } + + return null; + } + + public static string GetValue(this IConfiguration configuration, string settingName) => + configuration[settingName] ?? + throw new InvalidOperationException($"Undefined configuration setting: {settingName}"); + + /// + /// Initialize configuration and environment + /// + public static IConfigurationRoot InitializeConfig() => + new ConfigurationBuilder() + .AddUserSecrets(Assembly.GetExecutingAssembly()) + .AddEnvironmentVariables() + .Build(); +} diff --git a/dotnet/src/Shared/Workflows/Settings/README.md b/dotnet/src/Shared/Workflows/Settings/README.md new file mode 100644 index 0000000000..80b176131b --- /dev/null +++ b/dotnet/src/Shared/Workflows/Settings/README.md @@ -0,0 +1,11 @@ +# Workflow Settings + +Common support configuration and environment used in workflow samples. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/AgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/AgentProvider.cs new file mode 100644 index 0000000000..f90e6fbbd5 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/AgentProvider.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Azure.AI.Agents; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; + +internal abstract class AgentProvider(IConfiguration configuration) +{ + public static class Names + { + public const string FunctionTool = "FUNCTIONTOOL"; + public const string Marketing = "MARKETING"; + public const string MathChat = "MATHCHAT"; + } + + public static class Settings + { + public const string FoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT"; + public const string FoundryModelMini = "FOUNDRY_MODEL_DEPLOYMENT_NAME"; + public const string FoundryModelFull = "FOUNDRY_MEDIA_DEPLOYMENT_NAME"; + public const string FoundryGroundingTool = "FOUNDRY_CONNECTION_GROUNDING_TOOL"; + } + + public static AgentProvider Create(IConfiguration configuration, string providerType) => + providerType.ToUpperInvariant() switch + { + Names.FunctionTool => new FunctionToolAgentProvider(configuration), + Names.Marketing => new MarketingAgentProvider(configuration), + Names.MathChat => new MathChatAgentProvider(configuration), + _ => new TestAgentProvider(configuration), + }; + + public async ValueTask CreateAgentsAsync() + { + Uri foundryEndpoint = new(this.GetSetting(Settings.FoundryEndpoint)); + + await foreach (AgentVersion agent in this.CreateAgentsAsync(foundryEndpoint)) + { + Console.WriteLine($"Created agent: {agent.Name}:{agent.Version})"); + } + } + + protected abstract IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint); + + protected string GetSetting(string settingName) => + configuration[settingName] ?? + throw new InvalidOperationException($"Undefined configuration setting: {settingName}"); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs new file mode 100644 index 0000000000..514aac94fe --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using OpenAI.Responses; +using Shared.Foundry; + +namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; + +internal sealed class FunctionToolAgentProvider(IConfiguration configuration) : AgentProvider(configuration) +{ + protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) + { + MenuPlugin menuPlugin = new(); + AIFunction[] functions = + [ + AIFunctionFactory.Create(menuPlugin.GetMenu), + AIFunctionFactory.Create(menuPlugin.GetSpecials), + AIFunctionFactory.Create(menuPlugin.GetItemPrice), + ]; + + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + yield return + await agentsClient.CreateAgentAsync( + agentName: "MenuAgent", + agentDefinition: this.DefineMenuAgent(functions), + agentDescription: "Provides information about the restaurant menu"); + } + + private PromptAgentDefinition DefineMenuAgent(AIFunction[] functions) + { + PromptAgentDefinition agentDefinition = + new(this.GetSetting(Settings.FoundryModelMini)) + { + Instructions = + """ + Answer the users questions on the menu. + For questions or input that do not require searching the documentation, inform the + user that you can only answer questions what's on the menu. + """ + }; + + foreach (AIFunction function in functions) + { + agentDefinition.Tools.Add(function.AsOpenAIResponseTool()); + } + + return agentDefinition; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs new file mode 100644 index 0000000000..ac5b20cb61 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Shared.Foundry; + +namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; + +internal sealed class MarketingAgentProvider(IConfiguration configuration) : AgentProvider(configuration) +{ + protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) + { + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + yield return + await agentsClient.CreateAgentAsync( + agentName: "AnalystAgent", + agentDefinition: this.DefineAnalystAgent(), + agentDescription: "Analyst agent for Marketing workflow"); + + yield return + await agentsClient.CreateAgentAsync( + agentName: "WriterAgent", + agentDefinition: this.DefineWriterAgent(), + agentDescription: "Writer agent for Marketing workflow"); + + yield return + await agentsClient.CreateAgentAsync( + agentName: "EditorAgent", + agentDefinition: this.DefineEditorAgent(), + agentDescription: "Editor agent for Marketing workflow"); + } + + private PromptAgentDefinition DefineAnalystAgent() => + new(this.GetSetting(Settings.FoundryModelFull)) + { + Instructions = + """ + You are a marketing analyst. Given a product description, identify: + - Key features + - Target audience + - Unique selling points + """, + Tools = + { + //AgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available + // new BingGroundingSearchToolParameters( + // [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))])) + } + }; + + private PromptAgentDefinition DefineWriterAgent() => + new(this.GetSetting(Settings.FoundryModelFull)) + { + Instructions = + """ + You are a marketing copywriter. Given a block of text describing features, audience, and USPs, + compose a compelling marketing copy (like a newsletter section) that highlights these points. + Output should be short (around 150 words), output just the copy as a single text block. + """ + }; + + private PromptAgentDefinition DefineEditorAgent() => + new(this.GetSetting(Settings.FoundryModelFull)) + { + Instructions = + """ + You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone, + give format and make it polished. Output the final improved copy as a single text block. + """ + }; +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs new file mode 100644 index 0000000000..8fb352ea92 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Shared.Foundry; + +namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; + +internal sealed class MathChatAgentProvider(IConfiguration configuration) : AgentProvider(configuration) +{ + protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) + { + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + yield return + await agentsClient.CreateAgentAsync( + agentName: "StudentAgent", + agentDefinition: this.DefineStudentAgent(), + agentDescription: "Student agent for MathChat workflow"); + + yield return + await agentsClient.CreateAgentAsync( + agentName: "TeacherAgent", + agentDefinition: this.DefineTeacherAgent(), + agentDescription: "Teacher agent for MathChat workflow"); + } + + private PromptAgentDefinition DefineStudentAgent() => + new(this.GetSetting(Settings.FoundryModelMini)) + { + Instructions = + """ + Your job is help a math teacher practice teaching by making intentional mistakes. + You attempt to solve the given math problem, but with intentional mistakes so the teacher can help. + Always incorporate the teacher's advice to fix your next response. + You have the math-skills of a 6th grader. + """ + }; + + private PromptAgentDefinition DefineTeacherAgent() => + new(this.GetSetting(Settings.FoundryModelMini)) + { + Instructions = + """ + Review and coach the student's approach to solving the given math problem. + Don't repeat the solution or try and solve it. + If the student has demonstrated comprehension and responded to all of your feedback, + give the student your congratulations by using the word "congratulations". + """ + }; +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MenuPlugin.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MenuPlugin.cs index d69e856af8..38592e2e12 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MenuPlugin.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MenuPlugin.cs @@ -5,32 +5,33 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; +#pragma warning disable CA1822 + public sealed class MenuPlugin { public IEnumerable GetTools() { - yield return AIFunctionFactory.Create(this.GetMenu, name: $"{nameof(MenuPlugin)}_{nameof(GetMenu)}"); - yield return AIFunctionFactory.Create(this.GetSpecials, name: $"{nameof(MenuPlugin)}_{nameof(GetSpecials)}"); - yield return AIFunctionFactory.Create(this.GetItemPrice, name: $"{nameof(MenuPlugin)}_{nameof(GetItemPrice)}"); + yield return AIFunctionFactory.Create(this.GetMenu); + yield return AIFunctionFactory.Create(this.GetSpecials); + yield return AIFunctionFactory.Create(this.GetItemPrice); } - [KernelFunction, Description("Provides a list items on the menu.")] + [Description("Provides a list items on the menu.")] public MenuItem[] GetMenu() { return s_menuItems; } - [KernelFunction, Description("Provides a list of specials from the menu.")] + [Description("Provides a list of specials from the menu.")] public MenuItem[] GetSpecials() { return [.. s_menuItems.Where(i => i.IsSpecial)]; } - [KernelFunction, Description("Provides the price of the requested menu item.")] + [Description("Provides the price of the requested menu item.")] public float? GetItemPrice( [Description("The name of the menu item.")] string name) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgent.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgent.yaml deleted file mode 100644 index eddcc7b0aa..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgent.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: foundry_agent -name: BasicAgent -description: Basic agent for integration tests -model: - id: ${FOUNDRY_MEDIA_DEPLOYMENT_NAME} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs new file mode 100644 index 0000000000..c84ca52ca6 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Shared.Foundry; + +namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; + +internal sealed class TestAgentProvider(IConfiguration configuration) : AgentProvider(configuration) +{ + protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) + { + AgentsClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + yield return + await agentsClient.CreateAgentAsync( + agentName: "TestAgent", + agentDefinition: this.DefineMenuAgent(), + agentDescription: "Provides information about the restaurant menu"); + } + + private PromptAgentDefinition DefineMenuAgent() => + new(this.GetSetting(Settings.FoundryModelFull)); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/ToolAgent.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/ToolAgent.yaml deleted file mode 100644 index 17cc84b010..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/ToolAgent.yaml +++ /dev/null @@ -1,15 +0,0 @@ -type: foundry_agent -name: ToolAgent -description: Agent with a function tool defined. -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} -tools: - - id: MenuPlugin_GetMenu - type: function - description: Provides a list items on the menu. - - id: MenuPlugin_GetSpecials - type: function - description: Provides a list of specials from the menu. - - id: MenuPlugin_GetItemPrice - type: function - description: Provides the price of the requested menu item. diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs index 87b9160196..da3f6f2fd5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using System.Threading.Tasks; -using Azure; -using Azure.AI.Agents.Persistent; using Azure.Identity; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Extensions.AI; @@ -18,7 +15,7 @@ public sealed class AzureAgentProviderTest(ITestOutputHelper output) : Integrati public async Task ConversationTestAsync() { // Arrange - AzureAgentProvider provider = new(this.FoundryConfiguration.Endpoint, new AzureCliCredential()); + AzureAgentProvider provider = new(this.TestEndpoint, new AzureCliCredential()); // Act string conversationId = await provider.CreateConversationAsync(); // Assert @@ -43,35 +40,4 @@ public sealed class AzureAgentProviderTest(ITestOutputHelper output) : Integrati Assert.NotNull(message); Assert.Equal(messages[3].Text, message.Text); } - - [Fact] - public async Task GetAgentTestAsync() - { - // Arrange - AzureAgentProvider provider = new(this.FoundryConfiguration.Endpoint, new AzureCliCredential()); - string agentName = $"TestAgent-{DateTime.UtcNow:yyMMdd-HHmmss-fff}"; - - string agent1Id = await this.CreateAgentAsync(); - string agent2Id = await this.CreateAgentAsync(agentName); - - // Act - AIAgent agent1 = await provider.GetAgentAsync(agent1Id); - // Assert - Assert.Equal(agent1Id, agent1.Id); - - // Act - AIAgent agent2 = await provider.GetAgentAsync(agent2Id); - // Assert - Assert.Equal(agent2Id, agent2.Id); - - // Act & Assert - await Assert.ThrowsAsync(() => provider.GetAgentAsync(agentName)); - } - - private async ValueTask CreateAgentAsync(string? name = null) - { - PersistentAgentsClient client = new(this.FoundryConfiguration.Endpoint, new AzureCliCredential()); - PersistentAgent agent = await client.Administration.CreateAgentAsync(this.FoundryConfiguration.DeploymentName, name: name); - return agent.Id; - } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs index a57149c015..567683971b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Xunit.Abstractions; @@ -22,7 +23,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow [InlineData("ConversationMessages.yaml", "ConversationMessages.json")] [InlineData("ConversationMessages.yaml", "ConversationMessages.json", true)] public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) => - this.RunWorkflowAsync(Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName), testcaseFileName, externalConveration); + this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample: false), testcaseFileName, externalConveration); [Theory] [InlineData("Marketing.yaml", "Marketing.json")] @@ -30,14 +31,24 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow [InlineData("MathChat.yaml", "MathChat.json", true)] [InlineData("DeepResearch.yaml", "DeepResearch.json", Skip = "Long running")] public Task ValidateScenarioAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) => - this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName), testcaseFileName, externalConveration); + this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample: true), testcaseFileName, externalConveration); - [Fact] - public Task ValidateMultiTurnAsync() => - this.RunWorkflowAsync(Path.Combine(GetRepoFolder(), "workflow-samples", "HumanInLoop.yaml"), "HumanInLoop.json", useJsonCheckpoint: true); + [Theory] + [InlineData("ConfirmInput.yaml", "ConfirmInput.json", true)] + [InlineData("RequestExternalInput.yaml", "RequestExternalInput.json", false)] + public Task ValidateMultiTurnAsync(string workflowFileName, string testcaseFileName, bool isSample) => + this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample), testcaseFileName, useJsonCheckpoint: true); + + private static string GetWorkflowPath(string workflowFileName, bool isSample) => + isSample + ? Path.Combine(GetRepoFolder(), "workflow-samples", workflowFileName) + : Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName); protected override async Task RunAndVerifyAsync(Testcase testcase, string workflowPath, DeclarativeWorkflowOptions workflowOptions, TInput input, bool useJsonCheckpoint) { + AgentProvider agentProvider = AgentProvider.Create(this.Configuration, Path.GetFileNameWithoutExtension(workflowPath)); + await agentProvider.CreateAgentsAsync().ConfigureAwait(false); + Workflow workflow = DeclarativeWorkflowBuilder.Build(workflowPath, workflowOptions); WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowPath)); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs deleted file mode 100644 index 4ceee44c6b..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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; -using Microsoft.SemanticKernel.Agents.AzureAI; -using Shared.IntegrationTests; - -namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; - -internal static class AgentFactory -{ - private static readonly Dictionary s_agentDefinitions = - new() - { - ["FOUNDRY_AGENT_TEST"] = "TestAgent.yaml", - ["FOUNDRY_AGENT_TOOL"] = "ToolAgent.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", - }; - - private static FrozenDictionary? s_agentMap; - - public static async Task> GetAgentsAsync(AzureAIConfiguration config, IConfiguration configuration, CancellationToken cancellationToken = default) - { - if (s_agentMap is not null) - { - return s_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); - kernelBuilder.Plugins.AddFromType(); - AgentCreationOptions creationOptions = new() { Kernel = kernelBuilder.Build() }; - AzureAIAgentFactory factory = new(); - string repoRoot = WorkflowTest.GetRepoFolder(); - - return s_agentMap = (await Task.WhenAll(s_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(Environment.CurrentDirectory, "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}: {exception.Message}"); - throw; - } - } - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs index de83afd723..b525749b6c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs @@ -1,16 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Frozen; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; using Azure.Identity; +using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; -using Shared.IntegrationTests; using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; @@ -21,25 +20,20 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; public abstract class IntegrationTest : IDisposable { private IConfigurationRoot? _configuration; - private AzureAIConfiguration? _foundryConfiguration; protected IConfigurationRoot Configuration => this._configuration ??= InitializeConfig(); - internal AzureAIConfiguration FoundryConfiguration - { - get - { - this._foundryConfiguration ??= this.Configuration.GetSection("AzureAI").Get(); - Assert.NotNull(this._foundryConfiguration); - return this._foundryConfiguration; - } - } + public Uri TestEndpoint { get; } public TestOutputAdapter Output { get; } protected IntegrationTest(ITestOutputHelper output) { this.Output = new TestOutputAdapter(output); + this.TestEndpoint = + new Uri( + this.Configuration[AgentProvider.Settings.FoundryEndpoint] ?? + throw new InvalidOperationException($"Undefined configuration setting: {AgentProvider.Settings.FoundryEndpoint}")); Console.SetOut(this.Output); SetProduct(); } @@ -70,15 +64,8 @@ public abstract class IntegrationTest : IDisposable protected async ValueTask CreateOptionsAsync(bool externalConversation = false, params IEnumerable functionTools) { - FrozenDictionary agentMap = await AgentFactory.GetAgentsAsync(this.FoundryConfiguration, this.Configuration); - - IConfiguration workflowConfig = - new ConfigurationBuilder() - .AddInMemoryCollection(agentMap) - .Build(); - AzureAgentProvider agentProvider = - new(this.FoundryConfiguration.Endpoint, new AzureCliCredential()) + new(this.TestEndpoint, new AzureCliCredential()) { Functions = functionTools, }; @@ -92,7 +79,6 @@ public abstract class IntegrationTest : IDisposable return new DeclarativeWorkflowOptions(agentProvider) { - Configuration = workflowConfig, ConversationId = conversationId, LoggerFactory = this.Output }; @@ -100,7 +86,6 @@ public abstract class IntegrationTest : IDisposable private static IConfigurationRoot InitializeConfig() => new ConfigurationBuilder() - .AddJsonFile("appsettings.Development.json", true) .AddEnvironmentVariables() .AddUserSecrets(Assembly.GetExecutingAssembly()) .Build(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs index 391a4b63d6..ed3e0367f7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs @@ -17,23 +17,26 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; internal sealed class WorkflowHarness(Workflow workflow, string runId) { private CheckpointManager? _checkpointManager; - private CheckpointInfo? LastCheckpoint { get; set; } + private CheckpointInfo? _lastCheckpoint; public async Task RunTestcaseAsync(Testcase testcase, TInput input, bool useJson = false) where TInput : notnull { WorkflowEvents workflowEvents = await this.RunWorkflowAsync(input, useJson); - int requestCount = (workflowEvents.InputEvents.Count + 1) / 2; + int requestCount = workflowEvents.InputEvents.Count; int responseCount = 0; while (requestCount > responseCount) { + ExternalRequest request = workflowEvents.InputEvents[workflowEvents.InputEvents.Count - 1].Request; Assert.NotNull(testcase.Setup.Responses); Assert.NotEmpty(testcase.Setup.Responses); string inputText = testcase.Setup.Responses[responseCount].Value; + Console.WriteLine($"ID: {request.RequestId}"); Console.WriteLine($"INPUT: {inputText}"); ++responseCount; - WorkflowEvents runEvents = await this.ResumeAsync(new AnswerResponse(inputText)).ConfigureAwait(false); + ExternalResponse response = request.CreateResponse(new ExternalInputResponse(new ChatMessage(ChatRole.User, inputText))); + WorkflowEvents runEvents = await this.ResumeAsync(response).ConfigureAwait(false); workflowEvents = new WorkflowEvents([.. workflowEvents.Events, .. runEvents.Events]); - requestCount = (workflowEvents.InputEvents.Count + 1) / 2; + requestCount = workflowEvents.InputEvents.Count; } return workflowEvents; @@ -44,15 +47,15 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId) Console.WriteLine("RUNNING WORKFLOW..."); Checkpointed run = await InProcessExecution.StreamAsync(workflow, input, this.GetCheckpointManager(useJson), runId); IReadOnlyList workflowEvents = await MonitorAndDisposeWorkflowRunAsync(run).ToArrayAsync(); - this.LastCheckpoint = workflowEvents.OfType().LastOrDefault()?.CompletionInfo?.Checkpoint; + this._lastCheckpoint = workflowEvents.OfType().LastOrDefault()?.CompletionInfo?.Checkpoint; return new WorkflowEvents(workflowEvents); } - public async Task ResumeAsync(object response) + public async Task ResumeAsync(ExternalResponse response) { Console.WriteLine("\nRESUMING WORKFLOW..."); - Assert.NotNull(this.LastCheckpoint); - Checkpointed run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, this.GetCheckpointManager(), runId); + Assert.NotNull(this._lastCheckpoint); + Checkpointed run = await InProcessExecution.ResumeStreamAsync(workflow, this._lastCheckpoint, this.GetCheckpointManager(), runId); IReadOnlyList workflowEvents = await MonitorAndDisposeWorkflowRunAsync(run, response).ToArrayAsync(); return new WorkflowEvents(workflowEvents); } @@ -93,29 +96,32 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId) return this._checkpointManager; } - private static async IAsyncEnumerable MonitorAndDisposeWorkflowRunAsync(Checkpointed run, object? response = null) + private static async IAsyncEnumerable MonitorAndDisposeWorkflowRunAsync(Checkpointed run, ExternalResponse? response = null) { await using IAsyncDisposable disposeRun = run; + if (response is not null) + { + await run.Run.SendResponseAsync(response).ConfigureAwait(false); + } + + bool exitLoop = false; + bool hasRequest = false; + await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false)) { - bool exitLoop = false; - switch (workflowEvent) { - case RequestInfoEvent requestInfo: - Console.WriteLine($"REQUEST #{requestInfo.Request.RequestId}"); - if (response is not null) - { - ExternalResponse requestResponse = requestInfo.Request.CreateResponse(response); - await run.Run.SendResponseAsync(requestResponse).ConfigureAwait(false); - response = null; - } - else + case SuperStepCompletedEvent: + if (hasRequest) { exitLoop = true; } break; + case RequestInfoEvent requestInfo: + Console.WriteLine($"REQUEST #{requestInfo.Request.RequestId}"); + hasRequest = true; + break; case ConversationUpdateEvent conversationEvent: Console.WriteLine($"CONVERSATION: {conversationEvent.ConversationId}"); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs index 419c37e0eb..3238c59b54 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs @@ -33,6 +33,9 @@ public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : Inte private async Task RunWorkflowAsync(bool autoInvoke, params IEnumerable functionTools) { + AgentProvider agentProvider = AgentProvider.Create(this.Configuration, AgentProvider.Names.FunctionTool); + await agentProvider.CreateAgentsAsync().ConfigureAwait(false); + string workflowPath = GetWorkflowPath("FunctionTool.yaml"); Dictionary functionMap = autoInvoke ? [] : functionTools.ToDictionary(tool => tool.Name, tool => tool); DeclarativeWorkflowOptions workflowOptions = await this.CreateOptionsAsync(externalConversation: false, autoInvoke ? functionTools : []); @@ -47,11 +50,11 @@ public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : Inte Assert.False(autoInvoke); RequestInfoEvent inputEvent = workflowEvents.InputEvents[workflowEvents.InputEvents.Count - 1]; - AgentFunctionToolRequest? toolRequest = inputEvent.Request.Data.As(); + ExternalInputRequest? toolRequest = inputEvent.Request.Data.As(); Assert.NotNull(toolRequest); List<(FunctionCallContent, AIFunction)> functionCalls = []; - foreach (FunctionCallContent functionCall in toolRequest.FunctionCalls) + foreach (FunctionCallContent functionCall in toolRequest.AgentResponse.Messages.SelectMany(message => message.Contents).OfType()) { this.Output.WriteLine($"TOOL REQUEST: {functionCall.Name}"); if (!functionMap.TryGetValue(functionCall.Name, out AIFunction? functionTool)) @@ -62,11 +65,12 @@ public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : Inte functionCalls.Add((functionCall, functionTool)); } - IList functionResults = await InvokeToolsAsync(functionCalls); + IList functionResults = await InvokeToolsAsync(functionCalls); ++responseCount; - WorkflowEvents runEvents = await harness.ResumeAsync(AgentFunctionToolResponse.Create(toolRequest, functionResults)).ConfigureAwait(false); + ChatMessage resultMessage = new(ChatRole.Tool, functionResults); + WorkflowEvents runEvents = await harness.ResumeAsync(inputEvent.Request.CreateResponse(new ExternalInputResponse(resultMessage))).ConfigureAwait(false); workflowEvents = new WorkflowEvents([.. workflowEvents.Events, .. runEvents.Events]); } @@ -79,13 +83,13 @@ public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : Inte Assert.NotEmpty(workflowEvents.InputEvents); } - Assert.Equal(autoInvoke ? 3 : 5, workflowEvents.AgentResponseEvents.Count); + Assert.Equal(autoInvoke ? 3 : 4, workflowEvents.AgentResponseEvents.Count); Assert.All(workflowEvents.AgentResponseEvents, response => response.Response.Text.Contains("4.95")); } - private static async ValueTask> InvokeToolsAsync(IEnumerable<(FunctionCallContent, AIFunction)> functionCalls) + private static async ValueTask> InvokeToolsAsync(IEnumerable<(FunctionCallContent, AIFunction)> functionCalls) { - List results = []; + List results = []; foreach ((FunctionCallContent functionCall, AIFunction functionTool) in functionCalls) { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs index 280df88b4b..735a985c20 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs @@ -4,10 +4,11 @@ using System; using System.IO; using System.Net.Http; using System.Threading.Tasks; -using Azure.AI.Agents.Persistent; +using Azure.AI.Agents; using Azure.Identity; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Extensions.AI; +using OpenAI.Files; using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; @@ -18,9 +19,9 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(output) { private const string WorkflowFileName = "MediaInput.yaml"; - private const string ImageReference = "https://upload.wikimedia.org/wikipedia/commons/5/56/White_shark.jpg"; + private const string ImageReference = "https://sample-files.com/downloads/documents/pdf/basic-text.pdf"; - [Fact(Skip = "Service issue prevents this simple case")] + [Fact] public async Task ValidateImageUrlAsync() { this.Output.WriteLine($"Image: {ImageReference}"); @@ -30,20 +31,21 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o [Fact] public async Task ValidateImageDataAsync() { - byte[] imageData = await DownloadImageAsync(); + byte[] imageData = await DownloadFileAsync(); string encodedData = Convert.ToBase64String(imageData); string imageUrl = $"data:image/png;base64,{encodedData}"; this.Output.WriteLine($"Image: {imageUrl.Substring(0, 112)}..."); await this.ValidateImageAsync(new DataContent(imageUrl)); } - [Fact] + [Fact(Skip = "Not behaving will in git-hub build pipeline")] public async Task ValidateImageUploadAsync() { - byte[] imageData = await DownloadImageAsync(); - PersistentAgentsClient client = new(this.FoundryConfiguration.Endpoint, new AzureCliCredential()); + byte[] imageData = await DownloadFileAsync(); + AgentsClient client = new(this.TestEndpoint, new AzureCliCredential()); using MemoryStream contentStream = new(imageData); - PersistentAgentFileInfo fileInfo = await client.Files.UploadFileAsync(contentStream, PersistentAgentFilePurpose.Agents, "image.jpg"); + OpenAIFileClient fileClient = client.GetOpenAIClient().GetOpenAIFileClient(); + OpenAIFile fileInfo = await fileClient.UploadFileAsync(contentStream, "basic-text.pdf", FileUploadPurpose.Assistants); try { this.Output.WriteLine($"Image: {fileInfo.Id}"); @@ -51,11 +53,11 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o } finally { - await client.Files.DeleteFileAsync(fileInfo.Id); + await fileClient.DeleteFileAsync(fileInfo.Id); } } - private static async Task DownloadImageAsync() + private static async Task DownloadFileAsync() { using HttpClient client = new(); client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0"); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj index 39f590a220..1563b88719 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj @@ -5,8 +5,9 @@ - true true + true + true @@ -21,9 +22,6 @@ - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/ConfirmInput.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/ConfirmInput.json new file mode 100644 index 0000000000..4469633d23 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/ConfirmInput.json @@ -0,0 +1,29 @@ +{ + "description": "Human in the loop sample - RequestExternalInput.yaml.", + "setup": { + "input": { + "type": "String", + "value": "1234" + }, + "responses": [ + { + "type": "String", + "value": "1234" + } + ] + }, + "validation": { + "conversation_count": 1, + "min_action_count": 4, + "max_action_count": -1, + "min_response_count": 0, + "actions": { + "start": [ + "set_project" + ], + "final": [ + "sendActivity_confirmed" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json index 7a28a5b094..2b55754d91 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json @@ -9,7 +9,8 @@ "validation": { "conversation_count": 3, "min_action_count": 3, - "min_response_count": 2, + "min_response_count": 3, + "min_message_count": 4, "actions": { "start": [ "invoke_inner1", diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json index 68c40219d0..6af29b49c5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json @@ -10,6 +10,7 @@ "conversation_count": 1, "min_action_count": 3, "min_response_count": 3, + "min_message_count": 6, "actions": { "start": [ "invoke_analyst", diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json index 988732a7a8..e3a9b85836 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json @@ -12,6 +12,8 @@ "max_action_count": -1, "min_response_count": 2, "max_response_count": 8, + "min_message_count": 4, + "max_message_count": 17, "actions": { "start": [ ], diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/RequestExternalInput.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/RequestExternalInput.json new file mode 100644 index 0000000000..6d5fd5e3d7 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/RequestExternalInput.json @@ -0,0 +1,29 @@ +{ + "description": "Human in the loop sample - RequestExternalInput.yaml.", + "setup": { + "input": { + "type": "String", + "value": "n/a" + }, + "responses": [ + { + "type": "String", + "value": "This is external input" + } + ] + }, + "validation": { + "conversation_count": 1, + "min_action_count": 2, + "min_response_count": 0, + "min_message_count": 1, + "actions": { + "start": [ + "get_input" + ], + "final": [ + "show_input" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/CheckSystem.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/CheckSystem.yaml index c3542fb057..c20236fd69 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/CheckSystem.yaml +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/CheckSystem.yaml @@ -12,43 +12,43 @@ trigger: - condition: =IsBlank(System.Conversation) id: conversation_check actions: - - kind: EndDialog + - kind: EndWorkflow id: conversation_bad - condition: =IsBlank(System.Conversation.Id) id: conversation_id_check1 actions: - - kind: EndDialog + - kind: EndWorkflow id: conversation_id_bad1 - condition: =IsBlank(System.ConversationId) id: conversation_id_check2 actions: - - kind: EndDialog + - kind: EndWorkflow id: conversation_id_bad2 - condition: =IsBlank(System.LastMessage) id: message_check actions: - - kind: EndDialog + - kind: EndWorkflow id: message_bad - condition: =IsBlank(System.LastMessage.Id) id: message_id_check1 actions: - - kind: EndDialog + - kind: EndWorkflow id: message_id_bad1 - condition: =IsBlank(System.LastMessageId) id: message_id_check2 actions: - - kind: EndDialog + - kind: EndWorkflow id: message_id_bad2 - condition: =IsBlank(System.LastMessageText) id: message_text_check actions: - - kind: EndDialog + - kind: EndWorkflow id: message_text_bad elseActions: diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/FunctionTool.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/FunctionTool.yaml index 8cb65db8d6..3694845ee5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/FunctionTool.yaml +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/FunctionTool.yaml @@ -9,13 +9,13 @@ trigger: id: invoke_greet conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_TOOL + name: MenuAgent - kind: InvokeAzureAgent id: invoke_menu conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_TOOL + name: MenuAgent input: messages: =UserMessage("What's on today's menu?") @@ -23,6 +23,6 @@ trigger: id: invoke_item conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_TOOL + name: MenuAgent input: messages: =UserMessage("How much is the clam chowder?") diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml index 159637402f..371a821bc4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml @@ -8,27 +8,25 @@ trigger: - kind: InvokeAzureAgent id: invoke_inner1 agent: - name: =Env.FOUNDRY_AGENT_TEST + name: TestAgent input: messages: =UserMessage("Can an LLM think of funny jokes?") - output: - autoSend: false - kind: InvokeAzureAgent id: invoke_inner2 agent: - name: =Env.FOUNDRY_AGENT_TEST + name: TestAgent input: messages: =UserMessage("Do you know the joke about the chicken crossing the road? Tell me an improved version of that joke.") + output: + autoSend: true - kind: InvokeAzureAgent id: invoke_external conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_TEST + name: TestAgent input: - additionalInstructions: |- - Rate the originality of this well known joke that is being re-told on a scale of 1 to 10. - Take note on where improvements or changes were made. + messages: =UserMessage("Rate the originality of this well known joke that is being re-told on a scale of 1 to 10. Take note on where improvements or changes were made.") output: messages: Local.RatingResponse diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInput.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInput.yaml index c2a428f6d4..fce766988d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInput.yaml +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/MediaInput.yaml @@ -9,7 +9,7 @@ trigger: id: invoke_vision conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_TEST + name: TestAgent input: additionalInstructions: |- Describe the image contained in the user request, if any; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/RequestExternalInput.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/RequestExternalInput.yaml new file mode 100644 index 0000000000..1070316c4f --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/RequestExternalInput.yaml @@ -0,0 +1,14 @@ +kind: Workflow +trigger: + + kind: OnConversationStart + id: workflow_test + actions: + + - kind: RequestExternalInput + id: get_input + variable: Local.MyInput + + - kind: SendMessage + id: show_input + message: "You provided: {Local.MyInput}" diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml index 18384df2da..60bac51982 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml @@ -15,7 +15,7 @@ trigger: - kind: SetVariable id: set_user_name variable: Global.UserName - value: =Env.USERNAME + value: TestAgent # Respond with input - kind: SendActivity diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/DeclarativeEjectionTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/DeclarativeEjectionTest.cs index 8350a81524..6f87f77fb4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/DeclarativeEjectionTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/DeclarativeEjectionTest.cs @@ -15,6 +15,7 @@ public sealed class DeclarativeEjectionTest(ITestOutputHelper output) : Workflow { [Theory] [InlineData("AddConversationMessage.yaml")] + [InlineData("CancelWorkflow.yaml")] [InlineData("ClearAllVariables.yaml")] [InlineData("CopyConversationMessages.yaml")] [InlineData("Condition.yaml")] @@ -23,7 +24,7 @@ public sealed class DeclarativeEjectionTest(ITestOutputHelper output) : Workflow [InlineData("EditTable.yaml")] [InlineData("EditTableV2.yaml")] [InlineData("EndConversation.yaml")] - [InlineData("EndDialog.yaml")] + [InlineData("EndWorkflow.yaml")] [InlineData("Goto.yaml")] [InlineData("InvokeAgent.yaml")] [InlineData("LoopBreak.yaml")] diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/InvokeAzureAgentTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/InvokeAzureAgentTemplateTest.cs index e1727e5666..a22fcd9920 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/InvokeAzureAgentTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/InvokeAzureAgentTemplateTest.cs @@ -44,18 +44,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc BoolExpression.Expression("1 < 2")); } - [Fact] - public void AdditionalInstructions() - { - // Act, Assert - this.ExecuteTest( - nameof(VariableConversation), - StringExpression.Literal("asst_123abc"), - StringExpression.Variable(PropertyPath.TopicVariable("TestConversation")), - "MyMessages", - additionalInstructions: "Test instructions..."); - } - [Fact] public void InputMessagesVariable() { @@ -86,7 +74,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc StringExpression.Builder conversation, string? messagesVariable = null, BoolExpression.Builder? autoSend = null, - string? additionalInstructions = null, ValueExpression.Builder? messages = null) { // Arrange @@ -97,7 +84,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc conversation, messagesVariable, autoSend, - additionalInstructions is null ? null : (TemplateLine.Builder)TemplateLine.Parse(additionalInstructions), messages); // Act @@ -117,7 +103,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc StringExpression.Builder conversation, string? messagesVariable = null, BoolExpression.Builder? autoSend = null, - TemplateLine.Builder? additionalInstructions = null, ValueExpression.Builder? messages = null) { InitializablePropertyPath? outputMessages = null; @@ -141,7 +126,6 @@ public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowAc new AzureAgentInput.Builder { Messages = messages, - AdditionalInstructions = additionalInstructions, }, Output = new AzureAgentOutput.Builder diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs index 6f2282902f..4b1d54635b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs @@ -145,8 +145,9 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow } [Theory] + [InlineData("CancelWorkflow.yaml", 1, "end_all")] [InlineData("EndConversation.yaml", 1, "end_all")] - [InlineData("EndDialog.yaml", 1, "end_all")] + [InlineData("EndWorkflow.yaml", 1, "end_all")] [InlineData("EditTable.yaml", 2, "edit_var")] [InlineData("EditTableV2.yaml", 2, "edit_var")] [InlineData("ParseValue.yaml", 2, "parse_var")] @@ -170,8 +171,6 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow [InlineData(typeof(AdaptiveCardPrompt.Builder))] [InlineData(typeof(BeginDialog.Builder))] [InlineData(typeof(CSATQuestion.Builder))] - [InlineData(typeof(CancelAllDialogs.Builder))] - [InlineData(typeof(CancelDialog.Builder))] [InlineData(typeof(CreateSearchQuery.Builder))] [InlineData(typeof(DeleteActivity.Builder))] [InlineData(typeof(DisableTrigger.Builder))] @@ -231,7 +230,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow [InlineData("Condition.yaml", "setVariable_test")] [InlineData("ConditionElse.yaml", "setVariable_test")] [InlineData("EndConversation.yaml", "end_all")] - [InlineData("EndDialog.yaml", "end_all")] + [InlineData("EndWorkflow.yaml", "end_all")] [InlineData("EditTable.yaml", "edit_var")] [InlineData("EditTableV2.yaml", "edit_var")] [InlineData("Goto.yaml", "goto_end")] diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolRequestTest.cs deleted file mode 100644 index 64639251aa..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolRequestTest.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Microsoft.Extensions.AI; -using Xunit.Abstractions; - -namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; - -/// -/// Verify class -/// -public sealed class AgentFunctionToolRequestTest(ITestOutputHelper output) : EventTest(output) -{ - [Fact] - public void VerifySerializationEmpty() - { - // Arrange & Act - AgentFunctionToolRequest copy = VerifyEventSerialization(new AgentFunctionToolRequest("testagent", [])); - - // Assert - Assert.Equal("testagent", copy.AgentName); - Assert.Empty(copy.FunctionCalls); - } - - [Fact] - public void VerifySerializationWithRequests() - { - // Arrange & Act - AgentFunctionToolRequest copy = - VerifyEventSerialization( - new AgentFunctionToolRequest( - "agent", - [ - new FunctionCallContent("call1", "result1"), - new FunctionCallContent("call2", "result2", new Dictionary() { { "name", "Clam Chowder" } }), - ])); - - // Assert - Assert.Equal("agent", copy.AgentName); - Assert.Equal(2, copy.FunctionCalls.Count); - Assert.IsType(copy.FunctionCalls[0]); - Assert.Null(copy.FunctionCalls[0].Arguments); - Assert.IsType(copy.FunctionCalls[1]); - Assert.NotNull(copy.FunctionCalls[1].Arguments); - Assert.NotEmpty(copy.FunctionCalls[1].Arguments!); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolResponseTest.cs deleted file mode 100644 index 9467d67117..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolResponseTest.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Microsoft.Extensions.AI; -using Xunit.Abstractions; - -namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; - -/// -/// Verify class -/// -public sealed class AgentFunctionToolResponseTest(ITestOutputHelper output) : EventTest(output) -{ - [Fact] - public void VerifySerializationEmpty() - { - // Arrange & Act - AgentFunctionToolResponse copy = VerifyEventSerialization(new AgentFunctionToolResponse("testagent", [])); - - // Assert - Assert.Equal("testagent", copy.AgentName); - Assert.Empty(copy.FunctionResults); - } - - [Fact] - public void VerifySerializationWithResults() - { - // Arrange & Act - AgentFunctionToolResponse copy = - VerifyEventSerialization( - new AgentFunctionToolResponse( - "agent", - [ - new FunctionResultContent("call1", "result1"), - new FunctionResultContent("call2", "result2"), - ])); - - // Assert - Assert.Equal("agent", copy.AgentName); - Assert.Equal(2, copy.FunctionResults.Count); - Assert.IsType(copy.FunctionResults[0]); - Assert.Equal("call1", copy.FunctionResults[0].CallId); - Assert.IsType(copy.FunctionResults[1]); - Assert.Equal("call2", copy.FunctionResults[1].CallId); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs index 7dc1c895d3..a4965ebc61 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Text.Json; using Microsoft.Extensions.AI; using Xunit.Abstractions; @@ -19,4 +20,17 @@ public abstract class EventTest(ITestOutputHelper output) : WorkflowTest(output) Assert.NotNull(copy); return copy; } + + protected static void AssertMessage(ChatMessage source, ChatMessage copy) + { + Assert.Equal(source.Role, copy.Role); + Assert.Equal(source.Text, copy.Text); + Assert.Equal(source.Contents.Count, copy.Contents.Count); + } + + protected static TContent AssertContent(ChatMessage message) where TContent : AIContent + { + TContent[] contents = message.Contents.OfType().ToArray(); + return Assert.Single(contents); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputRequestTest.cs new file mode 100644 index 0000000000..49d06337fe --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputRequestTest.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; + +/// +/// Verify class +/// +public sealed class ExternalInputRequestTest(ITestOutputHelper output) : EventTest(output) +{ + [Fact] + public void VerifySerializationWithText() + { + // Arrange + ExternalInputRequest source = new(new AgentRunResponse(new ChatMessage(ChatRole.User, "Wassup?"))); + + // Act + ExternalInputRequest copy = VerifyEventSerialization(source); + + // Assert + ChatMessage messageCopy = Assert.Single(source.AgentResponse.Messages); + AssertMessage(messageCopy, copy.AgentResponse.Messages[0]); + } + + [Fact] + public void VerifySerializationWithRequests() + { + // Arrange + ExternalInputRequest source = + new(new AgentRunResponse( + new ChatMessage( + ChatRole.Assistant, + [ + new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")), + new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")), + new FunctionCallContent("call3", "myfunc"), + new TextContent("Heya"), + ]))); + + // Act + ExternalInputRequest copy = VerifyEventSerialization(source); + + // Assert + ChatMessage messageCopy = Assert.Single(source.AgentResponse.Messages); + Assert.Equal(messageCopy.Contents.Count, copy.AgentResponse.Messages[0].Contents.Count); + + McpServerToolApprovalRequestContent mcpRequest = AssertContent(messageCopy); + Assert.Equal("call1", mcpRequest.Id); + + FunctionApprovalRequestContent functionRequest = AssertContent(messageCopy); + Assert.Equal("call2", functionRequest.Id); + + FunctionCallContent functionCall = AssertContent(messageCopy); + Assert.Equal("call3", functionCall.CallId); + + TextContent textContent = AssertContent(messageCopy); + Assert.Equal("Heya", textContent.Text); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputResponseTest.cs new file mode 100644 index 0000000000..b1fb358727 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputResponseTest.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; + +/// +/// Verify class +/// +public sealed class ExternalInputResponseTest(ITestOutputHelper output) : EventTest(output) +{ + [Fact] + public void VerifySerializationEmpty() + { + // Arrange + ExternalInputResponse source = new(new ChatMessage(ChatRole.User, "Wassup?")); + + // Act + ExternalInputResponse copy = VerifyEventSerialization(source); + + // Assert + ChatMessage messageCopy = Assert.Single(source.Messages); + AssertMessage(messageCopy, copy.Messages[0]); + } + + [Fact] + public void VerifySerializationWithResponses() + { + // Arrange + ExternalInputResponse source = + new(new ChatMessage( + ChatRole.Assistant, + [ + new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")).CreateResponse(approved: true), + new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")).CreateResponse(approved: true), + new FunctionResultContent("call3", 33), + new TextContent("Heya"), + ])); + + // Act + ExternalInputResponse copy = VerifyEventSerialization(source); + + // Assert + ChatMessage responseMessage = Assert.Single(source.Messages); + Assert.Equal(responseMessage.Contents.Count, copy.Messages[0].Contents.Count); + + McpServerToolApprovalResponseContent mcpApproval = AssertContent(responseMessage); + Assert.Equal("call1", mcpApproval.Id); + + FunctionApprovalResponseContent functionApproval = AssertContent(responseMessage); + Assert.Equal("call2", functionApproval.Id); + + FunctionResultContent functionResult = AssertContent(responseMessage); + Assert.Equal("call3", functionResult.CallId); + + TextContent textContent = AssertContent(responseMessage); + Assert.Equal("Heya", textContent.Text); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputRequestTest.cs deleted file mode 100644 index c190e93193..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputRequestTest.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Microsoft.Extensions.AI; -using Xunit.Abstractions; - -namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; - -/// -/// Verify class -/// -public sealed class UserInputRequestTest(ITestOutputHelper output) : EventTest(output) -{ - [Fact] - public void VerifySerializationEmpty() - { - // Arrange & Act - UserInputRequest copy = VerifyEventSerialization(new UserInputRequest("test agent", [])); - - // Assert - Assert.Equal("test agent", copy.AgentName); - Assert.Empty(copy.InputRequests); - } - - [Fact] - public void VerifySerializationWithRequests() - { - // Arrange & Act - UserInputRequest copy = - VerifyEventSerialization( - new UserInputRequest( - "agent", - [ - new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")), - new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")), - ])); - - // Assert - Assert.Equal("agent", copy.AgentName); - Assert.Equal(2, copy.InputRequests.Count); - McpServerToolApprovalRequestContent mcpRequest = Assert.IsType(copy.InputRequests[0]); - Assert.Equal("call1", mcpRequest.Id); - FunctionApprovalRequestContent functionRequest = Assert.IsType(copy.InputRequests[1]); - Assert.Equal("call2", functionRequest.Id); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputResponseTest.cs deleted file mode 100644 index 2d36d01611..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputResponseTest.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Microsoft.Extensions.AI; -using Xunit.Abstractions; - -namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; - -/// -/// Verify class -/// -public sealed class UserInputResponseTest(ITestOutputHelper output) : EventTest(output) -{ - [Fact] - public void VerifySerializationEmpty() - { - // Arrange & Act - UserInputResponse copy = VerifyEventSerialization(new UserInputResponse("testagent", [])); - - // Assert - Assert.Equal("testagent", copy.AgentName); - Assert.Empty(copy.InputResponses); - } - - [Fact] - public void VerifySerializationWithResponses() - { - // Arrange & Act - UserInputResponse copy = - VerifyEventSerialization( - new UserInputResponse( - "agent", - [ - new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")).CreateResponse(approved: true), - new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")).CreateResponse(approved: true), - ])); - - // Assert - Assert.Equal("agent", copy.AgentName); - Assert.Equal(2, copy.InputResponses.Count); - McpServerToolApprovalResponseContent mcpResponse = Assert.IsType(copy.InputResponses[0]); - Assert.Equal("call1", mcpResponse.Id); - FunctionApprovalResponseContent functionResponse = Assert.IsType(copy.InputResponses[1]); - Assert.Equal("call2", functionResponse.Id); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageRequestTest.cs deleted file mode 100644 index db6ab351ce..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageRequestTest.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Xunit.Abstractions; - -namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; - -/// -/// Verify class -/// -public sealed class UserMessageRequestTest(ITestOutputHelper output) : EventTest(output) -{ - [Fact] - public void VerifySerialization() - { - // Arrange & Act - AnswerRequest copy = VerifyEventSerialization(new AnswerRequest("wassup")); - - // Assert - Assert.Equal("wassup", copy.Prompt); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageResponseTest.cs deleted file mode 100644 index efeef6052d..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageResponseTest.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Microsoft.Extensions.AI; -using Xunit.Abstractions; - -namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; - -/// -/// Verify class -/// -public sealed class UserMessageResponseTest(ITestOutputHelper output) : EventTest(output) -{ - [Fact] - public void VerifySerializationText() - { - // Arrange & Act - AnswerResponse copy = VerifyEventSerialization(new AnswerResponse("test response")); - - // Assert - Assert.Equal("test response", copy.Value.Text); - } - - [Fact] - public void VerifySerializationMessage() - { - // Arrange & Act - AnswerResponse copy = VerifyEventSerialization(new AnswerResponse(new ChatMessage(ChatRole.User, "test response"))); - - // Assert - Assert.Equal("test response", copy.Value.Text); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/JsonDocumentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/JsonDocumentExtensionsTests.cs index 8c27bf6c8c..bd86aa1958 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/JsonDocumentExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/JsonDocumentExtensionsTests.cs @@ -58,6 +58,38 @@ public sealed class JsonDocumentExtensionsTests Assert.Equal(expectedTimeSpan, result["time"]); } + [Fact] + public void ParseRecord_Object_NoSchema_Succeeds() + { + // Arrange + JsonDocument document = JsonDocument.Parse( + """ + { + "text": "hello", + "numberInt": 7, + "numberLong": 9223372036854775807, + "numberDecimal": 12.5, + "numberDouble": 3.99E99, + "flag": true, + "date": "2024-10-01T12:34:56Z", + "time": "12:34:56" + } + """); + + // Act + Dictionary result = document.ParseRecord(VariableType.RecordType); + + // Assert + Assert.Equal("hello", result["text"]); + Assert.Equal(7, result["numberInt"]); + Assert.Equal(9223372036854775807L, result["numberLong"]); + Assert.Equal(12.5m, result["numberDecimal"]); + Assert.Equal(3.99E99, result["numberDouble"]); + Assert.Equal(true, result["flag"]); + Assert.Equal("2024-10-01T12:34:56Z", result["date"]); + Assert.Equal("12:34:56", result["time"]); + } + [Fact] public void ParseRecord_Object_NestedRecord_Succeeds() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/TemplateExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/TemplateExtensionsTests.cs index 86591587c2..07421c9ecb 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/TemplateExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/TemplateExtensionsTests.cs @@ -14,8 +14,8 @@ public sealed class TemplateExtensionsTests { // Arrange RecalcEngine engine = new(); - IEnumerable template = new List - { + IEnumerable template = + [ new TemplateLine.Builder { Segments = @@ -24,7 +24,7 @@ public sealed class TemplateExtensionsTests new TextSegment.Builder { Value = "World" } } }.Build() - }; + ]; // Act string result = engine.Format(template); @@ -38,8 +38,8 @@ public sealed class TemplateExtensionsTests { // Arrange RecalcEngine engine = new(); - IEnumerable template = new List - { + IEnumerable template = + [ new TemplateLine.Builder { Segments = @@ -54,7 +54,7 @@ public sealed class TemplateExtensionsTests new TextSegment.Builder { Value = "Line 2" } } }.Build() - }; + ]; // Act string result = engine.Format(template); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndDialog.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CancelWorkflow.cs similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndDialog.cs rename to dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CancelWorkflow.cs diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CancelWorkflow.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CancelWorkflow.yaml new file mode 100644 index 0000000000..209e94f81b --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CancelWorkflow.yaml @@ -0,0 +1,13 @@ +kind: Workflow +trigger: + + kind: OnConversationStart + id: my_workflow + actions: + + - kind: CancelWorkflow + id: end_all + + - kind: SendActivity + id: send_activity_1 + activity: NEVER 1! diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CaseInsensitive.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CaseInsensitive.yaml index b6b76c6957..afdbee877a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CaseInsensitive.yaml +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/CaseInsensitive.yaml @@ -21,7 +21,7 @@ trigger: - id: condition_match condition: =Local.TestValue1 + Local.TestValue2 = 7 actions: - - kind: EndDialog + - kind: EndWorkflow id: end_when_match - kind: SendActivity diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndWorkflow.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndWorkflow.cs new file mode 100644 index 0000000000..d17ddeb3a6 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndWorkflow.cs @@ -0,0 +1,94 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// ------------------------------------------------------------------------------ + +#nullable enable +#pragma warning disable IDE0005 // Extra using directive is ok. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Agents.AI.Workflows.Declarative; +using Microsoft.Agents.AI.Workflows.Declarative.Kit; +using Microsoft.Extensions.AI; + +namespace Test.WorkflowProviders; + +/// +/// This class provides a factory method to create a instance. +/// +/// +/// The workflow defined here was generated from a declarative workflow definition. +/// Declarative workflows utilize Power FX for defining conditions and expressions. +/// To learn more about Power FX, see: +/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio +/// +public static class WorkflowProvider +{ + /// + /// The root executor for a declarative workflow. + /// + internal sealed class MyWorkflowRootExecutor( + DeclarativeWorkflowOptions options, + Func inputTransform) : + RootExecutor("my_workflow_Root", options, inputTransform) + where TInput : notnull + { + protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken) + { + } + } + + /// + /// Formats a message template and sends an activity event. + /// + internal sealed class SendActivity1Executor(FormulaSession session) : ActionExecutor(id: "send_activity_1", session) + { + // + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + string activityText = + await context.FormatTemplateAsync( + """ + NEVER 1! + """ + ); + AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]); + await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)).ConfigureAwait(false); + + return default; + } + } + + public static Workflow CreateWorkflow( + DeclarativeWorkflowOptions options, + Func? inputTransform = null) + where TInput : notnull + { + // Create root executor to initialize the workflow. + inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message); + MyWorkflowRootExecutor myWorkflowRoot = new(options, inputTransform); + DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session); + DelegateExecutor endAll = new(id: "end_all", myWorkflowRoot.Session); + DelegateExecutor endAllRestart = new(id: "end_all_Restart", myWorkflowRoot.Session); + SendActivity1Executor sendActivity1 = new(myWorkflowRoot.Session); + + // Define the workflow builder + WorkflowBuilder builder = new(myWorkflowRoot); + + // Connect executors + builder.AddEdge(myWorkflowRoot, myWorkflow); + builder.AddEdge(myWorkflow, endAll); + builder.AddEdge(endAllRestart, sendActivity1); + + // Build the workflow + return builder.Build(validateOrphans: false); + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndDialog.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndWorkflow.yaml similarity index 88% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndDialog.yaml rename to dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndWorkflow.yaml index e883b609d8..3aa0937667 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndDialog.yaml +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/EndWorkflow.yaml @@ -5,7 +5,7 @@ trigger: id: my_workflow actions: - - kind: EndDialog + - kind: EndWorkflow id: end_all - kind: SendActivity diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/InvokeAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/InvokeAgent.cs index efb5b29719..08d324cbcd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/InvokeAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Workflows/InvokeAgent.cs @@ -68,7 +68,6 @@ public static class WorkflowProvider string? conversationId = await context.ReadStateAsync(key: "ConversationId", scopeName: "System").ConfigureAwait(false); bool autoSend = true; - string? additionalInstructions = null; IList? inputMessages = await context.EvaluateListAsync("[UserMessage(System.LastMessageText)]").ConfigureAwait(false); AgentRunResponse agentResponse = @@ -77,7 +76,6 @@ public static class WorkflowProvider agentName, conversationId, autoSend, - additionalInstructions, inputMessages, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InProcessExecutionTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InProcessExecutionTests.cs index ac4f8a5d95..cffdb8c73c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InProcessExecutionTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InProcessExecutionTests.cs @@ -66,7 +66,7 @@ public class InProcessExecutionTests messageSent.Should().BeTrue("TurnToken should be accepted"); // Collect events - List events = new(); + List events = []; await foreach (WorkflowEvent evt in run.WatchStreamAsync()) { events.Add(evt); diff --git a/workflow-samples/HumanInLoop.yaml b/workflow-samples/ConfirmInput.yaml similarity index 88% rename from workflow-samples/HumanInLoop.yaml rename to workflow-samples/ConfirmInput.yaml index 11c63adb5e..9347a540da 100644 --- a/workflow-samples/HumanInLoop.yaml +++ b/workflow-samples/ConfirmInput.yaml @@ -1,8 +1,8 @@ # -# This workflow demonstrates a single agent interaction based on user input. +# This workflow demonstrates how to use the Question action +# to request user input and confirm it matches the original input. # -# Any Foundry Agent may be used to provide the response. -# See: ./setup/QuestionAgent.yaml +# Note: This workflow doesn't make use of any agents. # kind: Workflow trigger: diff --git a/workflow-samples/DeepResearch.yaml b/workflow-samples/DeepResearch.yaml index be84e92bfb..4e13b5b9f3 100644 --- a/workflow-samples/DeepResearch.yaml +++ b/workflow-samples/DeepResearch.yaml @@ -2,33 +2,18 @@ # This workflow coordinates multiple agents in order to address complex user requests # according to the "Magentic" orchestration pattern introduced by AutoGen. # -# For this workflow, several agents used, each with a prompt specific to their role: +# For this workflow, several agents used, each with specific roles. # -# 1. Analyst Agent: Able to analyze the current task. -# Enable "Bing Grounding Tool" in the agent settings. -# See: ./setup/AnalystAgent.yaml -# -# 2. Manager Agent: Able to create plans and delegate tasks to other agents. -# See: ./setup/ManagerAgent.yaml -# -# 3. Research Agent: -# Enable "Bing Grounding" in the agent settings. -# See: ./setup/WebAgent.yaml +# The following agents are responsible for overseeing and coordinating the workflow: +# - Research Agent: Analyze the current task and correlate relevant facts. +# - Planner Agent: Analyze the current task and devise an overall plan. +# - Manager Agent: Evaluates status and delegate tasks to other agents. +# - Summary Agent: Evaluates status and delegate tasks to other agents. # -# With instructions: -# -# Only provide requested information in a way that is throughfully organized and formatted. -# Never include any analysis or code. -# Never generate a file. -# Avoid repeating yourself. -# -# 4. Coder Agent: -# Enable "Code Interpreter" in the agent settings. -# See: ./setup/CoderAgent.yaml -# -# 5. Weather Agent: Able to retrieve factual information from the web. -# Enable "Open API" in the agent settings using the wttr.json schema. -# See: ./setup/WeatherAgent.yaml +# The following agents have capabilities that are utilized to address the input task: +# - Knowledge Agent: Performs generic web searches. +# - Coder Agent: Able to write and execute code. +# - Weather Agent: Provides weather information. # kind: Workflow trigger: @@ -45,18 +30,15 @@ trigger: =[ { name: "WeatherAgent", - description: "Able to retrieve weather information", - agentid: Env.FOUNDRY_AGENT_RESEARCHWEATHER + description: "Able to retrieve weather information" }, { name: "CoderAgent", - description: "Able to write and execute Python code", - agentid: Env.FOUNDRY_AGENT_RESEARCHCODER + description: "Able to write and execute Python code" }, { - name: "WebAgent", - description: "Able to perform generic websearches", - agentid: Env.FOUNDRY_AGENT_RESEARCHWEB + name: "KnowledgeAgent", + description: "Able to perform generic websearches" } ] @@ -84,38 +66,22 @@ trigger: - kind: CreateConversation id: conversation_1a2b3c - conversationId: Local.InternalConversationId + conversationId: Local.StatusConversationId + + - kind: CreateConversation + id: conversation_1x2y3z + conversationId: Local.TaskConversationId - kind: InvokeAzureAgent id: question_UDoMUw displayName: Get Facts - conversationId: =Local.InternalConversationId + conversationId: =Local.StatusConversationId agent: - name: =Env.FOUNDRY_AGENT_RESEARCHANALYST + name: ResearchAgent output: - autoSend: false messages: Local.TaskFacts input: 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. - - Here is the pre-survey: - - 1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none. - 2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself. - 3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation) - 4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc. - - When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings: - - 1. GIVEN OR VERIFIED FACTS - 2. FACTS TO LOOK UP - 3. FACTS TO DERIVE - 4. EDUCATED GUESSES - - DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so. - kind: SendActivity id: sendActivity_yFsbRz @@ -124,52 +90,42 @@ trigger: - kind: InvokeAzureAgent id: question_DsBaJU displayName: Create a Plan - conversationId: =Local.InternalConversationId + conversationId: =Local.StatusConversationId agent: - name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER + name: PlannerAgent + inputs: + arguments: + team: =Local.TeamDescription output: - autoSend: false messages: Local.Plan - input: - 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. - Only select the following team which is listed as "- [Name]: [Description]" - - {Local.TeamDescription} - - The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]" - - Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task. - - - kind: SetVariable + - kind: SetTextVariable id: setVariable_Kk2LDL displayName: Define instructions variable: Local.TaskInstructions value: |- - ="# TASK + # TASK Address the following user request: - " & Local.InputTask & " + {Local.InputTask} # TEAM Use the following team to answer this request: - " & Local.TeamDescription & " + {Local.TeamDescription} # FACTS Consider this initial fact sheet: - " & Trim(Last(Local.TaskFacts).Text) & " + {Trim(Last(Local.TaskFacts).Text)} # PLAN Here is the plan to follow as best as possible: - " & Last(Local.Plan).Text + {Last(Local.Plan).Text} - kind: SendActivity id: sendActivity_bwNZiM @@ -178,131 +134,41 @@ trigger: - kind: InvokeAzureAgent id: question_o3BQkf displayName: Progress Ledger Prompt - conversationId: =Local.InternalConversationId + conversationId: =Local.StatusConversationId agent: - name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER - output: - autoSend: false - messages: Local.ProgressLedgerUpdate + name: ManagerAgent input: messages: =UserMessage(Local.AgentResponseText) - additionalInstructions: |- - Recall we are working on the following request: - - {Local.InputTask} - - And we have assembled the following team: - - {Local.TeamDescription} - - To make progress on the request, please answer the following questions, including necessary reasoning: - - - Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed) - - Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times. - - Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file) - - Who should speak next? (select from: {Concat(Local.AvailableAgents, name, ",")}) - - What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need) - - Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA: - - {{ - "is_request_satisfied": {{ - "reason": string, - "answer": boolean - }}, - "is_in_loop": {{ - "reason": string, - "answer": boolean - }}, - "is_progress_being_made": {{ - "reason": string, - "answer": boolean - }}, - "next_speaker": {{ - "reason": string, - "answer": string (select from: {Concat(Local.AvailableAgents, name, ",")}) - }}, - "instruction_or_question": {{ - "reason": string, - "answer": string - }} - }} - - - kind: ParseValue - id: parse_rNZtlV - displayName: Parse ledger response - variable: Local.TypedProgressLedger - value: =Last(Local.ProgressLedgerUpdate).Text - valueType: - kind: Record - properties: - instruction_or_question: - type: - kind: Record - properties: - answer: String - reason: String - - is_in_loop: - type: - kind: Record - properties: - answer: Boolean - reason: String - - is_progress_being_made: - type: - kind: Record - properties: - answer: Boolean - reason: String - - is_request_satisfied: - type: - kind: Record - properties: - answer: Boolean - reason: String - - next_speaker: - type: - kind: Record - properties: - answer: String - reason: String + output: + responseObject: Local.ProgressLedger - kind: ConditionGroup id: conditionGroup_mVIecC conditions: - id: conditionItem_fj432c - condition: =Local.TypedProgressLedger.is_request_satisfied.answer + condition: =Local.ProgressLedger.is_request_satisfied.answer displayName: If Done actions: - kind: SendActivity id: sendActivity_kdl3mC - activity: Completed! {Local.TypedProgressLedger.is_request_satisfied.reason} + activity: Completed! {Local.ProgressLedger.is_request_satisfied.reason} - kind: InvokeAzureAgent id: question_Ke3l1d displayName: Generate Response - conversationId: =System.ConversationId + conversationId: =Local.TaskConversationId agent: - name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER + name: SummaryAgent output: + autoSend: true messages: Local.FinalResponse - input: - 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. - The user will only every see this last response and not the entire conversation, so please ensure it is complete and self-contained. - kind: EndConversation id: end_SVoNSV - id: conditionItem_yiqund - condition: =Local.TypedProgressLedger.is_in_loop.answer || Not(Local.TypedProgressLedger.is_progress_being_made.answer) + condition: =Local.ProgressLedger.is_in_loop.answer || Not(Local.ProgressLedger.is_progress_being_made.answer) displayName: If Stalling actions: @@ -312,26 +178,25 @@ trigger: variable: Local.StallCount value: =Local.StallCount + 1 - - kind: ConditionGroup id: conditionGroup_vBTQd3 conditions: - id: conditionItem_fpaNL9 - condition: =Local.TypedProgressLedger.is_in_loop.answer + condition: =Local.ProgressLedger.is_in_loop.answer displayName: Is Loop actions: - kind: SendActivity id: sendActivity_fpaNL9 - activity: {Local.TypedProgressLedger.is_in_loop.reason} + activity: {Local.ProgressLedger.is_in_loop.reason} - id: conditionItem_NnqvXh - condition: =Not(Local.TypedProgressLedger.is_progress_being_made.answer) + condition: =Not(Local.ProgressLedger.is_progress_being_made.answer) displayName: Is No Progress actions: - kind: SendActivity id: sendActivity_NnqvXh - activity: {Local.TypedProgressLedger.is_progress_being_made.reason} + activity: {Local.ProgressLedger.is_progress_being_made.reason} - kind: ConditionGroup @@ -366,28 +231,23 @@ trigger: - kind: InvokeAzureAgent id: question_wFJ123 displayName: Get New Facts Prompt - conversationId: =Local.InternalConversationId + conversationId: =Local.StatusConversationId agent: - name: =Env.FOUNDRY_AGENT_RESEARCHANALYST + name: ResearchAgent output: - autoSend: false messages: Local.TaskFacts input: messages: |- =UserMessage( - "As a reminder, we are working to solve the following task: + "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. + Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc. + Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited. + This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning. - " & 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. - Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc. - Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited. - This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning. + Here is the old fact sheet: - Here is the old fact sheet: - - {Local.TaskFacts} + {Local.TaskFacts}" - kind: SendActivity id: sendActivity_dsBaJU @@ -396,48 +256,48 @@ trigger: - kind: InvokeAzureAgent id: question_uEJ456 displayName: Create new Plan Prompt - conversationId: =Local.InternalConversationId + conversationId: =Local.StatusConversationId agent: - name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER + name: PlannerAgent output: - autoSend: false messages: Local.Plan input: - additionalInstructions: |- - Please briefly explain what went wrong on this last run (the root cause of the failure), - and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes. - As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition - (do not involve any other outside people since we cannot contact anyone else): + messages: |- + =UserMessage( + "Please briefly explain what went wrong on this last run (the root cause of the failure), + and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes. + As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition + (do not involve any other outside people since we cannot contact anyone else): - {Local.TeamDescription} + {Local.TeamDescription}") - - kind: SetVariable + - kind: SetTextVariable id: setVariable_jW7tmM displayName: Set Plan as Context variable: Local.TaskInstructions value: |- - ="# TASK + # TASK Address the following user request: - " & Local.InputTask & " + {Local.InputTask} # TEAM Use the following team to answer this request: - " & Local.TeamDescription & " + {Local.TeamDescription} # FACTS Consider this initial fact sheet: - " & Local.TaskFacts.Text & " + {Local.TaskFacts.Text} # PLAN Here is the plan to follow as best as possible: - - " & Local.Plan.Text + + {Local.Plan.Text} - kind: SetVariable id: setVariable_6J2snP @@ -459,19 +319,14 @@ trigger: - kind: SendActivity id: sendActivity_L7ooQO activity: |- - ({Local.TypedProgressLedger.next_speaker.reason}) + ({Local.ProgressLedger.next_speaker.reason}) - {Local.TypedProgressLedger.next_speaker.answer} - {Local.TypedProgressLedger.instruction_or_question.answer} - - - kind: SetVariable - id: setVariable_L7ooQO - variable: Local.StallCount - value: 0 + {Local.ProgressLedger.next_speaker.answer} - {Local.ProgressLedger.instruction_or_question.answer} - kind: SetVariable id: setVariable_nxN1mE variable: Local.NextSpeaker - value: =Search(Local.AvailableAgents, Local.TypedProgressLedger.next_speaker.answer, name) + value: =Search(Local.AvailableAgents, Local.ProgressLedger.next_speaker.answer, name) - kind: ConditionGroup id: conditionGroup_QFPiF5 @@ -480,19 +335,23 @@ trigger: condition: =CountRows(Local.NextSpeaker) = 1 displayName: If next Agent tool Exists actions: + + - kind: SetVariable + id: setVariable_L7ooQO + variable: Local.StallCount + value: 0 - kind: InvokeAzureAgent id: question_orsBf06 displayName: Progress Ledger Prompt - conversationId: =System.ConversationId + conversationId: =Local.TaskConversationId agent: - name: =First(Local.NextSpeaker).agentid + name: =First(Local.NextSpeaker).name output: + autoSend: true messages: Local.AgentResponse input: - messages: =Local.SeedTask - additionalInstructions: |- - {Local.TypedProgressLedger.instruction_or_question.answer} + messages: =UserMessage(Local.ProgressLedger.instruction_or_question.answer) - kind: SetVariable id: setVariable_XzNrdM diff --git a/workflow-samples/Marketing.yaml b/workflow-samples/Marketing.yaml index 2bdd9f3c4a..9fcafa717d 100644 --- a/workflow-samples/Marketing.yaml +++ b/workflow-samples/Marketing.yaml @@ -4,9 +4,6 @@ # Example input: # An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours. # -# Any Foundry Agent may be used to provide the response. -# See: ./setup/QuestionAgent.yaml -# kind: Workflow trigger: @@ -18,31 +15,16 @@ trigger: id: invoke_analyst conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_ANSWER - input: - additionalInstructions: |- - You are a marketing analyst. Given a product description, identify: - - Key features - - Target audience - - Unique selling points + name: AnalystAgent - kind: InvokeAzureAgent id: invoke_writer conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_ANSWER - input: - additionalInstructions: |- - You are a marketing copywriter. Given a block of text describing features, audience, and USPs, - compose a compelling marketing copy (like a newsletter section) that highlights these points. - Output should be short (around 150 words), output just the copy as a single text block. + name: WriterAgent - kind: InvokeAzureAgent id: invoke_editor conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_ANSWER - input: - additionalInstructions: |- - You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone, - give format and make it polished. Output the final improved copy as a single text block. \ No newline at end of file + name: EditorAgent diff --git a/workflow-samples/MathChat.yaml b/workflow-samples/MathChat.yaml index b3673e9baa..855d968546 100644 --- a/workflow-samples/MathChat.yaml +++ b/workflow-samples/MathChat.yaml @@ -2,27 +2,8 @@ # This workflow demonstrates conversation between two agents: a student and a teacher. # The student attempts to solve the input problem and the teacher provides guidance. # -# For this workflow, two agents are used, each with a prompt specific to their role. -# -# Student: -# See: ./setup/StudentAgent.yaml -# -# With instructions: -# -# Your job is help a math teacher practice teaching by making intentional mistakes. -# You Attempt to solve the given math problem, but with intentional mistakes so the teacher can help. -# Always incorporate the teacher's advice to fix your next response. -# You have the math-skills of a 6th grader. - -# Teacher: -# See: ./setup/TeacherAgent.yaml -# -# With instructions: -# -# Review and coach the student's approach to solving the given math problem. -# Don't repeat the solution or try and solve it. -# If the student has demonstrated comprehension and responded to all of your feedback, -# give the student your congratulations by using the word "congratulations". +# Example input: +# How would you compute the value of PI? # kind: Workflow trigger: @@ -35,13 +16,13 @@ trigger: id: question_student conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_STUDENT + name: StudentAgent - kind: InvokeAzureAgent id: question_teacher conversationId: =System.ConversationId agent: - name: =Env.FOUNDRY_AGENT_TEACHER + name: TeacherAgent output: messages: Local.TeacherResponse diff --git a/workflow-samples/README.md b/workflow-samples/README.md index c5937760c2..539ce1a253 100644 --- a/workflow-samples/README.md +++ b/workflow-samples/README.md @@ -1,28 +1,17 @@ # Declarative Workflows -This folder contains sample workflow definitions than be ran using the -[Declarative Workflow](../dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow) demo. +A _Declarative Workflow_ is defined as a single YAML file and +may be executed locally no different from any regular `Workflow` that is defined by code. -Each workflow is defined in a single YAML file and contains -comments with additional information specific to that workflow. - -A _Declarative Workflow_ may be executed locally no different from any `Workflow` defined by code. -The difference is that the workflow definition is loaded from a YAML file instead of being defined in code. +The difference is that the workflow definition is loaded from a YAML file instead of being defined in code: ```c# -Workflow workflow = DeclarativeWorkflowBuilder.Build("HelloWorld.yaml", options); +Workflow workflow = DeclarativeWorkflowBuilder.Build("HelloWorld.yaml", options); ``` -Workflows may also be hosted in your _Azure Foundry Project_. +These example workflows may be executed by the workflow +[Samples](../dotnet/samples/GettingStarted/Workflows/Declarative) +that are present in this repository. -> _Python_ support in the works! - -#### Agents - -The sample workflows rely on agents defined in your Azure Foundry Project. - -To create agents, run the [`Create.ps1`](./setup) script. -This will create the agents used in the sample workflows in your Azure Foundry Project and format a script you can copy and use to configure your environment. - -> Note: `Create.ps1` relies upon the `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_DEPLOYMENT_NAME`, and `FOUNDRY_CONNECTION_GROUNDING_TOOL` settings. -See [README.md](../dotnet/samples/GettingStarted/Workflows/Declarative/README.md) from the demo for configuration details. +> See the [README.md](../dotnet/samples/GettingStarted/Workflows/Declarative/README.md) + associated with the samples for configuration details. diff --git a/workflow-samples/setup/.gitignore b/workflow-samples/setup/.gitignore deleted file mode 100644 index ce1409abe9..0000000000 --- a/workflow-samples/setup/.gitignore +++ /dev/null @@ -1,405 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -[Aa][Rr][Mm]64[Ee][Cc]/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# AWS SAM Build and Temporary Artifacts folder -.aws-sam - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml \ No newline at end of file diff --git a/workflow-samples/setup/AnalystAgent.yaml b/workflow-samples/setup/AnalystAgent.yaml deleted file mode 100644 index dbe6ba4b7a..0000000000 --- a/workflow-samples/setup/AnalystAgent.yaml +++ /dev/null @@ -1,10 +0,0 @@ -type: foundry_agent -name: ResearchAnalyst -description: Demo agent for DeepResearch workflow -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} -tools: - - type: bing_grounding - options: - tool_connections: - - ${FOUNDRY_CONNECTION_GROUNDING_TOOL} \ No newline at end of file diff --git a/workflow-samples/setup/CoderAgent.yaml b/workflow-samples/setup/CoderAgent.yaml deleted file mode 100644 index 4d6e06b34c..0000000000 --- a/workflow-samples/setup/CoderAgent.yaml +++ /dev/null @@ -1,7 +0,0 @@ -type: foundry_agent -name: ResearchCoder -description: Demo agent for DeepResearch workflow -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} -tools: - - type: code_interpreter diff --git a/workflow-samples/setup/Create.ps1 b/workflow-samples/setup/Create.ps1 deleted file mode 100644 index 17c7f27fd0..0000000000 --- a/workflow-samples/setup/Create.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -pushd ./CreateAgents -dotnet run -popd \ No newline at end of file diff --git a/workflow-samples/setup/CreateAgents/CreateAgents.csproj b/workflow-samples/setup/CreateAgents/CreateAgents.csproj deleted file mode 100644 index 4b00b2279f..0000000000 --- a/workflow-samples/setup/CreateAgents/CreateAgents.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - Exe - net9.0 - enable - enable - 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 - SKEXP0110 - - - - - - - - - - - diff --git a/workflow-samples/setup/CreateAgents/CreateAgents.slnx b/workflow-samples/setup/CreateAgents/CreateAgents.slnx deleted file mode 100644 index 7ff049246b..0000000000 --- a/workflow-samples/setup/CreateAgents/CreateAgents.slnx +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/workflow-samples/setup/CreateAgents/Program.cs b/workflow-samples/setup/CreateAgents/Program.cs deleted file mode 100644 index ebd9243c50..0000000000 --- a/workflow-samples/setup/CreateAgents/Program.cs +++ /dev/null @@ -1,68 +0,0 @@ -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; -using Microsoft.SemanticKernel.Agents.AzureAI; -using System.Reflection; -using System.Text; - -// Define FOUNDRY_PROJECT_ENDPOINT as a user-secret or environment variable that -// points to your Foundry project endpoint. - -IConfigurationRoot config = - new ConfigurationBuilder() - .AddUserSecrets(Assembly.GetExecutingAssembly()) - .AddEnvironmentVariables() - .Build(); - -string projectEndpoint = config["FOUNDRY_PROJECT_ENDPOINT"] ?? throw new InvalidOperationException("Undefined configuration: FOUNDRY_PROJECT_ENDPOINT"); -Console.WriteLine($"{Environment.NewLine}Foundry: {projectEndpoint}"); - -StringBuilder scriptBuilder = new(); -StringBuilder secretBuilder = new(); -string[] files = args.Length > 0 ? args : Directory.GetFiles(@"..\", "*.yaml"); -foreach (string file in files) -{ - string agentText = await File.ReadAllTextAsync(file); - - PersistentAgentsClient clientAgents = new(projectEndpoint, new AzureCliCredential()); - - AIProjectClient clientProject = new(new Uri(projectEndpoint), new AzureCliCredential()); - - IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); - kernelBuilder.Services.AddSingleton(clientAgents); - kernelBuilder.Services.AddSingleton(clientProject); - Kernel kernel = kernelBuilder.Build(); - - AzureAIAgentFactory factory = new(); - Agent? agent = await factory.CreateAgentFromYamlAsync(agentText, new AgentCreationOptions() { Kernel = kernel }, config); - if (agent is null) - { - Console.WriteLine("Unexpected failure creating agent..."); - continue; - } - - Console.WriteLine(); - Console.WriteLine(Path.GetFileName(file)); - Console.WriteLine($" Id: {agent?.Id ?? "???"}"); - Console.WriteLine($" Name: {agent?.Name ?? agent?.Id}"); - Console.WriteLine($" Note: {agent?.Description}"); - - scriptBuilder.AppendLine($"$env:FOUNDRY_AGENT_{agent?.Name?.ToUpperInvariant()} = '{agent?.Id}'"); - secretBuilder.AppendLine($"dotnet user-secrets set FOUNDRY_AGENT_{agent?.Name?.ToUpperInvariant()} {agent?.Id}"); -} - -Console.WriteLine(); -Console.WriteLine(); -Console.WriteLine("To set these environment variables in your shell, run:"); -Console.WriteLine(); -Console.WriteLine(scriptBuilder); -Console.WriteLine(); -Console.WriteLine(); -Console.WriteLine("To define user secrets, run:"); -Console.WriteLine(); -Console.WriteLine(secretBuilder); -Console.WriteLine(); diff --git a/workflow-samples/setup/CreateAgents/nuget.config b/workflow-samples/setup/CreateAgents/nuget.config deleted file mode 100644 index d4475cea1b..0000000000 --- a/workflow-samples/setup/CreateAgents/nuget.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/workflow-samples/setup/ManagerAgent.yaml b/workflow-samples/setup/ManagerAgent.yaml deleted file mode 100644 index 9402c4b922..0000000000 --- a/workflow-samples/setup/ManagerAgent.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: foundry_agent -name: ResearchManager -description: Demo agent for DeepResearch workflow -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} diff --git a/workflow-samples/setup/QuestionAgent.yaml b/workflow-samples/setup/QuestionAgent.yaml deleted file mode 100644 index 767a656138..0000000000 --- a/workflow-samples/setup/QuestionAgent.yaml +++ /dev/null @@ -1,10 +0,0 @@ -type: foundry_agent -name: Answer -description: Demo agent for Question workflow -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} -tools: - - type: bing_grounding - options: - tool_connections: - - ${FOUNDRY_CONNECTION_GROUNDING_TOOL} \ No newline at end of file diff --git a/workflow-samples/setup/README.md b/workflow-samples/setup/README.md deleted file mode 100644 index 2a43e776bb..0000000000 --- a/workflow-samples/setup/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Agent Definitions - -The sample workflows rely on agents defined in your Azure Foundry Project. - -These agent definitions are based on _Semantic Kernel_'s _Declarative Agent_ feature: - -- [Semantic Kernel Agents](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/Agents) -- [Declarative Agent Extensions](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/Agents/Yaml) -- [Sample](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs) - -To create agents, run the [`Create.ps1`](./Create.ps1) script. -This will create the agents for the sample workflows in your Azure Foundry Project and format a script you can copy and use to configure your environment. - -> Note: `Create.ps1` relies upon the `FOUNDRY_PROJECT_ENDPOINT` setting. See [README.md](../../dotnet/samples/GettingStarted/Workflows/Declarative/README.md) from the demo for configuration details. diff --git a/workflow-samples/setup/StudentAgent.yaml b/workflow-samples/setup/StudentAgent.yaml deleted file mode 100644 index 990befba79..0000000000 --- a/workflow-samples/setup/StudentAgent.yaml +++ /dev/null @@ -1,10 +0,0 @@ -type: foundry_agent -name: Student -description: Student agent for MathChat workflow -instructions: |- - Your job is help a math teacher practice teaching by making intentional mistakes. - You Attempt to solve the given math problem, but with intentional mistakes so the teacher can help. - Always incorporate the teacher's advice to fix your next response. - You have the math-skills of a 6th grader. -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} diff --git a/workflow-samples/setup/TeacherAgent.yaml b/workflow-samples/setup/TeacherAgent.yaml deleted file mode 100644 index d120c9cf3c..0000000000 --- a/workflow-samples/setup/TeacherAgent.yaml +++ /dev/null @@ -1,10 +0,0 @@ -type: foundry_agent -name: Teacher -description: Teacher agent for MathChat workflow -instructions: |- - Review and coach the student's approach to solving the given math problem. - Don't repeat the solution or try and solve it. - If the student has demonstrated comprehension and responded to all of your feedback, - give the student your congraluations by using the word "congratulations". -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} diff --git a/workflow-samples/setup/WeatherAgent.yaml b/workflow-samples/setup/WeatherAgent.yaml deleted file mode 100644 index ac6b413163..0000000000 --- a/workflow-samples/setup/WeatherAgent.yaml +++ /dev/null @@ -1,62 +0,0 @@ -type: foundry_agent -name: ResearchWeather -description: Demo agent for DeepResearch workflow -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} -tools: - - type: openapi - id: GetCurrentWeather - description: Retrieves current weather data for a location based on wttr.in. - options: - specification: | - { - "openapi": "3.1.0", - "info": { - "title": "Get weather data", - "description": "Retrieves current weather data for a location based on wttr.in.", - "version": "v1.0.0" - }, - "servers": [ - { - "url": "https://wttr.in" - } - ], - "paths": { - "/{location}": { - "get": { - "description": "Get weather information for a specific location", - "operationId": "GetCurrentWeather", - "parameters": [ - { - "name": "location", - "in": "path", - "description": "City or location to retrieve the weather for", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful response", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "404": { - "description": "Location not found" - } - }, - "deprecated": false - } - } - }, - "components": { - "schemas": {} - } - } diff --git a/workflow-samples/setup/WebAgent.yaml b/workflow-samples/setup/WebAgent.yaml deleted file mode 100644 index 283fd3c26c..0000000000 --- a/workflow-samples/setup/WebAgent.yaml +++ /dev/null @@ -1,15 +0,0 @@ -type: foundry_agent -name: ResearchWeb -description: Demo agent for DeepResearch workflow -instructions: |- - Only provide requested information in a way that is throughfully organized and formatted. - Never include any analysis or code. - Never generate a file. - Avoid repeating yourself. -model: - id: ${FOUNDRY_MODEL_DEPLOYMENT_NAME} -tools: - - type: bing_grounding - options: - tool_connections: - - ${FOUNDRY_CONNECTION_GROUNDING_TOOL} \ No newline at end of file