Skip to content

Commit c0605cc

Browse files
authored
Python: Update kernel function span (#12285)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Closing #12262 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> This PR updates tracing support for function execution across both .NET and Python according to the latest OTel GenAI semantic conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄
1 parent 7295458 commit c0605cc

File tree

8 files changed

+83
-5
lines changed

8 files changed

+83
-5
lines changed

dotnet/src/InternalUtilities/src/Diagnostics/ActivityExtensions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ namespace Microsoft.SemanticKernel.Diagnostics;
1313
[ExcludeFromCodeCoverage]
1414
internal static class ActivityExtensions
1515
{
16+
/// <summary>
17+
/// Starts an activity with the appropriate tags for a kernel function execution.
18+
/// </summary>
19+
public static Activity? StartFunctionActivity(this ActivitySource source, string functionName, string functionDescription)
20+
{
21+
const string OperationName = "execute_tool";
22+
23+
return source.StartActivityWithTags($"{OperationName} {functionName}", [
24+
new KeyValuePair<string, object?>("gen_ai.operation.name", OperationName),
25+
new KeyValuePair<string, object?>("gen_ai.tool.name", functionName),
26+
new KeyValuePair<string, object?>("gen_ai.tool.description", functionDescription)
27+
], ActivityKind.Internal);
28+
}
29+
1630
/// <summary>
1731
/// Starts an activity with the specified name and tags.
1832
/// </summary>

dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ public async Task<FunctionResult> InvokeAsync(
239239
kernel ??= this.Kernel;
240240
Verify.NotNull(kernel);
241241

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

245245
// Ensure arguments are initialized.
@@ -342,7 +342,7 @@ public async IAsyncEnumerable<TResult> InvokeStreamingAsync<TResult>(
342342
kernel ??= this.Kernel;
343343
Verify.NotNull(kernel);
344344

345-
using var activity = s_activitySource.StartActivity(this.Name);
345+
using var activity = s_activitySource.StartFunctionActivity(this.Name, this.Description);
346346
ILogger logger = kernel.LoggerFactory.CreateLogger(this.Name) ?? NullLogger.Instance;
347347

348348
arguments ??= [];

python/semantic_kernel/filters/auto_function_invocation/auto_function_invocation_context.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
if TYPE_CHECKING:
88
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
99
from semantic_kernel.contents.chat_history import ChatHistory
10+
from semantic_kernel.contents.function_call_content import FunctionCallContent
1011
from semantic_kernel.functions.function_result import FunctionResult
1112

1213

@@ -26,6 +27,7 @@ class AutoFunctionInvocationContext(FilterContextBase):
2627
arguments: The arguments used to call the function.
2728
is_streaming: Whether the function is streaming.
2829
chat_history: The chat history or None.
30+
function_call_content: The function call content or None.
2931
function_result: The function result or None.
3032
request_sequence_index: The request sequence index.
3133
function_sequence_index: The function sequence index.
@@ -35,6 +37,7 @@ class AutoFunctionInvocationContext(FilterContextBase):
3537
"""
3638

3739
chat_history: "ChatHistory | None" = None
40+
function_call_content: "FunctionCallContent | None" = None
3841
function_result: "FunctionResult | None" = None
3942
execution_settings: "PromptExecutionSettings | None" = None
4043
request_sequence_index: int = 0

python/semantic_kernel/filters/kernel_filters_extension.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def construct_call_stack(
120120

121121
def _rebuild_auto_function_invocation_context() -> None:
122122
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings # noqa: F401
123+
from semantic_kernel.contents import FunctionCallContent # noqa: F401
123124
from semantic_kernel.contents.chat_history import ChatHistory # noqa: F401
124125
from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import (
125126
AutoFunctionInvocationContext,

python/semantic_kernel/functions/kernel_function.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from semantic_kernel.prompt_template.jinja2_prompt_template import Jinja2PromptTemplate
3232
from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate
3333
from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase
34+
from semantic_kernel.utils.telemetry.model_diagnostics import function_tracer
3435

3536
if TYPE_CHECKING:
3637
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@@ -236,7 +237,7 @@ async def invoke(
236237
_rebuild_function_invocation_context()
237238
function_context = FunctionInvocationContext(function=self, kernel=kernel, arguments=arguments)
238239

239-
with tracer.start_as_current_span(self.fully_qualified_name) as current_span:
240+
with function_tracer.start_as_current_span(tracer, self, metadata) as current_span:
240241
KernelFunctionLogMessages.log_function_invoking(logger, self.fully_qualified_name)
241242
KernelFunctionLogMessages.log_function_arguments(logger, arguments)
242243

@@ -298,7 +299,7 @@ async def invoke_stream(
298299
function=self, kernel=kernel, arguments=arguments, is_streaming=True
299300
)
300301

301-
with tracer.start_as_current_span(self.fully_qualified_name) as current_span:
302+
with function_tracer.start_as_current_span(tracer, self, metadata) as current_span:
302303
KernelFunctionLogMessages.log_function_streaming_invoking(logger, self.fully_qualified_name)
303304
KernelFunctionLogMessages.log_function_arguments(logger, arguments)
304305

python/semantic_kernel/kernel.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ async def invoke_function_call(
418418
arguments=args_cloned,
419419
is_streaming=is_streaming,
420420
chat_history=chat_history,
421+
function_call_content=function_call,
421422
execution_settings=execution_settings,
422423
function_result=FunctionResult(function=function_to_call.metadata, value=None),
423424
function_count=function_call_count or 0,
@@ -450,7 +451,13 @@ async def invoke_function_call(
450451
async def _inner_auto_function_invoke_handler(self, context: AutoFunctionInvocationContext):
451452
"""Inner auto function invocation handler."""
452453
try:
453-
result = await context.function.invoke(context.kernel, context.arguments)
454+
result = await context.function.invoke(
455+
context.kernel,
456+
context.arguments,
457+
metadata=context.function_call_content.metadata | context.function_call_content.to_dict()
458+
if context.function_call_content
459+
else {},
460+
)
454461
if result:
455462
context.function_result = result
456463
except Exception as exc:
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
from typing import TYPE_CHECKING, Any
4+
5+
from opentelemetry import trace
6+
7+
from semantic_kernel.utils.telemetry.model_diagnostics.gen_ai_attributes import (
8+
OPERATION,
9+
TOOL_CALL_ID,
10+
TOOL_DESCRIPTION,
11+
TOOL_NAME,
12+
)
13+
14+
if TYPE_CHECKING:
15+
from semantic_kernel.functions.kernel_function import KernelFunction
16+
17+
18+
# The operation name is defined by OTeL GenAI semantic conventions:
19+
# https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span
20+
OPERATION_NAME = "execute_tool"
21+
22+
23+
def start_as_current_span(
24+
tracer: trace.Tracer,
25+
function: "KernelFunction",
26+
metadata: dict[str, Any] | None = None,
27+
):
28+
"""Starts a span for the given function using the provided tracer.
29+
30+
Args:
31+
tracer (trace.Tracer): The OpenTelemetry tracer to use.
32+
function (KernelFunction): The function for which to start the span.
33+
metadata (dict[str, Any] | None): Optional metadata to include in the span attributes.
34+
35+
Returns:
36+
trace.Span: The started span as a context manager.
37+
"""
38+
attributes = {
39+
OPERATION: OPERATION_NAME,
40+
TOOL_NAME: function.fully_qualified_name,
41+
}
42+
43+
tool_call_id = metadata.get("id", None) if metadata else None
44+
if tool_call_id:
45+
attributes[TOOL_CALL_ID] = tool_call_id
46+
if function.description:
47+
attributes[TOOL_DESCRIPTION] = function.description
48+
49+
return tracer.start_as_current_span(f"{OPERATION_NAME} {function.fully_qualified_name}", attributes=attributes)

python/semantic_kernel/utils/telemetry/model_diagnostics/gen_ai_attributes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
RESPONSE_ID = "gen_ai.response.id"
2626
INPUT_TOKENS = "gen_ai.usage.input_tokens"
2727
OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
28+
TOOL_CALL_ID = "gen_ai.tool.call.id"
29+
TOOL_DESCRIPTION = "gen_ai.tool.description"
30+
TOOL_NAME = "gen_ai.tool.name"
2831
ADDRESS = "server.address"
2932

3033
# Activity events

0 commit comments

Comments
 (0)