Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -13,20 +13,6 @@ namespace Microsoft.SemanticKernel.Diagnostics;
[ExcludeFromCodeCoverage]
internal static class ActivityExtensions
{
/// <summary>
/// Starts an activity with the appropriate tags for a kernel function execution.
/// </summary>
public static Activity? StartFunctionActivity(this ActivitySource source, string functionName, string functionDescription)
{
const string OperationName = "execute_tool";

return source.StartActivityWithTags($"{OperationName} {functionName}", [
new KeyValuePair<string, object?>("gen_ai.operation.name", OperationName),
new KeyValuePair<string, object?>("gen_ai.tool.name", functionName),
new KeyValuePair<string, object?>("gen_ai.tool.description", functionDescription)
], ActivityKind.Internal);
}

/// <summary>
/// Starts an activity with the specified name and tags.
/// </summary>
Expand All @@ -42,7 +28,6 @@ public static Activity SetTags(this Activity activity, ReadOnlySpan<KeyValuePair
{
activity.SetTag(tag.Key, tag.Value);
}
;

return activity;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/Schema/*.cs" Link="%(RecursiveDir)/InternalUtilities/Schema/%(Filename)%(Extension)" />
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/Diagnostics/*.cs" Link="%(RecursiveDir)/InternalUtilities/Diagnostics/%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<Compile Remove="$(RepoRoot)/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs" Link="%(RecursiveDir)/InternalUtilities/Diagnostics/%(Filename)%(Extension)" />
</ItemGroup>
Expand Down
134 changes: 119 additions & 15 deletions dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
Expand Down Expand Up @@ -239,11 +240,12 @@ public async Task<FunctionResult> InvokeAsync(
kernel ??= this.Kernel;
Verify.NotNull(kernel);

using var activity = s_activitySource.StartFunctionActivity(this.Name, this.Description);
ILogger logger = kernel.LoggerFactory.CreateLogger(typeof(KernelFunction)) ?? NullLogger.Instance;

// Ensure arguments are initialized.
arguments ??= [];

using var activity = this.StartFunctionActivity(this.Name, this.Description, arguments, this._jsonSerializerOptions);
ILogger logger = kernel.LoggerFactory.CreateLogger(typeof(KernelFunction)) ?? NullLogger.Instance;

logger.LogFunctionInvoking(this.PluginName, this.Name);

this.LogFunctionArguments(logger, this.PluginName, this.Name, arguments);
Expand All @@ -267,6 +269,7 @@ public async Task<FunctionResult> InvokeAsync(

logger.LogFunctionInvokedSuccess(this.PluginName, this.Name);

this.SetFunctionResultTag(activity, functionResult, this._jsonSerializerOptions);
this.LogFunctionResult(logger, this.PluginName, this.Name, functionResult);

return functionResult;
Expand Down Expand Up @@ -342,17 +345,21 @@ public async IAsyncEnumerable<TResult> InvokeStreamingAsync<TResult>(
kernel ??= this.Kernel;
Verify.NotNull(kernel);

using var activity = s_activitySource.StartFunctionActivity(this.Name, this.Description);
// Ensure arguments are initialized.
arguments ??= [];

using var activity = this.StartFunctionActivity(this.Name, this.Description, arguments, this._jsonSerializerOptions);
ILogger logger = kernel.LoggerFactory.CreateLogger(this.Name) ?? NullLogger.Instance;

arguments ??= [];
logger.LogFunctionStreamingInvoking(this.PluginName, this.Name);

this.LogFunctionArguments(logger, this.PluginName, this.Name, arguments);

TagList tags = new() { { MeasurementFunctionTagName, this.Name } };
long startingTimestamp = Stopwatch.GetTimestamp();

// Prepare to collect results if activity is enabled.
List<TResult>? results = (activity is not null || logger.IsEnabled(LogLevel.Trace)) ? [] : null;
try
{
IAsyncEnumerator<TResult> enumerator;
Expand Down Expand Up @@ -407,6 +414,7 @@ public async IAsyncEnumerable<TResult> InvokeStreamingAsync<TResult>(
throw;
}

results?.Add(enumerator.Current);
// Yield the next streaming result.
yield return enumerator.Current;
}
Expand All @@ -418,6 +426,8 @@ public async IAsyncEnumerable<TResult> InvokeStreamingAsync<TResult>(
TimeSpan duration = new((long)((Stopwatch.GetTimestamp() - startingTimestamp) * (10_000_000.0 / Stopwatch.Frequency)));
s_streamingDuration.Record(duration.TotalSeconds, in tags);
logger.LogFunctionStreamingComplete(this.PluginName, this.Name, duration.TotalSeconds);
this.SetFunctionResultTag(activity, new FunctionResult(this, results, kernel.Culture), this._jsonSerializerOptions);
this.LogFunctionResult(logger, this.PluginName, this.Name, new FunctionResult(this, results, kernel.Culture));
}
}

Expand All @@ -437,6 +447,18 @@ public override string ToString() => string.IsNullOrWhiteSpace(this.PluginName)
this.Name :
$"{this.PluginName}.{this.Name}";

/// <summary>Creates an <see cref="AIFunction"/> for this <see cref="KernelFunction"/>.</summary>
/// <param name="kernel">
/// The <see cref="Kernel"/> instance to pass to the <see cref="KernelFunction"/> when it's invoked as part of the <see cref="AIFunction"/>'s invocation.
/// </param>
/// <returns>An instance of <see cref="AIFunction"/> that, when invoked, will in turn invoke the current <see cref="KernelFunction"/>.</returns>
[Experimental("SKEXP0001")]
[Obsolete("Use the kernel function directly or for similar behavior use Clone(Kernel) method instead.")]
public AIFunction AsAIFunction(Kernel? kernel = null)
{
return new KernelAIFunction(this, kernel);
}

/// <summary>
/// Invokes the <see cref="KernelFunction"/>.
/// </summary>
Expand Down Expand Up @@ -493,6 +515,8 @@ protected abstract IAsyncEnumerable<TResult> InvokeStreamingCoreAsync<TResult>(K
KernelArguments arguments,
CancellationToken cancellationToken);

#region Private

/// <summary>Handles special-cases for exception handling when invoking a function.</summary>
private static void HandleException(
Exception ex,
Expand Down Expand Up @@ -573,19 +597,99 @@ private void LogFunctionResult(ILogger logger, string? pluginName, string functi
}
}

/// <summary>Creates an <see cref="AIFunction"/> for this <see cref="KernelFunction"/>.</summary>
/// <param name="kernel">
/// The <see cref="Kernel"/> instance to pass to the <see cref="KernelFunction"/> when it's invoked as part of the <see cref="AIFunction"/>'s invocation.
/// </param>
/// <returns>An instance of <see cref="AIFunction"/> that, when invoked, will in turn invoke the current <see cref="KernelFunction"/>.</returns>
[Experimental("SKEXP0001")]
[Obsolete("Use the kernel function directly or for similar behavior use Clone(Kernel) method instead.")]
public AIFunction AsAIFunction(Kernel? kernel = null)
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "The warning is shown and should be addressed at the function creation site; there is no need to show it again at the function invocation sites.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "The warning is shown and should be addressed at the function creation site; there is no need to show it again at the function invocation sites.")]
private Activity? StartFunctionActivity(
string functionName,
string functionDescription,
KernelArguments arguments,
JsonSerializerOptions? jsonSerializerOptions = null)
{
return new KernelAIFunction(this, kernel);
if (!s_activitySource.HasListeners())
{
return null;
}

const string OperationName = "execute_tool";

List<KeyValuePair<string, object?>> tags =
[
new KeyValuePair<string, object?>("gen_ai.operation.name", OperationName),
new KeyValuePair<string, object?>("gen_ai.tool.name", functionName),
new KeyValuePair<string, object?>("gen_ai.tool.description", functionDescription),
];

#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
if (ModelDiagnostics.IsSensitiveEventsEnabled())
{
tags.Add(new KeyValuePair<string, object?>("gen_ai.tool.call.arguments", SerializeArguments(arguments, jsonSerializerOptions)));
}
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

return s_activitySource.StartActivityWithTags($"{OperationName} {functionName}", tags, ActivityKind.Internal);

[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
[RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
static string SerializeArguments(KernelArguments args, JsonSerializerOptions? jsonSerializerOptions)
{
try
{
if (jsonSerializerOptions is not null)
{
JsonTypeInfo<KernelArguments> typeInfo = (JsonTypeInfo<KernelArguments>)jsonSerializerOptions.GetTypeInfo(typeof(KernelArguments));
return JsonSerializer.Serialize(args, typeInfo);
}

return JsonSerializer.Serialize(args);
}
catch (NotSupportedException)
{
return "Failed to serialize arguments to Json";
}
}
}

#region Private
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "The warning is shown and should be addressed at the function creation site; there is no need to show it again at the function invocation sites.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "The warning is shown and should be addressed at the function creation site; there is no need to show it again at the function invocation sites.")]
private Activity? SetFunctionResultTag(Activity? activity, FunctionResult result, JsonSerializerOptions? jsonSerializerOptions = null)
{
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
if (ModelDiagnostics.IsSensitiveEventsEnabled())
{
activity?.SetTag("gen_ai.tool.call.result", SerializeResult(result, jsonSerializerOptions));
}
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

return activity;

[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "By design. See comment below.")]
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
[RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
static string SerializeResult(FunctionResult result, JsonSerializerOptions? jsonSerializerOptions)
{
// Try to retrieve the result as a string first
try
{
return result.GetValue<string>() ?? string.Empty;
}
catch { }

// Fallback to JSON serialization
try
{
if (jsonSerializerOptions is not null)
{
JsonTypeInfo<object?> typeInfo = (JsonTypeInfo<object?>)jsonSerializerOptions.GetTypeInfo(typeof(object));
return JsonSerializer.Serialize(result.Value, typeInfo);
}
return JsonSerializer.Serialize(result.Value);
}
catch (NotSupportedException)
{
return "Failed to serialize result to Json";
}
}
}

private JsonElement _jsonSchema;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
<ItemGroup>
<!-- Exclude utilities that are not used by the project and causing AOT warnings-->
<Compile Remove="$(RepoRoot)/dotnet/src/InternalUtilities/src/Data/**/*.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Remove="$(RepoRoot)/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Remove="$(RepoRoot)/dotnet/src/InternalUtilities/src/Http/**/*.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Remove="$(RepoRoot)/dotnet/src/InternalUtilities/src/Text/JsonOptionsCache.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
<Compile Remove="$(RepoRoot)/dotnet/src/InternalUtilities/src/Text/SseData.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
Expand Down
Loading