Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;

#pragma warning disable SA1202 // Elements should be ordered by access

namespace Microsoft.Extensions.AI;

/// <summary>
/// Provides an optional base class for an <see cref="AIFunction"/> that passes through calls to another instance.
/// </summary>
public class DelegatingAIFunction : AIFunction
{
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingAIFunction"/> class as a wrapper around <paramref name="innerFunction"/>.
/// </summary>
/// <param name="innerFunction">The inner AI function to which all calls are delegated by default.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerFunction"/> is <see langword="null"/>.</exception>
protected DelegatingAIFunction(AIFunction innerFunction)
{
InnerFunction = Throw.IfNull(innerFunction);
}

/// <summary>Gets the inner <see cref="AIFunction" />.</summary>
protected AIFunction InnerFunction { get; }

/// <inheritdoc />
public override string Name => InnerFunction.Name;

/// <inheritdoc />
public override string Description => InnerFunction.Description;

/// <inheritdoc />
public override JsonElement JsonSchema => InnerFunction.JsonSchema;

/// <inheritdoc />
public override JsonElement? ReturnJsonSchema => InnerFunction.ReturnJsonSchema;

/// <inheritdoc />
public override JsonSerializerOptions JsonSerializerOptions => InnerFunction.JsonSerializerOptions;

/// <inheritdoc />
public override MethodInfo? UnderlyingMethod => InnerFunction.UnderlyingMethod;

/// <inheritdoc />
public override IReadOnlyDictionary<string, object?> AdditionalProperties => InnerFunction.AdditionalProperties;

/// <inheritdoc />
public override string ToString() => InnerFunction.ToString();

/// <inheritdoc />
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) =>
InnerFunction.InvokeAsync(arguments, cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,58 @@
}
]
},
{
"Type": "class Microsoft.Extensions.AI.DelegatingAIFunction : Microsoft.Extensions.AI.AIFunction",
"Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.DelegatingAIFunction.DelegatingAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);",
"Stage": "Stable"
},
{
"Member": "override System.Threading.Tasks.ValueTask<object?> Microsoft.Extensions.AI.DelegatingAIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);",
"Stage": "Stable"
},
{
"Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.ToString();",
"Stage": "Experimental"
}
],
"Properties": [
{
"Member": "Microsoft.Extensions.AI.AIFunction Microsoft.Extensions.AI.DelegatingAIFunction.InnerFunction { get; }",
"Stage": "Stable"
},
{
"Member": "override System.Collections.Generic.IReadOnlyDictionary<string, object?> Microsoft.Extensions.AI.DelegatingAIFunction.AdditionalProperties { get; }",
"Stage": "Stable"
},
{
"Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Description { get; }",
"Stage": "Stable"
},
{
"Member": "override System.Text.Json.JsonElement Microsoft.Extensions.AI.DelegatingAIFunction.JsonSchema { get; }",
"Stage": "Stable"
},
{
"Member": "override System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.DelegatingAIFunction.JsonSerializerOptions { get; }",
"Stage": "Stable"
},
{
"Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Name { get; }",
"Stage": "Stable"
},
{
"Member": "override System.Text.Json.JsonElement? Microsoft.Extensions.AI.DelegatingAIFunction.ReturnJsonSchema { get; }",
"Stage": "Stable"
},
{
"Member": "override System.Reflection.MethodInfo? Microsoft.Extensions.AI.DelegatingAIFunction.UnderlyingMethod { get; }",
"Stage": "Stable"
}
]
},
{
"Type": "class Microsoft.Extensions.AI.DelegatingChatClient : Microsoft.Extensions.AI.IChatClient, System.IDisposable",
"Stage": "Stable",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Extensions.AI;

public class DelegatingAIFunctionTests
{
[Fact]
public void Constructor_NullInnerFunction_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>("innerFunction", () => new DerivedFunction(null!));
}

[Fact]
public void DefaultOverrides_DelegateToInnerFunction()
{
AIFunction expected = AIFunctionFactory.Create(() => 42);
DerivedFunction actual = new(expected);

Assert.Same(expected, actual.InnerFunction);
Assert.Equal(expected.Name, actual.Name);
Assert.Equal(expected.Description, actual.Description);
Assert.Equal(expected.JsonSchema, actual.JsonSchema);
Assert.Equal(expected.ReturnJsonSchema, actual.ReturnJsonSchema);
Assert.Same(expected.JsonSerializerOptions, actual.JsonSerializerOptions);
Assert.Same(expected.UnderlyingMethod, actual.UnderlyingMethod);
Assert.Same(expected.AdditionalProperties, actual.AdditionalProperties);
Assert.Equal(expected.ToString(), actual.ToString());
}

private sealed class DerivedFunction(AIFunction innerFunction) : DelegatingAIFunction(innerFunction)
{
public new AIFunction InnerFunction => base.InnerFunction;
}

[Fact]
public void Virtuals_AllOverridden()
{
Assert.All(typeof(DelegatingAIFunction).GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), m =>
{
switch (m)
{
case MethodInfo methodInfo when methodInfo.IsVirtual && methodInfo.Name is not ("Finalize" or "Equals" or "GetHashCode"):
Assert.True(methodInfo.DeclaringType == typeof(DelegatingAIFunction), $"{methodInfo.Name} not overridden");
break;

case PropertyInfo propertyInfo when propertyInfo.GetMethod?.IsVirtual is true:
Assert.True(propertyInfo.DeclaringType == typeof(DelegatingAIFunction), $"{propertyInfo.Name} not overridden");
break;
}
});
}

[Fact]
public async Task OverriddenInvocation_SuccessfullyInvoked()
{
bool innerInvoked = false;
AIFunction inner = AIFunctionFactory.Create(int () =>
{
innerInvoked = true;
throw new Exception("uh oh");
}, "TestFunction", "A test function for DelegatingAIFunction");

AIFunction actual = new OverridesInvocation(inner, (args, ct) => new ValueTask<object?>(84));

Assert.Equal(inner.Name, actual.Name);
Assert.Equal(inner.Description, actual.Description);
Assert.Equal(inner.JsonSchema, actual.JsonSchema);
Assert.Equal(inner.ReturnJsonSchema, actual.ReturnJsonSchema);
Assert.Same(inner.JsonSerializerOptions, actual.JsonSerializerOptions);
Assert.Same(inner.UnderlyingMethod, actual.UnderlyingMethod);
Assert.Same(inner.AdditionalProperties, actual.AdditionalProperties);
Assert.Equal(inner.ToString(), actual.ToString());

object? result = await actual.InvokeAsync(new(), CancellationToken.None);
Assert.Contains("84", result?.ToString());

Assert.False(innerInvoked);
}

private sealed class OverridesInvocation(AIFunction innerFunction, Func<AIFunctionArguments, CancellationToken, ValueTask<object?>> invokeAsync) : DelegatingAIFunction(innerFunction)
{
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) =>
invokeAsync(arguments, cancellationToken);
}
}
Loading