diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java index 6d8226879..b842e43d0 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java @@ -50,6 +50,7 @@ import com.microsoft.semantickernel.hooks.PreChatCompletionEvent; import com.microsoft.semantickernel.hooks.PreToolCallEvent; import com.microsoft.semantickernel.implementation.CollectionUtil; +import com.microsoft.semantickernel.implementation.telemetry.ChatCompletionSpan; import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; @@ -69,7 +70,6 @@ import com.microsoft.semantickernel.services.chatcompletion.message.ChatMessageContentType; import com.microsoft.semantickernel.services.chatcompletion.message.ChatMessageImageContent; import com.microsoft.semantickernel.services.openai.OpenAiServiceBuilder; -import io.opentelemetry.api.trace.Span; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -419,26 +419,32 @@ private Mono internalChatMessageContentsAsync( invocationContext))) .getOptions(); - Span span = SemanticKernelTelemetry.startChatCompletionSpan( - getModelId(), - SemanticKernelTelemetry.OPEN_AI_PROVIDER, - options.getMaxTokens(), - options.getTemperature(), - options.getTopP()); - return getClient() - .getChatCompletionsWithResponse(getDeploymentName(), options, - OpenAIRequestSettings.getRequestOptions()) - .flatMap(completionsResult -> { - if (completionsResult.getStatusCode() >= 400) { - SemanticKernelTelemetry.endSpanWithError(span); - return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR, - "Request failed: " + completionsResult.getStatusCode())); - } - SemanticKernelTelemetry.endSpanWithUsage(span, - completionsResult.getValue().getUsage()); + return Mono.deferContextual(contextView -> { + ChatCompletionSpan span = ChatCompletionSpan.startChatCompletionSpan( + SemanticKernelTelemetry.getTelemetry(invocationContext), + contextView, + getModelId(), + SemanticKernelTelemetry.OPEN_AI_PROVIDER, + options.getMaxTokens(), + options.getTemperature(), + options.getTopP()); + + return getClient() + .getChatCompletionsWithResponse(getDeploymentName(), options, + OpenAIRequestSettings.getRequestOptions()) + .contextWrite(span.getReactorContextModifier()) + .flatMap(completionsResult -> { + if (completionsResult.getStatusCode() >= 400) { + return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR, + "Request failed: " + completionsResult.getStatusCode())); + } - return Mono.just(completionsResult.getValue()); - }) + return Mono.just(completionsResult.getValue()); + }) + .doOnError(span::endSpanWithError) + .doOnSuccess(span::endSpanWithUsage) + .doOnTerminate(span::close); + }) .flatMap(completions -> { List responseMessages = completions diff --git a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java index 864c5f712..ec04c568d 100644 --- a/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java +++ b/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java @@ -14,8 +14,6 @@ import com.microsoft.semantickernel.services.StreamingTextContent; import com.microsoft.semantickernel.services.textcompletion.TextContent; import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; -import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; -import io.opentelemetry.api.trace.Span; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -84,23 +82,14 @@ protected Mono> internalCompleteTextAsync( CompletionsOptions completionsOptions = getCompletionsOptions(text, requestSettings); - Span span = SemanticKernelTelemetry.startTextCompletionSpan( - getModelId(), - SemanticKernelTelemetry.OPEN_AI_PROVIDER, - completionsOptions.getMaxTokens(), - completionsOptions.getTemperature(), - completionsOptions.getTopP()); return getClient() .getCompletionsWithResponse(getDeploymentName(), completionsOptions, OpenAIRequestSettings.getRequestOptions()) .flatMap(completionsResult -> { if (completionsResult.getStatusCode() >= 400) { - SemanticKernelTelemetry.endSpanWithError(span); return Mono.error(new AIException(ErrorCodes.SERVICE_ERROR, "Request failed: " + completionsResult.getStatusCode())); } - SemanticKernelTelemetry.endSpanWithUsage(span, - completionsResult.getValue().getUsage()); return Mono.just(completionsResult.getValue()); }) .map(completions -> { diff --git a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java index c8bde155b..c9136c554 100644 --- a/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java +++ b/aiservices/openai/src/test/java/com/microsoft/semantickernel/aiservices/openai/OtelCaptureTest.java @@ -75,57 +75,6 @@ public static void shutdown() { otel.shutdown(); } - @Test - public void otelTextCaptureTest() { - - OpenAIAsyncClient openAIAsyncClient = Mockito.mock(OpenAIAsyncClient.class); - - CompletionsUsage completionsUsage = Mockito.mock(CompletionsUsage.class); - Mockito.when(completionsUsage.getCompletionTokens()).thenReturn(22); - Mockito.when(completionsUsage.getPromptTokens()).thenReturn(55); - - Completions completions = Mockito.mock(Completions.class); - Mockito.when(completions.getUsage()).thenReturn(completionsUsage); - - Response response = Mockito.mock(Response.class); - Mockito.when(response.getStatusCode()).thenReturn(200); - Mockito.when(response.getValue()).thenReturn(completions); - - Mockito.when(openAIAsyncClient.getCompletionsWithResponse( - Mockito.any(), - Mockito.any(), - Mockito.any())).thenAnswer(invocation -> Mono.just(response)); - - TextGenerationService client = OpenAITextGenerationService.builder() - .withOpenAIAsyncClient(openAIAsyncClient) - .withModelId("a-model") - .build(); - - try { - client.getTextContentsAsync( - "foo", - null, - null).block(); - } catch (Exception e) { - // Expect to fail - } - - Assertions.assertFalse(spans.isEmpty()); - Assertions.assertEquals("a-model", - spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.request.model"))); - Assertions.assertEquals("text.completions", - spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name"))); - Assertions.assertEquals("openai", - spans.get(0).getAttributes().get(AttributeKey.stringKey("gen_ai.system"))); - Assertions.assertEquals(22, - spans.get(0).getAttributes() - .get(AttributeKey.longKey("gen_ai.response.completion_tokens"))); - Assertions.assertEquals(55, - spans.get(0).getAttributes() - .get(AttributeKey.longKey("gen_ai.response.prompt_tokens"))); - - } - @Test public void otelChatCaptureTest() { OpenAIAsyncClient openAIAsyncClient = Mockito.mock(OpenAIAsyncClient.class); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml b/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml index 51be5816a..4411858dc 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml @@ -26,6 +26,11 @@ + + io.opentelemetry.instrumentation + opentelemetry-reactor-3.1 + 2.9.0-alpha + com.microsoft.semantic-kernel semantickernel-api @@ -165,6 +170,7 @@ com.microsoft.semantickernel.samples.syntaxexamples.${sample} + false diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionTelemetry_Example.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionTelemetry_Example.java new file mode 100644 index 000000000..1d228250c --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/FunctionTelemetry_Example.java @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.java; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.exceptions.ConfigurationException; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.InvocationReturnMode; +import com.microsoft.semantickernel.orchestration.ToolCallBehavior; +import com.microsoft.semantickernel.plugin.KernelPluginFactory; +import com.microsoft.semantickernel.samples.syntaxexamples.functions.Example59_OpenAIFunctionCalling.PetPlugin; +import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; +import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; +import com.microsoft.semantickernel.services.ServiceNotFoundException; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import reactor.core.publisher.Mono; + +public class FunctionTelemetry_Example { + /* + * // Get the Application Insights agent from + * https://github.com/microsoft/ApplicationInsights-Java, e.g: + * ``` + * wget -O "/tmp/applicationinsights-agent-3.6.1.jar" + * "https://github.com/microsoft/ApplicationInsights-Java/releases/download/3.6.1/applicationinsights-agent-3.6.1.jar" + * ``` + * + * // Get your application insights connection string from the Azure portal + * ``` + * CLIENT_ENDPOINT="" \ + * AZURE_CLIENT_KEY="" \ + * APPLICATIONINSIGHTS_CONNECTION_STRING="" \ + * MAVEN_OPTS="-javaagent:/tmp/applicationinsights-agent-3.6.1.jar" \ + * ../../../mvnw package exec:java -Dsample="java.FunctionTelemetry_Example" + * ``` + * + * If you open the Application Insights "Live metrics" view while running this example, you + * should see the telemetry in real-time. + * Otherwise within a few minutes, you should see the telemetry in the Application Insights -> + * Investigate -> Transaction search ui in the Azure portal. + */ + + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = "gpt-4o"; + + public static void main(String[] args) + throws ConfigurationException, IOException, NoSuchMethodException, InterruptedException { + requestsWithSpanContext(); + testNestedCalls(); + requestsWithScope(); + + Thread.sleep(1000); + } + + private static void requestsWithSpanContext() throws IOException { + Span fakeRequest = GlobalOpenTelemetry.getTracer("Custom") + .spanBuilder("GET /requestsWithSpanContext") + .setSpanKind(SpanKind.SERVER) + .setAttribute("http.request.method", "GET") + .setAttribute("url.path", "/requestsWithSpanContext") + .setAttribute("url.scheme", "http") + .startSpan(); + + // Pass span context to the telemetry object to correlate telemetry with the request + SemanticKernelTelemetry telemetry = new SemanticKernelTelemetry( + GlobalOpenTelemetry.getTracer("Custom"), + fakeRequest.getSpanContext()); + + sequentialFunctionCalls(telemetry); + + fakeRequest.setStatus(StatusCode.OK); + fakeRequest.end(); + } + + private static void requestsWithScope() throws IOException { + Span fakeRequest = GlobalOpenTelemetry.getTracer("Custom") + .spanBuilder("GET /requestsWithScope") + .setSpanKind(SpanKind.SERVER) + .setAttribute("http.request.method", "GET") + .setAttribute("url.path", "/requestsWithScope") + .setAttribute("url.scheme", "http") + .startSpan(); + + // Pass span context to the telemetry object to correlate telemetry with the request + SemanticKernelTelemetry telemetry = new SemanticKernelTelemetry(); + + try (Scope scope = fakeRequest.makeCurrent()) { + sequentialFunctionCalls(telemetry); + } + + fakeRequest.setStatus(StatusCode.OK); + fakeRequest.end(); + } + + public static void sequentialFunctionCalls(SemanticKernelTelemetry telemetry) { + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + ChatCompletionService chat = OpenAIChatCompletion.builder() + .withModelId(MODEL_ID) + .withOpenAIAsyncClient(client) + .build(); + + var plugin = KernelPluginFactory.createFromObject(new PetPlugin(), "PetPlugin"); + + var kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chat) + .withPlugin(plugin) + .build(); + + var chatHistory = new ChatHistory(); + chatHistory.addUserMessage( + "What is the name and type of the pet with id ca2fc6bc-1307-4da6-a009-d7bf88dec37b?"); + + var messages = chat.getChatMessageContentsAsync( + chatHistory, + kernel, + InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.FULL_HISTORY) + .withTelemetry(telemetry) + .build()) + .block(); + + chatHistory = new ChatHistory(messages); + + System.out.println( + "THE NAME AND TYPE IS: " + chatHistory.getLastMessage().get().getContent()); + } + + public static void testNestedCalls() { + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + ChatCompletionService chat = OpenAIChatCompletion.builder() + .withModelId(MODEL_ID) + .withOpenAIAsyncClient(client) + .build(); + + var plugin = KernelPluginFactory.createFromObject(new TextAnalysisPlugin(), + "TextAnalysisPlugin"); + + var kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chat) + .withPlugin(plugin) + .build(); + + SemanticKernelTelemetry telemetry = new SemanticKernelTelemetry(); + + Span span = GlobalOpenTelemetry.getTracer("Test") + .spanBuilder("testNestedCalls span") + .setSpanKind(SpanKind.SERVER) + .startSpan(); + + try (Scope scope = span.makeCurrent()) { + String analysed = kernel + .invokePromptAsync( + """ + Analyse the following text: + Hello There + """, + KernelFunctionArguments.builder().build(), + InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY) + .withTelemetry(telemetry) + .build()) + .withResultType(String.class) + .map(result -> { + return result.getResult(); + }) + .block(); + System.out.println(analysed); + } finally { + span.end(); + } + + } + + public static class TextAnalysisPlugin { + + @DefineKernelFunction(description = "Change all string chars to uppercase.", name = "Uppercase") + public String uppercase( + @KernelFunctionParameter(description = "Text to uppercase", name = "input") String text) { + return text.toUpperCase(Locale.ROOT); + } + + @DefineKernelFunction(name = "sha256sum", description = "Calculates a sha256 of the input", returnType = "string") + public Mono sha256sum( + @KernelFunctionParameter(name = "input", description = "The input to checksum", type = String.class) String input, + Kernel kernel, + SemanticKernelTelemetry telemetry) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + String hashStr = new BigInteger(1, hash).toString(16); + + return kernel + .invokePromptAsync( + """ + Uppercase the following text: + === BEGIN TEXT === + %s + === END TEXT === + """.formatted(hashStr) + .stripIndent(), + null, + InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY) + .withTelemetry(telemetry) + .build()) + .withResultType(String.class) + .map(result -> { + return result.getResult(); + }); + } + + @DefineKernelFunction(name = "formatAnswer", description = "Formats an answer", returnType = "string") + public Mono formatAnswer( + @KernelFunctionParameter(name = "input", description = "The input to format", type = String.class) String input, + Kernel kernel, + SemanticKernelTelemetry telemetry) throws ServiceNotFoundException { + + return kernel + .invokePromptAsync( + """ + Translate the following text into Italian: + === BEGIN TEXT === + %s + === END TEXT === + """.formatted(input) + .stripIndent()) + .withResultType(String.class) + .map(result -> { + return result.getResult(); + }); + } + + @DefineKernelFunction(name = "analyseInput", description = "Gives a text analysis of the input", returnType = "string") + public Mono analyseInput( + @KernelFunctionParameter(name = "input", description = "The input to analyse", type = String.class) String input, + Kernel kernel, + SemanticKernelTelemetry telemetry) throws ServiceNotFoundException { + + return kernel + .invokePromptAsync( + """ + Calculating sha256sum of the following text: + === BEGIN TEXT === + %s + === END TEXT === + """.formatted(input) + .stripIndent(), + null, + InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY) + .withTelemetry(telemetry) + .build()) + .withResultType(String.class) + .map(result -> { + return result.getResult(); + }) + .flatMap(answer -> { + return kernel + .invokePromptAsync( + """ + Format the following text: + === BEGIN TEXT === + %s + === END TEXT === + """.formatted(answer) + .stripIndent()) + .withInvocationContext( + InvocationContext.builder() + .withToolCallBehavior( + ToolCallBehavior.allowAllKernelFunctions(true)) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY) + .withTelemetry(telemetry) + .build()) + .withArguments(null) + .withTelemetry(telemetry) + .withResultType(String.class); + }) + .map(it -> { + return it.getResult(); + }); + } + + } + +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/KernelFunctionYaml_Example.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/KernelFunctionYaml_Example.java index 900513c84..ec38aae05 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/KernelFunctionYaml_Example.java +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/KernelFunctionYaml_Example.java @@ -10,13 +10,14 @@ import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; import com.microsoft.semantickernel.exceptions.ConfigurationException; import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionYaml; import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; -import com.microsoft.semantickernel.services.textcompletion.TextGenerationService; import java.io.IOException; +import javax.annotation.Nullable; public class KernelFunctionYaml_Example { @@ -29,7 +30,10 @@ public class KernelFunctionYaml_Example { .getOrDefault("MODEL_ID", "gpt-35-turbo"); public static void main(String[] args) throws ConfigurationException, IOException { + run(null); + } + public static void run(@Nullable SemanticKernelTelemetry telemetry) throws IOException { OpenAIAsyncClient client; if (AZURE_CLIENT_KEY != null) { @@ -51,12 +55,13 @@ public static void main(String[] args) throws ConfigurationException, IOExceptio Builder kernelBuilder = Kernel.builder() .withAIService(ChatCompletionService.class, openAIChatCompletion); - semanticKernelTemplate(kernelBuilder.build()); - handlebarsTemplate(kernelBuilder.build()); - + semanticKernelTemplate(kernelBuilder.build(), telemetry); + handlebarsTemplate(kernelBuilder.build(), telemetry); } - private static void handlebarsTemplate(Kernel kernel) throws IOException { + private static void handlebarsTemplate(Kernel kernel, + @Nullable SemanticKernelTelemetry telemetry) + throws IOException { String yaml = EmbeddedResourceLoader.readFile("GenerateStoryHandlebars.yaml", KernelFunctionYaml_Example.class); @@ -69,12 +74,15 @@ private static void handlebarsTemplate(Kernel kernel) throws IOException { .withVariable("length", 5) .withVariable("topic", "dogs") .build()) + .withTelemetry(telemetry) .block(); System.out.println(result.getResult()); } - private static void semanticKernelTemplate(Kernel kernel) throws IOException { + private static void semanticKernelTemplate(Kernel kernel, + @Nullable SemanticKernelTelemetry telemetry) + throws IOException { String yaml = EmbeddedResourceLoader.readFile("GenerateStory.yaml", KernelFunctionYaml_Example.class); @@ -87,6 +95,7 @@ private static void semanticKernelTemplate(Kernel kernel) throws IOException { .withVariable("length", 5) .withVariable("topic", "cats") .build()) + .withTelemetry(telemetry) .block(); System.out.println(result.getResult()); diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/java/applicationinsights.json b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/java/applicationinsights.json new file mode 100644 index 000000000..a5c0a2275 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/resources/com/microsoft/semantickernel/samples/syntaxexamples/java/applicationinsights.json @@ -0,0 +1,16 @@ +{ + "connectionString": "InstrumentationKey=00000000-0000-0000-0000-0000-000000000000", + "preview": { + "processors": [ + { + "type": "span", + "include": { + "matchType": "regexp", + "spanNames": [ + ".*" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/semantickernel-api/pom.xml b/semantickernel-api/pom.xml index d67acb889..2846ea9db 100644 --- a/semantickernel-api/pom.xml +++ b/semantickernel-api/pom.xml @@ -1,6 +1,7 @@ - + 4.0.0 @@ -15,6 +16,11 @@ Semantic Kernel API Defines the public interface for the Semantic Kernel + + io.opentelemetry.instrumentation + opentelemetry-reactor-3.1 + 2.9.0-alpha + com.azure azure-ai-openai diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/ChatCompletionSpan.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/ChatCompletionSpan.java new file mode 100644 index 000000000..87945860a --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/ChatCompletionSpan.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.implementation.telemetry; + +import com.azure.ai.openai.models.ChatCompletions; +import com.azure.ai.openai.models.CompletionsUsage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import java.util.function.Function; +import javax.annotation.Nullable; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +public class ChatCompletionSpan extends SemanticKernelTelemetrySpan { + + public ChatCompletionSpan( + Span span, + Function reactorContextModifier, + Scope spanScope, + Scope contextScope) { + super(span, reactorContextModifier, spanScope, contextScope); + } + + public static ChatCompletionSpan startChatCompletionSpan( + SemanticKernelTelemetry telemetry, + ContextView contextView, + @Nullable String modelName, + String modelProvider, + @Nullable Integer maxTokens, + @Nullable Double temperature, + @Nullable Double topP) { + return startCompletionSpan( + telemetry, + contextView, + "chat.completions", + modelName, + modelProvider, + maxTokens, + temperature, topP); + } + + public ChatCompletionSpan startTextCompletionSpan( + SemanticKernelTelemetry telemetry, + ContextView contextView, + @Nullable String modelName, + String modelProvider, + @Nullable Integer maxTokens, + @Nullable Double temperature, + @Nullable Double topP) { + return startCompletionSpan( + telemetry, + contextView, + "text.completions", + modelName, + modelProvider, + maxTokens, + temperature, topP); + } + + public static ChatCompletionSpan startCompletionSpan( + SemanticKernelTelemetry telemetry, + ContextView contextView, + String operationName, + @Nullable String modelName, + String modelProvider, + @Nullable Integer maxTokens, + @Nullable Double temperature, + @Nullable Double topP) { + if (modelName == null) { + modelName = "unknown"; + } + + SpanBuilder builder = telemetry.spanBuilder(operationName + " " + modelName) + .setSpanKind(SpanKind.CLIENT) + .setAttribute("gen_ai.request.model", modelName) + .setAttribute("gen_ai.operation.name", operationName) + .setAttribute("gen_ai.system", modelProvider); + + if (maxTokens != null) { + builder.setAttribute("gen_ai.request.max_tokens", maxTokens); + } + if (temperature != null) { + builder.setAttribute("gen_ai.request.temperature", temperature); + } + if (topP != null) { + builder.setAttribute("gen_ai.request.top_p", topP); + } + + Span span = builder.startSpan(); + + return build( + span, + contextView, + (contextModifier, spanScope, contextScope) -> new ChatCompletionSpan( + span, + contextModifier, + spanScope, + contextScope)); + } + + public void endSpanWithUsage(ChatCompletions chatCompletions) { + CompletionsUsage usage = chatCompletions.getUsage(); + getSpan().setStatus(StatusCode.OK); + getSpan() + .setAttribute("gen_ai.response.completion_tokens", usage.getCompletionTokens()); + getSpan().setAttribute("gen_ai.response.prompt_tokens", usage.getPromptTokens()); + close(); + } + + public void endSpanWithError(Throwable throwable) { + getSpan().setStatus(StatusCode.ERROR, throwable.getMessage()); + close(); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/FunctionSpan.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/FunctionSpan.java new file mode 100644 index 000000000..601b41305 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/FunctionSpan.java @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.implementation.telemetry; + +import com.microsoft.semantickernel.orchestration.FunctionResult; +import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; +import java.util.function.Function; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +public class FunctionSpan extends SemanticKernelTelemetrySpan { + + public FunctionSpan( + Span span, + Function reactorContextModifier, + Scope spanScope, + Scope contextScope) { + super(span, reactorContextModifier, spanScope, contextScope); + } + + public static FunctionSpan build( + SemanticKernelTelemetry telemetry, + ContextView contextView, + String pluginName, + String name, + KernelFunctionArguments arguments) { + + SpanBuilder builder = telemetry.spanBuilder( + String.format("function_invocation %s-%s", pluginName, name)) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("semantic_kernel.function.invocation.name", name) + .setAttribute("semantic_kernel.function.invocation.plugin_name", pluginName); + + Span span = builder.startSpan(); + + return build( + span, + contextView, + (contextModifier, spanScope, contextScope) -> new FunctionSpan( + span, + contextModifier, + spanScope, + contextScope)); + } + + public void onFunctionSuccess(FunctionResult result) { + try { + getSpan().setStatus(StatusCode.OK); + } finally { + close(); + } + } + + public void onFunctionError(Throwable error) { + try { + getSpan().setStatus(StatusCode.ERROR, error.getMessage()); + getSpan().recordException(error); + } finally { + close(); + } + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetry.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetry.java index 2ba4b8467..4121881d8 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetry.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetry.java @@ -1,79 +1,54 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.implementation.telemetry; -import com.azure.ai.openai.models.CompletionsUsage; +import com.microsoft.semantickernel.orchestration.InvocationContext; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.Tracer; import javax.annotation.Nullable; public class SemanticKernelTelemetry { public static final String OPEN_AI_PROVIDER = "openai"; - public static Span startChatCompletionSpan( - @Nullable String modelName, - String modelProvider, - @Nullable Integer maxTokens, - @Nullable Double temperature, - @Nullable Double topP) { - return startCompletionSpan("chat.completions", modelName, modelProvider, maxTokens, - temperature, topP); - } + private final Tracer tracer; - public static Span startTextCompletionSpan( - @Nullable String modelName, - String modelProvider, - @Nullable Integer maxTokens, - @Nullable Double temperature, - @Nullable Double topP) { - return startCompletionSpan("text.completions", modelName, modelProvider, maxTokens, - temperature, topP); - } + @Nullable + private final SpanContext spanContext; - private static Span startCompletionSpan( - String operationName, - @Nullable String modelName, - String modelProvider, - @Nullable Integer maxTokens, - @Nullable Double temperature, - @Nullable Double topP) { - OpenTelemetry otel = GlobalOpenTelemetry.get(); + public SemanticKernelTelemetry( + Tracer tracer, + @Nullable SpanContext spanContext) { - if (modelName == null) { - modelName = "unknown"; - } - SpanBuilder builder = otel - .getTracer("SemanticKernel") - .spanBuilder(operationName + " " + modelName) - .setAttribute("gen_ai.request.model", modelName) - .setAttribute("gen_ai.operation.name", operationName) - .setAttribute("gen_ai.system", modelProvider); + this.tracer = tracer; + this.spanContext = spanContext; + } - if (maxTokens != null) { - builder.setAttribute("gen_ai.request.max_tokens", maxTokens); - } - if (temperature != null) { - builder.setAttribute("gen_ai.request.temperature", temperature); - } - if (topP != null) { - builder.setAttribute("gen_ai.request.top_p", topP); - } + public SemanticKernelTelemetry() { + this( + GlobalOpenTelemetry.getTracer("SemanticKernel"), + null); + } - return builder.startSpan(); + public static SemanticKernelTelemetry getTelemetry( + @Nullable InvocationContext invocationContext) { + if (invocationContext != null) { + return invocationContext.getTelemetry(); + } + return new SemanticKernelTelemetry(); } - public static void endSpanWithUsage(Span span, CompletionsUsage usage) { - span.setStatus(StatusCode.OK); - span.setAttribute("gen_ai.response.completion_tokens", usage.getCompletionTokens()); - span.setAttribute("gen_ai.response.prompt_tokens", usage.getPromptTokens()); - span.end(); + private Tracer getTracer() { + return tracer; } - public static void endSpanWithError(Span span) { - span.setStatus(StatusCode.ERROR); - span.end(); + public SpanBuilder spanBuilder(String operationName) { + SpanBuilder sb = tracer.spanBuilder(operationName); + + if (spanContext != null) { + sb.addLink(spanContext); + } + return sb; } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetrySpan.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetrySpan.java new file mode 100644 index 000000000..2ef6413ce --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/telemetry/SemanticKernelTelemetrySpan.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.implementation.telemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +import java.io.Closeable; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.slf4j.Logger; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +public abstract class SemanticKernelTelemetrySpan implements Closeable { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger( + SemanticKernelTelemetrySpan.class); + + private static final long SPAN_TIMEOUT_MS = Long.parseLong((String) System.getProperties() + .getOrDefault("semantickernel.telemetry.span_timeout", "120000")); + + private final Span span; + private final Function reactorContextModifier; + private final Scope spanScope; + private final Scope contextScope; + private final AtomicBoolean closed = new AtomicBoolean(false); + + // Timeout to close the span if it was not closed within the specified time to avoid memory leaks + private final Disposable watchdog; + + // This is a finalizer guardian to ensure that the span is closed if it was not closed explicitly + @SuppressWarnings("unused") + private final Object finalizerGuardian = new Object() { + @Override + protected void finalize() { + if (closed.get() == false) { + LOGGER.warn("Span was not closed"); + close(); + } + } + }; + + public SemanticKernelTelemetrySpan(Span span, + Function reactorContextModifier, + Scope spanScope, Scope contextScope) { + this.span = span; + this.reactorContextModifier = reactorContextModifier; + this.spanScope = spanScope; + this.contextScope = contextScope; + + watchdog = Mono.just(1) + .delay(Duration.ofMillis(SPAN_TIMEOUT_MS)) + .subscribe(i -> { + if (closed.get() == false) { + LOGGER.warn("Span was not closed, timing out"); + close(); + } + }); + } + + public interface SpanConstructor { + + public T build( + Function contextModifier, + Scope spanScope, + Scope contextScope); + } + + // Does need to be closed but as we are doing this in a reactive app, cant enforce the try with resources + @SuppressWarnings("MustBeClosedChecker") + public static T build( + Span span, + ContextView contextView, + SpanConstructor builder) { + LOGGER.trace("Starting Span: {}", span); + + Context currentOtelContext = ContextPropagationOperator + .getOpenTelemetryContextFromContextView( + contextView, + Context.current()); + + Context otelContext = span.storeInContext(currentOtelContext); + Scope contextScope = otelContext.makeCurrent(); + Scope spanScope = span.makeCurrent(); + + Function reactorContextModifier = ctx -> { + return ContextPropagationOperator.storeOpenTelemetryContext(ctx, otelContext); + }; + + return builder.build(reactorContextModifier, spanScope, contextScope); + } + + public Function getReactorContextModifier() { + return reactorContextModifier; + } + + public void close() { + if (closed.compareAndSet(false, true)) { + LOGGER.trace("Closing span: {}", span); + if (span.isRecording()) { + try { + span.end(); + } catch (Exception e) { + LOGGER.error("Error closing span", e); + } + } + if (contextScope != null) { + try { + contextScope.close(); + } catch (Exception e) { + LOGGER.error("Error closing context scope", e); + } + } + if (spanScope != null) { + try { + spanScope.close(); + } catch (Exception e) { + LOGGER.error("Error closing span scope", e); + } + } + watchdog.dispose(); + } + } + + public Span getSpan() { + return span; + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java index 630ce2864..435d8538f 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java @@ -11,6 +11,7 @@ import com.microsoft.semantickernel.hooks.KernelHook; import com.microsoft.semantickernel.hooks.KernelHooks; import com.microsoft.semantickernel.hooks.KernelHooks.UnmodifiableKernelHooks; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; @@ -47,6 +48,8 @@ public class FunctionInvocation extends Mono> { protected PromptExecutionSettings promptExecutionSettings; @Nullable protected ToolCallBehavior toolCallBehavior; + @Nullable + protected SemanticKernelTelemetry telemetry; private boolean isSubscribed = false; @@ -312,6 +315,17 @@ public FunctionInvocation withTypes(ContextVariableTypes contextVariableTypes return this; } + /** + * Supply a tracer to the function invocation. + * + * @param tracer The tracer to supply to the function invocation. + * @return this {@code FunctionInvocation} for fluent chaining. + */ + public FunctionInvocation withTelemetry(SemanticKernelTelemetry telemetry) { + this.telemetry = telemetry; + return this; + } + /** * Use an invocation context variable to supply the types, tool call behavior, prompt execution * settings, and kernel hooks to the function invocation. @@ -329,6 +343,7 @@ public FunctionInvocation withInvocationContext( withToolCallBehavior(invocationContext.getToolCallBehavior()); withPromptExecutionSettings(invocationContext.getPromptExecutionSettings()); addKernelHooks(invocationContext.getKernelHooks()); + withTelemetry(invocationContext.getTelemetry()); return this; } @@ -356,6 +371,10 @@ public void subscribe(CoreSubscriber> coreSubscriber) function.getPluginName(), function.getName()); } + if (telemetry == null) { + telemetry = new SemanticKernelTelemetry(); + } + isSubscribed = true; performSubscribe( @@ -369,7 +388,8 @@ public void subscribe(CoreSubscriber> coreSubscriber) promptExecutionSettings, toolCallBehavior, contextVariableTypes, - InvocationReturnMode.NEW_MESSAGES_ONLY)); + InvocationReturnMode.NEW_MESSAGES_ONLY, + telemetry)); } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationContext.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationContext.java index 3f1ba7e01..6fd3f0d21 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationContext.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/InvocationContext.java @@ -6,6 +6,7 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; import com.microsoft.semantickernel.hooks.KernelHooks; import com.microsoft.semantickernel.hooks.KernelHooks.UnmodifiableKernelHooks; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nullable; @@ -24,6 +25,7 @@ public class InvocationContext { private final ToolCallBehavior toolCallBehavior; private final ContextVariableTypes contextVariableTypes; private final InvocationReturnMode invocationReturnMode; + private final SemanticKernelTelemetry telemetry; /** * Create a new instance of InvocationContext. @@ -38,7 +40,8 @@ protected InvocationContext( @Nullable PromptExecutionSettings promptExecutionSettings, @Nullable ToolCallBehavior toolCallBehavior, @Nullable ContextVariableTypes contextVariableTypes, - InvocationReturnMode invocationReturnMode) { + InvocationReturnMode invocationReturnMode, + SemanticKernelTelemetry telemetry) { this.hooks = unmodifiableClone(hooks); this.promptExecutionSettings = promptExecutionSettings; this.toolCallBehavior = toolCallBehavior; @@ -48,6 +51,7 @@ protected InvocationContext( } else { this.contextVariableTypes = new ContextVariableTypes(contextVariableTypes); } + this.telemetry = telemetry; } /** @@ -59,6 +63,7 @@ protected InvocationContext() { this.toolCallBehavior = null; this.contextVariableTypes = new ContextVariableTypes(); this.invocationReturnMode = InvocationReturnMode.NEW_MESSAGES_ONLY; + this.telemetry = null; } /** @@ -73,12 +78,14 @@ protected InvocationContext(@Nullable InvocationContext context) { this.toolCallBehavior = null; this.contextVariableTypes = new ContextVariableTypes(); this.invocationReturnMode = InvocationReturnMode.NEW_MESSAGES_ONLY; + this.telemetry = null; } else { this.hooks = context.hooks; this.promptExecutionSettings = context.promptExecutionSettings; this.toolCallBehavior = context.toolCallBehavior; this.contextVariableTypes = context.contextVariableTypes; this.invocationReturnMode = context.invocationReturnMode; + this.telemetry = context.telemetry; } } @@ -114,7 +121,8 @@ public static Builder copy(InvocationContext context) { .withKernelHooks(context.getKernelHooks()) .withContextVariableConverter(context.contextVariableTypes) .withPromptExecutionSettings(context.getPromptExecutionSettings()) - .withToolCallBehavior(context.getToolCallBehavior()); + .withToolCallBehavior(context.getToolCallBehavior()) + .withTelemetry(context.getTelemetry()); } /** @@ -166,6 +174,10 @@ public InvocationReturnMode returnMode() { return invocationReturnMode; } + public SemanticKernelTelemetry getTelemetry() { + return telemetry; + } + /** * Builder for {@link InvocationContext}. */ @@ -179,6 +191,8 @@ public static class Builder implements SemanticKernelBuilder @Nullable private ToolCallBehavior toolCallBehavior; private InvocationReturnMode invocationReturnMode = InvocationReturnMode.NEW_MESSAGES_ONLY; + @Nullable + private SemanticKernelTelemetry telemetry; /** * Add kernel hooks to the builder. @@ -252,10 +266,24 @@ public Builder withReturnMode(InvocationReturnMode invocationReturnMode) { return this; } + /** + * Add a tracer to the builder. + * + * @param tracer the tracer to add. + * @return this {@link Builder} + */ + public Builder withTelemetry(@Nullable SemanticKernelTelemetry telemetry) { + this.telemetry = telemetry; + return this; + } + @Override public InvocationContext build() { + if (telemetry == null) { + telemetry = new SemanticKernelTelemetry(); + } return new InvocationContext(hooks, promptExecutionSettings, toolCallBehavior, - contextVariableTypes, invocationReturnMode); + contextVariableTypes, invocationReturnMode, telemetry); } } diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java index 680d93c0b..7173a7d47 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java @@ -14,6 +14,8 @@ import com.microsoft.semantickernel.hooks.FunctionInvokedEvent; import com.microsoft.semantickernel.hooks.FunctionInvokingEvent; import com.microsoft.semantickernel.hooks.KernelHooks; +import com.microsoft.semantickernel.implementation.telemetry.FunctionSpan; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.InvocationContext; @@ -150,9 +152,10 @@ private static MethodDetails getMethodDetails( /** * Gets the function from the method. - * @param method the method to invoke + * + * @param method the method to invoke * @param instance the instance to invoke the method on - * @param the return type of the function + * @param the return type of the function * @return the function representing the method */ @SuppressWarnings("unchecked") @@ -367,6 +370,8 @@ private static Object getArgumentValue( if (Kernel.class.isAssignableFrom(targetArgType)) { return kernel; + } else if (SemanticKernelTelemetry.class.isAssignableFrom(targetArgType)) { + return invocationContext.getTelemetry(); } String variableName = getGetVariableName(parameter); @@ -692,6 +697,7 @@ private static InputVariable toKernelParameterMetadata(Parameter parameter) { /** * Gets the constants from an enum type. + * * @param type the type to get the enum constants from * @return a list of the enum constants or {@code null} if the type is not an enum */ @@ -726,11 +732,27 @@ public Mono> invokeAsync( @Nullable KernelFunctionArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext invocationContext) { - return function.invokeAsync(kernel, this, arguments, variableType, invocationContext); + + return Mono.deferContextual(contextView -> { + FunctionSpan span = FunctionSpan.build( + SemanticKernelTelemetry.getTelemetry(invocationContext), + contextView, + this.getPluginName(), + this.getName(), + arguments); + + return function + .invokeAsync(kernel, this, arguments, variableType, invocationContext) + .contextWrite(span.getReactorContextModifier()) + .doOnSuccess(span::onFunctionSuccess) + .doOnError(span::onFunctionError) + .doOnTerminate(span::close); + }); } /** * Concrete implementation of the abstract method in KernelFunction. + * * @param the return type of the function */ public interface ImplementationFunc { @@ -775,6 +797,7 @@ default FunctionResult invoke( /** * A builder for {@link KernelFunction}. + * * @param the return type of the function */ public static class Builder { diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java index adcea185b..5c2c5412e 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java @@ -10,6 +10,8 @@ import com.microsoft.semantickernel.hooks.KernelHooks; import com.microsoft.semantickernel.hooks.PromptRenderedEvent; import com.microsoft.semantickernel.hooks.PromptRenderingEvent; +import com.microsoft.semantickernel.implementation.telemetry.FunctionSpan; +import com.microsoft.semantickernel.implementation.telemetry.SemanticKernelTelemetry; import com.microsoft.semantickernel.localization.SemanticKernelResources; import com.microsoft.semantickernel.orchestration.FunctionResult; import com.microsoft.semantickernel.orchestration.InvocationContext; @@ -274,8 +276,22 @@ public Mono> invokeAsync( @Nullable KernelFunctionArguments arguments, @Nullable ContextVariableType variableType, @Nullable InvocationContext invocationContext) { - return invokeInternalAsync(kernel, arguments, variableType, invocationContext) - .takeLast(1).single(); + return Mono.deferContextual(contextView -> { + FunctionSpan span = FunctionSpan.build( + SemanticKernelTelemetry.getTelemetry(invocationContext), + contextView, + this.getPluginName(), + this.getName(), + arguments); + + return invokeInternalAsync(kernel, arguments, variableType, invocationContext) + .contextWrite(span.getReactorContextModifier()) + .takeLast(1) + .single() + .doOnSuccess(span::onFunctionSuccess) + .doOnError(span::onFunctionError) + .doOnTerminate(span::close); + }); } /**