22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System ;
5+ using System . Collections . Generic ;
56using System . Diagnostics . CodeAnalysis ;
67using System . Text ;
78using System . Text . Json ;
89using System . Text . Json . Nodes ;
10+ using System . Text . Json . Serialization ;
11+ using Microsoft . Shared . Diagnostics ;
912using OpenAI ;
1013using OpenAI . Assistants ;
1114using OpenAI . Audio ;
1215using OpenAI . Chat ;
1316using OpenAI . Embeddings ;
17+ using OpenAI . RealtimeConversation ;
1418using OpenAI . Responses ;
1519
1620#pragma warning disable S103 // Lines should not be too long
@@ -24,7 +28,7 @@ namespace Microsoft.Extensions.AI;
2428public static class OpenAIClientExtensions
2529{
2630 /// <summary>Key into AdditionalProperties used to store a strict option.</summary>
27- internal const string StrictKey = "strictJsonSchema" ;
31+ private const string StrictKey = "strictJsonSchema" ;
2832
2933 /// <summary>Gets the default OpenAI endpoint.</summary>
3034 internal static Uri DefaultOpenAIEndpoint { get ; } = new ( "https://api.openai.com/v1" ) ;
@@ -106,12 +110,14 @@ static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode
106110 /// <summary>Gets an <see cref="IChatClient"/> for use with this <see cref="ChatClient"/>.</summary>
107111 /// <param name="chatClient">The client.</param>
108112 /// <returns>An <see cref="IChatClient"/> that can be used to converse via the <see cref="ChatClient"/>.</returns>
113+ /// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
109114 public static IChatClient AsIChatClient ( this ChatClient chatClient ) =>
110115 new OpenAIChatClient ( chatClient ) ;
111116
112117 /// <summary>Gets an <see cref="IChatClient"/> for use with this <see cref="OpenAIResponseClient"/>.</summary>
113118 /// <param name="responseClient">The client.</param>
114119 /// <returns>An <see cref="IChatClient"/> that can be used to converse via the <see cref="OpenAIResponseClient"/>.</returns>
120+ /// <exception cref="ArgumentNullException"><paramref name="responseClient"/> is <see langword="null"/>.</exception>
115121 public static IChatClient AsIChatClient ( this OpenAIResponseClient responseClient ) =>
116122 new OpenAIResponseChatClient ( responseClient ) ;
117123
@@ -124,13 +130,17 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient
124130 /// property. If no thread ID is provided via either mechanism, a new thread will be created for the request.
125131 /// </param>
126132 /// <returns>An <see cref="IChatClient"/> instance configured to interact with the specified agent and thread.</returns>
127- [ Experimental ( "OPENAI001" ) ]
133+ /// <exception cref="ArgumentNullException"><paramref name="assistantClient"/> is <see langword="null"/>.</exception>
134+ /// <exception cref="ArgumentNullException"><paramref name="assistantId"/> is <see langword="null"/>.</exception>
135+ /// <exception cref="ArgumentException"><paramref name="assistantId"/> is empty or composed entirely of whitespace.</exception>
136+ [ Experimental ( "OPENAI001" ) ] // AssistantClient itself is experimental with this ID
128137 public static IChatClient AsIChatClient ( this AssistantClient assistantClient , string assistantId , string ? threadId = null ) =>
129138 new OpenAIAssistantChatClient ( assistantClient , assistantId , threadId ) ;
130139
131140 /// <summary>Gets an <see cref="ISpeechToTextClient"/> for use with this <see cref="AudioClient"/>.</summary>
132141 /// <param name="audioClient">The client.</param>
133142 /// <returns>An <see cref="ISpeechToTextClient"/> that can be used to transcribe audio via the <see cref="AudioClient"/>.</returns>
143+ /// <exception cref="ArgumentNullException"><paramref name="audioClient"/> is <see langword="null"/>.</exception>
134144 [ Experimental ( "MEAI001" ) ]
135145 public static ISpeechToTextClient AsISpeechToTextClient ( this AudioClient audioClient ) =>
136146 new OpenAISpeechToTextClient ( audioClient ) ;
@@ -139,12 +149,74 @@ public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioCl
139149 /// <param name="embeddingClient">The client.</param>
140150 /// <param name="defaultModelDimensions">The number of dimensions to generate in each embedding.</param>
141151 /// <returns>An <see cref="IEmbeddingGenerator{String, Embedding}"/> that can be used to generate embeddings via the <see cref="EmbeddingClient"/>.</returns>
152+ /// <exception cref="ArgumentNullException"><paramref name="embeddingClient"/> is <see langword="null"/>.</exception>
142153 public static IEmbeddingGenerator < string , Embedding < float > > AsIEmbeddingGenerator ( this EmbeddingClient embeddingClient , int ? defaultModelDimensions = null ) =>
143154 new OpenAIEmbeddingGenerator ( embeddingClient , defaultModelDimensions ) ;
144155
145- /// <summary>Gets the JSON schema to use from the function.</summary>
146- internal static JsonElement GetSchema ( AIFunction function , bool ? strict ) =>
147- strict is true ?
148- StrictSchemaTransformCache . GetOrCreateTransformedSchema ( function ) :
149- function . JsonSchema ;
156+ /// <summary>Creates an OpenAI <see cref="ChatTool"/> from an <see cref="AIFunction"/>.</summary>
157+ /// <param name="function">The function to convert.</param>
158+ /// <returns>An OpenAI <see cref="ChatTool"/> representing <paramref name="function"/>.</returns>
159+ /// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
160+ public static ChatTool AsOpenAIChatTool ( this AIFunction function ) =>
161+ OpenAIChatClient . ToOpenAIChatTool ( Throw . IfNull ( function ) ) ;
162+
163+ /// <summary>Creates an OpenAI <see cref="FunctionToolDefinition"/> from an <see cref="AIFunction"/>.</summary>
164+ /// <param name="function">The function to convert.</param>
165+ /// <returns>An OpenAI <see cref="FunctionToolDefinition"/> representing <paramref name="function"/>.</returns>
166+ /// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
167+ [ Experimental ( "OPENAI001" ) ] // AssistantClient itself is experimental with this ID
168+ public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition ( this AIFunction function ) =>
169+ OpenAIAssistantChatClient . ToOpenAIAssistantsFunctionToolDefinition ( Throw . IfNull ( function ) ) ;
170+
171+ /// <summary>Creates an OpenAI <see cref="ResponseTool"/> from an <see cref="AIFunction"/>.</summary>
172+ /// <param name="function">The function to convert.</param>
173+ /// <returns>An OpenAI <see cref="ResponseTool"/> representing <paramref name="function"/>.</returns>
174+ /// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
175+ public static ResponseTool AsOpenAIResponseTool ( this AIFunction function ) =>
176+ OpenAIResponseChatClient . ToResponseTool ( Throw . IfNull ( function ) ) ;
177+
178+ /// <summary>Creates an OpenAI <see cref="ConversationFunctionTool"/> from an <see cref="AIFunction"/>.</summary>
179+ /// <param name="function">The function to convert.</param>
180+ /// <returns>An OpenAI <see cref="ConversationFunctionTool"/> representing <paramref name="function"/>.</returns>
181+ /// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
182+ public static ConversationFunctionTool AsOpenAIConversationFunctionTool ( this AIFunction function ) =>
183+ OpenAIRealtimeConversationClient . ToOpenAIConversationFunctionTool ( Throw . IfNull ( function ) ) ;
184+
185+ /// <summary>Extracts from an <see cref="AIFunction"/> the parameters and strictness setting for use with OpenAI's APIs.</summary>
186+ internal static ( BinaryData Parameters , bool ? Strict ) ToOpenAIFunctionParameters ( AIFunction aiFunction )
187+ {
188+ // Extract any strict setting from AdditionalProperties.
189+ bool ? strict =
190+ aiFunction . AdditionalProperties . TryGetValue ( OpenAIClientExtensions . StrictKey , out object ? strictObj ) &&
191+ strictObj is bool strictValue ?
192+ strictValue : null ;
193+
194+ // Perform any desirable transformations on the function's JSON schema, if it'll be used in a strict setting.
195+ JsonElement jsonSchema = strict is true ?
196+ StrictSchemaTransformCache . GetOrCreateTransformedSchema ( aiFunction ) :
197+ aiFunction . JsonSchema ;
198+
199+ // Roundtrip the schema through the ToolJson model type to remove extra properties
200+ // and force missing ones into existence, then return the serialized UTF8 bytes as BinaryData.
201+ var tool = JsonSerializer . Deserialize ( jsonSchema , OpenAIJsonContext . Default . ToolJson ) ! ;
202+ var functionParameters = BinaryData . FromBytes ( JsonSerializer . SerializeToUtf8Bytes ( tool , OpenAIJsonContext . Default . ToolJson ) ) ;
203+
204+ return ( functionParameters , strict ) ;
205+ }
206+
207+ /// <summary>Used to create the JSON payload for an OpenAI tool description.</summary>
208+ internal sealed class ToolJson
209+ {
210+ [ JsonPropertyName ( "type" ) ]
211+ public string Type { get ; set ; } = "object" ;
212+
213+ [ JsonPropertyName ( "required" ) ]
214+ public HashSet < string > Required { get ; set ; } = [ ] ;
215+
216+ [ JsonPropertyName ( "properties" ) ]
217+ public Dictionary < string , JsonElement > Properties { get ; set ; } = [ ] ;
218+
219+ [ JsonPropertyName ( "additionalProperties" ) ]
220+ public bool AdditionalProperties { get ; set ; }
221+ }
150222}
0 commit comments