chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.Configuration;
using Shared.IntegrationTests;
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 const string InputArguments = "INPUTARGUMENTS";
public const string Vision = "VISION";
}
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),
Names.InputArguments => new PoemAgentProvider(configuration),
Names.Vision => new VisionAgentProvider(configuration),
_ => new TestAgentProvider(configuration),
};
public async ValueTask CreateAgentsAsync()
{
Uri foundryEndpoint = new(this.GetSetting(TestSettings.AzureAIProjectEndpoint));
await foreach (ProjectsAgentVersion agent in this.CreateAgentsAsync(foundryEndpoint))
{
Console.WriteLine($"Created agent: {agent.Name}:{agent.Version}");
}
}
protected abstract IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint);
protected string GetSetting(string settingName) =>
configuration[settingName] ??
throw new InvalidOperationException($"Undefined configuration setting: {settingName}");
}
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using OpenAI.Responses;
using Shared.Foundry;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class FunctionToolAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
MenuPlugin menuPlugin = new();
AIFunction[] functions =
[
AIFunctionFactory.Create(menuPlugin.GetMenu),
AIFunctionFactory.Create(menuPlugin.GetSpecials),
AIFunctionFactory.Create(menuPlugin.GetItemPrice),
];
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "MenuAgent",
agentDefinition: this.DefineMenuAgent(functions),
agentDescription: "Provides information about the restaurant menu");
}
private DeclarativeAgentDefinition DefineMenuAgent(AIFunction[] functions)
{
DeclarativeAgentDefinition agentDefinition =
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
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;
}
}
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.Configuration;
using Shared.Foundry;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class MarketingAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "AnalystAgent",
agentDefinition: this.DefineAnalystAgent(),
agentDescription: "Analyst agent for Marketing workflow");
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "WriterAgent",
agentDefinition: this.DefineWriterAgent(),
agentDescription: "Writer agent for Marketing workflow");
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "EditorAgent",
agentDefinition: this.DefineEditorAgent(),
agentDescription: "Editor agent for Marketing workflow");
}
private DeclarativeAgentDefinition DefineAnalystAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
"""
You are a marketing analyst. Given a product description, identify:
- Key features
- Target audience
- Unique selling points
""",
Tools =
{
//ProjectsAgentTool.CreateBingGroundingTool( // TODO: Use Bing Grounding when available
// new BingGroundingSearchToolParameters(
// [new BingGroundingSearchConfiguration(this.GetSetting(Settings.FoundryGroundingTool))]))
}
};
private DeclarativeAgentDefinition DefineWriterAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
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 DeclarativeAgentDefinition DefineEditorAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
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.
"""
};
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.Configuration;
using Shared.Foundry;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class MathChatAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "StudentAgent",
agentDefinition: this.DefineStudentAgent(),
agentDescription: "Student agent for MathChat workflow");
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "TeacherAgent",
agentDefinition: this.DefineTeacherAgent(),
agentDescription: "Teacher agent for MathChat workflow");
}
private DeclarativeAgentDefinition DefineStudentAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
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 DeclarativeAgentDefinition DefineTeacherAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
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".
"""
};
}
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
#pragma warning disable CA1822
public sealed class MenuPlugin
{
public IEnumerable<AIFunction> GetTools()
{
yield return AIFunctionFactory.Create(this.GetMenu);
yield return AIFunctionFactory.Create(this.GetSpecials);
yield return AIFunctionFactory.Create(this.GetItemPrice);
}
[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; }
}
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.Configuration;
using Shared.Foundry;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class PoemAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "PoemAgent",
agentDefinition: this.DefinePoemAgent(),
agentDescription: "Authors original poems");
}
private DeclarativeAgentDefinition DefinePoemAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
"""
Write a one verse poem on the requested topic in the style of: {{style}}.
""",
StructuredInputs =
{
["style"] =
new StructuredInputDefinition
{
IsRequired = false,
DefaultValue = BinaryData.FromString(@"""haiku"""),
Description = "The style of poem to write",
}
}
};
}
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.Configuration;
using Shared.Foundry;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class TestAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "TestAgent",
agentDefinition: this.DefineMenuAgent(),
agentDescription: "Basic agent");
}
private DeclarativeAgentDefinition DefineMenuAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName));
}
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Extensions.Configuration;
using Shared.Foundry;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
internal sealed class VisionAgentProvider(IConfiguration configuration) : AgentProvider(configuration)
{
protected override async IAsyncEnumerable<ProjectsAgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
{
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
yield return
await aiProjectClient.CreateAgentAsync(
agentName: "VisionAgent",
agentDefinition: this.DefineVisionAgent(),
agentDescription: "Use computer vision to describe an image or document.");
}
private DeclarativeAgentDefinition DefineVisionAgent() =>
new(this.GetSetting(TestSettings.AzureAIModelDeploymentName))
{
Instructions =
"""
Describe the image or document contained in the user request, if any;
otherwise, suggest that the user provide an image or document.
""",
};
}
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
using Microsoft.Extensions.AI;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
public sealed class AzureAgentProviderTest(ITestOutputHelper output) : IntegrationTest(output)
{
[Fact]
public async Task ConversationTestAsync()
{
// Arrange
AzureAgentProvider provider = new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
// Act
string conversationId = await provider.CreateConversationAsync();
// Assert
Assert.NotEmpty(conversationId);
// Arrange & Act
for (int index = 0; index < 3; ++index)
{
await provider.CreateMessageAsync(conversationId, new ChatMessage(ChatRole.User, $"Message #{index * 2}"));
await provider.CreateMessageAsync(conversationId, new ChatMessage(ChatRole.Assistant, $"Message #{(index * 2) + 1}"));
}
// Act
ChatMessage[] messages = await provider.GetMessagesAsync(conversationId).ToArrayAsync();
// Assert
Assert.Equal(6, messages.Length);
Assert.NotNull(messages[3].MessageId);
// Act
ChatMessage message = await provider.GetMessageAsync(conversationId, messages[3].MessageId!);
// Assert
Assert.NotNull(message);
Assert.Equal(messages[3].Text, message.Text);
}
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
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;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
/// <summary>
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowBuilder"/>.
/// </summary>
public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : WorkflowTest(output)
{
[Theory]
[InlineData("CheckSystem.yaml", "CheckSystem.json")]
[InlineData("ConversationMessages.yaml", "ConversationMessages.json")]
[InlineData("ConversationMessages.yaml", "ConversationMessages.json", true)]
[InlineData("InputArguments.yaml", "InputArguments.json")]
[InlineData("InvokeAgent.yaml", "InvokeAgent.json")]
[InlineData("InvokeAgent.yaml", "InvokeAgent.json", true)]
[InlineData("SendActivity.yaml", "SendActivity.json")]
public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) =>
this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample: false), testcaseFileName, externalConveration);
[Theory]
[InlineData("Marketing.yaml", "Marketing.json")]
[InlineData("Marketing.yaml", "Marketing.json", true)]
[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(GetWorkflowPath(workflowFileName, isSample: true), testcaseFileName, externalConveration);
[Theory]
[InlineData("ConfirmInput.yaml", "ConfirmInput.json", false)]
[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(), "declarative-agents", "workflow-samples", workflowFileName)
: Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName);
protected override async Task RunAndVerifyAsync<TInput>(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<TInput>(workflowPath, workflowOptions);
WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowPath));
WorkflowEvents workflowEvents = await harness.RunTestcaseAsync(testcase, input, useJsonCheckpoint).ConfigureAwait(false);
// Verify executor events are present
Assert.NotEmpty(workflowEvents.ExecutorInvokeEvents);
Assert.NotEmpty(workflowEvents.ExecutorCompleteEvents);
// Verify the associated conversations
AssertWorkflow.Conversation(workflowEvents.ConversationEvents, testcase);
// Verify the agent responses
AssertWorkflow.Responses(workflowEvents.AgentResponseEvents, testcase);
// Verify the messages on the workflow conversation
await AssertWorkflow.MessagesAsync(
GetConversationId(workflowOptions.ConversationId, workflowEvents.ConversationEvents),
testcase,
workflowOptions.AgentProvider);
// Verify action events
AssertWorkflow.EventCounts(workflowEvents.ActionInvokeEvents.Count, testcase);
AssertWorkflow.EventCounts(workflowEvents.ActionCompleteEvents.Count, testcase, isCompletion: true);
// Verify action sequences
AssertWorkflow.EventSequence(workflowEvents.ActionInvokeEvents.Select(e => e.ActionId), testcase);
}
}
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
/// <summary>
/// Base class for workflow tests.
/// </summary>
public abstract class IntegrationTest : IDisposable
{
protected IConfigurationRoot Configuration => field ??= InitializeConfig();
public Uri TestEndpoint { get; }
public TestOutputAdapter Output { get; }
protected IntegrationTest(ITestOutputHelper output)
{
this.Output = new TestOutputAdapter(output);
this.TestEndpoint =
new Uri(
this.Configuration?[TestSettings.AzureAIProjectEndpoint] ??
throw new InvalidOperationException($"Undefined configuration setting: {TestSettings.AzureAIProjectEndpoint}"));
Console.SetOut(this.Output);
SetProduct();
}
public void Dispose()
{
this.Dispose(isDisposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)
{
this.Output.Dispose();
}
}
protected static void SetProduct()
{
if (!ProductContext.IsLocalScopeSupported())
{
ProductContext.SetContext(Product.Foundry);
}
}
internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? WorkflowFormulaState.DefaultScopeName}.{variableName}";
protected async ValueTask<DeclarativeWorkflowOptions> CreateOptionsAsync(bool externalConversation = false, params IEnumerable<AIFunction> functionTools)
{
return await this.CreateOptionsAsync(externalConversation, mcpToolProvider: null, httpRequestHandler: null, functionTools).ConfigureAwait(false);
}
protected async ValueTask<DeclarativeWorkflowOptions> CreateOptionsAsync(bool externalConversation, IMcpToolHandler? mcpToolProvider, params IEnumerable<AIFunction> functionTools)
{
return await this.CreateOptionsAsync(externalConversation, mcpToolProvider, httpRequestHandler: null, functionTools).ConfigureAwait(false);
}
protected async ValueTask<DeclarativeWorkflowOptions> CreateOptionsAsync(bool externalConversation, IHttpRequestHandler? httpRequestHandler, params IEnumerable<AIFunction> functionTools)
{
return await this.CreateOptionsAsync(externalConversation, mcpToolProvider: null, httpRequestHandler, functionTools).ConfigureAwait(false);
}
protected async ValueTask<DeclarativeWorkflowOptions> CreateOptionsAsync(bool externalConversation, IMcpToolHandler? mcpToolProvider, IHttpRequestHandler? httpRequestHandler, params IEnumerable<AIFunction> functionTools)
{
AzureAgentProvider agentProvider =
new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential())
{
Functions = functionTools,
};
string? conversationId = null;
if (externalConversation)
{
conversationId = await agentProvider.CreateConversationAsync().ConfigureAwait(false);
}
return
new DeclarativeWorkflowOptions(agentProvider)
{
ConversationId = conversationId,
LoggerFactory = this.Output,
McpToolHandler = mcpToolProvider,
HttpRequestHandler = httpRequestHandler,
};
}
private static IConfigurationRoot InitializeConfig() =>
new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.Build();
}
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
public sealed class TestOutputAdapter(ITestOutputHelper output) : TextWriter, ILogger, ILoggerFactory
{
private readonly Stack<string> _scopes = [];
public override Encoding Encoding { get; } = Encoding.UTF8;
public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException();
public ILogger CreateLogger(string categoryName) => this;
public bool IsEnabled(LogLevel logLevel) => true;
public override void WriteLine(object? value) => this.SafeWrite($"{value}");
public override void WriteLine(string? format, params object?[] arg) => this.SafeWrite(string.Format(format ?? string.Empty, arg));
public override void WriteLine(string? value) => this.SafeWrite(value ?? string.Empty);
public override void Write(object? value) => this.SafeWrite($"{value}");
public override void Write(char[]? buffer) => this.SafeWrite(new string(buffer));
public IDisposable BeginScope<TState>(TState state) where TState : notnull
{
this._scopes.Push($"{state}");
return new LoggerScope(() => this._scopes.Pop());
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
string message = formatter(state, exception);
string scope = this._scopes.Count > 0 ? $"[{this._scopes.Peek()}] " : string.Empty;
output.WriteLine($"{scope}{message}");
}
private void SafeWrite(string value)
{
try
{
output.WriteLine(value ?? string.Empty);
}
catch (InvalidOperationException exception) when (exception.Message == "There is no currently active test.")
{
// This exception is thrown when the test output is accessed outside of a test context.
// We can ignore it since we are not in a test context.
}
}
private sealed class LoggerScope(Action action) : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (!this._disposed)
{
action.Invoke();
this._disposed = true;
}
}
}
}
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
public sealed class Testcase
{
[JsonConstructor]
public Testcase(string description, TestcaseSetup setup, TestcaseValidation validation)
{
this.Description = description;
this.Setup = setup;
this.Validation = validation;
}
public string Description { get; }
public TestcaseSetup Setup { get; }
public TestcaseValidation Validation { get; }
}
public sealed class TestcaseSetup
{
[JsonConstructor]
public TestcaseSetup(TestcaseInput input)
{
this.Input = input;
}
public TestcaseInput Input { get; }
public IList<TestcaseInput> Responses { get; init; } = [];
}
public sealed class TestcaseInput
{
[JsonConstructor]
public TestcaseInput(string type, string value)
{
this.Type = type;
this.Value = value;
}
public string Type { get; }
public string Value { get; }
}
public sealed class TestcaseValidation
{
[JsonConstructor]
public TestcaseValidation(int conversationCount, int minActionCount, int minResponseCount)
{
this.ConversationCount = conversationCount;
this.MinActionCount = minActionCount;
this.MinResponseCount = minResponseCount;
}
public TestcaseValidationActions Actions { get; init; } = TestcaseValidationActions.Empty;
public int ConversationCount { get; }
public int MinActionCount { get; }
// Default expectation is MinActionCount when not defined
public int? MaxActionCount { get; init; }
// Default expectation is MinResponseCount when not defined
public int? MinMessageCount { get; init; }
// Default expectation is MaxResponseCount when not defined
public int? MaxMessageCount { get; init; }
public int MinResponseCount { get; }
// Default expectation is MinResponseCount when not defined
public int? MaxResponseCount { get; init; }
}
public sealed class TestcaseValidationActions
{
public static TestcaseValidationActions Empty { get; } = new([]);
[JsonConstructor]
public TestcaseValidationActions(IList<string> start)
{
this.Start = start;
}
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public IList<string> Start { get; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public IList<string> Repeat { get; init; } = [];
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public IList<string> Final { get; init; } = [];
}
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
internal sealed class WorkflowEvents
{
public WorkflowEvents(IReadOnlyList<WorkflowEvent> workflowEvents)
{
this.Events = workflowEvents;
this.EventCounts = workflowEvents.GroupBy(e => e.GetType()).ToDictionary(e => e.Key, e => e.Count());
this.ActionInvokeEvents = workflowEvents.OfType<DeclarativeActionInvokedEvent>().ToList();
this.ActionCompleteEvents = workflowEvents.OfType<DeclarativeActionCompletedEvent>().ToList();
this.ConversationEvents = workflowEvents.OfType<ConversationUpdateEvent>().ToList();
this.ExecutorInvokeEvents = workflowEvents.OfType<ExecutorInvokedEvent>().ToList();
this.ExecutorCompleteEvents = workflowEvents.OfType<ExecutorCompletedEvent>().ToList();
this.InputEvents = workflowEvents.OfType<RequestInfoEvent>().ToList();
this.AgentResponseEvents = workflowEvents.OfType<AgentResponseEvent>().ToList();
}
public IReadOnlyList<WorkflowEvent> Events { get; }
public IReadOnlyDictionary<Type, int> EventCounts { get; }
public IReadOnlyList<ConversationUpdateEvent> ConversationEvents { get; }
public IReadOnlyList<DeclarativeActionInvokedEvent> ActionInvokeEvents { get; }
public IReadOnlyList<DeclarativeActionCompletedEvent> ActionCompleteEvents { get; }
public IReadOnlyList<ExecutorInvokedEvent> ExecutorInvokeEvents { get; }
public IReadOnlyList<ExecutorCompletedEvent> ExecutorCompleteEvents { get; }
public IReadOnlyList<RequestInfoEvent> InputEvents { get; }
public IReadOnlyList<AgentResponseEvent> AgentResponseEvents { get; }
}
@@ -0,0 +1,159 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Extensions.AI;
using Xunit.Sdk;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
internal sealed class WorkflowHarness(Workflow workflow, string runId)
{
private CheckpointManager? _checkpointManager;
private CheckpointInfo? _lastCheckpoint;
public async Task<WorkflowEvents> RunTestcaseAsync<TInput>(Testcase testcase, TInput input, bool useJson = false) where TInput : notnull
{
WorkflowEvents workflowEvents = await this.RunWorkflowAsync(input, useJson);
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;
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;
}
return workflowEvents;
}
public async Task<WorkflowEvents> RunWorkflowAsync<TInput>(TInput input, bool useJson = false) where TInput : notnull
{
Console.WriteLine("RUNNING WORKFLOW...");
StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input, this.GetCheckpointManager(useJson), runId);
IReadOnlyList<WorkflowEvent> workflowEvents = await MonitorAndDisposeWorkflowRunAsync(run).ToArrayAsync();
this._lastCheckpoint = workflowEvents.OfType<SuperStepCompletedEvent>().LastOrDefault()?.CompletionInfo?.Checkpoint;
return new WorkflowEvents(workflowEvents);
}
public async Task<WorkflowEvents> ResumeAsync(ExternalResponse response)
{
Console.WriteLine("\nRESUMING WORKFLOW...");
Assert.NotNull(this._lastCheckpoint);
StreamingRun run = await InProcessExecution.ResumeStreamingAsync(workflow, this._lastCheckpoint, this.GetCheckpointManager());
IReadOnlyList<WorkflowEvent> workflowEvents = await MonitorAndDisposeWorkflowRunAsync(run, response).ToArrayAsync();
this._lastCheckpoint = workflowEvents.OfType<SuperStepCompletedEvent>().LastOrDefault()?.CompletionInfo?.Checkpoint;
return new WorkflowEvents(workflowEvents);
}
private CheckpointManager GetCheckpointManager(bool useJson = false)
{
if (useJson && this._checkpointManager is null)
{
DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:yyMMdd-hhmmss-ff}"));
this._checkpointManager = CheckpointManager.CreateJson(new FileSystemJsonCheckpointStore(checkpointFolder));
}
else
{
this._checkpointManager ??= CheckpointManager.CreateInMemory();
}
return this._checkpointManager;
}
private static async IAsyncEnumerable<WorkflowEvent> MonitorAndDisposeWorkflowRunAsync(StreamingRun run, ExternalResponse? response = null)
{
await using IAsyncDisposable disposeRun = run;
if (response is not null)
{
await run.SendResponseAsync(response).ConfigureAwait(false);
}
bool exitLoop = false;
bool hasRequest = false;
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync().ConfigureAwait(false))
{
switch (workflowEvent)
{
case SuperStepCompletedEvent:
if (hasRequest)
{
exitLoop = true;
}
break;
case RequestInfoEvent requestInfo:
Console.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
// Only count as a new request if it's not the one we're responding to
if (response is null || requestInfo.Request.RequestId != response.RequestId)
{
hasRequest = true;
}
else
{
// This is a republished event for the request we're already responding to
// (emitted by RepublishUnservicedRequestsAsync during checkpoint resume).
// Skip yielding it so downstream code doesn't treat it as a new pending request.
continue;
}
break;
case ConversationUpdateEvent conversationEvent:
Console.WriteLine($"CONVERSATION: {conversationEvent.ConversationId}");
break;
case ExecutorFailedEvent failureEvent:
Console.WriteLine($"Executor failed [{failureEvent.ExecutorId}]: {failureEvent.Data?.Message ?? "Unknown"}");
break;
case WorkflowErrorEvent errorEvent:
throw errorEvent.Data as Exception ?? new XunitException("Unexpected failure...");
case ExecutorInvokedEvent executorInvokeEvent:
Console.WriteLine($"EXEC: {executorInvokeEvent.ExecutorId}");
break;
case DeclarativeActionInvokedEvent actionInvokeEvent:
Console.WriteLine($"ACTION: {actionInvokeEvent.ActionId} [{actionInvokeEvent.ActionType}]");
break;
case AgentResponseEvent responseEvent:
if (!string.IsNullOrEmpty(responseEvent.Response.Text))
{
Console.WriteLine($"AGENT: {responseEvent.Response.AgentId}: {responseEvent.Response.Text}");
}
else
{
foreach (FunctionCallContent toolCall in responseEvent.Response.Messages.SelectMany(m => m.Contents.OfType<FunctionCallContent>()))
{
Console.WriteLine($"TOOL: {toolCall.Name} [{responseEvent.Response.AgentId}]");
}
}
break;
}
yield return workflowEvent;
if (exitLoop)
{
break;
}
}
Console.WriteLine("SUSPENDING WORKFLOW...\n");
}
}
@@ -0,0 +1,216 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Xunit.Sdk;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
/// <summary>
/// Base class for workflow tests.
/// </summary>
public abstract class WorkflowTest(ITestOutputHelper output) : IntegrationTest(output)
{
protected abstract Task RunAndVerifyAsync<TInput>(
Testcase testcase,
string workflowPath,
DeclarativeWorkflowOptions workflowOptions,
TInput input,
bool useJsonCheckpoint) where TInput : notnull;
protected Task RunWorkflowAsync(
string workflowPath,
string testcaseFileName,
bool externalConversation = false,
bool useJsonCheckpoint = false)
{
this.Output.WriteLine($"WORKFLOW: {workflowPath}");
this.Output.WriteLine($"TESTCASE: {testcaseFileName}");
Testcase testcase = ReadTestcase(testcaseFileName);
this.Output.WriteLine($" {testcase.Description}");
return
testcase.Setup.Input.Type switch
{
nameof(ChatMessage) => TestWorkflowAsync<ChatMessage>(),
nameof(String) => TestWorkflowAsync<string>(),
_ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."),
};
async Task TestWorkflowAsync<TInput>() where TInput : notnull
{
this.Output.WriteLine($"INPUT: {testcase.Setup.Input.Value}");
DeclarativeWorkflowOptions workflowOptions = await this.CreateOptionsAsync(externalConversation).ConfigureAwait(false);
TInput input = (TInput)GetInput<TInput>(testcase);
await this.RunAndVerifyAsync(testcase, workflowPath, workflowOptions, input, useJsonCheckpoint);
}
}
protected static string? GetConversationId(string? conversationId, IReadOnlyList<ConversationUpdateEvent> conversationEvents)
{
if (!string.IsNullOrEmpty(conversationId))
{
return conversationId;
}
if (conversationEvents.Count > 0)
{
return conversationEvents.SingleOrDefault(conversationEvent => conversationEvent.IsWorkflow)?.ConversationId;
}
return null;
}
protected static Testcase ReadTestcase(string testcaseFileName)
{
string testcaseJson = File.ReadAllText(Path.Combine("Testcases", testcaseFileName));
Testcase? testcase = JsonSerializer.Deserialize<Testcase>(testcaseJson, s_jsonSerializerOptions);
Assert.NotNull(testcase);
return testcase;
}
private static object GetInput<TInput>(Testcase testcase) where TInput : notnull =>
testcase.Setup.Input.Type switch
{
nameof(ChatMessage) => new ChatMessage(ChatRole.User, testcase.Setup.Input.Value),
nameof(String) => testcase.Setup.Input.Value,
_ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."),
};
internal static string GetRepoFolder()
{
DirectoryInfo? current = new(Directory.GetCurrentDirectory());
while (current is not null)
{
if (Directory.Exists(Path.Combine(current.FullName, "declarative-agents", "workflow-samples")))
{
return current.FullName;
}
current = current.Parent;
}
throw new XunitException("Unable to locate repository root folder.");
}
protected static class AssertWorkflow
{
public static void Conversation(IReadOnlyList<ConversationUpdateEvent> conversationEvents, Testcase testcase)
{
Assert.Equal(testcase.Validation.ConversationCount, conversationEvents.Count);
}
// "isCompletion" adjusts validation logic to account for when condition completion is not experienced due to goto. Remove this test logic once addressed.
public static void EventCounts(int actualCount, Testcase testcase, bool isCompletion = false)
{
Assert.True(actualCount + (isCompletion ? 1 : 0) >= testcase.Validation.MinActionCount, $"Event count less than expected: {testcase.Validation.MinActionCount} (Actual: {actualCount}).");
if (testcase.Validation.MaxActionCount != -1)
{
int maxExpectedCount = testcase.Validation.MaxActionCount ?? testcase.Validation.MinActionCount;
Assert.True(actualCount <= maxExpectedCount, $"Event count greater than expected: {maxExpectedCount} (Actual: {actualCount}).");
}
}
public static void Responses(IReadOnlyList<AgentResponseEvent> responseEvents, Testcase testcase)
{
Assert.True(responseEvents.Count >= testcase.Validation.MinResponseCount, $"Response count less than expected: {testcase.Validation.MinResponseCount} (Actual: {responseEvents.Count})");
if (testcase.Validation.MaxResponseCount != -1)
{
int maxExpectedCount = testcase.Validation.MaxResponseCount ?? testcase.Validation.MinResponseCount;
Assert.True(responseEvents.Count <= maxExpectedCount, $"Response count greater than expected: {maxExpectedCount} (Actual: {responseEvents.Count}).");
}
}
public static async ValueTask MessagesAsync(string? conversationId, Testcase testcase, ResponseAgentProvider agentProvider)
{
int minExpectedCount = testcase.Validation.MinMessageCount ?? testcase.Validation.MinResponseCount;
int maxExpectedCount = testcase.Validation.MaxMessageCount ?? testcase.Validation.MaxResponseCount ?? minExpectedCount;
int messageCount = 0;
if (!string.IsNullOrEmpty(conversationId))
{
messageCount = await agentProvider.GetMessagesAsync(conversationId).CountAsync();
}
++minExpectedCount;
Assert.True(messageCount >= minExpectedCount, $"Workflow message count less than expected: {minExpectedCount} (Actual: {messageCount}).");
if (maxExpectedCount != -1)
{
++maxExpectedCount;
Assert.True(messageCount <= maxExpectedCount, $"Workflow message count greater than expected: {maxExpectedCount} (Actual: {messageCount}).");
}
}
internal static void EventSequence(IEnumerable<string> sourceIds, Testcase testcase)
{
string lastId = string.Empty;
Queue<string> startIds = [];
Queue<string> repeatIds = [];
bool validateStart = false;
bool validateRepeat = false;
foreach (string sourceId in sourceIds)
{
if (!validateStart && testcase.Validation.Actions.Start.Count > 0)
{
if (testcase.Validation.Actions.Start.Count > 0 &&
startIds.Count == 0 &&
sourceId.Equals(testcase.Validation.Actions.Start[0], StringComparison.Ordinal))
{
// Initialize start sequence
startIds = new(testcase.Validation.Actions.Start);
}
// Verify start sequence
if (startIds.Count > 0)
{
Assert.Equal(startIds.Dequeue(), sourceId);
validateStart = startIds.Count == 0;
}
}
else
{
if (testcase.Validation.Actions.Repeat.Count > 0 &&
repeatIds.Count == 0 &&
sourceId.Equals(testcase.Validation.Actions.Repeat[0], StringComparison.Ordinal))
{
// Initialize repeat sequence
repeatIds = new(testcase.Validation.Actions.Repeat);
}
// Verify repeat sequence
if (repeatIds.Count > 0)
{
Assert.Equal(repeatIds.Dequeue(), sourceId);
validateRepeat = true;
}
}
lastId = sourceId;
}
Assert.Equal(testcase.Validation.Actions.Start.Count > 0, validateStart);
Assert.Equal(testcase.Validation.Actions.Repeat.Count > 0, validateRepeat);
Assert.NotEmpty(lastId);
HashSet<string> finalIds = [.. testcase.Validation.Actions.Final];
Assert.Contains(lastId, finalIds);
}
}
protected static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
ReadCommentHandling = JsonCommentHandling.Skip,
WriteIndented = true,
};
}
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
/// <summary>
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowBuilder"/>.
/// </summary>
public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : IntegrationTest(output)
{
[Fact]
public Task ValidateAutoInvokeAsync() =>
this.RunWorkflowAsync(autoInvoke: true, new MenuPlugin().GetTools());
[Fact]
public Task ValidateRequestInvokeAsync() =>
this.RunWorkflowAsync(autoInvoke: false, new MenuPlugin().GetTools());
private static string GetWorkflowPath(string workflowFileName) => Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName);
private async Task RunWorkflowAsync(bool autoInvoke, params IEnumerable<AIFunction> functionTools)
{
AgentProvider agentProvider = AgentProvider.Create(this.Configuration, AgentProvider.Names.FunctionTool);
await agentProvider.CreateAgentsAsync().ConfigureAwait(false);
string workflowPath = GetWorkflowPath("FunctionTool.yaml");
Dictionary<string, AIFunction> functionMap = autoInvoke ? [] : functionTools.ToDictionary(tool => tool.Name, tool => tool);
DeclarativeWorkflowOptions workflowOptions = await this.CreateOptionsAsync(externalConversation: false, autoInvoke ? functionTools : []);
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(workflowPath, workflowOptions);
WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowPath));
WorkflowEvents workflowEvents = await harness.RunWorkflowAsync("hi!").ConfigureAwait(false);
int requestCount = (workflowEvents.InputEvents.Count + 1) / 2;
int responseCount = 0;
while (requestCount > responseCount)
{
Assert.False(autoInvoke);
RequestInfoEvent inputEvent = workflowEvents.InputEvents[workflowEvents.InputEvents.Count - 1];
ExternalInputRequest? toolRequest = inputEvent.Request.Data.As<ExternalInputRequest>();
Assert.NotNull(toolRequest);
List<(FunctionCallContent, AIFunction)> functionCalls = [];
foreach (FunctionCallContent functionCall in toolRequest.AgentResponse.Messages.SelectMany(message => message.Contents).OfType<FunctionCallContent>())
{
this.Output.WriteLine($"TOOL REQUEST: {functionCall.Name}");
if (!functionMap.TryGetValue(functionCall.Name, out AIFunction? functionTool))
{
Assert.Fail($"TOOL FAILURE [{functionCall.Name}] - MISSING");
return;
}
functionCalls.Add((functionCall, functionTool));
}
IList<AIContent> functionResults = await InvokeToolsAsync(functionCalls);
++responseCount;
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]);
}
if (autoInvoke)
{
Assert.Empty(workflowEvents.InputEvents);
}
else
{
Assert.NotEmpty(workflowEvents.InputEvents);
}
Assert.Equal(autoInvoke ? 3 : 4, workflowEvents.AgentResponseEvents.Count);
Assert.All(workflowEvents.AgentResponseEvents, response => response.Response.Text.Contains("4.95"));
}
private static async ValueTask<IList<AIContent>> InvokeToolsAsync(IEnumerable<(FunctionCallContent, AIFunction)> functionCalls)
{
List<AIContent> results = [];
foreach ((FunctionCallContent functionCall, AIFunction functionTool) in functionCalls)
{
AIFunctionArguments? functionArguments = functionCall.Arguments is null ? null : new(functionCall.Arguments.NormalizePortableValues());
object? result = await functionTool.InvokeAsync(functionArguments).ConfigureAwait(false);
results.Add(new FunctionResultContent(functionCall.CallId, JsonSerializer.Serialize(result)));
}
return results;
}
}
@@ -0,0 +1,378 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.Mcp;
using Microsoft.Extensions.AI;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
/// <summary>
/// Integration tests for InvokeFunctionTool and InvokeMcpTool actions.
/// </summary>
public sealed class InvokeToolWorkflowTest(ITestOutputHelper output) : IntegrationTest(output)
{
#region InvokeFunctionTool Tests
[Theory]
[InlineData("InvokeFunctionTool.yaml", new string[] { "GetSpecials", "GetItemPrice" }, "2.95")]
[InlineData("InvokeFunctionToolWithApproval.yaml", new string[] { "GetItemPrice" }, "4.9")]
public Task ValidateInvokeFunctionToolAsync(string workflowFileName, string[] expectedFunctionCalls, string? expectedResultContains) =>
this.RunInvokeFunctionToolTestAsync(workflowFileName, expectedFunctionCalls, expectedResultContains);
#endregion
#region InvokeMcpTool Tests
[Theory]
[InlineData("InvokeMcpTool.yaml", "Azure OpenAI")]
public Task ValidateInvokeMcpToolAsync(string workflowFileName, string? expectedResultContains) =>
this.RunInvokeMcpToolTestAsync(workflowFileName, expectedResultContains, requireApproval: false);
[Theory]
[InlineData("InvokeMcpToolWithApproval.yaml", "Azure OpenAI", true)]
[InlineData("InvokeMcpToolWithApproval.yaml", "MCP tool invocation was not approved by user", false)]
public Task ValidateInvokeMcpToolWithApprovalAsync(string workflowFileName, string? expectedResultContains, bool approveRequest) =>
this.RunInvokeMcpToolTestAsync(workflowFileName, expectedResultContains, requireApproval: true, approveRequest: approveRequest);
#endregion
#region InvokeHttpRequest Tests
[RetryTheory(3, 5000)]
[InlineData("HttpRequest.yaml")]
public Task ValidateHttpRequestAsync(string workflowFileName) =>
this.RunHttpRequestTestAsync(workflowFileName);
#endregion
#region InvokeFunctionTool Test Helpers
/// <summary>
/// Runs an InvokeFunctionTool workflow test with the specified configuration.
/// </summary>
private async Task RunInvokeFunctionToolTestAsync(
string workflowFileName,
string[] expectedFunctionCalls,
string? expectedResultContains = null)
{
// Arrange
string workflowPath = GetWorkflowPath(workflowFileName);
IEnumerable<AIFunction> functionTools = new MenuPlugin().GetTools();
Dictionary<string, AIFunction> functionMap = functionTools.ToDictionary(tool => tool.Name, tool => tool);
DeclarativeWorkflowOptions workflowOptions = await this.CreateOptionsAsync(externalConversation: false);
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(workflowPath, workflowOptions);
WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowPath));
List<string> invokedFunctions = [];
// Act - Run workflow and handle function invocations
WorkflowEvents workflowEvents = await harness.RunWorkflowAsync("start").ConfigureAwait(false);
while (workflowEvents.InputEvents.Count > 0)
{
RequestInfoEvent inputEvent = workflowEvents.InputEvents[^1];
ExternalInputRequest? toolRequest = inputEvent.Request.Data.As<ExternalInputRequest>();
Assert.NotNull(toolRequest);
IList<AIContent> functionResults = await this.ProcessFunctionCallsAsync(
toolRequest,
functionMap,
invokedFunctions).ConfigureAwait(false);
ChatMessage resultMessage = new(ChatRole.Tool, functionResults);
WorkflowEvents resumeEvents = await harness.ResumeAsync(
inputEvent.Request.CreateResponse(new ExternalInputResponse(resultMessage))).ConfigureAwait(false);
workflowEvents = new WorkflowEvents([.. workflowEvents.Events, .. resumeEvents.Events]);
// Continue processing until there are no more pending input events from the resumed workflow
if (resumeEvents.InputEvents.Count == 0)
{
break;
}
}
// Assert - Verify function calls were made in expected order
Assert.Equal(expectedFunctionCalls.Length, invokedFunctions.Count);
for (int i = 0; i < expectedFunctionCalls.Length; i++)
{
Assert.Equal(expectedFunctionCalls[i], invokedFunctions[i]);
}
// Assert - Verify executor and action events
AssertWorkflowEventsEmitted(workflowEvents);
// Assert - Verify expected result if specified
if (expectedResultContains is not null)
{
AssertResultContains(workflowEvents, expectedResultContains);
}
}
/// <summary>
/// Processes function calls from an external input request.
/// Handles both regular function calls and approval requests.
/// </summary>
private async Task<IList<AIContent>> ProcessFunctionCallsAsync(
ExternalInputRequest toolRequest,
Dictionary<string, AIFunction> functionMap,
List<string> invokedFunctions)
{
List<AIContent> results = [];
foreach (ChatMessage message in toolRequest.AgentResponse.Messages)
{
// Handle approval requests if present
foreach (ToolApprovalRequestContent approvalRequest in message.Contents.OfType<ToolApprovalRequestContent>())
{
this.Output.WriteLine($"APPROVAL REQUEST: {((FunctionCallContent)approvalRequest.ToolCall).Name}");
// Auto-approve for testing
results.Add(approvalRequest.CreateResponse(approved: true));
}
// Handle function calls
foreach (FunctionCallContent functionCall in message.Contents.OfType<FunctionCallContent>())
{
this.Output.WriteLine($"FUNCTION CALL: {functionCall.Name}");
if (!functionMap.TryGetValue(functionCall.Name, out AIFunction? functionTool))
{
Assert.Fail($"Function not found: {functionCall.Name}");
continue;
}
invokedFunctions.Add(functionCall.Name);
// Execute the function
AIFunctionArguments? functionArguments = functionCall.Arguments is null
? null
: new(functionCall.Arguments.NormalizePortableValues());
object? result = await functionTool.InvokeAsync(functionArguments).ConfigureAwait(false);
results.Add(new FunctionResultContent(functionCall.CallId, JsonSerializer.Serialize(result)));
this.Output.WriteLine($"FUNCTION RESULT: {JsonSerializer.Serialize(result)}");
}
}
return results;
}
#endregion
#region InvokeMcpTool Test Helpers
/// <summary>
/// Runs an InvokeMcpTool workflow test with the specified configuration.
/// </summary>
private async Task RunInvokeMcpToolTestAsync(
string workflowFileName,
string? expectedResultContains = null,
bool requireApproval = false,
bool approveRequest = true)
{
// Arrange
string workflowPath = GetWorkflowPath(workflowFileName);
DefaultMcpToolHandler mcpToolProvider = new();
DeclarativeWorkflowOptions workflowOptions = await this.CreateOptionsAsync(
externalConversation: false,
mcpToolProvider: mcpToolProvider);
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(workflowPath, workflowOptions);
WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowPath));
// Act - Run workflow and handle MCP tool invocations
WorkflowEvents workflowEvents = await harness.RunWorkflowAsync("start").ConfigureAwait(false);
while (workflowEvents.InputEvents.Count > 0)
{
RequestInfoEvent inputEvent = workflowEvents.InputEvents[^1];
ExternalInputRequest? toolRequest = inputEvent.Request.Data.As<ExternalInputRequest>();
Assert.NotNull(toolRequest);
IList<AIContent> mcpResults = this.ProcessMcpToolRequests(
toolRequest,
approveRequest);
ChatMessage resultMessage = new(ChatRole.Tool, mcpResults);
WorkflowEvents resumeEvents = await harness.ResumeAsync(
inputEvent.Request.CreateResponse(new ExternalInputResponse(resultMessage))).ConfigureAwait(false);
workflowEvents = new WorkflowEvents([.. workflowEvents.Events, .. resumeEvents.Events]);
// Continue processing until there are no more pending input events from the resumed workflow
if (resumeEvents.InputEvents.Count == 0)
{
break;
}
}
// Assert - Verify executor and action events
AssertWorkflowEventsEmitted(workflowEvents);
// Assert - Verify expected result if specified
if (expectedResultContains is not null)
{
AssertResultContains(workflowEvents, expectedResultContains);
}
// Cleanup
await mcpToolProvider.DisposeAsync().ConfigureAwait(false);
}
/// <summary>
/// Processes MCP tool requests from an external input request.
/// Handles approval requests for MCP tools.
/// </summary>
private List<AIContent> ProcessMcpToolRequests(
ExternalInputRequest toolRequest,
bool approveRequest)
{
List<AIContent> results = [];
foreach (ChatMessage message in toolRequest.AgentResponse.Messages)
{
// Handle MCP approval requests if present
foreach (ToolApprovalRequestContent approvalRequest in message.Contents.OfType<ToolApprovalRequestContent>())
{
this.Output.WriteLine($"MCP APPROVAL REQUEST: {approvalRequest.RequestId}");
// Respond based on test configuration
ToolApprovalResponseContent response = approvalRequest.CreateResponse(approved: approveRequest);
results.Add(response);
this.Output.WriteLine($"MCP APPROVAL RESPONSE: {(approveRequest ? "Approved" : "Rejected")}");
}
}
return results;
}
#endregion
#region InvokeHttpRequest Test Helpers
/// <summary>
/// The Azure ARM scope used to acquire bearer tokens for the HttpRequestAction
/// integration test. Matches the URL configured in <c>HttpRequest.yaml</c>.
/// </summary>
private const string ArmScope = "https://management.azure.com/.default";
/// <summary>
/// The expected ARM endpoint. Only requests whose absolute URL exactly matches
/// this scheme and host receive the authenticated <see cref="HttpClient"/>; all
/// other URLs (including subdomain look-alikes such as
/// <c>https://management.azure.com.evil.com</c>) fall through to the handler
/// default and never see the bearer token.
/// </summary>
private static readonly Uri s_armEndpoint = new("https://management.azure.com/");
/// <summary>
/// Runs an HttpRequestAction workflow test with the specified configuration.
/// </summary>
/// <remarks>
/// The workflow under test calls an authenticated Azure ARM endpoint. We acquire a
/// single bearer token via the same Azure CLI credential used elsewhere in the
/// integration test suite, attach it to a cached <see cref="HttpClient"/>, and route
/// matching requests through that client via <see cref="DefaultHttpRequestHandler"/>'s
/// <c>httpClientProvider</c> callback. The test owns the <see cref="HttpClient"/>'s
/// lifetime and disposes it explicitly — <see cref="DefaultHttpRequestHandler"/> does
/// not dispose provider-returned clients.
/// </remarks>
private async Task RunHttpRequestTestAsync(
string workflowFileName)
{
// Arrange
string workflowPath = GetWorkflowPath(workflowFileName);
AccessToken accessToken =
await TestAzureCliCredentials
.CreateAzureCliCredential()
.GetTokenAsync(new TokenRequestContext([ArmScope]), CancellationToken.None)
.ConfigureAwait(false);
using HttpClient authenticatedClient = new();
authenticatedClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken.Token);
await using DefaultHttpRequestHandler httpRequestHandler =
new(httpClientProvider: (request, _) =>
{
if (Uri.TryCreate(request.Url, UriKind.Absolute, out Uri? requestUri) &&
string.Equals(requestUri.Scheme, s_armEndpoint.Scheme, StringComparison.OrdinalIgnoreCase) &&
string.Equals(requestUri.Host, s_armEndpoint.Host, StringComparison.OrdinalIgnoreCase))
{
#pragma warning disable CA2025 // authenticatedClient outlives the handler (LIFO using disposal) and the workflow awaits all dispatches.
return Task.FromResult<HttpClient?>(authenticatedClient);
#pragma warning restore CA2025
}
// Fall back to the handler's internal client for any non-ARM URLs.
return Task.FromResult<HttpClient?>(null);
});
DeclarativeWorkflowOptions workflowOptions = await this.CreateOptionsAsync(
externalConversation: false,
httpRequestHandler: httpRequestHandler);
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(workflowPath, workflowOptions);
WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowPath));
// Act
WorkflowEvents workflowEvents = await harness.RunWorkflowAsync("start").ConfigureAwait(false);
// Assert - Verify executor and action events
AssertWorkflowEventsEmitted(workflowEvents);
MessageActivityEvent? messageEvent = workflowEvents.Events
.OfType<MessageActivityEvent>()
.LastOrDefault();
Assert.NotNull(messageEvent);
Assert.NotNull(messageEvent.Message);
Assert.True(
Guid.TryParse(messageEvent.Message, out Guid retrievedTenantId),
$"Expected the SendMessage payload to be a tenant GUID, but got: '{messageEvent.Message}'");
Assert.NotEqual(Guid.Empty, retrievedTenantId);
}
#endregion
#region Shared Helpers
private static void AssertWorkflowEventsEmitted(WorkflowEvents workflowEvents)
{
Assert.NotEmpty(workflowEvents.ExecutorInvokeEvents);
Assert.NotEmpty(workflowEvents.ExecutorCompleteEvents);
Assert.NotEmpty(workflowEvents.ActionInvokeEvents);
}
private static void AssertResultContains(WorkflowEvents workflowEvents, string expectedResultContains)
{
MessageActivityEvent? messageEvent = workflowEvents.Events
.OfType<MessageActivityEvent>()
.LastOrDefault();
Assert.NotNull(messageEvent);
Assert.Contains(expectedResultContains, messageEvent.Message, StringComparison.OrdinalIgnoreCase);
}
private static string GetWorkflowPath(string workflowFileName) =>
Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName);
#endregion
}
@@ -0,0 +1,129 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.IO;
using System.Threading.Tasks;
using Azure.AI.Projects;
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents;
using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework;
using Microsoft.Extensions.AI;
using OpenAI.Files;
using Shared.IntegrationTests;
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
/// <summary>
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowBuilder"/>.
/// </summary>
public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(output)
{
private const string WorkflowWithConversationFileName = "MediaInputConversation.yaml";
private const string WorkflowWithAutoSendFileName = "MediaInputAutoSend.yaml";
private const string ImageReferenceUrl = "https://sample-files.com/downloads/images/jpg/web_optimized_1200x800_97kb.jpg";
private const string PdfLocalFile = "TestFiles/basic-text.pdf";
private const string ImageLocalFile = "TestFiles/test-image.jpg";
[Theory]
[InlineData(ImageReferenceUrl, "image/jpeg", true)]
[InlineData(ImageReferenceUrl, "image/jpeg", false)]
public async Task ValidateFileUrlAsync(string fileSource, string mediaType, bool useConversation)
{
// Arrange
this.Output.WriteLine($"File: {fileSource}");
// Act & Assert
await this.ValidateFileAsync(new UriContent(fileSource, mediaType), useConversation);
}
// Temporarily disabled
[Theory]
[Trait("Category", "IntegrationDisabled")]
[InlineData(ImageLocalFile, "image/jpeg", true)]
[InlineData(ImageLocalFile, "image/jpeg", false)]
public async Task ValidateImageFileDataAsync(string fileSource, string mediaType, bool useConversation)
{
// Arrange
byte[] fileData = ReadLocalFile(fileSource);
string encodedData = Convert.ToBase64String(fileData);
string fileUrl = $"data:{mediaType};base64,{encodedData}";
this.Output.WriteLine($"Content: {fileUrl.Substring(0, Math.Min(112, fileUrl.Length))}...");
// Act & Assert
await this.ValidateFileAsync(new DataContent(fileUrl), useConversation);
}
[Theory]
[InlineData(PdfLocalFile, "application/pdf", true)]
[InlineData(PdfLocalFile, "application/pdf", false)]
public async Task ValidateFileDataAsync(string fileSource, string mediaType, bool useConversation)
{
// Arrange
byte[] fileData = ReadLocalFile(fileSource);
string encodedData = Convert.ToBase64String(fileData);
string fileUrl = $"data:{mediaType};base64,{encodedData}";
this.Output.WriteLine($"Content: {fileUrl.Substring(0, Math.Min(112, fileUrl.Length))}...");
// Act & Assert
await this.ValidateFileAsync(new DataContent(fileUrl), useConversation);
}
// Temporarily disabled
[Theory]
[Trait("Category", "IntegrationDisabled")]
[InlineData(PdfLocalFile, "doc.pdf", true)]
[InlineData(PdfLocalFile, "doc.pdf", false)]
public async Task ValidateFileUploadAsync(string fileSource, string documentName, bool useConversation)
{
// Arrange
byte[] fileData = ReadLocalFile(fileSource);
AIProjectClient client = new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
using MemoryStream contentStream = new(fileData);
OpenAIFileClient fileClient = client.GetProjectOpenAIClient().GetOpenAIFileClient();
OpenAIFile fileInfo = await fileClient.UploadFileAsync(contentStream, documentName, FileUploadPurpose.Assistants);
// Act & Assert
try
{
this.Output.WriteLine($"File: {fileInfo.Id}");
await this.ValidateFileAsync(new HostedFileContent(fileInfo.Id), useConversation);
}
finally
{
await fileClient.DeleteFileAsync(fileInfo.Id);
}
}
private static byte[] ReadLocalFile(string relativePath)
{
string fullPath = Path.Combine(AppContext.BaseDirectory, relativePath);
return File.ReadAllBytes(fullPath);
}
private async Task ValidateFileAsync(AIContent fileContent, bool useConversation)
{
// Act
AgentProvider agentProvider = AgentProvider.Create(this.Configuration, AgentProvider.Names.Vision);
await agentProvider.CreateAgentsAsync().ConfigureAwait(false);
ChatMessage inputMessage =
new(ChatRole.User,
[
new TextContent("I've provided a file:"),
fileContent
]);
string workflowFileName = useConversation ? WorkflowWithConversationFileName : WorkflowWithAutoSendFileName;
DeclarativeWorkflowOptions options = await this.CreateOptionsAsync();
Workflow workflow = DeclarativeWorkflowBuilder.Build<ChatMessage>(Path.Combine(Environment.CurrentDirectory, "Workflows", workflowFileName), options);
WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowFileName));
WorkflowEvents workflowEvents = await harness.RunWorkflowAsync(inputMessage).ConfigureAwait(false);
// Assert
Assert.Equal(useConversation ? 1 : 2, workflowEvents.ConversationEvents.Count);
this.Output.WriteLine("CONVERSATION: " + workflowEvents.ConversationEvents[0].ConversationId);
AgentResponseEvent agentResponseEvent = Assert.Single(workflowEvents.AgentResponseEvents);
this.Output.WriteLine("RESPONSE: " + agentResponseEvent.Response.Text);
Assert.NotEmpty(agentResponseEvent.Response.Text);
}
}
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
<InjectSharedIntegrationTestCode>true</InjectSharedIntegrationTestCode>
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Workflows.Declarative.Foundry\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Workflows.Declarative.Mcp\Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
<ItemGroup>
<None Update="Agents\*.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)\..\..\..\declarative-agents\workflow-samples\Setup\*.yaml" LinkBase="Agents">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Testcases\*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Workflows\*.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestFiles\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,39 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>
endobj
4 0 obj
<< /Length 44 >>
stream
BT /F1 12 Tf 100 700 Td (Hello World) Tj ET
endstream
endobj
5 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
xref
0 6
0000000000 65535 f
0000000015 00000 n
0000000065 00000 n
0000000123 00000 n
0000000250 00000 n
0000000345 00000 n
trailer
<< /Size 6 /Root 1 0 R >>
startxref
416
%%EOF
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

@@ -0,0 +1,27 @@
{
"description": "Send an activity message.",
"setup": {
"input": {
"type": "String",
"value": "Everything good?"
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 2,
"max_action_count": -1,
"min_response_count": 1,
"max_response_count": 1,
"min_message_count": 0,
"max_message_count": 0,
"actions": {
"start": [
"check_system"
],
"final": [
"activity_passed",
"check_system_Post"
]
}
}
}
@@ -0,0 +1,42 @@
{
"description": "Human in the loop sample - ConfirmInput.yaml. First response mismatches to exercise the GotoAction re-prompt path; second response matches and completes the workflow.",
"setup": {
"input": {
"type": "String",
"value": "1234"
},
"responses": [
{
"type": "String",
"value": "9999"
},
{
"type": "String",
"value": "1234"
}
]
},
"validation": {
"conversation_count": 1,
"min_action_count": 6,
"max_action_count": -1,
"min_response_count": 2,
"max_response_count": -1,
"min_message_count": 0,
"max_message_count": -1,
"actions": {
"start": [
"set_project",
"question_confirm"
],
"repeat": [
"sendActivity_mismatch",
"goto_again",
"question_confirm"
],
"final": [
"sendActivity_confirmed"
]
}
}
}
@@ -0,0 +1,31 @@
{
"description": "Create conversation and manipulate messages.",
"setup": {
"input": {
"type": "String",
"value": "Why is the sky blue?"
}
},
"validation": {
"conversation_count": 2,
"min_action_count": 8,
"min_message_count": 1,
"min_response_count": 1,
"max_response_count": 4,
"actions": {
"start": [
"conversation_create1",
"sendActivity_conversation",
"add_message",
"get_message_single",
"sendActivity_message",
"copy_messages",
"get_messages_all",
"sendActivity_copy"
],
"final": [
"sendActivity_copy"
]
}
}
}
@@ -0,0 +1,43 @@
{
"description": "Planned orchestration sample - DeepResearch.yaml.",
"setup": {
"input": {
"type": "String",
"value": "What is the closest bus-stop that is next to ISHONI YAKINIKU in Seattle?"
}
},
"validation": {
"conversation_count": 2,
"min_action_count": 25,
"max_action_count": -1,
"min_response_count": 1,
"max_response_count": -1,
"actions": {
"start": [
"setVariable_aASlmF",
"setVariable_V6yEbo",
"setVariable_NZ2u0l",
"setVariable_10u2ZN",
"sendActivity_yFsbRy",
"conversation_1a2b3c",
"question_UDoMUw",
"sendActivity_yFsbRz",
"question_DsBaJU",
"setVariable_Kk2LDL",
"sendActivity_bwNZiM",
"question_o3BQkf",
"parse_rNZtlV",
"conditionGroup_mVIecC"
],
"repeat": [
"question_o3BQkf",
"parse_rNZtlV",
"conditionGroup_mVIecC"
],
"final": [
"end_SVoNSV",
"end_GHVrFh"
]
}
}
}
@@ -0,0 +1,35 @@
{
"description": "Human in the loop sample - HumanInLoop.yaml.",
"setup": {
"input": {
"type": "String",
"value": "Iko"
},
"responses": [
{
"type": "String",
"value": "Adsf"
},
{
"type": "String",
"value": "Iko"
}
]
},
"validation": {
"conversation_count": 1,
"min_action_count": 8,
"min_response_count": 0,
"actions": {
"start": [
"set_project"
],
"repeat": [
"question_confirm"
],
"final": [
"sendActivity_confirmed"
]
}
}
}
@@ -0,0 +1,22 @@
{
"description": "Authors a poem in the style specified by the input argument.",
"setup": {
"input": {
"type": "String",
"value": "Why is the sky blue?"
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 1,
"min_response_count": 1,
"actions": {
"start": [
"invoke_poem"
],
"final": [
"invoke_poem"
]
}
}
}
@@ -0,0 +1,25 @@
{
"description": "Produce a single response from an agent.",
"setup": {
"input": {
"type": "String",
"value": "Why is the sky blue?"
}
},
"validation": {
"conversation_count": 3,
"min_action_count": 3,
"min_response_count": 3,
"min_message_count": 4,
"actions": {
"start": [
"invoke_inner1",
"invoke_inner2",
"invoke_external"
],
"final": [
"invoke_external"
]
}
}
}
@@ -0,0 +1,24 @@
{
"description": "Sequential agent invocation sample - Marketing.yaml.",
"setup": {
"input": {
"type": "String",
"value": "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours."
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 3,
"min_response_count": 3,
"actions": {
"start": [
"invoke_analyst",
"invoke_writer",
"invoke_editor"
],
"final": [
"invoke_editor"
]
}
}
}
@@ -0,0 +1,33 @@
{
"description": "Student/Teacher sample - MathChat.yaml.",
"setup": {
"input": {
"type": "String",
"value": "How could one compute the value of PI?"
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 6,
"max_action_count": -1,
"min_response_count": 2,
"max_response_count": 9,
"min_message_count": 4,
"max_message_count": -1,
"actions": {
"start": [
],
"repeat": [
"question_student",
"question_teacher",
"set_count_increment",
"check_completion"
],
"final": [
"sendActivity_done",
"sendActivity_tired",
"check_completion_Post"
]
}
}
}
@@ -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": 1,
"min_message_count": 1,
"actions": {
"start": [
"get_input"
],
"final": [
"show_input"
]
}
}
}
@@ -0,0 +1,27 @@
{
"description": "Send an activity message.",
"setup": {
"input": {
"type": "String",
"value": "Why is the sky blue?"
}
},
"validation": {
"conversation_count": 1,
"min_action_count": 3,
"min_message_count": 0,
"max_message_count": 0,
"min_response_count": 1,
"max_response_count": 1,
"actions": {
"start": [
"set_user_input",
"set_user_name",
"send_result"
],
"final": [
"send_result"
]
}
}
}
@@ -0,0 +1,57 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: ConditionGroup
id: check_system
conditions:
- condition: =IsBlank(System.Conversation)
id: conversation_check
actions:
- kind: EndWorkflow
id: conversation_bad
- condition: =IsBlank(System.Conversation.Id)
id: conversation_id_check1
actions:
- kind: EndWorkflow
id: conversation_id_bad1
- condition: =IsBlank(System.ConversationId)
id: conversation_id_check2
actions:
- kind: EndWorkflow
id: conversation_id_bad2
- condition: =IsBlank(System.LastMessage)
id: message_check
actions:
- kind: EndWorkflow
id: message_bad
- condition: =IsBlank(System.LastMessage.Id)
id: message_id_check1
actions:
- kind: EndWorkflow
id: message_id_bad1
- condition: =IsBlank(System.LastMessageId)
id: message_id_check2
actions:
- kind: EndWorkflow
id: message_id_bad2
- condition: =IsBlank(System.LastMessageText)
id: message_text_check
actions:
- kind: EndWorkflow
id: message_text_bad
elseActions:
- kind: SendActivity
id: activity_passed
activity: PASSED!
@@ -0,0 +1,61 @@
#
# This workflow demonstrates how to use the Question action
# to request user input and confirm it matches the original input.
#
# Note: This workflow doesn't make use of any agents.
#
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_demo
actions:
# Capture original input
- kind: SetVariable
id: set_project
variable: Local.OriginalInput
value: =System.LastMessage.Text
# Request input from user
- kind: Question
id: question_confirm
alwaysPrompt: false
autoSend: false
property: Local.ConfirmedInput
prompt:
kind: Message
text:
- "CONFIRM:"
entity:
kind: StringPrebuiltEntity
# Confirm input
- kind: ConditionGroup
id: check_completion
conditions:
# Didn't match
- condition: =Local.OriginalInput <> Local.ConfirmedInput
id: check_confirm
actions:
- kind: SendActivity
id: sendActivity_mismatch
activity: |-
"{Local.ConfirmedInput}" does not match the original input of "{Local.OriginalInput}". Please try again.
- kind: GotoAction
id: goto_again
actionId: question_confirm
# Confirmed
elseActions:
- kind: SendActivity
id: sendActivity_confirmed
activity: |-
You entered:
{Local.OriginalInput}
Confirmed input:
{Local.ConfirmedInput}
@@ -0,0 +1,50 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: CreateConversation
id: conversation_create1
conversationId: Local.PrivateConversationId
- kind: SendActivity
id: sendActivity_conversation
activity: |-
Conversation 1: {Local.PrivateConversationId}
Conversation 2: {System.ConversationId}
- kind: AddConversationMessage
id: add_message
message: Local.MyMessage1
role: User
conversationId: =Local.PrivateConversationId
content:
- type: Text
value: {System.LastMessage.Text}
- kind: RetrieveConversationMessage
id: get_message_single
message: Local.MyMessage1Copy
conversationId: =Local.PrivateConversationId
messageId: =Local.MyMessage1.Id
- kind: SendActivity
id: sendActivity_message
activity: |-
Message 1: {Local.MyMessage1}
- kind: CopyConversationMessages
id: copy_messages
conversationId: =System.ConversationId
messages: =[Local.MyMessage1]
- kind: RetrieveConversationMessages
id: get_messages_all
messages: Local.AllMessages
conversationId: =System.ConversationId
- kind: SendActivity
id: sendActivity_copy
activity: Done!
@@ -0,0 +1,28 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: InvokeAzureAgent
id: invoke_greet
conversationId: =System.ConversationId
agent:
name: MenuAgent
- kind: InvokeAzureAgent
id: invoke_menu
conversationId: =System.ConversationId
agent:
name: MenuAgent
input:
messages: =UserMessage("What's on today's menu?")
- kind: InvokeAzureAgent
id: invoke_item
conversationId: =System.ConversationId
agent:
name: MenuAgent
input:
messages: =UserMessage("How much is the clam chowder?")
@@ -0,0 +1,31 @@
#
# This workflow tests invoking HttpRequestAction end-to-end.
# Uses the Azure ARM tenants endpoint, which is authenticated, fully static, and
# reachable with the credentials the integration test pipeline already provides
# (via az login). The bearer token is supplied by the test through a custom
# HttpClient passed to DefaultHttpRequestHandler; the YAML deliberately does not
# carry an Authorization header.
#
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_http_request_test
actions:
# Invoke the Azure ARM tenants list API.
- kind: HttpRequestAction
id: fetch_tenants
conversationId: =System.ConversationId
method: GET
url: https://management.azure.com/tenants?api-version=2022-09-01
headers:
Accept: application/json
response: Local.TenantsResponse
# Surface the first tenant id from the parsed JSON response. Every
# authenticated principal belongs to at least one tenant, so this path
# always resolves on a successful call.
- kind: SendMessage
id: show_first_tenant
message: "{First(Local.TenantsResponse.value).tenantId}"
@@ -0,0 +1,15 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: InvokeAzureAgent
id: invoke_poem
conversationId: =System.ConversationId
agent:
name: PoemAgent
input:
arguments:
style: "ee cummings"
@@ -0,0 +1,32 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: InvokeAzureAgent
id: invoke_inner1
agent:
name: TestAgent
input:
messages: =UserMessage("Can an LLM think of funny jokes?")
- kind: InvokeAzureAgent
id: invoke_inner2
agent:
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: TestAgent
input:
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
@@ -0,0 +1,52 @@
#
# This workflow tests invoking function tools directly from a workflow.
# Uses the MenuPlugin functions: GetMenu, GetSpecials, GetItemPrice
#
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_invoke_function_tool_test
actions:
# Set the item name we want to look up
- kind: SetVariable
id: set_item_name
variable: Local.ItemName
value: Chai Tea
# Invoke GetSpecials function to get today's specials
- kind: InvokeFunctionTool
id: invoke_get_specials
functionName: GetSpecials
conversationId: =System.ConversationId
output:
autoSend: false
result: Local.Specials
# Invoke GetItemPrice function to get the price of a specific item
- kind: InvokeFunctionTool
id: invoke_get_item_price
functionName: GetItemPrice
conversationId: =System.ConversationId
arguments:
name: =Local.ItemName
output:
autoSend: true
result: Local.ItemPrice
# Ask an agent the price from the results in the conversation
- kind: InvokeAzureAgent
id: invoke_menu
conversationId: =System.ConversationId
agent:
name: TestAgent
input:
messages: =UserMessage("What's the price of Chai Tea?")
output:
messages: Local.AgentResponse
# Send the result as an activity
- kind: SendMessage
id: show_price_result
message: "{Local.AgentResponse}"
@@ -0,0 +1,33 @@
#
# This workflow tests invoking function tools with approval requirement.
# Uses the MenuPlugin function: GetItemPrice with requireApproval: true
#
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_invoke_function_tool_approval_test
actions:
# Set the item name we want to look up
- kind: SetVariable
id: set_item_name
variable: Local.ItemName
value: Clam Chowder
# Invoke GetItemPrice function with approval requirement
- kind: InvokeFunctionTool
id: invoke_get_item_price
functionName: GetItemPrice
conversationId: =System.ConversationId
requireApproval: true
arguments:
name: =Local.ItemName
output:
autoSend: false
result: Local.ItemPrice
# Send the result as an activity
- kind: SendMessage
id: show_price_result
message: "The price of {Local.ItemName} is ${Text(Local.ItemPrice)}"
@@ -0,0 +1,35 @@
#
# This workflow tests invoking MCP tools directly from a workflow.
# Uses the Microsoft Learn MCP server: search tool
#
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_invoke_mcp_tool_test
actions:
# Set the search query we want to use
- kind: SetVariable
id: set_search_query
variable: Local.SearchQuery
value: Azure OpenAI
# Invoke MCP search tool on Microsoft Learn server
- kind: InvokeMcpTool
id: invoke_mcp_search
serverUrl: https://learn.microsoft.com/api/mcp
serverLabel: microsoft_docs
toolName: microsoft_docs_search
conversationId: =System.ConversationId
arguments:
query: =Local.SearchQuery
output:
autoSend: true
result: Local.SearchResult
# Send the result as an activity
- kind: SendMessage
id: show_search_result
message: "Search results: {Local.SearchResult}"
# message: "Search results for {Local.SearchQuery}: {Local.SearchResult}"
@@ -0,0 +1,35 @@
#
# This workflow tests invoking MCP tools with approval requirement.
# Uses the Microsoft Learn MCP server: search tool with requireApproval: true
#
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_invoke_mcp_tool_approval_test
actions:
# Set the search query we want to use
- kind: SetVariable
id: set_search_query
variable: Local.ContentUrl
value: https://learn.microsoft.com/azure/ai-foundry/openai/concepts/use-your-data
# Invoke MCP search tool with approval requirement
- kind: InvokeMcpTool
id: invoke_mcp_search
serverUrl: https://learn.microsoft.com/api/mcp
serverLabel: MicrosoftLearn
toolName: microsoft_docs_fetch
requireApproval: true
arguments:
url: =Local.ContentUrl
output:
autoSend: false
result: Local.FetchResult
messages: Local.FetchMessages
# Send the result as an activity
- kind: SendMessage
id: show_search_result
message: "Content for {Local.ContentUrl}: {Local.FetchResult}"
@@ -0,0 +1,15 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: InvokeAzureAgent
id: invoke_vision
agent:
name: VisionAgent
input:
messages: =System.LastMessage
output:
autoSend: true
@@ -0,0 +1,12 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: InvokeAzureAgent
id: invoke_vision
conversationId: =System.ConversationId
agent:
name: VisionAgent
@@ -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}"
@@ -0,0 +1,25 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
# Capture input
- kind: SetVariable
id: set_user_input
variable: Local.UserInput
value: =System.LastMessage.Text
# Capture environment variable
- kind: SetVariable
id: set_user_name
variable: Global.UserName
value: TestAgent
# Respond with input
- kind: SendActivity
id: send_result
activity: |-
Hello {Global.UserName},
You said, "{Local.UserInput}"