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
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:
+49
@@ -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}");
|
||||
}
|
||||
+56
@@ -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;
|
||||
}
|
||||
}
|
||||
+76
@@ -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.
|
||||
"""
|
||||
};
|
||||
}
|
||||
+55
@@ -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".
|
||||
"""
|
||||
};
|
||||
}
|
||||
+92
@@ -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; }
|
||||
}
|
||||
}
|
||||
+44
@@ -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",
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+28
@@ -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));
|
||||
}
|
||||
+35
@@ -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.
|
||||
""",
|
||||
};
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
+75
@@ -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);
|
||||
}
|
||||
}
|
||||
+105
@@ -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();
|
||||
}
|
||||
+72
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -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; } = [];
|
||||
}
|
||||
+33
@@ -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; }
|
||||
}
|
||||
+159
@@ -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");
|
||||
}
|
||||
}
|
||||
+216
@@ -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,
|
||||
};
|
||||
}
|
||||
+101
@@ -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;
|
||||
}
|
||||
}
|
||||
+378
@@ -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
|
||||
}
|
||||
+129
@@ -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);
|
||||
}
|
||||
}
|
||||
+42
@@ -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>
|
||||
+39
@@ -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
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 148 B |
+27
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -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!
|
||||
+61
@@ -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}
|
||||
+50
@@ -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!
|
||||
+28
@@ -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?")
|
||||
+31
@@ -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}"
|
||||
+15
@@ -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"
|
||||
+32
@@ -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
|
||||
+52
@@ -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}"
|
||||
+33
@@ -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)}"
|
||||
+35
@@ -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}"
|
||||
+35
@@ -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}"
|
||||
+15
@@ -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
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: InvokeAzureAgent
|
||||
id: invoke_vision
|
||||
conversationId: =System.ConversationId
|
||||
agent:
|
||||
name: VisionAgent
|
||||
+14
@@ -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}"
|
||||
+25
@@ -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}"
|
||||
Reference in New Issue
Block a user