mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET [WIP]: introduce Hosting extensions for Workflows (#1359)
* skeleton * wip * rename + fix tests * implement workflow tests * fix comments * Update dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fixes * fix worfklow build logic * rollback + new overload on workflow builder * address PR comments * :) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
f42a3ee6b9
commit
0331331dbc
@@ -66,6 +66,25 @@ builder.AddAIAgent("knights-and-knaves", (sp, key) =>
|
||||
#pragma warning restore VSTHRD002
|
||||
});
|
||||
|
||||
// Workflow consisting of multiple specialized agents
|
||||
var chemistryAgent = builder.AddAIAgent("chemist",
|
||||
instructions: "You are a chemistry expert. Answer thinking from the chemistry perspective",
|
||||
description: "An agent that helps with chemistry.",
|
||||
chatClientServiceKey: "chat-model");
|
||||
|
||||
var mathsAgent = builder.AddAIAgent("mathematician",
|
||||
instructions: "You are a mathematics expert. Answer thinking from the maths perspective",
|
||||
description: "An agent that helps with mathematics.",
|
||||
chatClientServiceKey: "chat-model");
|
||||
|
||||
var literatureAgent = builder.AddAIAgent("literator",
|
||||
instructions: "You are a literature expert. Answer thinking from the literature perspective",
|
||||
description: "An agent that helps with literature.",
|
||||
chatClientServiceKey: "chat-model");
|
||||
|
||||
builder.AddSequentialWorkflow("science-sequential-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent();
|
||||
builder.AddConcurrentWorkflow("science-concurrent-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapOpenApi();
|
||||
@@ -92,6 +111,10 @@ app.MapOpenAIResponses("knights-and-knaves");
|
||||
app.MapOpenAIChatCompletions("pirate");
|
||||
app.MapOpenAIChatCompletions("knights-and-knaves");
|
||||
|
||||
// workflow-agents
|
||||
app.MapOpenAIResponses("science-sequential-workflow");
|
||||
app.MapOpenAIResponses("science-concurrent-workflow");
|
||||
|
||||
// Map the agents HTTP endpoints
|
||||
app.MapAgentDiscovery("/agents");
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Hosting.Local;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -22,7 +23,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="instructions">The instructions for the agent.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
|
||||
public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
@@ -38,7 +39,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="chatClient">The chat client which the agent will use for inference.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
|
||||
public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
@@ -55,7 +56,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
|
||||
public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
@@ -75,7 +76,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <param name="chatClientServiceKey">The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved.</param>
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="instructions"/> is null.</exception>
|
||||
public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNullOrEmpty(name);
|
||||
@@ -95,7 +96,7 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
/// <returns>The configured host application builder.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="createAgentDelegate"/> is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the agent factory delegate returns null or an invalid AI agent instance.</exception>
|
||||
public static IHostApplicationBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate)
|
||||
public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, AIAgent> createAgentDelegate)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(name);
|
||||
@@ -117,7 +118,8 @@ public static class HostApplicationBuilderAgentExtensions
|
||||
// Register the agent by name for discovery.
|
||||
var agentHostBuilder = GetAgentRegistry(builder);
|
||||
agentHostBuilder.AgentNames.Add(name);
|
||||
return builder;
|
||||
|
||||
return new HostedAgentBuilder(name, builder);
|
||||
}
|
||||
|
||||
private static LocalAgentRegistry GetAgentRegistry(IHostApplicationBuilder builder)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Hosting.Local;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring AI workflows in a host application builder.
|
||||
/// </summary>
|
||||
public static class HostApplicationBuilderWorkflowExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a concurrent workflow that executes multiple agents in parallel.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
|
||||
/// <param name="name">The unique name for the workflow.</param>
|
||||
/// <param name="agentBuilders">A collection of <see cref="IHostedAgentBuilder"/> instances representing agents to execute concurrently.</param>
|
||||
/// <returns>An <see cref="IHostedWorkflowBuilder"/> that can be used to further configure the workflow.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="agentBuilders"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> or <paramref name="agentBuilders"/> is empty.</exception>
|
||||
public static IHostedWorkflowBuilder AddConcurrentWorkflow(this IHostApplicationBuilder builder, string name, IEnumerable<IHostedAgentBuilder> agentBuilders)
|
||||
{
|
||||
Throw.IfNullOrEmpty(agentBuilders);
|
||||
|
||||
return builder.AddWorkflow(name, (sp, key) =>
|
||||
{
|
||||
var agents = agentBuilders.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
|
||||
return AgentWorkflowBuilder.BuildConcurrent(workflowName: name, agents: agents);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a sequential workflow that executes agents in a specific order.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
|
||||
/// <param name="name">The unique name for the workflow.</param>
|
||||
/// <param name="agentBuilders">A collection of <see cref="IHostedAgentBuilder"/> instances representing agents to execute in sequence.</param>
|
||||
/// <returns>An <see cref="IHostedWorkflowBuilder"/> that can be used to further configure the workflow.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="agentBuilders"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> or <paramref name="agentBuilders"/> is empty.</exception>
|
||||
public static IHostedWorkflowBuilder AddSequentialWorkflow(this IHostApplicationBuilder builder, string name, IEnumerable<IHostedAgentBuilder> agentBuilders)
|
||||
{
|
||||
Throw.IfNullOrEmpty(agentBuilders);
|
||||
|
||||
return builder.AddWorkflow(name, (sp, key) =>
|
||||
{
|
||||
var agents = agentBuilders.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
|
||||
return AgentWorkflowBuilder.BuildSequential(workflowName: name, agents: agents);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom workflow using a factory delegate.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
|
||||
/// <param name="name">The unique name for the workflow.</param>
|
||||
/// <param name="createWorkflowDelegate">A factory function that creates the <see cref="Workflow"/> instance. The function receives the service provider and workflow name as parameters.</param>
|
||||
/// <returns>An <see cref="IHostedWorkflowBuilder"/> that can be used to further configure the workflow.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="name"/>, or <paramref name="createWorkflowDelegate"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is empty.</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the factory delegate returns null or a workflow with a name that doesn't match the expected name.
|
||||
/// </exception>
|
||||
public static IHostedWorkflowBuilder AddWorkflow(this IHostApplicationBuilder builder, string name, Func<IServiceProvider, string, Workflow> createWorkflowDelegate)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
Throw.IfNull(name);
|
||||
Throw.IfNull(createWorkflowDelegate);
|
||||
|
||||
builder.Services.AddKeyedSingleton(name, (sp, key) =>
|
||||
{
|
||||
Throw.IfNull(key);
|
||||
var keyString = key as string;
|
||||
Throw.IfNullOrEmpty(keyString);
|
||||
var workflow = createWorkflowDelegate(sp, keyString) ?? throw new InvalidOperationException($"The agent factory did not return a valid {nameof(Workflow)} instance for key '{keyString}'.");
|
||||
if (!string.Equals(workflow.Name, keyString, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException($"The workflow factory returned workflow with name '{workflow.Name}', but the expected name is '{keyString}'.");
|
||||
}
|
||||
|
||||
return workflow;
|
||||
});
|
||||
|
||||
// Register the workflow by name for discovery.
|
||||
var workflowRegistry = GetWorkflowRegistry(builder);
|
||||
workflowRegistry.WorkflowNames.Add(name);
|
||||
|
||||
return new HostedWorkflowBuilder(name, builder);
|
||||
}
|
||||
|
||||
private static LocalWorkflowRegistry GetWorkflowRegistry(IHostApplicationBuilder builder)
|
||||
{
|
||||
var descriptor = builder.Services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalWorkflowRegistry)));
|
||||
if (descriptor?.ImplementationInstance is not LocalWorkflowRegistry instance)
|
||||
{
|
||||
instance = new LocalWorkflowRegistry();
|
||||
ConfigureHostBuilder(builder, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static void ConfigureHostBuilder(IHostApplicationBuilder builder, LocalWorkflowRegistry agentHostBuilderContext)
|
||||
{
|
||||
builder.Services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext));
|
||||
builder.Services.AddSingleton<WorkflowCatalog, LocalWorkflowCatalog>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
internal sealed class HostedAgentBuilder : IHostedAgentBuilder
|
||||
{
|
||||
public string Name { get; }
|
||||
public IHostApplicationBuilder HostApplicationBuilder { get; }
|
||||
|
||||
public HostedAgentBuilder(string name, IHostApplicationBuilder hostApplicationBuilder)
|
||||
{
|
||||
this.Name = name;
|
||||
this.HostApplicationBuilder = hostApplicationBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
internal sealed class HostedWorkflowBuilder : IHostedWorkflowBuilder
|
||||
{
|
||||
public string Name { get; }
|
||||
public IHostApplicationBuilder HostApplicationBuilder { get; }
|
||||
|
||||
public HostedWorkflowBuilder(string name, IHostApplicationBuilder hostApplicationBuilder)
|
||||
{
|
||||
this.Name = name;
|
||||
this.HostApplicationBuilder = hostApplicationBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="IHostedWorkflowBuilder"/> to enable additional workflow configuration scenarios.
|
||||
/// </summary>
|
||||
public static class HostedWorkflowBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the workflow as an AI agent in the dependency injection container.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostedWorkflowBuilder"/> instance to extend.</param>
|
||||
/// <returns>An <see cref="IHostedAgentBuilder"/> that can be used to further configure the agent.</returns>
|
||||
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder)
|
||||
=> builder.AddAsAIAgent(name: null);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the workflow as an AI agent in the dependency injection container.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHostedWorkflowBuilder"/> instance to extend.</param>
|
||||
/// <param name="name">The optional name for the AI agent. If not specified, the workflow name is used.</param>
|
||||
/// <returns>An <see cref="IHostedAgentBuilder"/> that can be used to further configure the agent.</returns>
|
||||
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name)
|
||||
{
|
||||
var agentName = name ?? builder.Name;
|
||||
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) =>
|
||||
{
|
||||
var workflow = sp.GetRequiredKeyedService<Workflow>(key);
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
return workflow.AsAgentAsync(name: key).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a builder for configuring AI agents within a hosting environment.
|
||||
/// </summary>
|
||||
public interface IHostedAgentBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the agent being configured.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application host builder for configuring additional services.
|
||||
/// </summary>
|
||||
IHostApplicationBuilder HostApplicationBuilder { get; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a builder for configuring workflows within a hosting environment.
|
||||
/// </summary>
|
||||
public interface IHostedWorkflowBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the workflow being configured.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application host builder for configuring additional services.
|
||||
/// </summary>
|
||||
IHostApplicationBuilder HostApplicationBuilder { get; }
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
namespace Microsoft.Agents.AI.Hosting.Local;
|
||||
|
||||
// Implementation of an AgentCatalog which enumerates agents registered in the local service provider.
|
||||
internal sealed class LocalAgentCatalog : AgentCatalog
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
namespace Microsoft.Agents.AI.Hosting.Local;
|
||||
|
||||
internal sealed class LocalAgentRegistry
|
||||
{
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.Local;
|
||||
|
||||
internal sealed class LocalWorkflowCatalog : WorkflowCatalog
|
||||
{
|
||||
public readonly HashSet<string> _registeredWorkflows;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public LocalWorkflowCatalog(LocalWorkflowRegistry workflowRegistry, IServiceProvider serviceProvider)
|
||||
{
|
||||
this._registeredWorkflows = [.. workflowRegistry.WorkflowNames];
|
||||
this._serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<Workflow> GetWorkflowsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
|
||||
foreach (var name in this._registeredWorkflows)
|
||||
{
|
||||
var workflow = this._serviceProvider.GetKeyedService<Workflow>(name);
|
||||
if (workflow is not null)
|
||||
{
|
||||
yield return workflow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.Local;
|
||||
|
||||
internal sealed class LocalWorkflowRegistry
|
||||
{
|
||||
public HashSet<string> WorkflowNames { get; } = [];
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a catalog of registered workflows within the hosting environment.
|
||||
/// </summary>
|
||||
public abstract class WorkflowCatalog
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkflowCatalog"/> class.
|
||||
/// </summary>
|
||||
protected WorkflowCatalog()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves all registered workflows from the catalog.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public abstract IAsyncEnumerable<Workflow> GetWorkflowsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -26,6 +26,18 @@ public static partial class AgentWorkflowBuilder
|
||||
/// <param name="agents">The sequence of agents to compose into a sequential workflow.</param>
|
||||
/// <returns>The built workflow composed of the supplied <paramref name="agents"/>, in the order in which they were yielded from the source.</returns>
|
||||
public static Workflow BuildSequential(params IEnumerable<AIAgent> agents)
|
||||
=> BuildSequentialCore(workflowName: null, agents);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="Workflow{T}"/> composed of a pipeline of agents where the output of one agent is the input to the next.
|
||||
/// </summary>
|
||||
/// <param name="workflowName">The name of workflow.</param>
|
||||
/// <param name="agents">The sequence of agents to compose into a sequential workflow.</param>
|
||||
/// <returns>The built workflow composed of the supplied <paramref name="agents"/>, in the order in which they were yielded from the source.</returns>
|
||||
public static Workflow BuildSequential(string workflowName, params IEnumerable<AIAgent> agents)
|
||||
=> BuildSequentialCore(workflowName, agents);
|
||||
|
||||
private static Workflow BuildSequentialCore(string? workflowName, params IEnumerable<AIAgent> agents)
|
||||
{
|
||||
Throw.IfNull(agents);
|
||||
|
||||
@@ -60,9 +72,12 @@ public static partial class AgentWorkflowBuilder
|
||||
Debug.Assert(builder is not null);
|
||||
|
||||
OutputMessagesExecutor end = new();
|
||||
return builder.AddEdge(previous, end)
|
||||
.WithOutputFrom(end)
|
||||
.Build();
|
||||
builder = builder.AddEdge(previous, end).WithOutputFrom(end);
|
||||
if (workflowName is not null)
|
||||
{
|
||||
builder = builder.WithName(workflowName);
|
||||
}
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,6 +94,30 @@ public static partial class AgentWorkflowBuilder
|
||||
public static Workflow BuildConcurrent(
|
||||
IEnumerable<AIAgent> agents,
|
||||
Func<IList<List<ChatMessage>>, List<ChatMessage>>? aggregator = null)
|
||||
=> BuildConcurrentCore(workflowName: null, agents, aggregator);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="Workflow{T}"/> composed of agents that operate concurrently on the same input,
|
||||
/// aggregating their outputs into a single collection.
|
||||
/// </summary>
|
||||
/// <param name="workflowName">The name of the workflow.</param>
|
||||
/// <param name="agents">The set of agents to compose into a concurrent workflow.</param>
|
||||
/// <param name="aggregator">
|
||||
/// The aggregation function that accepts a list of the output messages from each <paramref name="agents"/> and produces
|
||||
/// a single result list. If <see langword="null"/>, the default behavior is to return a list containing the last message
|
||||
/// from each agent that produced at least one message.
|
||||
/// </param>
|
||||
/// <returns>The built workflow composed of the supplied concurrent <paramref name="agents"/>.</returns>
|
||||
public static Workflow BuildConcurrent(
|
||||
string workflowName,
|
||||
IEnumerable<AIAgent> agents,
|
||||
Func<IList<List<ChatMessage>>, List<ChatMessage>>? aggregator = null)
|
||||
=> BuildConcurrentCore(workflowName, agents, aggregator);
|
||||
|
||||
private static Workflow BuildConcurrentCore(
|
||||
string? workflowName,
|
||||
IEnumerable<AIAgent> agents,
|
||||
Func<IList<List<ChatMessage>>, List<ChatMessage>>? aggregator = null)
|
||||
{
|
||||
Throw.IfNull(agents);
|
||||
|
||||
@@ -105,7 +144,12 @@ public static partial class AgentWorkflowBuilder
|
||||
ConcurrentEndExecutor end = new(agentExecutors.Length, aggregator);
|
||||
builder.AddFanInEdge(end, sources: accumulators);
|
||||
|
||||
return builder.WithOutputFrom(end).Build();
|
||||
builder = builder.WithOutputFrom(end);
|
||||
if (workflowName is not null)
|
||||
{
|
||||
builder = builder.WithName(workflowName);
|
||||
}
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
/// <summary>Creates a new <see cref="HandoffsWorkflowBuilder"/> using <paramref name="initialAgent"/> as the starting agent in the workflow.</summary>
|
||||
@@ -536,57 +580,57 @@ public static partial class AgentWorkflowBuilder
|
||||
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder.AddHandler<HandoffState>(async (handoffState, context, cancellationToken) =>
|
||||
{
|
||||
string? requestedHandoff = null;
|
||||
List<AgentRunResponseUpdate> updates = [];
|
||||
List<ChatMessage> allMessages = handoffState.Messages;
|
||||
|
||||
List<ChatMessage>? roleChanges = ChangeAssistantToUserForOtherParticipants(this._agent.DisplayName, allMessages);
|
||||
|
||||
await foreach (var update in this._agent.RunStreamingAsync(allMessages,
|
||||
options: this._agentOptions,
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
string? requestedHandoff = null;
|
||||
List<AgentRunResponseUpdate> updates = [];
|
||||
List<ChatMessage> allMessages = handoffState.Messages;
|
||||
await AddUpdateAsync(update, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
List<ChatMessage>? roleChanges = ChangeAssistantToUserForOtherParticipants(this._agent.DisplayName, allMessages);
|
||||
|
||||
await foreach (var update in this._agent.RunStreamingAsync(allMessages,
|
||||
options: this._agentOptions,
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
foreach (var c in update.Contents)
|
||||
{
|
||||
await AddUpdateAsync(update, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var c in update.Contents)
|
||||
if (c is FunctionCallContent fcc && this._handoffFunctionNames.Contains(fcc.Name))
|
||||
{
|
||||
if (c is FunctionCallContent fcc && this._handoffFunctionNames.Contains(fcc.Name))
|
||||
{
|
||||
requestedHandoff = fcc.Name;
|
||||
await AddUpdateAsync(
|
||||
new AgentRunResponseUpdate
|
||||
{
|
||||
AgentId = this._agent.Id,
|
||||
AuthorName = this._agent.DisplayName,
|
||||
Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
Role = ChatRole.Tool,
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
requestedHandoff = fcc.Name;
|
||||
await AddUpdateAsync(
|
||||
new AgentRunResponseUpdate
|
||||
{
|
||||
AgentId = this._agent.Id,
|
||||
AuthorName = this._agent.DisplayName,
|
||||
Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
Role = ChatRole.Tool,
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allMessages.AddRange(updates.ToAgentRunResponse().Messages);
|
||||
allMessages.AddRange(updates.ToAgentRunResponse().Messages);
|
||||
|
||||
ResetUserToAssistantForChangedRoles(roleChanges);
|
||||
ResetUserToAssistantForChangedRoles(roleChanges);
|
||||
|
||||
await context.SendMessageAsync(new HandoffState(handoffState.TurnToken, requestedHandoff, allMessages), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(new HandoffState(handoffState.TurnToken, requestedHandoff, allMessages), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
async Task AddUpdateAsync(AgentRunResponseUpdate update, CancellationToken cancellationToken)
|
||||
async Task AddUpdateAsync(AgentRunResponseUpdate update, CancellationToken cancellationToken)
|
||||
{
|
||||
updates.Add(update);
|
||||
if (handoffState.TurnToken.EmitEvents is true)
|
||||
{
|
||||
updates.Add(update);
|
||||
if (handoffState.TurnToken.EmitEvents is true)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
public ValueTask ResetAsync() => default;
|
||||
}
|
||||
|
||||
+7
-55
@@ -19,25 +19,6 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => HostApplicationBuilderAgentExtensions.AddAIAgent(null!, "agent", "instructions"));
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent with valid parameters returns the same builder instance.
|
||||
/// </summary>
|
||||
/// <param name="chatClientKey">The chat client key to use, or null to use the default service.</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("customKey")]
|
||||
public void AddAIAgent_ValidParameters_ReturnsBuilder(string? chatClientKey)
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent("agentName", "instructions", chatClientKey);
|
||||
|
||||
// Assert
|
||||
Assert.Same(builder, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent without chat client key throws ArgumentNullException for null name.
|
||||
/// </summary>
|
||||
@@ -59,14 +40,9 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
[Fact]
|
||||
public void AddAIAgent_NullInstructions_AllowsNull()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent("agentName", (string)null!);
|
||||
|
||||
// Assert
|
||||
Assert.Same(builder, result);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,14 +66,9 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
[Fact]
|
||||
public void AddAIAgentWithKey_NullInstructions_AllowsNull()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent("agentName", null!, "key");
|
||||
|
||||
// Assert
|
||||
Assert.Same(builder, result);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -148,15 +119,11 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
[Fact]
|
||||
public void AddAIAgentWithFactory_ValidParameters_ReturnsBuilder()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent("agentName", (sp, key) => mockAgent.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Same(builder, result);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -192,9 +159,9 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
// Act
|
||||
builder.AddAIAgent("agent1", "instructions1")
|
||||
.AddAIAgent("agent2", "instructions2")
|
||||
.AddAIAgent("agent3", "instructions3");
|
||||
builder.AddAIAgent("agent1", "instructions1");
|
||||
builder.AddAIAgent("agent2", "instructions2");
|
||||
builder.AddAIAgent("agent3", "instructions3");
|
||||
|
||||
// Assert
|
||||
var agentDescriptors = builder.Services
|
||||
@@ -227,14 +194,9 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
[Fact]
|
||||
public void AddAIAgent_EmptyInstructions_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent("agentName", "");
|
||||
|
||||
// Assert
|
||||
Assert.Same(builder, result);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
/// <summary>
|
||||
/// Verifies that AddAIAgent without chat client key calls the overload with null key.
|
||||
@@ -242,14 +204,9 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
[Fact]
|
||||
public void AddAIAgent_WithoutKey_CallsOverloadWithNullKey()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent("agentName", "instructions");
|
||||
|
||||
// Assert
|
||||
Assert.Same(builder, result);
|
||||
// The agent should be registered (proving the method chain worked)
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => d.ServiceKey is "agentName" &&
|
||||
@@ -270,14 +227,9 @@ public class HostApplicationBuilderAgentExtensionsTests
|
||||
[InlineData("my.agent_1:type-name")] // complex valid name
|
||||
public void AddAIAgent_ValidSpecialCharactersInName_Succeeds(string name)
|
||||
{
|
||||
// Arrange
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
// Act
|
||||
var result = builder.AddAIAgent(name, "instructions");
|
||||
|
||||
// Assert
|
||||
Assert.Same(builder, result);
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == name &&
|
||||
d.ServiceType == typeof(AIAgent));
|
||||
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.UnitTests;
|
||||
|
||||
public class HostApplicationBuilderWorkflowExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that providing a null builder to AddWorkflow throws an ArgumentNullException.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_NullBuilder_ThrowsArgumentNullException() =>
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => HostApplicationBuilderWorkflowExtensions.AddWorkflow(
|
||||
null!,
|
||||
"workflow",
|
||||
(sp, key) => CreateTestWorkflow(key)));
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow throws ArgumentNullException for null name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
builder.AddWorkflow(null!, (sp, key) => CreateTestWorkflow(key)));
|
||||
Assert.Equal("name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow throws ArgumentNullException for null factory delegate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_NullFactory_ThrowsArgumentNullException()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
builder.AddWorkflow("workflowName", null!));
|
||||
Assert.Equal("createWorkflowDelegate", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow returns the IHostWorkflowBuilder instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_ValidParameters_ReturnsBuilder()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var result = builder.AddWorkflow("workflowName", (sp, key) => CreateTestWorkflow(key));
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.IsAssignableFrom<IHostedWorkflowBuilder>(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow registers the workflow as a keyed singleton service.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_RegistersKeyedSingleton()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
const string WorkflowName = "testWorkflow";
|
||||
|
||||
builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key));
|
||||
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == WorkflowName &&
|
||||
d.ServiceType == typeof(Workflow));
|
||||
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow can be called multiple times with different workflow names.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_MultipleCalls_RegistersMultipleWorkflows()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
builder.AddWorkflow("workflow1", (sp, key) => CreateTestWorkflow(key));
|
||||
builder.AddWorkflow("workflow2", (sp, key) => CreateTestWorkflow(key));
|
||||
builder.AddWorkflow("workflow3", (sp, key) => CreateTestWorkflow(key));
|
||||
|
||||
var workflowDescriptors = builder.Services
|
||||
.Where(d => d.ServiceType == typeof(Workflow) && d.ServiceKey is string)
|
||||
.ToList();
|
||||
|
||||
Assert.Equal(3, workflowDescriptors.Count);
|
||||
Assert.Contains(workflowDescriptors, d => (string)d.ServiceKey! == "workflow1");
|
||||
Assert.Contains(workflowDescriptors, d => (string)d.ServiceKey! == "workflow2");
|
||||
Assert.Contains(workflowDescriptors, d => (string)d.ServiceKey! == "workflow3");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow handles empty strings for name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddWorkflow_EmptyName_ThrowsArgumentException()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
var result = builder.AddWorkflow("", (sp, key) => CreateTestWorkflow(key));
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddWorkflow with special characters in name works correctly for valid names.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("workflow_name")] // underscore is allowed
|
||||
[InlineData("Workflow123")] // alphanumeric is allowed
|
||||
[InlineData("_workflow")] // can start with underscore
|
||||
[InlineData("workflow-name")] // dash is allowed
|
||||
[InlineData("workflow.name")] // period is allowed
|
||||
[InlineData("workflow:type")] // colon is allowed
|
||||
[InlineData("my.workflow_1:type-name")] // complex valid name
|
||||
public void AddWorkflow_ValidSpecialCharactersInName_Succeeds(string name)
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var result = builder.AddWorkflow(name, (sp, key) => CreateTestWorkflow(key));
|
||||
|
||||
var descriptor = builder.Services.FirstOrDefault(
|
||||
d => (d.ServiceKey as string) == name &&
|
||||
d.ServiceType == typeof(Workflow));
|
||||
Assert.NotNull(descriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that providing a null builder to AddConcurrentWorkflow throws an ArgumentNullException.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddConcurrentWorkflow_NullBuilder_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
HostApplicationBuilderWorkflowExtensions.AddConcurrentWorkflow(null!, "workflow", [null!]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddConcurrentWorkflow throws ArgumentNullException for null name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddConcurrentWorkflow_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
builder.AddConcurrentWorkflow(null!, [new HostedAgentBuilder("test", builder)]));
|
||||
Assert.Equal("name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddConcurrentWorkflow throws ArgumentNullException for null agent builders.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddConcurrentWorkflow_NullAgentBuilders_ThrowsArgumentNullException()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
builder.AddConcurrentWorkflow("workflowName", null!));
|
||||
Assert.Equal("agentBuilders", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddConcurrentWorkflow returns IHostWorkflowBuilder instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddConcurrentWorkflow_ValidParameters_ReturnsBuilder()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var result = builder.AddConcurrentWorkflow("concurrentWorkflow", [new HostedAgentBuilder("test", builder)]);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.IsAssignableFrom<IHostedWorkflowBuilder>(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that providing a null builder to AddSequentialWorkflow throws an ArgumentNullException.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddSequentialWorkflow_NullBuilder_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
HostApplicationBuilderWorkflowExtensions.AddSequentialWorkflow(null!, "workflow", [null!]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddSequentialWorkflow throws ArgumentNullException for null name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddSequentialWorkflow_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
builder.AddSequentialWorkflow(null!, [new HostedAgentBuilder("test", builder)]));
|
||||
Assert.Equal("name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddSequentialWorkflow throws ArgumentNullException for null agent builders.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddSequentialWorkflow_NullAgentBuilders_ThrowsArgumentNullException()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
builder.AddSequentialWorkflow("workflowName", null!));
|
||||
Assert.Equal("agentBuilders", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSequentialWorkflow_EmptyAgentBuilders_Throws()
|
||||
{
|
||||
var builder = new HostApplicationBuilder();
|
||||
|
||||
var exception = Assert.Throws<ArgumentException>(() =>
|
||||
builder.AddSequentialWorkflow("sequentialWorkflow", Array.Empty<IHostedAgentBuilder>()));
|
||||
Assert.Equal("agentBuilders", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to create a simple test workflow with a given name.
|
||||
/// </summary>
|
||||
private static Workflow CreateTestWorkflow(string name)
|
||||
{
|
||||
// Create a simple workflow using AgentWorkflowBuilder
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns("testAgent");
|
||||
|
||||
return AgentWorkflowBuilder.BuildSequential(workflowName: name, agents: [mockAgent.Object]);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public class AgentWorkflowBuilderTests
|
||||
[Fact]
|
||||
public void BuildSequential_InvalidArguments_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>("agents", () => AgentWorkflowBuilder.BuildSequential(null!));
|
||||
Assert.Throws<ArgumentNullException>("agents", () => AgentWorkflowBuilder.BuildSequential(workflowName: null!, null!));
|
||||
Assert.Throws<ArgumentException>("agents", () => AgentWorkflowBuilder.BuildSequential());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user