// Copyright (c) Microsoft. All rights reserved. using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; using OpenAI.Assistants; namespace OpenAI; /// /// Provides extension methods for OpenAI /// to simplify the creation of AI agents that work with OpenAI services. /// /// /// These extensions bridge the gap between OpenAI SDK client objects and the Microsoft Extensions AI Agent framework, /// allowing developers to easily create AI agents that leverage OpenAI's chat completion and response services. /// The methods handle the conversion from OpenAI clients to instances and then wrap them /// in objects that implement the interface. /// public static class OpenAIAssistantClientExtensions { /// Key into AdditionalProperties used to store a strict option. private const string StrictKey = "strictJsonSchema"; /// /// Creates an AI agent from an using the OpenAI Assistant API. /// /// The OpenAI to use for the agent. /// The model identifier to use (e.g., "gpt-4"). /// Optional system instructions that define the agent's behavior and personality. /// Optional name for the agent for identification purposes. /// Optional description of the agent's capabilities and purpose. /// Optional collection of AI tools that the agent can use during conversations. /// Optional logger factory for enabling logging within the agent. /// An instance backed by the OpenAI Assistant service. /// Thrown when or is . /// Thrown when is empty or whitespace. public static AIAgent CreateAIAgent(this AssistantClient client, string model, string? instructions = null, string? name = null, string? description = null, IList? tools = null, ILoggerFactory? loggerFactory = null) { return client.CreateAIAgent( model, new ChatClientAgentOptions() { Name = name, Description = description, Instructions = instructions, ChatOptions = tools is null ? null : new ChatOptions() { Tools = tools, } }, loggerFactory); } /// /// Creates an AI agent from an using the OpenAI Assistant API. /// /// The OpenAI to use for the agent. /// The model identifier to use (e.g., "gpt-4"). /// Full set of options to configure the agent. /// Optional logger factory for enabling logging within the agent. /// An instance backed by the OpenAI Assistant service. /// Thrown when or or is . /// Thrown when is empty or whitespace. public static AIAgent CreateAIAgent(this AssistantClient client, string model, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) { Throw.IfNull(client); Throw.IfNullOrEmpty(model); Throw.IfNull(options); var assistantOptions = new AssistantCreationOptions() { Name = options.Name, Description = options.Description, Instructions = options.Instructions, }; if (options.ChatOptions?.Tools is not null) { foreach (AITool tool in options.ChatOptions.Tools) { switch (tool) { case AIFunction aiFunction: assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction)); break; case HostedCodeInterpreterTool: var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition(); assistantOptions.Tools.Add(codeInterpreterToolDefinition); break; } } } var assistantCreateResult = client.CreateAssistant(model, assistantOptions); var assistantId = assistantCreateResult.Value.Id; var agentOptions = new ChatClientAgentOptions() { Id = assistantId, Name = options.Name, Description = options.Description, Instructions = options.Instructions, ChatOptions = options.ChatOptions?.Tools is null ? null : new ChatOptions() { Tools = options.ChatOptions.Tools, } }; #pragma warning disable CA2000 // Dispose objects before losing scope var chatClient = client.AsIChatClient(assistantId); #pragma warning restore CA2000 // Dispose objects before losing scope return new ChatClientAgent(chatClient, agentOptions, loggerFactory); } /// /// Creates an AI agent from an using the OpenAI Assistant API. /// /// The OpenAI to use for the agent. /// The model identifier to use (e.g., "gpt-4"). /// Optional system instructions that define the agent's behavior and personality. /// Optional name for the agent for identification purposes. /// Optional description of the agent's capabilities and purpose. /// Optional collection of AI tools that the agent can use during conversations. /// Optional logger factory for enabling logging within the agent. /// An instance backed by the OpenAI Assistant service. /// Thrown when or is . /// Thrown when is empty or whitespace. public static async Task CreateAIAgentAsync(this AssistantClient client, string model, string? instructions = null, string? name = null, string? description = null, IList? tools = null, ILoggerFactory? loggerFactory = null) { return await client.CreateAIAgentAsync( model, new ChatClientAgentOptions() { Name = name, Description = description, Instructions = instructions, ChatOptions = tools is null ? null : new ChatOptions() { Tools = tools, } }, loggerFactory).ConfigureAwait(false); } /// /// Creates an AI agent from an using the OpenAI Assistant API. /// /// The OpenAI to use for the agent. /// The model identifier to use (e.g., "gpt-4"). /// Full set of options to configure the agent. /// Optional logger factory for enabling logging within the agent. /// An instance backed by the OpenAI Assistant service. /// Thrown when or is . /// Thrown when is empty or whitespace. public static async Task CreateAIAgentAsync(this AssistantClient client, string model, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) { Throw.IfNull(client); Throw.IfNull(model); Throw.IfNull(options); var assistantOptions = new AssistantCreationOptions() { Name = options.Name, Description = options.Description, Instructions = options.Instructions, }; if (options.ChatOptions?.Tools is not null) { foreach (AITool tool in options.ChatOptions.Tools) { switch (tool) { case AIFunction aiFunction: assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction)); break; case HostedCodeInterpreterTool: var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition(); assistantOptions.Tools.Add(codeInterpreterToolDefinition); break; } } } var assistantCreateResult = await client.CreateAssistantAsync(model, assistantOptions).ConfigureAwait(false); var assistantId = assistantCreateResult.Value.Id; var agentOptions = new ChatClientAgentOptions() { Id = assistantId, Name = options.Name, Description = options.Description, Instructions = options.Instructions, ChatOptions = options.ChatOptions?.Tools is null ? null : new ChatOptions() { Tools = options.ChatOptions.Tools, } }; #pragma warning disable CA2000 // Dispose objects before losing scope var chatClient = client.AsIChatClient(assistantId); #pragma warning restore CA2000 // Dispose objects before losing scope return new ChatClientAgent(chatClient, agentOptions, loggerFactory); } /// Converts an Extensions function to an OpenAI assistants function tool. private static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction, ChatOptions? options = null) { bool? strict = HasStrict(aiFunction.AdditionalProperties) ?? HasStrict(options?.AdditionalProperties); return new FunctionToolDefinition(aiFunction.Name) { Description = aiFunction.Description, Parameters = ToOpenAIFunctionParameters(aiFunction, strict), StrictParameterSchemaEnabled = strict, }; } /// Extracts from an the parameters and strictness setting for use with OpenAI's APIs. private static BinaryData ToOpenAIFunctionParameters(AIFunction aiFunction, bool? strict) { // Perform any desirable transformations on the function's JSON schema, if it'll be used in a strict setting. JsonElement jsonSchema = strict is true ? StrictSchemaTransformCache.GetOrCreateTransformedSchema(aiFunction) : aiFunction.JsonSchema; // Roundtrip the schema through the ToolJson model type to remove extra properties // and force missing ones into existence, then return the serialized UTF8 bytes as BinaryData. var tool = jsonSchema.Deserialize(OpenAIJsonContext.Default.ToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, OpenAIJsonContext.Default.ToolJson)); return functionParameters; } /// Gets whether the properties specify that strict schema handling is desired. private static bool? HasStrict(IReadOnlyDictionary? additionalProperties) => additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true && strictObj is bool strictValue ? strictValue : null; private static AIJsonSchemaTransformCache StrictSchemaTransformCache { get; } = new(new() { DisallowAdditionalProperties = true, ConvertBooleanSchemas = true, MoveDefaultKeywordToDescription = true, RequireAllProperties = true, TransformSchemaNode = (ctx, node) => { // Move content from common but unsupported properties to description. In particular, we focus on properties that // the AIJsonUtilities schema generator might produce and/or that are explicitly mentioned in the OpenAI documentation. if (node is JsonObject schemaObj) { StringBuilder? additionalDescription = null; ReadOnlySpan unsupportedProperties = [ // Produced by AIJsonUtilities but not in allow list at https://platform.openai.com/docs/guides/structured-outputs#supported-properties: "contentEncoding", "contentMediaType", "not", // Explicitly mentioned at https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#key-ordering as being unsupported with some models: "minLength", "maxLength", "pattern", "format", "minimum", "maximum", "multipleOf", "patternProperties", "minItems", "maxItems", // Explicitly mentioned at https://learn.microsoft.com/azure/ai-services/openai/how-to/structured-outputs?pivots=programming-language-csharp&tabs=python-secure%2Cdotnet-entra-id#unsupported-type-specific-keywords // as being unsupported with Azure OpenAI: "unevaluatedProperties", "propertyNames", "minProperties", "maxProperties", "unevaluatedItems", "contains", "minContains", "maxContains", "uniqueItems", ]; foreach (string propName in unsupportedProperties) { if (schemaObj[propName] is { } propNode) { _ = schemaObj.Remove(propName); AppendLine(ref additionalDescription, propName, propNode); } } if (additionalDescription is not null) { schemaObj["description"] = schemaObj["description"] is { } descriptionNode && descriptionNode.GetValueKind() == JsonValueKind.String ? $"{descriptionNode.GetValue()}{Environment.NewLine}{additionalDescription}" : additionalDescription.ToString(); } return node; static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode) { sb ??= new(); if (sb.Length > 0) { _ = sb.AppendLine(); } _ = sb.Append(propName).Append(": ").Append(propNode); } } return node; }, }); /// Used to create the JSON payload for an OpenAI tool description. internal sealed class ToolJson { [JsonPropertyName("type")] public string Type { get; set; } = "object"; [JsonPropertyName("required")] public HashSet Required { get; set; } = []; [JsonPropertyName("properties")] public Dictionary Properties { get; set; } = []; [JsonPropertyName("additionalProperties")] public bool AdditionalProperties { get; set; } } } /// Source-generated JSON type information for use by all OpenAI implementations. [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, UseStringEnumConverter = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true)] [JsonSerializable(typeof(OpenAIAssistantClientExtensions.ToolJson))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(JsonElement))] internal sealed partial class OpenAIJsonContext : JsonSerializerContext;