Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,16 @@ void IDisposable.Dispose()
}

/// <summary>Converts an Extensions function to an OpenAI assistants function tool.</summary>
internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction)
internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction, ChatOptions? options = null)
{
(BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction);
bool? strict =
OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ??
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties);

return new FunctionToolDefinition(aiFunction.Name)
{
Description = aiFunction.Description,
Parameters = parameters,
Parameters = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict),
StrictParameterSchemaEnabled = strict,
};
}
Expand Down Expand Up @@ -296,7 +298,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(
switch (tool)
{
case AIFunction aiFunction:
runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options));
break;

case HostedCodeInterpreterTool:
Expand Down Expand Up @@ -342,7 +344,8 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(
runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonSchemaFormat(
jsonFormat.SchemaName,
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
jsonFormat.SchemaDescription);
jsonFormat.SchemaDescription,
OpenAIClientExtensions.HasStrict(options.AdditionalProperties));
break;

case ChatResponseFormatJson jsonFormat:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,17 @@ void IDisposable.Dispose()
}

/// <summary>Converts an Extensions function to an OpenAI chat tool.</summary>
internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction)
internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? options = null)
{
(BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction);

return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, parameters, strict);
bool? strict =
OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ??
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties);

return ChatTool.CreateFunctionTool(
aiFunction.Name,
aiFunction.Description,
OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict),
strict);
}

/// <summary>Converts an Extensions chat message enumerable to an OpenAI chat message enumerable.</summary>
Expand Down Expand Up @@ -517,7 +523,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
{
if (tool is AIFunction af)
{
result.Tools.Add(ToOpenAIChatTool(af));
result.Tools.Add(ToOpenAIChatTool(af, options));
}
}

Expand Down Expand Up @@ -555,7 +561,8 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat(
jsonFormat.SchemaName ?? "json_schema",
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
jsonFormat.SchemaDescription) :
jsonFormat.SchemaDescription,
OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) :
OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
#pragma warning disable S1067 // Expressions should not be too complex
#pragma warning disable SA1515 // Single-line comment should be preceded by blank line
#pragma warning disable CA1305 // Specify IFormatProvider
#pragma warning disable S1135 // Track uses of "TODO" tags

namespace Microsoft.Extensions.AI;

/// <summary>Provides extension methods for working with <see cref="OpenAIClient"/>s.</summary>
public static class OpenAIClientExtensions
{
/// <summary>Key into AdditionalProperties used to store a strict option.</summary>
private const string StrictKey = "strictJsonSchema";
private const string StrictKey = "strict";

/// <summary>Gets the default OpenAI endpoint.</summary>
internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1");
Expand Down Expand Up @@ -182,15 +183,17 @@ public static ResponseTool AsOpenAIResponseTool(this AIFunction function) =>
public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) =>
OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function));

// TODO: Once we're ready to rely on C# 14 features, add an extension property ChatOptions.Strict.

/// <summary>Gets whether the properties specify that strict schema handling is desired.</summary>
internal static bool? HasStrict(IReadOnlyDictionary<string, object?>? additionalProperties) =>
additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true &&
strictObj is bool strictValue ?
strictValue : null;

/// <summary>Extracts from an <see cref="AIFunction"/> the parameters and strictness setting for use with OpenAI's APIs.</summary>
internal static (BinaryData Parameters, bool? Strict) ToOpenAIFunctionParameters(AIFunction aiFunction)
internal static BinaryData ToOpenAIFunctionParameters(AIFunction aiFunction, bool? strict)
{
// Extract any strict setting from AdditionalProperties.
bool? strict =
aiFunction.AdditionalProperties.TryGetValue(OpenAIClientExtensions.StrictKey, out object? strictObj) &&
strictObj is bool strictValue ?
strictValue : null;

// 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) :
Expand All @@ -201,7 +204,7 @@ strictObj is bool strictValue ?
var tool = JsonSerializer.Deserialize(jsonSchema, OpenAIJsonContext.Default.ToolJson)!;
var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, OpenAIJsonContext.Default.ToolJson));

return (functionParameters, strict);
return functionParameters;
}

/// <summary>Used to create the JSON payload for an OpenAI tool description.</summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using OpenAI.RealtimeConversation;

namespace Microsoft.Extensions.AI;

/// <summary>Provides helpers for interacting with OpenAI Realtime.</summary>
internal sealed class OpenAIRealtimeConversationClient
{
public static ConversationFunctionTool ToOpenAIConversationFunctionTool(AIFunction aiFunction)
public static ConversationFunctionTool ToOpenAIConversationFunctionTool(AIFunction aiFunction, ChatOptions? options = null)
{
(BinaryData parameters, _) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction);
bool? strict =
OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ??
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties);

return new ConversationFunctionTool(aiFunction.Name)
{
Description = aiFunction.Description,
Parameters = parameters,
Parameters = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,17 @@ void IDisposable.Dispose()
// Nothing to dispose. Implementation required for the IChatClient interface.
}

internal static ResponseTool ToResponseTool(AIFunction aiFunction)
internal static ResponseTool ToResponseTool(AIFunction aiFunction, ChatOptions? options = null)
{
(BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction);

return ResponseTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, parameters, strict ?? false);
bool? strict =
OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ??
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties);

return ResponseTool.CreateFunctionTool(
aiFunction.Name,
aiFunction.Description,
OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict),
strict ?? false);
}

/// <summary>Creates a <see cref="ChatRole"/> from a <see cref="MessageRole"/>.</summary>
Expand Down Expand Up @@ -380,7 +386,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
switch (tool)
{
case AIFunction aiFunction:
ResponseTool rtool = ToResponseTool(aiFunction);
ResponseTool rtool = ToResponseTool(aiFunction, options);
result.Tools.Add(rtool);
break;

Expand Down Expand Up @@ -442,7 +448,8 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
ResponseTextFormat.CreateJsonSchemaFormat(
jsonFormat.SchemaName ?? "json_schema",
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
jsonFormat.SchemaDescription) :
jsonFormat.SchemaDescription,
OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) :
ResponseTextFormat.CreateJsonObjectFormat(),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ private async Task AvailableTools_SchemasAreAccepted(bool strict)

if (strict)
{
aiFuncOptions.AdditionalProperties = new Dictionary<string, object?> { ["strictJsonSchema"] = true };
aiFuncOptions.AdditionalProperties = new Dictionary<string, object?> { ["strict"] = true };
}

return aiFuncOptions;
Expand All @@ -402,7 +402,7 @@ private async Task AvailableTools_SchemasAreAccepted(bool strict)

if (strict)
{
additionalProperties["strictJsonSchema"] = true;
additionalProperties["strict"] = true;
}

return new CustomAIFunction($"CustomMethod{methodCount++}", schema, additionalProperties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,74 @@ public async Task BasicRequestResponse_Streaming()
}, usage.Details.AdditionalCounts);
}

[Fact]
public async Task ChatOptions_StrictRespected()
{
const string Input = """
{
"tools": [
{
"function": {
"description": "Gets the age of the specified person.",
"name": "GetPersonAge",
"strict": true,
"parameters": {
"type": "object",
"required": [],
"properties": {},
"additionalProperties": false
}
},
"type": "function"
}
],
"messages": [
{
"role": "user",
"content": "hello"
}
],
"model": "gpt-4o-mini",
"tool_choice": "auto"
}
""";

const string Output = """
{
"id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI",
"object": "chat.completion",
"created": 1727888631,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I assist you today?",
"refusal": null
},
"logprobs": null,
"finish_reason": "stop"
}
]
}
""";

using VerbatimHttpHandler handler = new(Input, Output);
using HttpClient httpClient = new(handler);
using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini");

var response = await client.GetResponseAsync("hello", new()
{
Tools = [AIFunctionFactory.Create(() => 42, "GetPersonAge", "Gets the age of the specified person.")],
AdditionalProperties = new()
{
["strict"] = true,
},
});
Assert.NotNull(response);
}

[Fact]
public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_NonStreaming()
{
Expand Down Expand Up @@ -337,7 +405,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio
ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat()
};
openAIOptions.StopSequences.Add("hello");
openAIOptions.Tools.Add(ToOpenAIChatTool(tool));
openAIOptions.Tools.Add(OpenAIClientExtensions.AsOpenAIChatTool(tool));
return openAIOptions;
},
ModelId = null,
Expand Down Expand Up @@ -416,7 +484,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio
ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat()
};
openAIOptions.StopSequences.Add("hello");
openAIOptions.Tools.Add(ToOpenAIChatTool(tool));
openAIOptions.Tools.Add(OpenAIClientExtensions.AsOpenAIChatTool(tool));
return openAIOptions;
},
ModelId = null, // has no effect, you cannot change the model of an OpenAI's ChatClient.
Expand Down Expand Up @@ -600,20 +668,6 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Stream
Assert.Equal("Hello! How can I assist you today?", responseText);
}

/// <summary>Converts an Extensions function to an OpenAI chat tool.</summary>
private static ChatTool ToOpenAIChatTool(AIFunction aiFunction)
{
bool? strict =
aiFunction.AdditionalProperties.TryGetValue("strictJsonSchema", out object? strictObj) &&
strictObj is bool strictValue ?
strictValue : null;

// Map to an intermediate model so that redundant properties are skipped.
var tool = JsonSerializer.Deserialize<ChatToolJson>(aiFunction.JsonSchema)!;
var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool));
return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict);
}

/// <summary>Used to create the JSON payload for an OpenAI chat tool description.</summary>
internal sealed class ChatToolJson
{
Expand Down
Loading
Loading