diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AITool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AITool.cs index ebbc6751c04..ab9f010ae57 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AITool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AITool.cs @@ -1,13 +1,55 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.Shared.Collections; + namespace Microsoft.Extensions.AI; +#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods + /// Represents a tool that can be specified to an AI service. -public class AITool +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public abstract class AITool { /// Initializes a new instance of the class. protected AITool() { } + + /// Gets the name of the tool. + public virtual string Name => GetType().Name; + + /// Gets a description of the tool, suitable for use in describing the purpose to a model. + public virtual string Description => string.Empty; + + /// Gets any additional properties associated with the tool. + public virtual IReadOnlyDictionary AdditionalProperties => EmptyReadOnlyDictionary.Instance; + + /// + public override string ToString() => Name; + + /// Gets the string to display in the debugger for this instance. + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + StringBuilder sb = new(Name); + + if (Description is string description && !string.IsNullOrEmpty(description)) + { + _ = sb.Append(" (").Append(description).Append(')'); + } + + foreach (var entry in AdditionalProperties) + { + _ = sb.Append(", ").Append(entry.Key).Append(" = ").Append(entry.Value); + } + + return sb.ToString(); + } + } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CodeInterpreterTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CodeInterpreterTool.cs new file mode 100644 index 00000000000..408810ca6f7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CodeInterpreterTool.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AI; + +/// Represents a tool that can be specified to an AI service to enable it to execute code it generates. +/// +/// This tool does not itself implement code interpration. It is a marker that can be used to inform a service +/// that the service is allowed to execute its generated code if the service is capable of doing so. +/// +public class CodeInterpreterTool : AITool +{ + /// Initializes a new instance of the class. + public CodeInterpreterTool() + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs index 84cde4bc82a..667a956a2f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunction.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; using System.Reflection; using System.Text.Json; using System.Threading; @@ -12,15 +11,8 @@ namespace Microsoft.Extensions.AI; /// Represents a function that can be described to an AI service and invoked. -[DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class AIFunction : AITool { - /// Gets the name of the function. - public abstract string Name { get; } - - /// Gets a description of the function, suitable for use in describing the purpose to a model. - public abstract string Description { get; } - /// Gets a JSON Schema describing the function and its input parameters. /// /// @@ -56,11 +48,8 @@ public abstract class AIFunction : AITool /// public virtual MethodInfo? UnderlyingMethod => null; - /// Gets any additional properties associated with the function. - public virtual IReadOnlyDictionary AdditionalProperties => EmptyReadOnlyDictionary.Instance; - /// Gets a that can be used to marshal function parameters. - public virtual JsonSerializerOptions? JsonSerializerOptions => AIJsonUtilities.DefaultOptions; + public virtual JsonSerializerOptions JsonSerializerOptions => AIJsonUtilities.DefaultOptions; /// Invokes the and returns its result. /// The arguments to pass to the function's invocation. @@ -75,9 +64,6 @@ public abstract class AIFunction : AITool return InvokeCoreAsync(arguments, cancellationToken); } - /// - public override string ToString() => Name; - /// Invokes the and returns its result. /// The arguments to pass to the function's invocation. /// The to monitor for cancellation requests. @@ -85,8 +71,4 @@ public abstract class AIFunction : AITool protected abstract Task InvokeCoreAsync( IEnumerable> arguments, CancellationToken cancellationToken); - - /// Gets the string to display in the debugger for this instance. - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => string.IsNullOrWhiteSpace(Description) ? Name : $"{Name} ({Description})"; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs index 513ce3652c8..7b68ce5e15e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs @@ -212,19 +212,25 @@ private static (RunCreationOptions RunOptions, List? Tool { foreach (AITool tool in tools) { - if (tool is AIFunction aiFunction) + switch (tool) { - bool? strict = - aiFunction.AdditionalProperties.TryGetValue("Strict", out object? strictObj) && - strictObj is bool strictValue ? - strictValue : null; - - var functionParameters = BinaryData.FromBytes( - JsonSerializer.SerializeToUtf8Bytes( - JsonSerializer.Deserialize(aiFunction.JsonSchema, OpenAIJsonContext.Default.OpenAIChatToolJson)!, - OpenAIJsonContext.Default.OpenAIChatToolJson)); - - runOptions.ToolsOverride.Add(ToolDefinition.CreateFunction(aiFunction.Name, aiFunction.Description, functionParameters, strict)); + case AIFunction aiFunction: + bool? strict = + aiFunction.AdditionalProperties.TryGetValue("Strict", out object? strictObj) && + strictObj is bool strictValue ? + strictValue : null; + + var functionParameters = BinaryData.FromBytes( + JsonSerializer.SerializeToUtf8Bytes( + JsonSerializer.Deserialize(aiFunction.JsonSchema, OpenAIJsonContext.Default.OpenAIChatToolJson)!, + OpenAIJsonContext.Default.OpenAIChatToolJson)); + + runOptions.ToolsOverride.Add(ToolDefinition.CreateFunction(aiFunction.Name, aiFunction.Description, functionParameters, strict)); + break; + + case CodeInterpreterTool: + runOptions.ToolsOverride.Add(ToolDefinition.CreateCodeInterpreter()); + break; } } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs index c1e0189c8cd..f5c21be3678 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs @@ -240,8 +240,10 @@ public static ChatOptions FromOpenAIOptions(ChatCompletionOptions? options) { foreach (ChatTool tool in tools) { - result.Tools ??= []; - result.Tools.Add(FromOpenAIChatTool(tool)); + if (FromOpenAIChatTool(tool) is { } convertedTool) + { + (result.Tools ??= []).Add(convertedTool); + } } using var toolChoiceJson = JsonDocument.Parse(JsonModelHelpers.Serialize(options.ToolChoice).ToMemory()); @@ -407,17 +409,24 @@ public static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) return result; } - private static AITool FromOpenAIChatTool(ChatTool chatTool) + private static AITool? FromOpenAIChatTool(ChatTool chatTool) { - AdditionalPropertiesDictionary additionalProperties = []; - if (chatTool.FunctionSchemaIsStrict is bool strictValue) + switch (chatTool.Kind) { - additionalProperties["Strict"] = strictValue; - } + case ChatToolKind.Function: + AdditionalPropertiesDictionary additionalProperties = []; + if (chatTool.FunctionSchemaIsStrict is bool strictValue) + { + additionalProperties["Strict"] = strictValue; + } + + OpenAIChatToolJson openAiChatTool = JsonSerializer.Deserialize(chatTool.FunctionParameters.ToMemory().Span, OpenAIJsonContext.Default.OpenAIChatToolJson)!; + JsonElement schema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson); + return new MetadataOnlyAIFunction(chatTool.FunctionName, chatTool.FunctionDescription, schema, additionalProperties); - OpenAIChatToolJson openAiChatTool = JsonSerializer.Deserialize(chatTool.FunctionParameters.ToMemory().Span, OpenAIJsonContext.Default.OpenAIChatToolJson)!; - JsonElement schema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson); - return new MetadataOnlyAIFunction(chatTool.FunctionName, chatTool.FunctionDescription, schema, additionalProperties); + default: + return null; + } } private sealed class MetadataOnlyAIFunction(string name, string description, JsonElement schema, IReadOnlyDictionary additionalProps) : AIFunction diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs index d0d3385749e..50a5afd14e7 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs @@ -340,7 +340,7 @@ static bool IsAsyncMethod(MethodInfo method) JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(parameterType); // Create a marshaller that simply looks up the parameter by name in the arguments dictionary. - return (IReadOnlyDictionary arguments, AIFunctionContext? _) => + return (arguments, _) => { // If the parameter has an argument specified in the dictionary, return that argument. if (arguments.TryGetValue(parameter.Name, out object? value)) diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryOptions.cs index ac285241469..2f61dce7262 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryOptions.cs @@ -49,7 +49,7 @@ public AIFunctionFactoryOptions() public string? Description { get; set; } /// - /// Gets or sets additional values to store on the resulting property. + /// Gets or sets additional values to store on the resulting property. /// /// /// This property can be used to provide arbitrary information about the function. diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIToolTests.cs new file mode 100644 index 00000000000..5e092d107ec --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIToolTests.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class AIToolTests +{ + [Fact] + public void Constructor_Roundtrips() + { + DerivedAITool tool = new(); + Assert.Equal(nameof(DerivedAITool), tool.Name); + Assert.Equal(nameof(DerivedAITool), tool.ToString()); + Assert.Empty(tool.Description); + Assert.Empty(tool.AdditionalProperties); + } + + private sealed class DerivedAITool : AITool; +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/CodeInterpreterToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/CodeInterpreterToolTests.cs new file mode 100644 index 00000000000..3bf9f568e96 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/CodeInterpreterToolTests.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class CodeInterpreterToolTests +{ + [Fact] + public void Constructor_Roundtrips() + { + var tool = new CodeInterpreterTool(); + Assert.Equal(nameof(CodeInterpreterTool), tool.Name); + Assert.Empty(tool.Description); + Assert.Empty(tool.AdditionalProperties); + Assert.Equal(nameof(CodeInterpreterTool), tool.ToString()); + } +}