mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into crickman/workflows-declarative-nocodegen
This commit is contained in:
@@ -38,6 +38,7 @@ internal static class SourceBuilder
|
||||
sb.AppendLine("using System.Collections.Generic;");
|
||||
sb.AppendLine("using Microsoft.Agents.AI.Workflows;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("using RouteBuilder = Microsoft.Agents.AI.Workflows.RouteBuilder;");
|
||||
|
||||
// Namespace
|
||||
if (!string.IsNullOrWhiteSpace(info.Namespace))
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extensions methods for creating <see cref="Configured{TSubject}"/> objects
|
||||
/// Provides extension methods for creating <see cref="Configured{TSubject}"/> objects
|
||||
/// </summary>
|
||||
public static class ConfigurationExtensions
|
||||
internal static class ConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new configuration that treats the subject as its base type, allowing configuration to be applied at
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// <summary>
|
||||
/// Provides methods for creating <see cref="Configured{TSubject}"/> instances.
|
||||
/// </summary>
|
||||
public static class Configured
|
||||
internal static class Configured
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Configured{TSubject}"/> instance from an existing subject instance.
|
||||
@@ -50,10 +50,10 @@ public static class Configured
|
||||
/// A representation of a preconfigured, lazy-instantiatable instance of <typeparamref name="TSubject"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSubject">The type of the preconfigured subject.</typeparam>
|
||||
/// <param name="factoryAsync">A factory to intantiate the subject when desired.</param>
|
||||
/// <param name="factoryAsync">A factory to instantiate the subject when desired.</param>
|
||||
/// <param name="id">The unique identifier for the configured subject.</param>
|
||||
/// <param name="raw"></param>
|
||||
public class Configured<TSubject>(Func<Config, string, ValueTask<TSubject>> factoryAsync, string id, object? raw = null)
|
||||
internal class Configured<TSubject>(Func<ExecutorConfig, string, ValueTask<TSubject>> factoryAsync, string id, object? raw = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the raw representation of the configured object, if any.
|
||||
@@ -66,14 +66,14 @@ public class Configured<TSubject>(Func<Config, string, ValueTask<TSubject>> fact
|
||||
public string Id => id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the factory function to create an instance of <typeparamref name="TSubject"/> given a <see cref="Config"/>.
|
||||
/// Gets the factory function to create an instance of <typeparamref name="TSubject"/> given a <see cref="ExecutorConfig"/>.
|
||||
/// </summary>
|
||||
public Func<Config, string, ValueTask<TSubject>> FactoryAsync => factoryAsync;
|
||||
public Func<ExecutorConfig, string, ValueTask<TSubject>> FactoryAsync => factoryAsync;
|
||||
|
||||
/// <summary>
|
||||
/// The configuration for this configured instance.
|
||||
/// </summary>
|
||||
public Config Configuration => new(this.Id);
|
||||
public ExecutorConfig Configuration => new(this.Id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a "partially" applied factory function that only requires no parameters to create an instance of
|
||||
@@ -87,11 +87,11 @@ public class Configured<TSubject>(Func<Config, string, ValueTask<TSubject>> fact
|
||||
/// </summary>
|
||||
/// <typeparam name="TSubject">The type of the preconfigured subject.</typeparam>
|
||||
/// <typeparam name="TOptions">The type of configuration options for the preconfigured subject.</typeparam>
|
||||
/// <param name="factoryAsync">A factory to intantiate the subject when desired.</param>
|
||||
/// <param name="factoryAsync">A factory to instantiate the subject when desired.</param>
|
||||
/// <param name="id">The unique identifier for the configured subject.</param>
|
||||
/// <param name="options">Additional configuration options for the subject.</param>
|
||||
/// <param name="raw"></param>
|
||||
public class Configured<TSubject, TOptions>(Func<Config<TOptions>, string, ValueTask<TSubject>> factoryAsync, string id, TOptions? options = default, object? raw = null)
|
||||
internal class Configured<TSubject, TOptions>(Func<ExecutorConfig<TOptions>, string, ValueTask<TSubject>> factoryAsync, string id, TOptions? options = default, object? raw = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// The raw representation of the configured object, if any.
|
||||
@@ -109,14 +109,14 @@ public class Configured<TSubject, TOptions>(Func<Config<TOptions>, string, Value
|
||||
public TOptions? Options => options;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the factory function to create an instance of <typeparamref name="TSubject"/> given a <see cref="Config{TOptions}"/>.
|
||||
/// Gets the factory function to create an instance of <typeparamref name="TSubject"/> given a <see cref="ExecutorConfig{TOptions}"/>.
|
||||
/// </summary>
|
||||
public Func<Config<TOptions>, string, ValueTask<TSubject>> FactoryAsync => factoryAsync;
|
||||
public Func<ExecutorConfig<TOptions>, string, ValueTask<TSubject>> FactoryAsync => factoryAsync;
|
||||
|
||||
/// <summary>
|
||||
/// The configuration for this configured instance.
|
||||
/// </summary>
|
||||
public Config<TOptions> Configuration => new(this.Id, this.Options);
|
||||
public ExecutorConfig<TOptions> Configuration => new(this.Id, this.Options);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a "partially" applied factory function that only requires no parameters to create an instance of
|
||||
@@ -124,11 +124,11 @@ public class Configured<TSubject, TOptions>(Func<Config<TOptions>, string, Value
|
||||
/// </summary>
|
||||
internal Func<string, ValueTask<TSubject>> BoundFactoryAsync => (sessionId) => this.CreateValidatingMemoizedFactory()(this.Configuration, sessionId);
|
||||
|
||||
private Func<Config, string, ValueTask<TSubject>> CreateValidatingMemoizedFactory()
|
||||
private Func<ExecutorConfig, string, ValueTask<TSubject>> CreateValidatingMemoizedFactory()
|
||||
{
|
||||
return FactoryAsync;
|
||||
|
||||
async ValueTask<TSubject> FactoryAsync(Config configuration, string sessionId)
|
||||
async ValueTask<TSubject> FactoryAsync(ExecutorConfig configuration, string sessionId)
|
||||
{
|
||||
if (this.Id != configuration.Id)
|
||||
{
|
||||
|
||||
@@ -113,7 +113,7 @@ public static class ExecutorBindingExtensions
|
||||
/// <param name="id">An id for the executor to be instantiated.</param>
|
||||
/// <param name="options">An optional parameter specifying the options.</param>
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
public static ExecutorBinding BindExecutor<TExecutor, TOptions>(this Func<Config<TOptions>, string, ValueTask<TExecutor>> factoryAsync, string id, TOptions? options = null)
|
||||
public static ExecutorBinding BindExecutor<TExecutor, TOptions>(this Func<ExecutorConfig<TOptions>, string, ValueTask<TExecutor>> factoryAsync, string id, TOptions? options = null)
|
||||
where TExecutor : Executor
|
||||
where TOptions : ExecutorOptions
|
||||
{
|
||||
@@ -139,7 +139,7 @@ public static class ExecutorBindingExtensions
|
||||
/// <returns>An <see cref="ExecutorBinding"/> instance that resolves to the result of the factory call when messages get sent to it.</returns>
|
||||
[Obsolete("Use BindExecutor() instead")]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static ExecutorBinding ConfigureFactory<TExecutor, TOptions>(this Func<Config<TOptions>, string, ValueTask<TExecutor>> factoryAsync, string id, TOptions? options = null)
|
||||
public static ExecutorBinding ConfigureFactory<TExecutor, TOptions>(this Func<ExecutorConfig<TOptions>, string, ValueTask<TExecutor>> factoryAsync, string id, TOptions? options = null)
|
||||
where TExecutor : Executor
|
||||
where TOptions : ExecutorOptions
|
||||
=> factoryAsync.BindExecutor(id, options);
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// Represents a configuration for an object with a string identifier. For example, <see cref="IIdentified"/> object.
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for the configurable object.</param>
|
||||
public class Config(string id)
|
||||
public class ExecutorConfig(string id)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a unique identifier for the configurable object.
|
||||
@@ -23,7 +23,7 @@ public class Config(string id)
|
||||
/// <typeparam name="TOptions">The type of options for the configurable object.</typeparam>
|
||||
/// <param name="id">A unique identifier for the configurable object.</param>
|
||||
/// <param name="options">The options for the configurable object.</param>
|
||||
public class Config<TOptions>(string id, TOptions? options = default) : Config(id)
|
||||
public class ExecutorConfig<TOptions>(string id, TOptions? options = default) : ExecutorConfig(id)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the options for the configured object.
|
||||
+2
@@ -161,6 +161,8 @@ internal sealed class AIContextProviderChatClient : DelegatingChatClient
|
||||
}
|
||||
|
||||
// Materialize the accumulated context back into messages and options.
|
||||
// Clone options to avoid mutating the caller's instance across calls.
|
||||
options = options?.Clone();
|
||||
var enrichedMessages = aiContext.Messages ?? [];
|
||||
|
||||
var tools = aiContext.Tools as IList<AITool> ?? aiContext.Tools?.ToList();
|
||||
|
||||
+161
@@ -250,6 +250,129 @@ public class AIContextProviderChatClientTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Shared Options Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsync_SharedOptions_ProviderToolsDoNotAccumulateAcrossCallsAsync()
|
||||
{
|
||||
// Arrange: track tool count seen by the inner client on each call
|
||||
var toolCountsSeenByInner = new List<int>();
|
||||
|
||||
var innerClient = CreateMockChatClient(
|
||||
onGetResponse: (_, options, _) =>
|
||||
{
|
||||
toolCountsSeenByInner.Add(options?.Tools?.Count ?? 0);
|
||||
return Task.FromResult(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Response")]));
|
||||
});
|
||||
|
||||
var provider = new TestAIContextProvider("key1", provideTools: [new TestAITool()]);
|
||||
var chatClient = new AIContextProviderChatClient(innerClient, [provider]);
|
||||
|
||||
var sharedOptions = new ChatOptions
|
||||
{
|
||||
Tools = new List<AITool> { new TestAITool() }
|
||||
};
|
||||
|
||||
// Act: make 3 calls reusing the same ChatOptions
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await RunWithAgentContextAsync(chatClient, sharedOptions);
|
||||
}
|
||||
|
||||
// Assert: each call should see exactly 2 tools (1 baseline + 1 injected)
|
||||
Assert.Equal(3, toolCountsSeenByInner.Count);
|
||||
Assert.All(toolCountsSeenByInner, count => Assert.Equal(2, count));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsync_SharedOptions_OriginalToolsNotMutatedAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerClient = CreateMockChatClient(
|
||||
onGetResponse: (_, _, _) =>
|
||||
Task.FromResult(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Response")])));
|
||||
|
||||
var provider = new TestAIContextProvider("key1", provideTools: [new TestAITool()]);
|
||||
var chatClient = new AIContextProviderChatClient(innerClient, [provider]);
|
||||
|
||||
var baselineTool = new TestAITool();
|
||||
var originalTools = new List<AITool> { baselineTool };
|
||||
var sharedOptions = new ChatOptions
|
||||
{
|
||||
Tools = originalTools
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunWithAgentContextAsync(chatClient, sharedOptions);
|
||||
|
||||
// Assert: the original list should still contain only the baseline tool
|
||||
Assert.Single(originalTools);
|
||||
Assert.Same(baselineTool, originalTools[0]);
|
||||
Assert.Same(originalTools, sharedOptions.Tools);
|
||||
Assert.Same(baselineTool, originalTools[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_SharedOptions_ProviderToolsDoNotAccumulateAcrossCallsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var toolCountsSeenByInner = new List<int>();
|
||||
|
||||
var innerClient = CreateMockStreamingChatClient(
|
||||
onGetStreamingResponse: (_, options, _) =>
|
||||
{
|
||||
toolCountsSeenByInner.Add(options?.Tools?.Count ?? 0);
|
||||
return ToAsyncEnumerableAsync(
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Response"));
|
||||
});
|
||||
|
||||
var provider = new TestAIContextProvider("key1", provideTools: [new TestAITool()]);
|
||||
var chatClient = new AIContextProviderChatClient(innerClient, [provider]);
|
||||
|
||||
var sharedOptions = new ChatOptions
|
||||
{
|
||||
Tools = new List<AITool> { new TestAITool() }
|
||||
};
|
||||
|
||||
// Act: make 3 streaming calls reusing the same ChatOptions
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await RunStreamingWithAgentContextAsync(chatClient, [], sharedOptions);
|
||||
}
|
||||
|
||||
// Assert: each call should see exactly 2 tools (1 baseline + 1 injected)
|
||||
Assert.Equal(3, toolCountsSeenByInner.Count);
|
||||
Assert.All(toolCountsSeenByInner, count => Assert.Equal(2, count));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsync_SharedOptions_OriginalToolsNotMutatedAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerClient = CreateMockStreamingChatClient(
|
||||
onGetStreamingResponse: (_, _, _) => ToAsyncEnumerableAsync(
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Response")));
|
||||
|
||||
var provider = new TestAIContextProvider("key1", provideTools: [new TestAITool()]);
|
||||
var chatClient = new AIContextProviderChatClient(innerClient, [provider]);
|
||||
|
||||
var baselineTool = new TestAITool();
|
||||
var originalTools = new List<AITool> { baselineTool };
|
||||
var sharedOptions = new ChatOptions
|
||||
{
|
||||
Tools = originalTools
|
||||
};
|
||||
|
||||
// Act
|
||||
await RunStreamingWithAgentContextAsync(chatClient, [], sharedOptions);
|
||||
|
||||
// Assert: the original list should still contain only the baseline tool
|
||||
Assert.Single(originalTools);
|
||||
Assert.Same(baselineTool, originalTools[0]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Builder Extension Tests
|
||||
|
||||
[Fact]
|
||||
@@ -341,6 +464,44 @@ public class AIContextProviderChatClientTests
|
||||
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hello")], s_mockSession);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a chat client within an agent context with the specified options.
|
||||
/// </summary>
|
||||
private static async Task RunWithAgentContextAsync(AIContextProviderChatClient chatClient, ChatOptions options)
|
||||
{
|
||||
var agent = new TestAIAgent
|
||||
{
|
||||
RunAsyncFunc = async (messages, session, agentOptions, ct) =>
|
||||
{
|
||||
var response = await chatClient.GetResponseAsync(messages, options, ct);
|
||||
return new AgentResponse(response);
|
||||
}
|
||||
};
|
||||
|
||||
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hello")], s_mockSession);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a streaming chat client within an agent context with the specified options.
|
||||
/// </summary>
|
||||
private static async Task RunStreamingWithAgentContextAsync(AIContextProviderChatClient chatClient, List<ChatResponseUpdate> updates, ChatOptions options)
|
||||
{
|
||||
var agent = new TestAIAgent
|
||||
{
|
||||
RunAsyncFunc = async (messages, session, agentOptions, ct) =>
|
||||
{
|
||||
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, ct))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
return new AgentResponse([new ChatMessage(ChatRole.Assistant, "done")]);
|
||||
}
|
||||
};
|
||||
|
||||
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hello")], s_mockSession);
|
||||
}
|
||||
|
||||
private static IChatClient CreateMockChatClient(
|
||||
Func<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken, Task<ChatResponse>> onGetResponse)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user