diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 1ff0c423..00000000 --- a/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain diff --git a/OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj b/OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj index 782b8fe8..841a82c2 100644 --- a/OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj +++ b/OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 latest disable OpenAI API Proxy diff --git a/OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj b/OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj index 92a41f5d..71bc8545 100644 --- a/OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj +++ b/OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj @@ -1,6 +1,6 @@ - net6.0 + net8.0 enable false false diff --git a/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj b/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj index 0343d83b..491a81aa 100644 --- a/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj +++ b/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 false false latest diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs index c79488a3..5f3cec94 100644 --- a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs @@ -146,5 +146,51 @@ public void Test_02_01_GenerateJsonSchema() JsonSchema mathSchema = typeof(MathResponse); Console.WriteLine(mathSchema.ToString()); } + + [Test] + public void Test_02_02_GenerateJsonSchema_PrimitiveTypes() + { + JsonSchema schema = typeof(TestSchema); + Console.WriteLine(schema.ToString()); + } + + private class TestSchema + { + // test all primitive types can be serialized + public bool Bool { get; set; } + public byte Byte { get; set; } + public sbyte SByte { get; set; } + public short Short { get; set; } + public ushort UShort { get; set; } + public int Integer { get; set; } + public uint UInteger { get; set; } + public long Long { get; set; } + public ulong ULong { get; set; } + public float Float { get; set; } + public double Double { get; set; } + public decimal Decimal { get; set; } + public char Char { get; set; } + public string String { get; set; } + public DateTime DateTime { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } + public Guid Guid { get; set; } + // test nullables + public int? NullInt { get; set; } + public DateTime? NullDateTime { get; set; } + public TestEnum TestEnum { get; set; } + public TestEnum? NullEnum { get; set; } + public Dictionary Dictionary { get; set; } + public IDictionary IntDictionary { get; set; } + public IReadOnlyDictionary StringDictionary { get; set; } + public Dictionary CustomDictionary { get; set; } + } + + private enum TestEnum + { + Enum1, + Enum2, + Enum3, + Enum4 + } } } diff --git a/OpenAI-DotNet/Audio/SpeechRequest.cs b/OpenAI-DotNet/Audio/SpeechRequest.cs index 24ed5355..072ae40a 100644 --- a/OpenAI-DotNet/Audio/SpeechRequest.cs +++ b/OpenAI-DotNet/Audio/SpeechRequest.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using OpenAI.Models; using System; using System.Text.Json.Serialization; @@ -49,7 +48,7 @@ public SpeechRequest(string input, Model model = null, SpeechVoice voice = Speec /// [JsonPropertyName("response_format")] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public SpeechResponseFormat ResponseFormat { get; } /// diff --git a/OpenAI-DotNet/Batch/BatchResponse.cs b/OpenAI-DotNet/Batch/BatchResponse.cs index 7abf8f07..5cc1acec 100644 --- a/OpenAI-DotNet/Batch/BatchResponse.cs +++ b/OpenAI-DotNet/Batch/BatchResponse.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using System; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -53,7 +52,7 @@ public sealed class BatchResponse : BaseResponse /// [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public BatchStatus Status { get; private set; } /// diff --git a/OpenAI-DotNet/Chat/ChatRequest.cs b/OpenAI-DotNet/Chat/ChatRequest.cs index 92f165df..c2ecd4cc 100644 --- a/OpenAI-DotNet/Chat/ChatRequest.cs +++ b/OpenAI-DotNet/Chat/ChatRequest.cs @@ -36,7 +36,7 @@ public ChatRequest( { var toolList = tools?.ToList(); - if (toolList != null && toolList.Any()) + if (toolList is { Count: > 0 }) { if (string.IsNullOrWhiteSpace(toolChoice)) { @@ -57,6 +57,15 @@ public ChatRequest( ToolChoice = toolChoice; } } + + foreach (var tool in toolList) + { + if (tool?.Function?.Arguments != null) + { + // just in case clear any lingering func args. + tool.Function.Arguments = null; + } + } } Tools = toolList?.ToList(); diff --git a/OpenAI-DotNet/Common/Annotation.cs b/OpenAI-DotNet/Common/Annotation.cs index 57599ddd..49b2f164 100644 --- a/OpenAI-DotNet/Common/Annotation.cs +++ b/OpenAI-DotNet/Common/Annotation.cs @@ -14,7 +14,7 @@ public sealed class Annotation : IAppendable [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public AnnotationType Type { get; private set; } /// diff --git a/OpenAI-DotNet/Common/Content.cs b/OpenAI-DotNet/Common/Content.cs index 4cfa015d..59a2a6f9 100644 --- a/OpenAI-DotNet/Common/Content.cs +++ b/OpenAI-DotNet/Common/Content.cs @@ -59,7 +59,7 @@ public Content(ContentType type, string input) [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public ContentType Type { get; private set; } diff --git a/OpenAI-DotNet/Common/Function.cs b/OpenAI-DotNet/Common/Function.cs index 4e6a5f7a..d89f846f 100644 --- a/OpenAI-DotNet/Common/Function.cs +++ b/OpenAI-DotNet/Common/Function.cs @@ -92,6 +92,7 @@ internal Function(string name, JsonNode arguments, bool? strict = null) { Name = name; Arguments = arguments; + Strict = strict; } private Function(string name, string description, MethodInfo method, object instance = null, bool? strict = null) diff --git a/OpenAI-DotNet/Common/ResponseFormatObject.cs b/OpenAI-DotNet/Common/ResponseFormatObject.cs index 32aac488..cb3581fc 100644 --- a/OpenAI-DotNet/Common/ResponseFormatObject.cs +++ b/OpenAI-DotNet/Common/ResponseFormatObject.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using System.Text.Json.Serialization; namespace OpenAI @@ -26,7 +25,7 @@ public ResponseFormatObject(JsonSchema schema) [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public ChatResponseFormat Type { get; private set; } [JsonInclude] diff --git a/OpenAI-DotNet/Common/Tool.cs b/OpenAI-DotNet/Common/Tool.cs index cb969e8b..5dba0dc3 100644 --- a/OpenAI-DotNet/Common/Tool.cs +++ b/OpenAI-DotNet/Common/Tool.cs @@ -175,11 +175,11 @@ public async Task InvokeFunctionAsync(CancellationToken cancellationToken #region Tool Cache - private static readonly List toolCache = new() - { + private static readonly List toolCache = + [ FileSearch, CodeInterpreter - }; + ]; /// /// Gets a list of all available tools. diff --git a/OpenAI-DotNet/Extensions/TypeExtensions.cs b/OpenAI-DotNet/Extensions/TypeExtensions.cs index 4ca7fa1f..3584f1d1 100644 --- a/OpenAI-DotNet/Extensions/TypeExtensions.cs +++ b/OpenAI-DotNet/Extensions/TypeExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; @@ -87,6 +88,7 @@ public static JsonObject GenerateJsonSchema(this Type type, JsonObject rootSchem { options ??= OpenAIClient.JsonSerializationOptions; var schema = new JsonObject(); + type = UnwrapNullableType(type); if (!type.IsPrimitive && type != typeof(Guid) && @@ -98,60 +100,45 @@ public static JsonObject GenerateJsonSchema(this Type type, JsonObject rootSchem return new JsonObject { ["$ref"] = $"#/definitions/{type.FullName}" }; } - if (type == typeof(string) || type == typeof(char)) + if (type.TryGetSimpleTypeSchema(out var schemaType)) { - schema["type"] = "string"; - } - else if (type == typeof(int) || - type == typeof(long) || - type == typeof(uint) || - type == typeof(byte) || - type == typeof(sbyte) || - type == typeof(ulong) || - type == typeof(short) || - type == typeof(ushort)) - { - schema["type"] = "integer"; - } - else if (type == typeof(float) || - type == typeof(double) || - type == typeof(decimal)) - { - schema["type"] = "number"; - } - else if (type == typeof(bool)) - { - schema["type"] = "boolean"; + schema["type"] = schemaType; + + if (type == typeof(DateTime) || + type == typeof(DateTimeOffset)) + { + schema["format"] = "date-time"; + } + else if (type == typeof(Guid)) + { + schema["format"] = "uuid"; + } } - else if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) + else if (type.IsEnum) { schema["type"] = "string"; - schema["format"] = "date-time"; + schema["enum"] = new JsonArray(Enum.GetNames(type).Select(name => JsonValue.Create(name)).ToArray()); } - else if (type == typeof(Guid)) + else if (type.TryGetDictionaryValueType(out var valueType)) { - schema["type"] = "string"; - schema["format"] = "uuid"; - } - else if (type.IsEnum) - { - schema["type"] = "string"; - schema["enum"] = new JsonArray(); + schema["type"] = "object"; - foreach (var value in Enum.GetValues(type)) + if (rootSchema["definitions"] != null && + rootSchema["definitions"].AsObject().ContainsKey(valueType!.FullName!)) { - schema["enum"].AsArray().Add(JsonNode.Parse(JsonSerializer.Serialize(value, options))); + schema["additionalProperties"] = new JsonObject { ["$ref"] = $"#/definitions/{valueType.FullName}" }; + } + else + { + schema["additionalProperties"] = GenerateJsonSchema(valueType, rootSchema); } } - else if (type.IsArray || - type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>) || - type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))) + else if (type.TryGetCollectionElementType(out var elementType)) { schema["type"] = "array"; - var elementType = type.GetElementType() ?? type.GetGenericArguments()[0]; if (rootSchema["definitions"] != null && - rootSchema["definitions"].AsObject().ContainsKey(elementType.FullName!)) + rootSchema["definitions"].AsObject().ContainsKey(elementType!.FullName!)) { schema["items"] = new JsonObject { ["$ref"] = $"#/definitions/{elementType.FullName}" }; } @@ -282,6 +269,127 @@ public static JsonObject GenerateJsonSchema(this Type type, JsonObject rootSchem return schema; } + private static bool TryGetSimpleTypeSchema(this Type type, out string schemaType) + { + switch (type) + { + case not null when type == typeof(object): + schemaType = "object"; + return true; + case not null when type == typeof(bool): + schemaType = "boolean"; + return true; + case not null when type == typeof(float) || + type == typeof(double) || + type == typeof(decimal): + schemaType = "number"; + return true; + case not null when type == typeof(char) || + type == typeof(string) || + type == typeof(Guid) || + type == typeof(DateTime) || + type == typeof(DateTimeOffset): + schemaType = "string"; + return true; + case not null when type == typeof(int) || + type == typeof(long) || + type == typeof(uint) || + type == typeof(byte) || + type == typeof(sbyte) || + type == typeof(ulong) || + type == typeof(short) || + type == typeof(ushort): + schemaType = "integer"; + return true; + default: + schemaType = null; + return false; + } + } + + private static bool TryGetDictionaryValueType(this Type type, out Type valueType) + { + valueType = null; + + if (!type.IsGenericType) { return false; } + + var genericTypeDefinition = type.GetGenericTypeDefinition(); + + if (genericTypeDefinition == typeof(Dictionary<,>) || + genericTypeDefinition == typeof(IDictionary<,>) || + genericTypeDefinition == typeof(IReadOnlyDictionary<,>)) + { + return InternalTryGetDictionaryValueType(type, out valueType); + } + + // Check implemented interfaces for dictionary types + foreach (var @interface in type.GetInterfaces()) + { + if (!@interface.IsGenericType) { continue; } + + var interfaceTypeDefinition = @interface.GetGenericTypeDefinition(); + + if (interfaceTypeDefinition == typeof(IDictionary<,>) || + interfaceTypeDefinition == typeof(IReadOnlyDictionary<,>)) + { + return InternalTryGetDictionaryValueType(@interface, out valueType); + } + } + + return false; + + bool InternalTryGetDictionaryValueType(Type dictType, out Type dictValueType) + { + dictValueType = null; + var genericArgs = dictType.GetGenericArguments(); + + // The key type is not string, which cannot be represented in JSON object property names + if (genericArgs[0] != typeof(string)) + { + throw new InvalidOperationException($"Cannot generate schema for dictionary type '{dictType.FullName}' with non-string key type."); + } + + dictValueType = genericArgs[1].UnwrapNullableType(); + return true; + } + } + + private static readonly Type[] arrayTypes = + [ + typeof(IEnumerable<>), + typeof(ICollection<>), + typeof(IReadOnlyCollection<>), + typeof(List<>), + typeof(IList<>), + typeof(IReadOnlyList<>), + typeof(HashSet<>), + typeof(ISet<>), + typeof(IReadOnlySet<>) + ]; + + private static bool TryGetCollectionElementType(this Type type, out Type elementType) + { + elementType = null; + + if (type.IsArray) + { + elementType = type.GetElementType(); + return true; + } + + if (!type.IsGenericType) { return false; } + + var genericTypeDefinition = type.GetGenericTypeDefinition(); + + if (!arrayTypes.Contains(genericTypeDefinition)) { return false; } + + elementType = type.GetGenericArguments()[0].UnwrapNullableType(); + return true; + } + + private static Type UnwrapNullableType(this Type type) + => Nullable.GetUnderlyingType(type) ?? type; + private static Type GetMemberType(MemberInfo member) => member switch { diff --git a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs index d0cd0f37..90644983 100644 --- a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs +++ b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -56,7 +55,7 @@ public DateTime? FinishedAt [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public JobStatus Status { get; private set; } [JsonInclude] diff --git a/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs b/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs index e05b07f6..6562c1b9 100644 --- a/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs +++ b/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using OpenAI.Models; using System; using System.Text.Json.Serialization; @@ -68,7 +67,7 @@ protected AbstractBaseImageRequest(Model model = null, int numberOfResults = 1, /// Defaults to /// [JsonPropertyName("response_format")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.")] public ImageResponseFormat ResponseFormat { get; } diff --git a/OpenAI-DotNet/Images/ImageGenerationRequest.cs b/OpenAI-DotNet/Images/ImageGenerationRequest.cs index acab3721..6ae757c8 100644 --- a/OpenAI-DotNet/Images/ImageGenerationRequest.cs +++ b/OpenAI-DotNet/Images/ImageGenerationRequest.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using OpenAI.Models; using System.Text.Json.Serialization; @@ -107,7 +106,7 @@ public ImageGenerationRequest( /// Defaults to /// [JsonPropertyName("response_format")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.", true)] public ImageResponseFormat ResponseFormat { get; } diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs index 2801d77b..c42238ac 100644 --- a/OpenAI-DotNet/Images/ImagesEndpoint.cs +++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs @@ -1,7 +1,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using OpenAI.Extensions; -using System; using System.Collections.Generic; using System.IO; using System.Net.Http; diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index cc68457b..b872e1ef 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 latest OpenAI-DotNet OpenAI-DotNet @@ -29,8 +29,14 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- OpenAI-DotNet.pfx true true - 8.2.5 + 8.3.0 +Version 8.3.0 +- Updated library to .net 8 +- Refactored TypeExtensions and JsonSchema generation + - Improved JsonSchema generation for enums and dictionaries + - Ensured JsonSchema properly handles nullable types +- Ensure that function args are not re-serialized and passed back into tool function for future calls Version 8.2.5 - Fixed ResponseObjectFormat deserialization when maxNumberOfResults is null Version 8.2.4 diff --git a/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs b/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs index 575bcfa1..33829d5f 100644 --- a/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs +++ b/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs @@ -16,7 +16,7 @@ public sealed class CodeInterpreterOutputs : IAppendable /// [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public CodeInterpreterOutputType Type { get; private set; } /// diff --git a/OpenAI-DotNet/Threads/CreateRunRequest.cs b/OpenAI-DotNet/Threads/CreateRunRequest.cs index 7e1326a1..77c18bce 100644 --- a/OpenAI-DotNet/Threads/CreateRunRequest.cs +++ b/OpenAI-DotNet/Threads/CreateRunRequest.cs @@ -147,7 +147,7 @@ public CreateRunRequest( var toolList = tools?.ToList(); - if (toolList != null && toolList.Any()) + if (toolList is { Count: > 0 }) { if (string.IsNullOrWhiteSpace(toolChoice)) { @@ -168,6 +168,15 @@ public CreateRunRequest( ToolChoice = toolChoice; } } + + foreach (var tool in toolList) + { + if (tool?.Function?.Arguments != null) + { + // just in case clear any lingering func args. + tool.Function.Arguments = null; + } + } } Tools = toolList?.ToList(); diff --git a/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs b/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs index 94c68909..9af054eb 100644 --- a/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs +++ b/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs @@ -147,7 +147,7 @@ public CreateThreadAndRunRequest( var toolList = tools?.ToList(); - if (toolList != null && toolList.Any()) + if (toolList is { Count: > 0 }) { if (string.IsNullOrWhiteSpace(toolChoice)) { @@ -168,6 +168,15 @@ public CreateThreadAndRunRequest( ToolChoice = toolChoice; } } + + foreach (var tool in toolList) + { + if (tool?.Function?.Arguments != null) + { + // just in case clear any lingering func args. + tool.Function.Arguments = null; + } + } } Tools = toolList?.ToList(); diff --git a/OpenAI-DotNet/Threads/IncompleteDetails.cs b/OpenAI-DotNet/Threads/IncompleteDetails.cs index e85dc41c..c5630211 100644 --- a/OpenAI-DotNet/Threads/IncompleteDetails.cs +++ b/OpenAI-DotNet/Threads/IncompleteDetails.cs @@ -1,4 +1,5 @@ -using OpenAI.Extensions; +// Licensed under the MIT License. See LICENSE in the project root for license information. + using System.Text.Json.Serialization; namespace OpenAI.Threads @@ -7,7 +8,7 @@ public sealed class IncompleteDetails { [JsonInclude] [JsonPropertyName("reason")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public IncompleteMessageReason Reason { get; private set; } } } diff --git a/OpenAI-DotNet/Threads/MessageResponse.cs b/OpenAI-DotNet/Threads/MessageResponse.cs index 5b953840..466d9b65 100644 --- a/OpenAI-DotNet/Threads/MessageResponse.cs +++ b/OpenAI-DotNet/Threads/MessageResponse.cs @@ -61,7 +61,7 @@ public MessageResponse() { } /// [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public MessageStatus Status { get; private set; } /// diff --git a/OpenAI-DotNet/Threads/RunResponse.cs b/OpenAI-DotNet/Threads/RunResponse.cs index e12797b4..1986b048 100644 --- a/OpenAI-DotNet/Threads/RunResponse.cs +++ b/OpenAI-DotNet/Threads/RunResponse.cs @@ -62,7 +62,7 @@ public RunResponse() { } /// [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public RunStatus Status { get; private set; } /// diff --git a/OpenAI-DotNet/Threads/RunStepResponse.cs b/OpenAI-DotNet/Threads/RunStepResponse.cs index 46733e1f..c2f830ea 100644 --- a/OpenAI-DotNet/Threads/RunStepResponse.cs +++ b/OpenAI-DotNet/Threads/RunStepResponse.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using System; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -74,7 +73,7 @@ public DateTime? CreatedAt /// [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public RunStepType Type { get; private set; } /// @@ -82,7 +81,7 @@ public DateTime? CreatedAt /// [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public RunStatus Status { get; private set; } /// diff --git a/OpenAI-DotNet/Threads/TruncationStrategy.cs b/OpenAI-DotNet/Threads/TruncationStrategy.cs index 2241e526..db3e0bbd 100644 --- a/OpenAI-DotNet/Threads/TruncationStrategy.cs +++ b/OpenAI-DotNet/Threads/TruncationStrategy.cs @@ -1,4 +1,5 @@ -using OpenAI.Extensions; +// Licensed under the MIT License. See LICENSE in the project root for license information. + using System.Text.Json.Serialization; namespace OpenAI.Threads @@ -13,7 +14,7 @@ public sealed class TruncationStrategy /// [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public TruncationStrategies Type { get; private set; } /// diff --git a/OpenAI-DotNet/VectorStores/ChunkingStrategy.cs b/OpenAI-DotNet/VectorStores/ChunkingStrategy.cs index eef15bfe..5ab7fe73 100644 --- a/OpenAI-DotNet/VectorStores/ChunkingStrategy.cs +++ b/OpenAI-DotNet/VectorStores/ChunkingStrategy.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using System.Text.Json.Serialization; namespace OpenAI.VectorStores @@ -23,7 +22,7 @@ public ChunkingStrategy(ChunkingStrategyType type) [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public ChunkingStrategyType Type { get; private set; } [JsonInclude] diff --git a/README.md b/README.md index 9266a787..724dc900 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Discord](https://img.shields.io/discord/855294214065487932.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/xQgMW9ufN4) [![NuGet version (OpenAI-DotNet)](https://img.shields.io/nuget/v/OpenAI-DotNet.svg?label=OpenAI-DotNet&logo=nuget)](https://www.nuget.org/packages/OpenAI-DotNet/) +[![NuGet Downloads](https://img.shields.io/nuget/dt/OpenAI-DotNet)](https://www.nuget.org/packages/OpenAI-DotNet/) [![NuGet version (OpenAI-DotNet-Proxy)](https://img.shields.io/nuget/v/OpenAI-DotNet-Proxy.svg?label=OpenAI-DotNet-Proxy&logo=nuget)](https://www.nuget.org/packages/OpenAI-DotNet-Proxy/) [![Nuget Publish](https://github.com/RageAgainstThePixel/OpenAI-DotNet/actions/workflows/Publish-Nuget.yml/badge.svg)](https://github.com/RageAgainstThePixel/OpenAI-DotNet/actions/workflows/Publish-Nuget.yml) @@ -9,12 +10,12 @@ A simple C# .NET client library for [OpenAI](https://openai.com/) to use though Independently developed, this is not an official library and I am not affiliated with OpenAI. An OpenAI API account is required. -Forked from [OpenAI-API-dotnet](https://github.com/OkGoDoIt/OpenAI-API-dotnet). +Originally Forked from [OpenAI-API-dotnet](https://github.com/OkGoDoIt/OpenAI-API-dotnet). More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet-api). ## Requirements -- This library targets .NET 6.0 and above. +- This library targets .NET 8.0 and above. - It should work across console apps, winforms, wpf, asp.net, etc. - It should also work across Windows, Linux, and Mac. @@ -44,7 +45,7 @@ dotnet add package OpenAI-DotNet > Check out our new api docs! - :new: + ### Table of Contents @@ -82,7 +83,7 @@ dotnet add package OpenAI-DotNet - [Retrieve Run](#retrieve-thread-run) - [Modify Run](#modify-thread-run) - [Submit Tool Outputs to Run](#thread-submit-tool-outputs-to-run) - - [Structured Outputs](#thread-structured-outputs) :new: + - [Structured Outputs](#thread-structured-outputs) - [List Run Steps](#list-thread-run-steps) - [Retrieve Run Step](#retrieve-thread-run-step) - [Cancel Run](#cancel-thread-run) @@ -107,7 +108,7 @@ dotnet add package OpenAI-DotNet - [Streaming](#chat-streaming) - [Tools](#chat-tools) - [Vision](#chat-vision) - - [Json Schema](#chat-structured-outputs) :new: + - [Json Schema](#chat-structured-outputs) - [Json Mode](#chat-json-mode) - [Audio](#audio) - [Create Speech](#create-speech) @@ -846,7 +847,8 @@ public class MathStep To use, simply specify the `MathResponse` type as a generic constraint in either `CreateAssistantAsync`, `CreateRunAsync`, or `CreateThreadAndRunAsync`. ```csharp -var assistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync( +using var api = new OpenAIClient(); +var assistant = await api.AssistantsEndpoint.CreateAssistantAsync( new CreateAssistantRequest( name: "Math Tutor", instructions: "You are a helpful math tutor. Guide the user through the solution step by step.", @@ -915,6 +917,81 @@ finally } ``` +You can also manually create json schema json string as well, but you will be responsible for deserializing your response data: + +```csharp +using var api = new OpenAIClient(); +var mathSchema = new JsonSchema("math_response", @" +{ + ""type"": ""object"", + ""properties"": { + ""steps"": { + ""type"": ""array"", + ""items"": { + ""type"": ""object"", + ""properties"": { + ""explanation"": { + ""type"": ""string"" + }, + ""output"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""explanation"", + ""output"" + ], + ""additionalProperties"": false + } + }, + ""final_answer"": { + ""type"": ""string"" + } + }, + ""required"": [ + ""steps"", + ""final_answer"" + ], + ""additionalProperties"": false +}"); +var assistant = await api.AssistantsEndpoint.CreateAssistantAsync( + new CreateAssistantRequest( + name: "Math Tutor", + instructions: "You are a helpful math tutor. Guide the user through the solution step by step.", + model: "gpt-4o-2024-08-06", + jsonSchema: mathSchema)); +ThreadResponse thread = null; + +try +{ + var run = await assistant.CreateThreadAndRunAsync("how can I solve 8x + 7 = -23", + async @event => + { + Console.WriteLine(@event.ToJsonString()); + await Task.CompletedTask; + }); + thread = await run.GetThreadAsync(); + run = await run.WaitForStatusChangeAsync(); + Console.WriteLine($"Created thread and run: {run.ThreadId} -> {run.Id} -> {run.CreatedAt}"); + var messages = await thread.ListMessagesAsync(); + + foreach (var response in messages.Items) + { + Console.WriteLine($"{response.Role}: {response.PrintContent()}"); + } +} +finally +{ + await assistant.DeleteAsync(deleteToolResources: thread == null); + + if (thread != null) + { + var isDeleted = await thread.DeleteAsync(deleteToolResources: true); + Assert.IsTrue(isDeleted); + } +} +``` + ###### [List Thread Run Steps](https://platform.openai.com/docs/api-reference/runs/listRunSteps) Returns a list of run steps belonging to a run. @@ -970,7 +1047,7 @@ Returns a list of vector stores. ```csharp using var api = new OpenAIClient(); -var vectorStores = await OpenAIClient.VectorStoresEndpoint.ListVectorStoresAsync(); +var vectorStores = await api.VectorStoresEndpoint.ListVectorStoresAsync(); foreach (var vectorStore in vectorStores.Items) { @@ -1181,7 +1258,7 @@ var messages = new List }; var cumulativeDelta = string.Empty; var chatRequest = new ChatRequest(messages); -await foreach (var partialResponse in OpenAIClient.ChatEndpoint.StreamCompletionEnumerableAsync(chatRequest)) +await foreach (var partialResponse in api.ChatEndpoint.StreamCompletionEnumerableAsync(chatRequest)) { foreach (var choice in partialResponse.Choices.Where(choice => choice.Delta?.Content != null)) { @@ -1325,6 +1402,7 @@ public class MathStep To use, simply specify the `MathResponse` type as a generic constraint when requesting a completion. ```csharp +using var api = new OpenAIClient(); var messages = new List { new(Role.System, "You are a helpful math tutor. Guide the user through the solution step by step."), @@ -1332,7 +1410,7 @@ var messages = new List }; var chatRequest = new ChatRequest(messages, model: new("gpt-4o-2024-08-06")); -var (mathResponse, chatResponse) = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); +var (mathResponse, chatResponse) = await api.ChatEndpoint.GetCompletionAsync(chatRequest); for (var i = 0; i < mathResponse.Steps.Count; i++) { @@ -1530,7 +1608,7 @@ Returns information about a specific file. ```csharp using var api = new OpenAIClient(); -var file = await api.FilesEndpoint.GetFileInfoAsync(fileId); +var file = await api.FilesEndpoint.GetFileInfoAsync(fileId); Console.WriteLine($"{file.Id} -> {file.Object}: {file.FileName} | {file.Size} bytes"); ``` @@ -1630,7 +1708,7 @@ List your organization's batches. ```csharp using var api = new OpenAIClient(); -var batches = await api.await OpenAIClient.BatchEndpoint.ListBatchesAsync(); +var batches = await api.BatchEndpoint.ListBatchesAsync(); foreach (var batch in listResponse.Items) {