Skip to content

Commit e57e605

Browse files
authored
Split AIFunction into a base class (#6695)
1 parent 18bfe72 commit e57e605

34 files changed

+530
-131
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release History
22

3+
## NOT YET RELEASED
4+
5+
- Added non-invocable `AIFunctionDeclaration` (base class for `AIFunction`), `AIFunctionFactory.CreateDeclaration`, and `AIFunction.AsDeclarationOnly`.
6+
37
## 9.8.0
48

59
- Added `AIAnnotation` and related types to represent citations and other annotations in chat messages.

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatToolMode.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ private protected ChatToolMode()
5555

5656
/// <summary>
5757
/// Instantiates a <see cref="ChatToolMode"/> indicating that tool usage is required,
58-
/// and that the specified <see cref="AIFunction"/> must be selected. The function name
59-
/// must match an entry in <see cref="ChatOptions.Tools"/>.
58+
/// and that the specified function name must be selected.
6059
/// </summary>
6160
/// <param name="functionName">The name of the required function.</param>
6261
/// <returns>An instance of <see cref="RequiredChatToolMode"/> for the specified function name.</returns>

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ namespace Microsoft.Extensions.AI;
1515
public sealed class RequiredChatToolMode : ChatToolMode
1616
{
1717
/// <summary>
18-
/// Gets the name of a specific <see cref="AIFunction"/> that must be called.
18+
/// Gets the name of a specific tool that must be called.
1919
/// </summary>
2020
/// <remarks>
21-
/// If the value is <see langword="null"/>, any available function can be selected (but at least one must be).
21+
/// If the value is <see langword="null"/>, any available tool can be selected (but at least one must be).
2222
/// </remarks>
2323
public string? RequiredFunctionName { get; }
2424

2525
/// <summary>
26-
/// Initializes a new instance of the <see cref="RequiredChatToolMode"/> class that requires a specific function to be called.
26+
/// Initializes a new instance of the <see cref="RequiredChatToolMode"/> class that requires a specific tool to be called.
2727
/// </summary>
28-
/// <param name="requiredFunctionName">The name of the function that must be called.</param>
28+
/// <param name="requiredFunctionName">The name of the tool that must be called.</param>
2929
/// <exception cref="ArgumentException"><paramref name="requiredFunctionName"/> is empty or composed entirely of whitespace.</exception>
3030
/// <remarks>
3131
/// <paramref name="requiredFunctionName"/> can be <see langword="null"/>. However, it's preferable to use

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,17 @@
66
using System.Threading;
77
using System.Threading.Tasks;
88

9+
#pragma warning disable SA1202 // Elements should be ordered by access
10+
911
namespace Microsoft.Extensions.AI;
1012

1113
/// <summary>Represents a function that can be described to an AI service and invoked.</summary>
12-
public abstract class AIFunction : AITool
14+
public abstract class AIFunction : AIFunctionDeclaration
1315
{
14-
/// <summary>Gets a JSON Schema describing the function and its input parameters.</summary>
15-
/// <remarks>
16-
/// <para>
17-
/// When specified, declares a self-contained JSON schema document that describes the function and its input parameters.
18-
/// A simple example of a JSON schema for a function that adds two numbers together is shown below:
19-
/// </para>
20-
/// <code>
21-
/// {
22-
/// "title" : "addNumbers",
23-
/// "description": "A simple function that adds two numbers together.",
24-
/// "type": "object",
25-
/// "properties": {
26-
/// "a" : { "type": "number" },
27-
/// "b" : { "type": "number", "default": 1 }
28-
/// },
29-
/// "required" : ["a"]
30-
/// }
31-
/// </code>
32-
/// <para>
33-
/// The metadata present in the schema document plays an important role in guiding AI function invocation.
34-
/// </para>
35-
/// <para>
36-
/// When no schema is specified, consuming chat clients should assume the "{}" or "true" schema, indicating that any JSON input is admissible.
37-
/// </para>
38-
/// </remarks>
39-
public virtual JsonElement JsonSchema => AIJsonUtilities.DefaultJsonSchema;
40-
41-
/// <summary>Gets a JSON Schema describing the function's return value.</summary>
42-
/// <remarks>
43-
/// A <see langword="null"/> typically reflects a function that doesn't specify a return schema
44-
/// or a function that returns <see cref="void"/>, <see cref="Task"/>, or <see cref="ValueTask"/>.
45-
/// </remarks>
46-
public virtual JsonElement? ReturnJsonSchema => null;
16+
/// <summary>Initializes a new instance of the <see cref="AIFunction"/> class.</summary>
17+
protected AIFunction()
18+
{
19+
}
4720

4821
/// <summary>
4922
/// Gets the underlying <see cref="MethodInfo"/> that this <see cref="AIFunction"/> might be wrapping.
@@ -72,4 +45,14 @@ public abstract class AIFunction : AITool
7245
protected abstract ValueTask<object?> InvokeCoreAsync(
7346
AIFunctionArguments arguments,
7447
CancellationToken cancellationToken);
48+
49+
/// <summary>Creates a <see cref="AIFunctionDeclaration"/> representation of this <see cref="AIFunction"/> that can't be invoked.</summary>
50+
/// <returns>The created instance.</returns>
51+
/// <remarks>
52+
/// <see cref="AIFunction"/> derives from <see cref="AIFunctionDeclaration"/>, layering on the ability to invoke the function in addition
53+
/// to describing it. <see cref="AsDeclarationOnly"/> creates a new object that describes the function but that can't be invoked.
54+
/// </remarks>
55+
public AIFunctionDeclaration AsDeclarationOnly() => new NonInvocableAIFunction(this);
56+
57+
private sealed class NonInvocableAIFunction(AIFunction function) : DelegatingAIFunctionDeclaration(function);
7558
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json;
5+
using System.Threading.Tasks;
6+
7+
#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods
8+
9+
namespace Microsoft.Extensions.AI;
10+
11+
/// <summary>Represents a function that can be described to an AI service.</summary>
12+
/// <remarks>
13+
/// <see cref="AIFunctionDeclaration"/> is the base class for <see cref="AIFunction"/>, which
14+
/// adds the ability to invoke the function. Components may type test <see cref="AITool"/> instances
15+
/// for <see cref="AIFunctionDeclaration"/> to determine whether they can be described as functions,
16+
/// and may type test for <see cref="AIFunction"/> to determine whether they can be invoked.
17+
/// </remarks>
18+
public abstract class AIFunctionDeclaration : AITool
19+
{
20+
/// <summary>Initializes a new instance of the <see cref="AIFunctionDeclaration"/> class.</summary>
21+
protected AIFunctionDeclaration()
22+
{
23+
}
24+
25+
/// <summary>Gets a JSON Schema describing the function and its input parameters.</summary>
26+
/// <remarks>
27+
/// <para>
28+
/// When specified, declares a self-contained JSON schema document that describes the function and its input parameters.
29+
/// A simple example of a JSON schema for a function that adds two numbers together is shown below:
30+
/// </para>
31+
/// <code>
32+
/// {
33+
/// "title" : "addNumbers",
34+
/// "description": "A simple function that adds two numbers together.",
35+
/// "type": "object",
36+
/// "properties": {
37+
/// "a" : { "type": "number" },
38+
/// "b" : { "type": "number", "default": 1 }
39+
/// },
40+
/// "required" : ["a"]
41+
/// }
42+
/// </code>
43+
/// <para>
44+
/// The metadata present in the schema document plays an important role in guiding AI function invocation.
45+
/// </para>
46+
/// <para>
47+
/// When no schema is specified, consuming chat clients should assume the "{}" or "true" schema, indicating that any JSON input is admissible.
48+
/// </para>
49+
/// </remarks>
50+
public virtual JsonElement JsonSchema => AIJsonUtilities.DefaultJsonSchema;
51+
52+
/// <summary>Gets a JSON Schema describing the function's return value.</summary>
53+
/// <remarks>
54+
/// A <see langword="null"/> typically reflects a function that doesn't specify a return schema
55+
/// or a function that returns <see cref="void"/>, <see cref="Task"/>, or <see cref="ValueTask"/>.
56+
/// </remarks>
57+
public virtual JsonElement? ReturnJsonSchema => null;
58+
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public static partial class AIFunctionFactory
4949
/// <para>
5050
/// By default, any parameters to <paramref name="method"/> are sourced from the <see cref="AIFunctionArguments"/>'s dictionary
5151
/// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned <see cref="AIFunction"/>'s
52-
/// <see cref="AIFunction.JsonSchema"/>. There are a few exceptions to this:
52+
/// <see cref="AIFunctionDeclaration.JsonSchema"/>. There are a few exceptions to this:
5353
/// <list type="bullet">
5454
/// <item>
5555
/// <description>
@@ -131,7 +131,7 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio
131131
/// <para>
132132
/// Any parameters to <paramref name="method"/> are sourced from the <see cref="AIFunctionArguments"/>'s dictionary
133133
/// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned <see cref="AIFunction"/>'s
134-
/// <see cref="AIFunction.JsonSchema"/>. There are a few exceptions to this:
134+
/// <see cref="AIFunctionDeclaration.JsonSchema"/>. There are a few exceptions to this:
135135
/// <list type="bullet">
136136
/// <item>
137137
/// <description>
@@ -212,7 +212,7 @@ public static AIFunction Create(Delegate method, string? name = null, string? de
212212
/// <para>
213213
/// By default, any parameters to <paramref name="method"/> are sourced from the <see cref="AIFunctionArguments"/>'s dictionary
214214
/// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned <see cref="AIFunction"/>'s
215-
/// <see cref="AIFunction.JsonSchema"/>. There are a few exceptions to this:
215+
/// <see cref="AIFunctionDeclaration.JsonSchema"/>. There are a few exceptions to this:
216216
/// <list type="bullet">
217217
/// <item>
218218
/// <description>
@@ -304,7 +304,7 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac
304304
/// <para>
305305
/// Any parameters to <paramref name="method"/> are sourced from the <see cref="AIFunctionArguments"/>'s dictionary
306306
/// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned <see cref="AIFunction"/>'s
307-
/// <see cref="AIFunction.JsonSchema"/>. There are a few exceptions to this:
307+
/// <see cref="AIFunctionDeclaration.JsonSchema"/>. There are a few exceptions to this:
308308
/// <list type="bullet">
309309
/// <item>
310310
/// <description>
@@ -398,7 +398,7 @@ public static AIFunction Create(MethodInfo method, object? target, string? name
398398
/// <para>
399399
/// By default, any parameters to <paramref name="method"/> are sourced from the <see cref="AIFunctionArguments"/>'s dictionary
400400
/// of key/value pairs and are represented in the JSON schema for the function, as exposed in the returned <see cref="AIFunction"/>'s
401-
/// <see cref="AIFunction.JsonSchema"/>. There are a few exceptions to this:
401+
/// <see cref="AIFunctionDeclaration.JsonSchema"/>. There are a few exceptions to this:
402402
/// <list type="bullet">
403403
/// <item>
404404
/// <description>
@@ -467,6 +467,39 @@ public static AIFunction Create(
467467
AIFunctionFactoryOptions? options = null) =>
468468
ReflectionAIFunction.Build(method, createInstanceFunc, options ?? _defaultOptions);
469469

470+
/// <summary>Creates an <see cref="AIFunctionDeclaration"/> using the specified parameters as the implementation of its corresponding properties.</summary>
471+
/// <param name="name">The name of the function.</param>
472+
/// <param name="description">A description of the function, suitable for use in describing the purpose to a model.</param>
473+
/// <param name="jsonSchema">A JSON schema describing the function and its input parameters.</param>
474+
/// <param name="returnJsonSchema">A JSON schema describing the function's return value.</param>
475+
/// <returns>The created <see cref="AIFunctionDeclaration"/> that describes a function.</returns>
476+
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
477+
/// <remarks>
478+
/// <see cref="CreateDeclaration"/> creates an <see cref="AIFunctionDeclaration"/> that can be used to describe a function
479+
/// but not invoke it. To create an invocable <see cref="AIFunction"/>, use Create. A non-invocable <see cref="AIFunctionDeclaration"/>
480+
/// may also be created from an invocable <see cref="AIFunction"/> using that function's <see cref="AIFunction.AsDeclarationOnly"/> method.
481+
/// </remarks>
482+
public static AIFunctionDeclaration CreateDeclaration(
483+
string name,
484+
string? description,
485+
JsonElement jsonSchema,
486+
JsonElement? returnJsonSchema = null) =>
487+
new DefaultAIFunctionDeclaration(
488+
Throw.IfNullOrEmpty(name),
489+
description ?? string.Empty,
490+
jsonSchema,
491+
returnJsonSchema);
492+
493+
private sealed class DefaultAIFunctionDeclaration(
494+
string name, string description, JsonElement jsonSchema, JsonElement? returnJsonSchema) :
495+
AIFunctionDeclaration
496+
{
497+
public override string Name => name;
498+
public override string Description => description;
499+
public override JsonElement JsonSchema => jsonSchema;
500+
public override JsonElement? ReturnJsonSchema => returnJsonSchema;
501+
}
502+
470503
private sealed class ReflectionAIFunction : AIFunction
471504
{
472505
public static ReflectionAIFunction Build(MethodInfo method, object? target, AIFunctionFactoryOptions options)

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,14 @@ public AIFunctionFactoryOptions()
107107
public Func<object?, Type?, CancellationToken, ValueTask<object?>>? MarshalResult { get; set; }
108108

109109
/// <summary>
110-
/// Gets or sets a value indicating whether a schema should be created for the function's result type, if possible, and included as <see cref="AIFunction.ReturnJsonSchema" />.
110+
/// Gets or sets a value indicating whether a schema should be created for the function's result type, if possible, and included as <see cref="AIFunctionDeclaration.ReturnJsonSchema" />.
111111
/// </summary>
112112
/// <remarks>
113113
/// <para>
114114
/// The default value is <see langword="false"/>.
115115
/// </para>
116116
/// <para>
117-
/// When set to <see langword="true"/>, results in the produced <see cref="AIFunction.ReturnJsonSchema"/> to always be <see langword="null"/>.
117+
/// When set to <see langword="true"/>, results in the produced <see cref="AIFunctionDeclaration.ReturnJsonSchema"/> to always be <see langword="null"/>.
118118
/// </para>
119119
/// </remarks>
120120
public bool ExcludeResultSchema { get; set; }
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text.Json;
7+
using Microsoft.Shared.Diagnostics;
8+
9+
#pragma warning disable SA1202 // Elements should be ordered by access
10+
11+
namespace Microsoft.Extensions.AI;
12+
13+
/// <summary>
14+
/// Provides an optional base class for an <see cref="AIFunctionDeclaration"/> that passes through calls to another instance.
15+
/// </summary>
16+
internal class DelegatingAIFunctionDeclaration : AIFunctionDeclaration // could be made public in the future if there's demand
17+
{
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="DelegatingAIFunctionDeclaration"/> class as a wrapper around <paramref name="innerFunction"/>.
20+
/// </summary>
21+
/// <param name="innerFunction">The inner AI function to which all calls are delegated by default.</param>
22+
/// <exception cref="ArgumentNullException"><paramref name="innerFunction"/> is <see langword="null"/>.</exception>
23+
protected DelegatingAIFunctionDeclaration(AIFunctionDeclaration innerFunction)
24+
{
25+
InnerFunction = Throw.IfNull(innerFunction);
26+
}
27+
28+
/// <summary>Gets the inner <see cref="AIFunctionDeclaration" />.</summary>
29+
protected AIFunctionDeclaration InnerFunction { get; }
30+
31+
/// <inheritdoc />
32+
public override string Name => InnerFunction.Name;
33+
34+
/// <inheritdoc />
35+
public override string Description => InnerFunction.Description;
36+
37+
/// <inheritdoc />
38+
public override JsonElement JsonSchema => InnerFunction.JsonSchema;
39+
40+
/// <inheritdoc />
41+
public override JsonElement? ReturnJsonSchema => InnerFunction.ReturnJsonSchema;
42+
43+
/// <inheritdoc />
44+
public override IReadOnlyDictionary<string, object?> AdditionalProperties => InnerFunction.AdditionalProperties;
45+
46+
/// <inheritdoc />
47+
public override string ToString() => InnerFunction.ToString();
48+
}

0 commit comments

Comments
 (0)