Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: adds support for strict mode with OpenAI #9924

Open
wants to merge 51 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d3d5edc
Replace stj-schema-mapper source code with M.E.AI schema generation
eiriktsarpalis Nov 25, 2024
535bd6e
feat: adds strict mode flag to function calling
baywet Dec 6, 2024
ecc2cf8
feat: adds strict schema function behaviour and maps it to the metadata
baywet Dec 6, 2024
5ea8112
chore: adds unit test for additional properties false in strict mode
baywet Dec 6, 2024
19e7cca
chore: adds tests for tool call behaviour and strict mode
baywet Dec 6, 2024
9e780e5
chore: adds unit test for new function choice behaviour options property
baywet Dec 6, 2024
733fea9
chore: cleanup reference to default
baywet Dec 6, 2024
6c32a37
fix: badly formatted doc comment
baywet Dec 6, 2024
a504d03
chore: adds test for function metadata to OAI function strict more ma…
baywet Dec 6, 2024
5b6d51a
chore: adds validation for strict property mapping on OpenAIFunction
baywet Dec 6, 2024
d6ca753
chore: migrates to foreach
baywet Dec 6, 2024
4b111cb
chore: adds unit test for required properties behaviour with strict mode
baywet Dec 6, 2024
ef8de5d
chore: adds test for metadata copy constructor
baywet Dec 6, 2024
873ae6a
feat: adds strict parameter to OpenAPI based functions
baywet Dec 6, 2024
dcaaa15
fix: pass strict when cloning function
baywet Dec 6, 2024
8342d73
smell: having to set strict in the function prompt
baywet Dec 6, 2024
1152c7a
fix: reverts additional strict property
baywet Dec 9, 2024
5a2fae9
fix: tests after strict property removal
baywet Dec 9, 2024
99aa4e0
chore: code linting
baywet Dec 9, 2024
4bbff9c
fix: makes schema less parameters optional in strict mode
baywet Dec 9, 2024
34f887c
feat; sanitizes forbidden strict mode keywords
baywet Dec 9, 2024
dff7719
fix: adds missing null type in strict mode
baywet Dec 9, 2024
f34ff32
docs: add links to null type behaviour
baywet Dec 9, 2024
cd7c7bd
chore: adds obsolete method to maintain binary compatibility
baywet Dec 10, 2024
bfa397c
fix: no sanitation in strict mode
baywet Dec 10, 2024
e49cf34
chore: adds experimental tag
baywet Dec 10, 2024
56baeb3
fix: adds missing import
baywet Dec 10, 2024
c5c92d7
chore: fixes nits
baywet Dec 10, 2024
fe37ea7
Merge branch 'main' into feat/strict-mode
baywet Dec 10, 2024
d18da04
fix: assigns parameter filter since it can't be assigned externally
baywet Dec 10, 2024
aec57bb
chore: enables payload namespacing for CAPs sample
baywet Dec 10, 2024
b082a68
docs: adds requested doc comments
baywet Dec 11, 2024
0f1d114
chore: adds doc comment for static method
baywet Dec 13, 2024
e5c7a9d
fix: adds support for normalizing nested properties
baywet Dec 13, 2024
161b199
fix: normalizes additional properties
baywet Dec 13, 2024
1137aea
fix: additional properties can be different types
baywet Dec 13, 2024
167f4f4
fix: handles array types for additional properties normalization
baywet Dec 13, 2024
6a87dd6
fix: makes all properties required
baywet Dec 13, 2024
7049cee
Merge branch 'main' into feat/strict-mode
crickman Dec 16, 2024
f5601b8
chore: enables strict mode for declarative agents
baywet Dec 16, 2024
43f1973
chore: enables strict mode for CAPs and API plugins
baywet Dec 16, 2024
1630cdf
fix: excludes forbidden default keyword
baywet Dec 16, 2024
b8961c3
chore: removes extraneous odata parameter filter
baywet Dec 17, 2024
829812c
chore: removes extraneous filter in api manifest extension
baywet Dec 17, 2024
652b3b8
fix: parameter filter not being passed
baywet Dec 17, 2024
1b3004c
fix: a bug where rest api filters would only work for dynamic payload…
baywet Dec 17, 2024
dcdbe46
feat: makes filtering properties available to the application
baywet Dec 17, 2024
55ca088
fix: adds properties trimming for CAPs demo
baywet Dec 17, 2024
aa7856f
Merge branch 'feat/strict-mode' of https://github.com/baywet/semantic…
baywet Dec 17, 2024
d59e70a
Merge branch 'main' into feat/strict-mode
baywet Dec 17, 2024
3b296e9
fix: drive item test for CAPs
baywet Dec 17, 2024
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
11 changes: 10 additions & 1 deletion dotnet/samples/Concepts/Agents/DeclarativeAgents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,16 @@ public async Task LoadsAgentFromDeclarativeAgentManifestAsync(string agentFileNa
ChatMessageContent message = new(AuthorRole.User, input);
ChatHistory chatHistory = [message];
StringBuilder sb = new();
await foreach (ChatMessageContent response in agent.InvokeAsync(chatHistory))
var kernelArguments = new KernelArguments(new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
options: new FunctionChoiceBehaviorOptions
{
AllowStrictSchemaAdherence = true
}
)
});
await foreach (ChatMessageContent response in agent.InvokeAsync(chatHistory, kernelArguments))
{
chatHistory.Add(response);
sb.Append(response.Content);
Expand Down
29 changes: 21 additions & 8 deletions dotnet/samples/Concepts/Plugins/ApiManifestBasedPlugins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,28 @@ namespace Plugins;
/// <param name="output">The output helper to use to the test can emit status information</param>
public class ApiManifestBasedPlugins(ITestOutputHelper output) : BaseTest(output)
{
private static readonly PromptExecutionSettings s_promptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
options: new FunctionChoiceBehaviorOptions
{
AllowStrictSchemaAdherence = true
}
)
};
public static readonly IEnumerable<object[]> s_parameters =
[
// function names are sanitized operationIds from the OpenAPI document
["MessagesPlugin", "me_ListMessages", new KernelArguments { { "_top", "1" } }, "MessagesPlugin"],
["DriveItemPlugin", "drive_root_GetChildrenContent", new KernelArguments { { "driveItem-Id", "test.txt" } }, "DriveItemPlugin", "MessagesPlugin"],
["ContactsPlugin", "me_ListContacts", new KernelArguments() { { "_count", "true" } }, "ContactsPlugin", "MessagesPlugin"],
["CalendarPlugin", "me_calendar_ListEvents", new KernelArguments() { { "_top", "1" } }, "CalendarPlugin", "MessagesPlugin"],
["MessagesPlugin", "me_ListMessages", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "MessagesPlugin"],
["DriveItemPlugin", "drive_root_GetChildrenContent", new KernelArguments(s_promptExecutionSettings) { { "driveItem-Id", "test.txt" } }, "DriveItemPlugin", "MessagesPlugin"],
["ContactsPlugin", "me_ListContacts", new KernelArguments(s_promptExecutionSettings) { { "_count", "true" } }, "ContactsPlugin", "MessagesPlugin"],
["CalendarPlugin", "me_calendar_ListEvents", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "CalendarPlugin", "MessagesPlugin"],

#region Multiple API dependencies (multiple auth requirements) scenario within the same plugin
// Graph API uses MSAL
["AstronomyPlugin", "me_ListMessages", new KernelArguments { { "_top", "1" } }, "AstronomyPlugin"],
["AstronomyPlugin", "me_ListMessages", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "AstronomyPlugin"],
// Astronomy API uses API key authentication
["AstronomyPlugin", "apod", new KernelArguments { { "_date", "2022-02-02" } }, "AstronomyPlugin"],
["AstronomyPlugin", "apod", new KernelArguments(s_promptExecutionSettings) { { "_date", "2022-02-02" } }, "AstronomyPlugin"],
#endregion
];

Expand Down Expand Up @@ -116,7 +125,9 @@ private async Task AddApiManifestPluginsAsync(Kernel kernel, params string[] plu
// Microsoft Graph API execution parameters
var graphOpenApiFunctionExecutionParameters = new OpenApiFunctionExecutionParameters(
authCallback: authenticationProvider.AuthenticateRequestAsync,
serverUrlOverride: new Uri("https://graph.microsoft.com/v1.0"));
serverUrlOverride: new Uri("https://graph.microsoft.com/v1.0"),
enableDynamicOperationPayload: false,
enablePayloadNamespacing: true);

// NASA API execution parameters
var nasaOpenApiFunctionExecutionParameters = new OpenApiFunctionExecutionParameters(
Expand All @@ -127,7 +138,9 @@ private async Task AddApiManifestPluginsAsync(Kernel kernel, params string[] plu
query["api_key"] = "DEMO_KEY";
uriBuilder.Query = query.ToString();
request.RequestUri = uriBuilder.Uri;
});
},
enableDynamicOperationPayload: false,
enablePayloadNamespacing: true);

var apiManifestPluginParameters = new ApiManifestPluginParameters(
functionExecutionParameters: new()
Expand Down
108 changes: 100 additions & 8 deletions dotnet/samples/Concepts/Plugins/CopilotAgentBasedPlugins.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json;
using System.Text.Json.Nodes;
using System.Web;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.CredentialManagers;
Expand Down Expand Up @@ -52,19 +54,28 @@ namespace Plugins;
/// <param name="output">The output helper to use to the test can emit status information</param>
public class CopilotAgentBasedPlugins(ITestOutputHelper output) : BaseTest(output)
{
private static readonly PromptExecutionSettings s_promptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
options: new FunctionChoiceBehaviorOptions
{
AllowStrictSchemaAdherence = true
}
)
};
public static readonly IEnumerable<object[]> s_parameters =
[
// function names are sanitized operationIds from the OpenAPI document
["MessagesPlugin", "me_ListMessages", new KernelArguments { { "_top", "1" } }, "MessagesPlugin"],
["DriveItemPlugin", "drive_root_GetChildrenContent", new KernelArguments { { "driveItem-Id", "test.txt" } }, "DriveItemPlugin", "MessagesPlugin"],
["ContactsPlugin", "me_ListContacts", new KernelArguments() { { "_count", "true" } }, "ContactsPlugin", "MessagesPlugin"],
["CalendarPlugin", "me_calendar_ListEvents", new KernelArguments() { { "_top", "1" } }, "CalendarPlugin", "MessagesPlugin"],
["MessagesPlugin", "me_ListMessages", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "MessagesPlugin"],
["DriveItemPlugin", "drives_GetItemsContent", new KernelArguments(s_promptExecutionSettings) { { "driveItem-Id", "test.txt" } }, "DriveItemPlugin", "MessagesPlugin"],
["ContactsPlugin", "me_ListContacts", new KernelArguments(s_promptExecutionSettings) { { "_count", "true" } }, "ContactsPlugin", "MessagesPlugin"],
["CalendarPlugin", "me_calendar_ListEvents", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "CalendarPlugin", "MessagesPlugin"],

// Multiple API dependencies (multiple auth requirements) scenario within the same plugin
// Graph API uses MSAL
["AstronomyPlugin", "me_ListMessages", new KernelArguments { { "_top", "1" } }, "AstronomyPlugin"],
["AstronomyPlugin", "me_ListMessages", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "AstronomyPlugin"],
// Astronomy API uses API key authentication
["AstronomyPlugin", "apod", new KernelArguments { { "_date", "2022-02-02" } }, "AstronomyPlugin"],
["AstronomyPlugin", "apod", new KernelArguments(s_promptExecutionSettings) { { "_date", "2022-02-02" } }, "AstronomyPlugin"],
];
[Theory, MemberData(nameof(s_parameters))]
public async Task RunCopilotAgentPluginAsync(string pluginToTest, string functionToTest, KernelArguments? arguments, params string[] pluginsToLoad)
Expand All @@ -87,6 +98,80 @@ private void WriteSampleHeadingToConsole(string pluginToTest, string functionToT
Console.WriteLine($"======== Calling Plugin Function: {pluginToTest}.{functionToTest} with parameters {arguments?.Select(x => x.Key + " = " + x.Value).Aggregate((x, y) => x + ", " + y)} ========");
Console.WriteLine();
}
private static readonly HashSet<string> s_fieldsToIgnore = new(
[
"bodyPreview",
"categories",
"conversationId",
"conversationIndex",
"inferenceClassification",
"internetMessageHeaders",
"isDeliveryReceiptRequested",
"multiValueExtendedProperties",
"singleValueExtendedProperties",
"uniqueBody",
"webLink",
],
StringComparer.OrdinalIgnoreCase
);
private const string RequiredPropertyName = "required";
private const string PropertiesPropertyName = "properties";
/// <summary>
/// Trims the properties from the request body schema.
/// Most models in strict mode enforce a limit on the properties.
/// </summary>
/// <param name="schema">Source schema</param>
/// <returns>the trimmed schema for the request body</returns>
private static KernelJsonSchema? TrimPropertiesFromRequestBody(KernelJsonSchema? schema)
{
if (schema is null)
{
return null;
}

var originalSchema = JsonSerializer.Serialize(schema.RootElement);
var node = JsonNode.Parse(originalSchema);
if (node is not JsonObject jsonNode)
{
return schema;
}
if (jsonNode.TryGetPropertyValue(RequiredPropertyName, out var requiredRawValue) && requiredRawValue is JsonArray requiredArray)
{
jsonNode[RequiredPropertyName] = new JsonArray(requiredArray.Where(x => x is not null).Select(x => x!.GetValue<string>()).Where(x => !s_fieldsToIgnore.Contains(x)).Select(x => JsonValue.Create(x)).ToArray());
}

if (jsonNode.TryGetPropertyValue(PropertiesPropertyName, out var propertiesRawValue) && propertiesRawValue is JsonObject propertiesObject)
{
var properties = propertiesObject.Where(x => s_fieldsToIgnore.Contains(x.Key)).Select(x => x.Key).ToArray();
foreach (var property in properties)
{
propertiesObject.Remove(property);
}
}

return KernelJsonSchema.Parse(node.ToString());
}
private static readonly RestApiParameterFilter s_restApiParameterFilter = (RestApiParameterFilterContext context) =>
{
if ("me_CreateMessages".Equals(context.Operation.Id, StringComparison.OrdinalIgnoreCase) &&
"payload".Equals(context.Parameter.Name, StringComparison.OrdinalIgnoreCase))
{
return new RestApiParameter(
context.Parameter.Name,
context.Parameter.Type,
context.Parameter.IsRequired,
context.Parameter.Expand,
context.Parameter.Location,
context.Parameter.Style,
context.Parameter.ArrayItemType,
context.Parameter.DefaultValue,
context.Parameter.Description,
context.Parameter.Format,
TrimPropertiesFromRequestBody(context.Parameter.Schema)
);
}
return context.Parameter;
};
internal static async Task<CopilotAgentPluginParameters> GetAuthenticationParametersAsync()
{
if (TestConfiguration.MSGraph.Scopes is null)
Expand All @@ -109,7 +194,12 @@ internal static async Task<CopilotAgentPluginParameters> GetAuthenticationParame
// Microsoft Graph API execution parameters
var graphOpenApiFunctionExecutionParameters = new OpenApiFunctionExecutionParameters(
authCallback: authenticationProvider.AuthenticateRequestAsync,
serverUrlOverride: new Uri("https://graph.microsoft.com/v1.0"));
serverUrlOverride: new Uri("https://graph.microsoft.com/v1.0"),
enableDynamicOperationPayload: false,
enablePayloadNamespacing: true)
{
ParameterFilter = s_restApiParameterFilter
};

// NASA API execution parameters
var nasaOpenApiFunctionExecutionParameters = new OpenApiFunctionExecutionParameters(
Expand All @@ -120,7 +210,9 @@ internal static async Task<CopilotAgentPluginParameters> GetAuthenticationParame
query["api_key"] = "DEMO_KEY";
uriBuilder.Query = query.ToString();
request.RequestUri = uriBuilder.Uri;
});
},
enableDynamicOperationPayload: false,
enablePayloadNamespacing: true);

var apiManifestPluginParameters = new CopilotAgentPluginParameters
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ info:
servers:
- url: https://graph.microsoft.com/v1.0
paths:
'/drives/{drive-id}/items/{driveItem-id}/content':
"/me/drive/root/children/{driveItem-id}/content":
get:
tags:
- drives.driveItem
summary: Get content for the navigation property items from drives
description: 'The content stream, if the item represents a file.'
description: "The content stream, if the item represents a file."
operationId: drives_GetItemsContent
parameters:
- name: $format
Expand All @@ -30,18 +30,11 @@ paths:
type: string
format: binary
parameters:
- name: drive-id
in: path
description: The unique identifier of drive
required: true
style: simple
schema:
type: string
- name: driveItem-id
in: path
description: The unique identifier of driveItem
required: true
style: simple
schema:
type: string
components: { }
components: {}
6 changes: 3 additions & 3 deletions dotnet/samples/Demos/OpenAIRealtime/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static async Task Main(string[] args)
await session.AddItemAsync(
ConversationItem.CreateUserMessage(["I'm trying to decide what to wear on my trip."]));

// Use audio file that contains a recorded question: "What's the weather like in San Francisco, California?"
// Use audio file that contains a recorded question: "What's the weather like in San Francisco, California?"
string inputAudioPath = FindFile("Assets\\realtime_whats_the_weather_pcm16_24khz_mono.wav");
using Stream inputAudioStream = File.OpenRead(inputAudioPath);

Expand Down Expand Up @@ -165,7 +165,7 @@ await session.AddItemAsync(
var argumentsString = functionArgumentBuildersById[itemStreamingFinishedUpdate.ItemId].ToString();
var arguments = DeserializeArguments(argumentsString);

// Create a function call content based on received data.
// Create a function call content based on received data.
var functionCallContent = new FunctionCallContent(
functionName: functionName,
pluginName: pluginName,
Expand Down Expand Up @@ -346,7 +346,7 @@ private static IEnumerable<ConversationTool> ConvertFunctions(Kernel kernel)

foreach (var metadata in functionsMetadata)
{
var toolDefinition = metadata.ToOpenAIFunction().ToFunctionDefinition();
var toolDefinition = metadata.ToOpenAIFunction().ToFunctionDefinition(false);
markwallace-microsoft marked this conversation as resolved.
Show resolved Hide resolved

yield return new ConversationFunctionTool()
{
Expand Down
4 changes: 2 additions & 2 deletions dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public static async IAsyncEnumerable<ChatMessageContent> GetMessagesAsync(Assist

FunctionCallsProcessor functionProcessor = new(logger);
// This matches current behavior. Will be configurable upon integrating with `FunctionChoice` (#6795/#5200)
FunctionChoiceBehaviorOptions functionOptions = new() { AllowConcurrentInvocation = true, AllowParallelCalls = true };
FunctionChoiceBehaviorOptions functionOptions = new() { AllowConcurrentInvocation = true, AllowParallelCalls = true, AllowStrictSchemaAdherence = true };
Copy link
Member

@SergeyMenshykh SergeyMenshykh Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if agents will fully follow the options and use the new strict property. Also, if they do, it could change their behavior and affect existing scenarios once customers start using the new package.
CC: @crickman

Copy link
Contributor

@crickman crickman Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct invocation of agents would support all FunctionChoiceBehaviorOptions no different that direct usage if IChatCompletionService. Its the AgentChat usage that would be limited for certain FunctionChoiceBehavior; although the strict option should be properly regarded across all usage patterns.


// Evaluate status and process steps and messages, as encountered.
HashSet<string> processedStepIds = [];
Expand Down Expand Up @@ -412,7 +412,7 @@ public static async IAsyncEnumerable<StreamingChatMessageContent> InvokeStreamin

FunctionCallsProcessor functionProcessor = new(logger);
// This matches current behavior. Will be configurable upon integrating with `FunctionChoice` (#6795/#5200)
FunctionChoiceBehaviorOptions functionOptions = new() { AllowConcurrentInvocation = true, AllowParallelCalls = true };
FunctionChoiceBehaviorOptions functionOptions = new() { AllowConcurrentInvocation = true, AllowParallelCalls = true, AllowStrictSchemaAdherence = true };

IAsyncEnumerable<StreamingUpdate> asyncUpdates = client.CreateRunStreamingAsync(threadId, agent.Id, options, cancellationToken);
do
Expand Down
Loading
Loading