diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index a032edea6116..ab1574e945e6 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -5,9 +5,9 @@
true
-
+
-
+
@@ -91,7 +91,7 @@
-
+
@@ -106,7 +106,7 @@
-
+
diff --git a/dotnet/samples/Concepts/FunctionCalling/AzureAIInference_FunctionCalling.cs b/dotnet/samples/Concepts/FunctionCalling/AzureAIInference_FunctionCalling.cs
new file mode 100644
index 000000000000..6315b9c9c88e
--- /dev/null
+++ b/dotnet/samples/Concepts/FunctionCalling/AzureAIInference_FunctionCalling.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.AzureAIInference;
+
+namespace FunctionCalling;
+public class AzureAIInference_FunctionCalling : BaseTest
+{
+ private readonly LoggingHandler _handler;
+ private readonly HttpClient _httpClient;
+ private bool _isDisposed;
+
+ public AzureAIInference_FunctionCalling(ITestOutputHelper output) : base(output)
+ {
+ // Create a logging handler to output HTTP requests and responses
+ this._handler = new LoggingHandler(new HttpClientHandler(), this.Output);
+ this._httpClient = new(this._handler);
+ }
+
+ ///
+ /// This example demonstrates usage of that advertises all kernel functions to the AI model.
+ ///
+ [Fact]
+ public async Task FunctionCallingAsync()
+ {
+ var kernel = CreateKernel();
+ AzureAIInferencePromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
+ Console.WriteLine(await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)));
+ }
+
+ ///
+ /// This example demonstrates usage of that advertises all kernel functions to the AI model.
+ ///
+ [Fact]
+ public async Task FunctionCallingWithPromptExecutionSettingsAsync()
+ {
+ var kernel = CreateKernel();
+ PromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
+ Console.WriteLine(await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)));
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!this._isDisposed)
+ {
+ if (disposing)
+ {
+ this._handler.Dispose();
+ this._httpClient.Dispose();
+ }
+
+ this._isDisposed = true;
+ }
+ base.Dispose(disposing);
+ }
+
+ private Kernel CreateKernel()
+ {
+ // Create kernel
+ var kernel = Kernel.CreateBuilder()
+ .AddAzureAIInferenceChatCompletion(
+ modelId: TestConfiguration.AzureAIInference.ChatModelId,
+ endpoint: new Uri(TestConfiguration.AzureAIInference.Endpoint),
+ apiKey: TestConfiguration.AzureAIInference.ApiKey,
+ httpClient: this._httpClient)
+ .Build();
+
+ // Add a plugin with some helper functions we want to allow the model to call.
+ kernel.ImportPluginFromFunctions("HelperFunctions",
+ [
+ kernel.CreateFunctionFromMethod(() => new List { "Squirrel Steals Show", "Dog Wins Lottery" }, "GetLatestNewsTitles", "Retrieves latest news titles."),
+ kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcDateTime", "Retrieves the current date time in UTC."),
+ kernel.CreateFunctionFromMethod((string cityName, string currentDateTime) =>
+ cityName switch
+ {
+ "Boston" => "61 and rainy",
+ "London" => "55 and cloudy",
+ "Miami" => "80 and sunny",
+ "Paris" => "60 and rainy",
+ "Tokyo" => "50 and sunny",
+ "Sydney" => "75 and sunny",
+ "Tel Aviv" => "80 and sunny",
+ _ => "31 and snowing",
+ }, "GetWeatherForCity", "Gets the current weather for the specified city"),
+ ]);
+
+ return kernel;
+ }
+}
diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md
index deb3a6a43a20..0b4fb374b884 100644
--- a/dotnet/samples/Concepts/README.md
+++ b/dotnet/samples/Concepts/README.md
@@ -39,8 +39,9 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom
### FunctionCalling - Examples on `Function Calling` with function call capable models
-- [Gemini_FunctionCalling](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/FunctionCalling/Gemini_FunctionCalling.cs)
- [FunctionCalling](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/FunctionCalling/FunctionCalling.cs)
+- [Gemini_FunctionCalling](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/FunctionCalling/Gemini_FunctionCalling.cs)
+- [AzureAIInference_FunctionCalling](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/FunctionCalling/AzureAIInference_FunctionCalling.cs)
- [NexusRaven_HuggingFaceTextGeneration](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/FunctionCalling/NexusRaven_FunctionCalling.cs)
- [MultipleFunctionsVsParameters](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/FunctionCalling/MultipleFunctionsVsParameters.cs)
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step05/Step05_MapReduce.cs b/dotnet/samples/GettingStartedWithProcesses/Step05/Step05_MapReduce.cs
index 144d4ab79d3f..ca8d33818d53 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step05/Step05_MapReduce.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step05/Step05_MapReduce.cs
@@ -116,7 +116,7 @@ public async ValueTask ComputeAsync(KernelProcessStepContext context, string chu
{
Dictionary counts = [];
- string[] words = chunk.Split([' ', '\n', '\r', '.', ',', '’'], StringSplitOptions.RemoveEmptyEntries);
+ string[] words = chunk.Split([" ", "\n", "\r", ".", ",", "’"], StringSplitOptions.RemoveEmptyEntries);
foreach (string word in words)
{
if (s_notInteresting.Contains(word))
diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs
index c4acca58770f..1a4b6fc2fbf6 100644
--- a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs
+++ b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs
@@ -81,6 +81,11 @@ private static string ConvertType(Type? type)
return "array";
}
+ if (type == typeof(DateTime) || type == typeof(DateTimeOffset))
+ {
+ return "date-time";
+ }
+
return Type.GetTypeCode(type) switch
{
TypeCode.SByte or TypeCode.Byte or
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs
index acf195840366..3710b4841ab3 100644
--- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs
@@ -58,7 +58,7 @@ public void TestFunction1() { }
[KernelFunction]
[Description("test description")]
#pragma warning disable IDE0060 // Unused parameter for mock kernel function
- public void TestFunction2(string p1, bool p2, int p3, string[] p4, ConsoleColor p5, OpenAIAssistantDefinition p6) { }
+ public void TestFunction2(string p1, bool p2, int p3, string[] p4, ConsoleColor p5, OpenAIAssistantDefinition p6, DateTime p7) { }
#pragma warning restore IDE0060 // Unused parameter
}
}
diff --git a/dotnet/src/Connectors/Connectors.AzureAIInference.UnitTests/Settings/AzureAIInferencePromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureAIInference.UnitTests/Settings/AzureAIInferencePromptExecutionSettingsTests.cs
index c61a261e7d30..0ef1459b52d3 100644
--- a/dotnet/src/Connectors/Connectors.AzureAIInference.UnitTests/Settings/AzureAIInferencePromptExecutionSettingsTests.cs
+++ b/dotnet/src/Connectors/Connectors.AzureAIInference.UnitTests/Settings/AzureAIInferencePromptExecutionSettingsTests.cs
@@ -26,9 +26,9 @@ public void ItCreatesAzureAIInferenceExecutionSettingsWithCorrectDefaults()
Assert.Null(executionSettings.ResponseFormat);
Assert.Null(executionSettings.Seed);
Assert.Null(executionSettings.MaxTokens);
+ Assert.Null(executionSettings.Tools);
+ Assert.Null(executionSettings.StopSequences);
Assert.Empty(executionSettings.ExtensionData!);
- Assert.Empty(executionSettings.Tools);
- Assert.Empty(executionSettings.StopSequences!);
}
[Fact]
diff --git a/dotnet/src/Connectors/Connectors.AzureAIInference/Settings/AzureAIInferencePromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureAIInference/Settings/AzureAIInferencePromptExecutionSettings.cs
index 5bab73330fa9..3146cb94fb78 100644
--- a/dotnet/src/Connectors/Connectors.AzureAIInference/Settings/AzureAIInferencePromptExecutionSettings.cs
+++ b/dotnet/src/Connectors/Connectors.AzureAIInference/Settings/AzureAIInferencePromptExecutionSettings.cs
@@ -153,7 +153,7 @@ public object? ResponseFormat
/// A collection of textual sequences that will end completions generation.
[JsonPropertyName("stop")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public IList StopSequences
+ public IList? StopSequences
{
get => this._stopSequences;
set
@@ -170,7 +170,7 @@ public IList StopSequences
///
[JsonPropertyName("tools")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public IList Tools
+ public IList? Tools
{
get => this._tools;
set
@@ -229,8 +229,8 @@ public override PromptExecutionSettings Clone()
NucleusSamplingFactor = this.NucleusSamplingFactor,
MaxTokens = this.MaxTokens,
ResponseFormat = this.ResponseFormat,
- StopSequences = new List(this.StopSequences),
- Tools = new List(this.Tools),
+ StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null,
+ Tools = this.Tools is not null ? new List(this.Tools) : null,
Seed = this.Seed,
ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null,
};
@@ -273,8 +273,8 @@ public static AzureAIInferencePromptExecutionSettings FromExecutionSettings(Prom
private float? _nucleusSamplingFactor;
private int? _maxTokens;
private object? _responseFormat;
- private IList _stopSequences = [];
- private IList _tools = [];
+ private IList? _stopSequences;
+ private IList? _tools;
private long? _seed;
#endregion
diff --git a/dotnet/src/Functions/Functions.OpenApi/Extensions/RestApiOperationExtensions.cs b/dotnet/src/Functions/Functions.OpenApi/Extensions/RestApiOperationExtensions.cs
index 478306812f10..ad2a7a7c5101 100644
--- a/dotnet/src/Functions/Functions.OpenApi/Extensions/RestApiOperationExtensions.cs
+++ b/dotnet/src/Functions/Functions.OpenApi/Extensions/RestApiOperationExtensions.cs
@@ -183,6 +183,12 @@ private static List GetParametersFromPayloadMetadata(RestApiOp
if (!property.Properties.Any())
{
+ // Assign an argument name (sanitized form of the property name) so that the parameter value look-up / resolution functionality in the RestApiOperationRunner
+ // class can find the value for the parameter by the argument name in the arguments dictionary. If the argument name is not assigned here, the resolution mechanism
+ // will try to find the parameter value by the parameter's original name. However, because the parameter was advertised with the sanitized name by the RestApiOperationExtensions.GetParameters
+ // method, no value will be found, and an exception will be thrown: "No argument is found for the 'customerid_contact@odata.bind' payload property."
+ property.ArgumentName ??= InvalidSymbolsRegex().Replace(parameterName, "_");
+
var parameter = new RestApiParameter(
name: parameterName,
type: property.Type,
diff --git a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs
index 7c0ada6d830c..c32a3811e6bb 100644
--- a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs
+++ b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs
@@ -175,7 +175,7 @@ public Task RunAsync(
var (Payload, Content) = this._payloadFactory?.Invoke(operation, arguments, this._enableDynamicPayload, this._enablePayloadNamespacing, options) ?? this.BuildOperationPayload(operation, arguments);
- return this.SendAsync(url, operation.Method, headers, Payload, Content, operation.Responses.ToDictionary(item => item.Key, item => item.Value.Schema), options, cancellationToken);
+ return this.SendAsync(operation, url, headers, Payload, Content, options, cancellationToken);
}
#region private
@@ -183,26 +183,24 @@ public Task RunAsync(
///
/// Sends an HTTP request.
///
+ /// The REST API operation.
/// The url to send request to.
- /// The HTTP request method.
/// Headers to include into the HTTP request.
/// HTTP request payload.
/// HTTP request content.
- /// The dictionary of expected response schemas.
/// Options for REST API operation run.
/// The cancellation token.
/// Response content and content type
private async Task SendAsync(
+ RestApiOperation operation,
Uri url,
- HttpMethod method,
IDictionary? headers = null,
object? payload = null,
HttpContent? requestContent = null,
- IDictionary? expectedSchemas = null,
RestApiOperationRunOptions? options = null,
CancellationToken cancellationToken = default)
{
- using var requestMessage = new HttpRequestMessage(method, url);
+ using var requestMessage = new HttpRequestMessage(operation.Method, url);
#if NET5_0_OR_GREATER
requestMessage.Options.Set(OpenApiKernelFunctionContext.KernelFunctionContextKey, new OpenApiKernelFunctionContext(options?.Kernel, options?.KernelFunction, options?.KernelArguments));
@@ -237,9 +235,7 @@ private async Task SendAsync(
{
responseMessage = await this._httpClient.SendWithSuccessCheckAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- response = await this.ReadContentAndCreateOperationResponseAsync(requestMessage, responseMessage, payload, cancellationToken).ConfigureAwait(false);
-
- response.ExpectedSchema ??= GetExpectedSchema(expectedSchemas, responseMessage.StatusCode);
+ response = await this.BuildResponseAsync(operation, requestMessage, responseMessage, payload, cancellationToken).ConfigureAwait(false);
return response;
}
@@ -352,7 +348,15 @@ private async Task ReadContentAndCreateOperationRespon
mediaType = mediaTypeFallback;
}
- if (!this._payloadFactoryByMediaType.TryGetValue(mediaType!, out var payloadFactory))
+ // Remove media type parameters, such as x-api-version, from the "text/plain; x-api-version=2.0" media type string.
+ mediaType = mediaType!.Split(';').First();
+
+ // Normalize the media type to lowercase and remove trailing whitespaces.
+#pragma warning disable CA1308 // Normalize strings to uppercase
+ mediaType = mediaType!.ToLowerInvariant().Trim();
+#pragma warning restore CA1308 // Normalize strings to uppercase
+
+ if (!this._payloadFactoryByMediaType.TryGetValue(mediaType, out var payloadFactory))
{
throw new KernelException($"The media type {mediaType} of the {operation.Id} operation is not supported by {nameof(RestApiOperationRunner)}.");
}
@@ -562,5 +566,23 @@ private Uri BuildsOperationUrl(RestApiOperation operation, IDictionary
+ /// Builds the operation response.
+ ///
+ /// The REST API operation.
+ /// The HTTP request message.
+ /// The HTTP response message.
+ /// The payload sent in the HTTP request.
+ /// The cancellation token.
+ /// The operation response.
+ private async Task BuildResponseAsync(RestApiOperation operation, HttpRequestMessage requestMessage, HttpResponseMessage responseMessage, object? payload, CancellationToken cancellationToken)
+ {
+ var response = await this.ReadContentAndCreateOperationResponseAsync(requestMessage, responseMessage, payload, cancellationToken).ConfigureAwait(false);
+
+ response.ExpectedSchema ??= GetExpectedSchema(operation.Responses.ToDictionary(item => item.Key, item => item.Value.Schema), responseMessage.StatusCode);
+
+ return response;
+ }
+
#endregion
}
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserExtensionsTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserExtensionsTests.cs
index 88cb52d183e6..52fdd6371496 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserExtensionsTests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserExtensionsTests.cs
@@ -91,7 +91,7 @@ public async Task ItCanParseMediaTypeAsync(string documentName)
// Assert.
Assert.NotNull(restApi.Operations);
- Assert.Equal(7, restApi.Operations.Count);
+ Assert.Equal(8, restApi.Operations.Count);
var operation = restApi.Operations.Single(o => o.Id == "Joke");
Assert.NotNull(operation);
Assert.Equal("application/json; x-api-version=2.0", operation.Payload?.MediaType);
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs
index 9313297ace66..ae60700f5811 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs
@@ -236,7 +236,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync()
var restApi = await this._sut.ParseAsync(this._openApiDocument);
// Assert
- Assert.Equal(6, restApi.Operations.Count);
+ Assert.Equal(7, restApi.Operations.Count);
}
[Fact]
@@ -419,7 +419,7 @@ public async Task ItCanParsePropertiesOfObjectDataTypeAsync()
public async Task ItCanFilterOutSpecifiedOperationsAsync()
{
// Arrange
- var operationsToExclude = new[] { "Excuses", "TestDefaultValues", "OpenApiExtensions", "TestParameterDataTypes" };
+ string[] operationsToExclude = ["Excuses", "TestDefaultValues", "OpenApiExtensions", "TestParameterDataTypes", "TestParameterNamesSanitization"];
var options = new OpenApiDocumentParserOptions
{
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs
index 02b3d363ebfb..b9c7e96378ce 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs
@@ -237,7 +237,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync()
var restApi = await this._sut.ParseAsync(this._openApiDocument);
// Assert
- Assert.Equal(7, restApi.Operations.Count);
+ Assert.Equal(8, restApi.Operations.Count);
}
[Fact]
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs
index 5fc59c70a8f9..0e51bfe35b24 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs
@@ -237,7 +237,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync()
var restApi = await this._sut.ParseAsync(this._openApiDocument);
// Assert
- Assert.Equal(7, restApi.Operations.Count);
+ Assert.Equal(8, restApi.Operations.Count);
}
[Fact]
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs
index 2242f5032610..c85002493181 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs
@@ -8,6 +8,7 @@
using System.Net.Http;
using System.Net.Mime;
using System.Text;
+using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
@@ -353,7 +354,7 @@ public async Task ItShouldHandleEmptyOperationNameAsync()
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", content, this._executionParameters);
// Assert
- Assert.Equal(7, plugin.Count());
+ Assert.Equal(8, plugin.Count());
Assert.True(plugin.TryGetFunction("GetSecretsSecretname", out var _));
}
@@ -372,7 +373,7 @@ public async Task ItShouldHandleNullOperationNameAsync()
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", content, this._executionParameters);
// Assert
- Assert.Equal(7, plugin.Count());
+ Assert.Equal(8, plugin.Count());
Assert.True(plugin.TryGetFunction("GetSecretsSecretname", out var _));
}
@@ -517,6 +518,94 @@ public void ItCreatesPluginFromOpenApiSpecificationModel()
Assert.Same(operations[0], function.Metadata.AdditionalProperties["operation"]);
}
+ [Fact]
+ public async Task ItShouldResolveArgumentsByParameterNamesAsync()
+ {
+ // Arrange
+ using var messageHandlerStub = new HttpMessageHandlerStub();
+ using var httpClient = new HttpClient(messageHandlerStub, false);
+
+ this._executionParameters.EnableDynamicPayload = true;
+ this._executionParameters.HttpClient = httpClient;
+
+ var arguments = new KernelArguments
+ {
+ ["string_parameter"] = "fake-secret-name",
+ ["boolean@parameter"] = true,
+ ["integer+parameter"] = 6,
+ ["float?parameter"] = 23.4f
+ };
+
+ var kernel = new Kernel();
+
+ var openApiDocument = ResourcePluginsProvider.LoadFromResource("documentV3_0.json");
+
+ var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", openApiDocument, this._executionParameters);
+
+ // Act
+ var result = await kernel.InvokeAsync(plugin["TestParameterNamesSanitization"], arguments);
+
+ // Assert path and query parameters added to the request uri
+ Assert.NotNull(messageHandlerStub.RequestUri);
+ Assert.Equal("https://my-key-vault.vault.azure.net/test-parameter-names-sanitization/fake-secret-name?boolean@parameter=true", messageHandlerStub.RequestUri.AbsoluteUri);
+
+ // Assert header parameters added to the request
+ Assert.Equal("6", messageHandlerStub.RequestHeaders!.GetValues("integer+parameter").First());
+
+ // Assert payload parameters added to the request
+ var messageContent = messageHandlerStub.RequestContent;
+ Assert.NotNull(messageContent);
+
+ var deserializedPayload = await JsonNode.ParseAsync(new MemoryStream(messageContent));
+ Assert.NotNull(deserializedPayload);
+
+ Assert.Equal(23.4f, deserializedPayload["float?parameter"]!.GetValue());
+ }
+
+ [Fact]
+ public async Task ItShouldResolveArgumentsBySanitizedParameterNamesAsync()
+ {
+ // Arrange
+ using var messageHandlerStub = new HttpMessageHandlerStub();
+ using var httpClient = new HttpClient(messageHandlerStub, false);
+
+ this._executionParameters.EnableDynamicPayload = true;
+ this._executionParameters.HttpClient = httpClient;
+
+ var arguments = new KernelArguments
+ {
+ ["string_parameter"] = "fake-secret-name", // Original parameter name - string-parameter
+ ["boolean_parameter"] = true, // Original parameter name - boolean@parameter
+ ["integer_parameter"] = 6, // Original parameter name - integer+parameter
+ ["float_parameter"] = 23.4f // Original parameter name - float?parameter
+ };
+
+ var kernel = new Kernel();
+
+ var openApiDocument = ResourcePluginsProvider.LoadFromResource("documentV3_0.json");
+
+ var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", openApiDocument, this._executionParameters);
+
+ // Act
+ var result = await kernel.InvokeAsync(plugin["TestParameterNamesSanitization"], arguments);
+
+ // Assert path and query parameters added to the request uri
+ Assert.NotNull(messageHandlerStub.RequestUri);
+ Assert.Equal("https://my-key-vault.vault.azure.net/test-parameter-names-sanitization/fake-secret-name?boolean@parameter=true", messageHandlerStub.RequestUri.AbsoluteUri);
+
+ // Assert header parameters added to the request
+ Assert.Equal("6", messageHandlerStub.RequestHeaders!.GetValues("integer+parameter").First());
+
+ // Assert payload parameters added to the request
+ var messageContent = messageHandlerStub.RequestContent;
+ Assert.NotNull(messageContent);
+
+ var deserializedPayload = await JsonNode.ParseAsync(new MemoryStream(messageContent));
+ Assert.NotNull(deserializedPayload);
+
+ Assert.Equal(23.4f, deserializedPayload["float?parameter"]!.GetValue());
+ }
+
///
/// Generate theory data for ItAddSecurityMetadataToOperationAsync
///
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs
index fd48bbcb6a50..764e5e2b4f07 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs
@@ -1705,6 +1705,44 @@ public async Task ItShouldReturnExpectedSchemaAsync(string expectedStatusCode, p
Assert.Equal(JsonSerializer.Serialize(expected), JsonSerializer.Serialize(result.ExpectedSchema));
}
+ [Theory]
+ [InlineData("application/json;x-api-version=2.0", "application/json")]
+ [InlineData("application/json ; x-api-version=2.0", "application/json")]
+ [InlineData(" application/JSON; x-api-version=2.0", "application/json")]
+ [InlineData(" TEXT/PLAIN ; x-api-version=2.0", "text/plain")]
+ public async Task ItShouldNormalizeContentTypeArgumentAsync(string actualContentType, string normalizedContentType)
+ {
+ // Arrange
+ this._httpMessageHandlerStub.ResponseToReturn.Content = new StringContent("fake-content", Encoding.UTF8, MediaTypeNames.Text.Plain);
+
+ var operation = new RestApiOperation(
+ id: "fake-id",
+ servers: [new RestApiServer("https://fake-random-test-host")],
+ path: "fake-path",
+ method: HttpMethod.Post,
+ description: "fake-description",
+ parameters: [],
+ responses: new Dictionary(),
+ securityRequirements: [],
+ payload: null
+ );
+
+ var arguments = new KernelArguments
+ {
+ { "payload", "fake-input-value" },
+ { "content-type", actualContentType },
+ };
+
+ var sut = new RestApiOperationRunner(this._httpClient, this._authenticationHandlerMock.Object, enableDynamicPayload: false);
+
+ // Act
+ var result = await sut.RunAsync(operation, arguments);
+
+ // Assert
+ Assert.NotNull(this._httpMessageHandlerStub.ContentHeaders);
+ Assert.Contains(this._httpMessageHandlerStub.ContentHeaders, h => h.Key == "Content-Type" && h.Value.Any(h => h.StartsWith(normalizedContentType, StringComparison.InvariantCulture)));
+ }
+
///
/// Disposes resources used by this class.
///
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV2_0.json b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV2_0.json
index cc71b0f46737..cb83ae5a809a 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV2_0.json
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV2_0.json
@@ -434,6 +434,63 @@
},
"summary": "Get secret"
}
+ },
+ "/test-parameter-names-sanitization/{string-parameter}": {
+ "put": {
+ "summary": "Operation to test parameter names sanitization.",
+ "description": "Operation to test that forbidden characters in parameter names are sanitized.",
+ "operationId": "TestParameterNamesSanitization",
+ "consumes": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "string-parameter",
+ "required": true,
+ "type": "string",
+ "default": "string-value"
+ },
+ {
+ "in": "query",
+ "name": "boolean@parameter",
+ "required": true,
+ "type": "boolean",
+ "default": true
+ },
+ {
+ "in": "header",
+ "name": "integer+parameter",
+ "required": true,
+ "type": "integer",
+ "format": "int32",
+ "default": 281
+ },
+ {
+ "in": "body",
+ "name": "body",
+ "required": true,
+ "schema": {
+ "required": [
+ "float?parameter"
+ ],
+ "type": "object",
+ "properties": {
+ "float?parameter": {
+ "format": "float",
+ "default": 12.01,
+ "type": "number"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "The OK response"
+ }
+ }
+ }
}
},
"produces": [],
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json
index a2990fb86f90..abc7eef32825 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json
@@ -486,6 +486,67 @@
}
}
}
+ },
+ "/test-parameter-names-sanitization/{string-parameter}": {
+ "put": {
+ "summary": "Operation to test parameter names sanitization.",
+ "description": "Operation to test that forbidden characters in parameter names are sanitized.",
+ "operationId": "TestParameterNamesSanitization",
+ "parameters": [
+ {
+ "name": "string-parameter",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "default": "string-value"
+ }
+ },
+ {
+ "name": "boolean@parameter",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ },
+ {
+ "name": "integer+parameter",
+ "in": "header",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "default": 281
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "float?parameter": {
+ "type": "number",
+ "format": "float",
+ "default": 12.01
+ }
+ },
+ "required": [ "float?parameter" ]
+ }
+ }
+ },
+ "required": true,
+ "x-bodyName": "body"
+ },
+ "responses": {
+ "200": {
+ "description": "The OK response"
+ }
+ }
+ }
}
},
"components": {
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml
index 8c250db741cb..3bf24812684a 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml
@@ -140,7 +140,7 @@ paths:
type: string
/FunPlugin/Joke:
post:
- summary: Gneerate a funny joke
+ summary: Generate a funny joke
operationId: Joke
requestBody:
description: Joke subject
@@ -244,6 +244,8 @@ paths:
description: The OK response
/api-with-open-api-extensions:
get:
+ summary: Get API with open-api specification extensions
+ description: 'For more information on specification extensions see the specification extensions section of the open api spec: https://swagger.io/specification/v3/'
operationId: OpenApiExtensions
responses:
'200':
@@ -318,6 +320,48 @@ paths:
responses:
'200':
description: The OK response
+ '/test-parameter-names-sanitization/{string-parameter}':
+ put:
+ summary: Operation to test parameter names sanitization.
+ description: Operation to test that forbidden characters in parameter names are sanitized.
+ operationId: TestParameterNamesSanitization
+ parameters:
+ - name: string-parameter
+ in: path
+ required: true
+ schema:
+ type: string
+ default: string-value
+ - name: boolean@parameter
+ in: query
+ required: true
+ schema:
+ type: boolean
+ default: true
+ - name: integer+parameter
+ in: header
+ required: true
+ schema:
+ type: integer
+ format: int32
+ default: 281
+ requestBody:
+ content:
+ application/json:
+ schema:
+ required:
+ - float?parameter
+ type: object
+ properties:
+ float?parameter:
+ type: number
+ format: float
+ default: 12.01
+ required: true
+ x-bodyName: body
+ responses:
+ '200':
+ description: The OK response
components:
securitySchemes:
oauth2_auth:
diff --git a/python/samples/concepts/chat_completion/simple_chatbot_store_metadata.py b/python/samples/concepts/chat_completion/simple_chatbot_store_metadata.py
index 44484aed7122..0c59ff9dc5ec 100644
--- a/python/samples/concepts/chat_completion/simple_chatbot_store_metadata.py
+++ b/python/samples/concepts/chat_completion/simple_chatbot_store_metadata.py
@@ -32,7 +32,7 @@
# Create a chat history object with the system message.
chat_history = ChatHistory(system_message=system_message)
-# Configure the store amd metadata settings for the chat completion service.
+# Configure the store and metadata settings for the chat completion service.
request_settings.store = True
request_settings.metadata = {"chatbot": "Mosscap"}