From 22204aaf8a5897c9566fe9e459e0c2ff89165fda Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Sun, 23 Jun 2024 16:09:35 -0700 Subject: [PATCH 01/26] loggin --- python/poetry.lock | 98 ++++++------ python/pyproject.toml | 2 + .../services/open_ai_chat_completion_base.py | 36 ++++- .../utils/model_diagnostics.py | 151 ++++++++++++++++++ 4 files changed, 236 insertions(+), 51 deletions(-) create mode 100644 python/semantic_kernel/utils/model_diagnostics.py diff --git a/python/poetry.lock b/python/poetry.lock index b7a889b48a9d..06db9d210ba1 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohttp" @@ -3383,42 +3383,42 @@ openapi-schema-validator = ">=0.6.0,<0.7.0" [[package]] name = "opentelemetry-api" -version = "1.24.0" +version = "1.25.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_api-1.24.0-py3-none-any.whl", hash = "sha256:0f2c363d98d10d1ce93330015ca7fd3a65f60be64e05e30f557c61de52c80ca2"}, - {file = "opentelemetry_api-1.24.0.tar.gz", hash = "sha256:42719f10ce7b5a9a73b10a4baf620574fb8ad495a9cbe5c18d76b75d8689c67e"}, + {file = "opentelemetry_api-1.25.0-py3-none-any.whl", hash = "sha256:757fa1aa020a0f8fa139f8959e53dec2051cc26b832e76fa839a6d76ecefd737"}, + {file = "opentelemetry_api-1.25.0.tar.gz", hash = "sha256:77c4985f62f2614e42ce77ee4c9da5fa5f0bc1e1821085e9a47533a9323ae869"}, ] [package.dependencies] deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<=7.0" +importlib-metadata = ">=6.0,<=7.1" [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.24.0" +version = "1.25.0" description = "OpenTelemetry Protobuf encoding" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.24.0-py3-none-any.whl", hash = "sha256:e51f2c9735054d598ad2df5d3eca830fecfb5b0bda0a2fa742c9c7718e12f641"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.24.0.tar.gz", hash = "sha256:5d31fa1ff976cacc38be1ec4e3279a3f88435c75b38b1f7a099a1faffc302461"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.25.0-py3-none-any.whl", hash = "sha256:15637b7d580c2675f70246563363775b4e6de947871e01d0f4e3881d1848d693"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.25.0.tar.gz", hash = "sha256:c93f4e30da4eee02bacd1e004eb82ce4da143a2f8e15b987a9f603e0a85407d3"}, ] [package.dependencies] -opentelemetry-proto = "1.24.0" +opentelemetry-proto = "1.25.0" [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.24.0" +version = "1.25.0" description = "OpenTelemetry Collector Protobuf over gRPC Exporter" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.24.0-py3-none-any.whl", hash = "sha256:f40d62aa30a0a43cc1657428e59fcf82ad5f7ea8fff75de0f9d9cb6f739e0a3b"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.24.0.tar.gz", hash = "sha256:217c6e30634f2c9797999ea9da29f7300479a94a610139b9df17433f915e7baa"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.25.0-py3-none-any.whl", hash = "sha256:3131028f0c0a155a64c430ca600fd658e8e37043cb13209f0109db5c1a3e4eb4"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.25.0.tar.gz", hash = "sha256:c0b1661415acec5af87625587efa1ccab68b873745ca0ee96b69bb1042087eac"}, ] [package.dependencies] @@ -3426,22 +3426,19 @@ deprecated = ">=1.2.6" googleapis-common-protos = ">=1.52,<2.0" grpcio = ">=1.0.0,<2.0.0" opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.24.0" -opentelemetry-proto = "1.24.0" -opentelemetry-sdk = ">=1.24.0,<1.25.0" - -[package.extras] -test = ["pytest-grpc"] +opentelemetry-exporter-otlp-proto-common = "1.25.0" +opentelemetry-proto = "1.25.0" +opentelemetry-sdk = ">=1.25.0,<1.26.0" [[package]] name = "opentelemetry-instrumentation" -version = "0.45b0" +version = "0.46b0" description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_instrumentation-0.45b0-py3-none-any.whl", hash = "sha256:06c02e2c952c1b076e8eaedf1b82f715e2937ba7eeacab55913dd434fbcec258"}, - {file = "opentelemetry_instrumentation-0.45b0.tar.gz", hash = "sha256:6c47120a7970bbeb458e6a73686ee9ba84b106329a79e4a4a66761f933709c7e"}, + {file = "opentelemetry_instrumentation-0.46b0-py3-none-any.whl", hash = "sha256:89cd721b9c18c014ca848ccd11181e6b3fd3f6c7669e35d59c48dc527408c18b"}, + {file = "opentelemetry_instrumentation-0.46b0.tar.gz", hash = "sha256:974e0888fb2a1e01c38fbacc9483d024bb1132aad92d6d24e2e5543887a7adda"}, ] [package.dependencies] @@ -3451,55 +3448,55 @@ wrapt = ">=1.0.0,<2.0.0" [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.45b0" +version = "0.46b0" description = "ASGI instrumentation for OpenTelemetry" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_instrumentation_asgi-0.45b0-py3-none-any.whl", hash = "sha256:8be1157ed62f0db24e45fdf7933c530c4338bd025c5d4af7830e903c0756021b"}, - {file = "opentelemetry_instrumentation_asgi-0.45b0.tar.gz", hash = "sha256:97f55620f163fd3d20323e9fd8dc3aacc826c03397213ff36b877e0f4b6b08a6"}, + {file = "opentelemetry_instrumentation_asgi-0.46b0-py3-none-any.whl", hash = "sha256:f13c55c852689573057837a9500aeeffc010c4ba59933c322e8f866573374759"}, + {file = "opentelemetry_instrumentation_asgi-0.46b0.tar.gz", hash = "sha256:02559f30cf4b7e2a737ab17eb52aa0779bcf4cc06573064f3e2cb4dcc7d3040a"}, ] [package.dependencies] asgiref = ">=3.0,<4.0" opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.45b0" -opentelemetry-semantic-conventions = "0.45b0" -opentelemetry-util-http = "0.45b0" +opentelemetry-instrumentation = "0.46b0" +opentelemetry-semantic-conventions = "0.46b0" +opentelemetry-util-http = "0.46b0" [package.extras] instruments = ["asgiref (>=3.0,<4.0)"] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.45b0" +version = "0.46b0" description = "OpenTelemetry FastAPI Instrumentation" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_instrumentation_fastapi-0.45b0-py3-none-any.whl", hash = "sha256:77d9c123a363129148f5f66d44094f3d67aaaa2b201396d94782b4a7f9ce4314"}, - {file = "opentelemetry_instrumentation_fastapi-0.45b0.tar.gz", hash = "sha256:5a6b91e1c08a01601845fcfcfdefd0a2aecdb3c356d4a436a3210cb58c21487e"}, + {file = "opentelemetry_instrumentation_fastapi-0.46b0-py3-none-any.whl", hash = "sha256:e0f5d150c6c36833dd011f0e6ef5ede6d7406c1aed0c7c98b2d3b38a018d1b33"}, + {file = "opentelemetry_instrumentation_fastapi-0.46b0.tar.gz", hash = "sha256:928a883a36fc89f9702f15edce43d1a7104da93d740281e32d50ffd03dbb4365"}, ] [package.dependencies] opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.45b0" -opentelemetry-instrumentation-asgi = "0.45b0" -opentelemetry-semantic-conventions = "0.45b0" -opentelemetry-util-http = "0.45b0" +opentelemetry-instrumentation = "0.46b0" +opentelemetry-instrumentation-asgi = "0.46b0" +opentelemetry-semantic-conventions = "0.46b0" +opentelemetry-util-http = "0.46b0" [package.extras] instruments = ["fastapi (>=0.58,<1.0)"] [[package]] name = "opentelemetry-proto" -version = "1.24.0" +version = "1.25.0" description = "OpenTelemetry Python Proto" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_proto-1.24.0-py3-none-any.whl", hash = "sha256:bcb80e1e78a003040db71ccf83f2ad2019273d1e0828089d183b18a1476527ce"}, - {file = "opentelemetry_proto-1.24.0.tar.gz", hash = "sha256:ff551b8ad63c6cabb1845ce217a6709358dfaba0f75ea1fa21a61ceddc78cab8"}, + {file = "opentelemetry_proto-1.25.0-py3-none-any.whl", hash = "sha256:f07e3341c78d835d9b86665903b199893befa5e98866f63d22b00d0b7ca4972f"}, + {file = "opentelemetry_proto-1.25.0.tar.gz", hash = "sha256:35b6ef9dc4a9f7853ecc5006738ad40443701e52c26099e197895cbda8b815a3"}, ] [package.dependencies] @@ -3507,40 +3504,43 @@ protobuf = ">=3.19,<5.0" [[package]] name = "opentelemetry-sdk" -version = "1.24.0" +version = "1.25.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_sdk-1.24.0-py3-none-any.whl", hash = "sha256:fa731e24efe832e98bcd90902085b359dcfef7d9c9c00eb5b9a18587dae3eb59"}, - {file = "opentelemetry_sdk-1.24.0.tar.gz", hash = "sha256:75bc0563affffa827700e0f4f4a68e1e257db0df13372344aebc6f8a64cde2e5"}, + {file = "opentelemetry_sdk-1.25.0-py3-none-any.whl", hash = "sha256:d97ff7ec4b351692e9d5a15af570c693b8715ad78b8aafbec5c7100fe966b4c9"}, + {file = "opentelemetry_sdk-1.25.0.tar.gz", hash = "sha256:ce7fc319c57707ef5bf8b74fb9f8ebdb8bfafbe11898410e0d2a761d08a98ec7"}, ] [package.dependencies] -opentelemetry-api = "1.24.0" -opentelemetry-semantic-conventions = "0.45b0" +opentelemetry-api = "1.25.0" +opentelemetry-semantic-conventions = "0.46b0" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.45b0" +version = "0.46b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_semantic_conventions-0.45b0-py3-none-any.whl", hash = "sha256:a4a6fb9a7bacd9167c082aa4681009e9acdbfa28ffb2387af50c2fef3d30c864"}, - {file = "opentelemetry_semantic_conventions-0.45b0.tar.gz", hash = "sha256:7c84215a44ac846bc4b8e32d5e78935c5c43482e491812a0bb8aaf87e4d92118"}, + {file = "opentelemetry_semantic_conventions-0.46b0-py3-none-any.whl", hash = "sha256:6daef4ef9fa51d51855d9f8e0ccd3a1bd59e0e545abe99ac6203804e36ab3e07"}, + {file = "opentelemetry_semantic_conventions-0.46b0.tar.gz", hash = "sha256:fbc982ecbb6a6e90869b15c1673be90bd18c8a56ff1cffc0864e38e2edffaefa"}, ] +[package.dependencies] +opentelemetry-api = "1.25.0" + [[package]] name = "opentelemetry-util-http" -version = "0.45b0" +version = "0.46b0" description = "Web util for OpenTelemetry" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_util_http-0.45b0-py3-none-any.whl", hash = "sha256:6628868b501b3004e1860f976f410eeb3d3499e009719d818000f24ce17b6e33"}, - {file = "opentelemetry_util_http-0.45b0.tar.gz", hash = "sha256:4ce08b6a7d52dd7c96b7705b5b4f06fdb6aa3eac1233b3b0bfef8a0cab9a92cd"}, + {file = "opentelemetry_util_http-0.46b0-py3-none-any.whl", hash = "sha256:8dc1949ce63caef08db84ae977fdc1848fe6dc38e6bbaad0ae3e6ecd0d451629"}, + {file = "opentelemetry_util_http-0.46b0.tar.gz", hash = "sha256:03b6e222642f9c7eae58d9132343e045b50aca9761fcb53709bd2b663571fdf6"}, ] [[package]] @@ -7060,4 +7060,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "67c1f9735668da90ef18378d7f0c8dabab55f64de5e90535ec0972402de2351d" +content-hash = "cd65c97511b132fa3827e834760ce83e5f35476fd0b0009fa1d629c919eadfab" diff --git a/python/pyproject.toml b/python/pyproject.toml index 3379b35aac21..8b7f34eaa57c 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -24,6 +24,8 @@ grpcio = [ openai = ">=1.0" regex = ">=2023.6.3,<2025.0.0" openapi_core = ">=0.18,<0.20" +opentelemetry-api = "^1.25.0" +opentelemetry-sdk = "^1.25.0" prance = "^23.6.21.0" pydantic = "^2" pydantic-settings = "^2.2.1" diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index be08fc3f77fc..8b9cfdc2bbc3 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio +import json import logging from collections.abc import AsyncGenerator from copy import copy @@ -11,6 +12,7 @@ from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice +from opentelemetry import trace from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase from semantic_kernel.connectors.ai.function_call_behavior import ( @@ -44,11 +46,16 @@ from semantic_kernel.filters.filter_types import FilterTypes from semantic_kernel.filters.kernel_filters_extension import _rebuild_auto_function_invocation_context from semantic_kernel.functions.function_result import FunctionResult +from semantic_kernel.utils import model_diagnostics if TYPE_CHECKING: from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel + +MODEL_PROVIDER_NAME = 'openai' + + logger: logging.Logger = logging.getLogger(__name__) @@ -269,9 +276,25 @@ def _chat_message_content_to_dict(self, message: "ChatMessageContent") -> dict[s async def _send_chat_request(self, settings: OpenAIChatPromptExecutionSettings) -> list["ChatMessageContent"]: """Send the chat request.""" + span = model_diagnostics.start_completion_activity(settings.ai_model_id, MODEL_PROVIDER_NAME, + self._settings_messages_to_prompt(settings.messages), + settings) + response = await self._send_request(request_settings=settings) response_metadata = self._get_metadata_from_chat_response(response) - return [self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices] + + chat_message_contents = [self._create_chat_message_content(response, choice, response_metadata) + for choice in response.choices] + + if span is not None: + finish_reasons: list[str] = [] + for choice in response.choices: + finish_reasons.append(choice.finish_reason) + with trace.use_span(span, end_on_exit=True): + model_diagnostics.set_completion_response(span, chat_message_contents, finish_reasons, response.id, + response.usage.prompt_tokens, + response.usage.completion_tokens) + return chat_message_contents async def _send_chat_stream_request( self, settings: OpenAIChatPromptExecutionSettings @@ -291,9 +314,18 @@ async def _send_chat_stream_request( # endregion # region content creation + def _settings_messages_to_prompt(self, messages: list[dict[str, Any]]) -> str: + entries: list[dict[str, str]] = [] + for message in messages: + entries.append({ + "role": str(message.get("role", "unknown")), + "content": str(message.get("content", "unknown")) + }) + return json.dumps(entries) + def _create_chat_message_content( self, response: ChatCompletion, choice: Choice, response_metadata: dict[str, Any] - ) -> "ChatMessageContent": + ) -> ChatMessageContent: """Create a chat message content object from a choice.""" metadata = self._get_metadata_from_chat_choice(choice) metadata.update(response_metadata) diff --git a/python/semantic_kernel/utils/model_diagnostics.py b/python/semantic_kernel/utils/model_diagnostics.py new file mode 100644 index 000000000000..d34ed1bde80d --- /dev/null +++ b/python/semantic_kernel/utils/model_diagnostics.py @@ -0,0 +1,151 @@ +# Copyright (c) Microsoft. All rights reserved. +# +# Model diagnostics to trace model activities with the OTel semantic conventions. +# This code contains experimental features and may change in the future. +# To enable these features, set one of the following senvironment variables to true: +# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS +# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE + +import json +import os +from typing import Optional + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace +from opentelemetry.trace import Span + +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.contents import ChatHistory +from semantic_kernel.contents.chat_message_content import ITEM_TYPES, ChatMessageContent +from semantic_kernel.contents.function_call_content import FunctionCallContent + +# Activity tags +_SYSTEM = "gen_ai.system" +_OPERATION = "gen_ai.operation.name" +_MODEL = "gen_ai.request.model" +_MAX_TOKEN = "gen_ai.request.max_tokens" +_TEMPERATURE = "gen_ai.request.temperature" +_TOP_P = "gen_ai.request.top_p" +_RESPONSE_ID = "gen_ai.response.id" +_FINISH_REASON = "gen_ai.response.finish_reason" +_PROMPT_TOKEN = "gen_ai.response.prompt_tokens" +_COMPLETION_TOKEN = "gen_ai.response.completion_tokens" + +# Activity events +PROMPT_EVENT_PROMPT = "gen_ai.prompt" +COMPLETION_EVENT_COMPLETION = "gen_ai.completion" + +_enable_diagnostics = os.getenv("SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS", + "false").lower() in ("true", "1", "t") + +_enable_sensitive_events = os.getenv("SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE", + "false").lower() in ("true", "1", "t") + +if _enable_diagnostics or _enable_sensitive_events: + # Configure OpenTelemetry to use Azure Monitor with the + # APPLICATIONINSIGHTS_CONNECTION_STRING environment variable. + connection_string = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") + if connection_string: + configure_azure_monitor(connection_string=connection_string) + +# Sets the global default tracer provider +tracer = trace.get_tracer(__name__) + + +def are_model_diagnostics_enabled() -> bool: + """Check if model diagnostics are enabled. + + Model diagnostics are enabled if either EnableModelDiagnostics or EnableSensitiveEvents is set. + """ + return _enable_diagnostics or _enable_sensitive_events + + +def are_sensitive_events_enabled() -> bool: + """Check if sensitive events are enabled. + + Sensitive events are enabled if EnableSensitiveEvents is set. + """ + return _enable_sensitive_events + + +def start_completion_activity(model_name: str, model_provider: str, prompt: str, + execution_settings: Optional[PromptExecutionSettings]) -> Optional[Span]: + """Start a text completion activity for a given model.""" + if not are_model_diagnostics_enabled(): + return None + + operation_name: str = "chat.completions" if isinstance(prompt, ChatHistory) else "text.completions" + + span = tracer.start_span(f"{operation_name} {model_name}") + + # Set attributes on the span + span.set_attributes({ + _OPERATION: operation_name, + _SYSTEM: model_provider, + _MODEL: model_name, + }) + + if execution_settings is not None: + span.set_attributes({ + _MAX_TOKEN: str(execution_settings.extension_data.get("max_tokens")), + _TEMPERATURE: str(execution_settings.extension_data.get("temperature")), + _TOP_P: str(execution_settings.extension_data.get("top_p")), + }) + + if are_sensitive_events_enabled(): + span.add_event(PROMPT_EVENT_PROMPT, {PROMPT_EVENT_PROMPT: prompt}) + + return span + + +def set_completion_response(span: Span, completions: list[ChatMessageContent], finish_reasons: list[str], + response_id: str, prompt_tokens: Optional[int] = None, + completion_tokens: Optional[int] = None) -> None: + """Set the text completion response for a given activity.""" + if not are_model_diagnostics_enabled(): + return + + if prompt_tokens: + span.set_attribute(_PROMPT_TOKEN, prompt_tokens) + + if completion_tokens: + span.set_attribute(_COMPLETION_TOKEN, completion_tokens) + + if finish_reasons: + span.set_attribute(_FINISH_REASON, ",".join(finish_reasons)) + + span.set_attribute(_RESPONSE_ID, response_id) + + if are_sensitive_events_enabled() and len(completions) > 0: + span.add_event(COMPLETION_EVENT_COMPLETION, + {COMPLETION_EVENT_COMPLETION: _messages_to_openai_format(completions)}) + + +def _messages_to_openai_format(chat_history: list[ChatMessageContent]) -> str: + formatted_messages = [] + for message in chat_history: + message_dict = { + "role": message.role, + "content": json.dumps(message.content) + } + if any(isinstance(item, FunctionCallContent) for item in message.items): + message_dict["tool_calls"] = _tool_calls_to_openai_format(message.items) + formatted_messages.append(json.dumps(message_dict)) + + return f"[{', \n'.join(formatted_messages)}]" + + +def _tool_calls_to_openai_format(items: list[ITEM_TYPES]) -> str: + tool_calls: list[str] = [] + for item in items: + if isinstance(item, FunctionCallContent): + tool_call = { + "id": item.id, + "function": { + "arguments": json.dumps(item.arguments), + "name": item.function_name + }, + "type": "function" + } + tool_calls.append(json.dumps(tool_call)) + return f"[{', '.join(tool_calls)}]" \ No newline at end of file From a2c5fd7c7535297e052c1b18d0e41c701940ef33 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 24 Jun 2024 11:45:44 -0700 Subject: [PATCH 02/26] Update python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py Co-authored-by: Tao Chen --- .../ai/open_ai/services/open_ai_chat_completion_base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index 28e168f59e1b..2c5abe5b3833 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -287,9 +287,7 @@ async def _send_chat_request(self, settings: OpenAIChatPromptExecutionSettings) for choice in response.choices] if span is not None: - finish_reasons: list[str] = [] - for choice in response.choices: - finish_reasons.append(choice.finish_reason) + finish_reasons: list[str] = [choice.finish_reason for choice in response.choices] with trace.use_span(span, end_on_exit=True): model_diagnostics.set_completion_response(span, chat_message_contents, finish_reasons, response.id, response.usage.prompt_tokens, From 2848051af205571f57ab9fe197ab0011083929e0 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Thu, 27 Jun 2024 14:39:20 -0700 Subject: [PATCH 03/26] Addressed PR issues --- python/poetry.lock | 109 ++++++++++++- python/pyproject.toml | 5 +- .../10-multiple-results-per-prompt.ipynb | 4 +- .../services/open_ai_chat_completion_base.py | 53 ++++--- .../utils/model_diagnostics.py | 143 ++++++++++-------- .../semantic_kernel/utils/tracing/__init__.py | 0 python/semantic_kernel/utils/tracing/const.py | 26 ++++ 7 files changed, 254 insertions(+), 86 deletions(-) create mode 100644 python/semantic_kernel/utils/tracing/__init__.py create mode 100644 python/semantic_kernel/utils/tracing/const.py diff --git a/python/poetry.lock b/python/poetry.lock index 5b3a8f2038b5..f76fba893b74 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -295,6 +295,25 @@ cryptography = ">=2.5" msal = ">=1.24.0" msal-extensions = ">=0.3.0" +[[package]] +name = "azure-monitor-opentelemetry-exporter" +version = "1.0.0b27" +description = "Microsoft Azure Monitor Opentelemetry Exporter Client Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure-monitor-opentelemetry-exporter-1.0.0b27.tar.gz", hash = "sha256:ee5eb0bb37c29da800cc479084f42181a98d7ad192a27a9b2fdd9cb9957320ad"}, + {file = "azure_monitor_opentelemetry_exporter-1.0.0b27-py2.py3-none-any.whl", hash = "sha256:92f222e11415c6606588be0166b02ba4970159c6bf016160a2023b3713db9f31"}, +] + +[package.dependencies] +azure-core = ">=1.28.0,<2.0.0" +fixedint = "0.1.6" +msrest = ">=0.6.10" +opentelemetry-api = ">=1.21,<2.0" +opentelemetry-sdk = ">=1.21,<2.0" +psutil = ">=5.9,<6.0" + [[package]] name = "azure-search-documents" version = "11.6.0b4" @@ -1154,6 +1173,18 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "fixedint" +version = "0.1.6" +description = "simple fixed-width integers" +optional = false +python-versions = "*" +files = [ + {file = "fixedint-0.1.6-py2-none-any.whl", hash = "sha256:41953193f08cbe984f584d8513c38fe5eea5fbd392257433b2210391c8a21ead"}, + {file = "fixedint-0.1.6-py3-none-any.whl", hash = "sha256:b8cf9f913735d2904deadda7a6daa9f57100599da1de57a7448ea1be75ae8c9c"}, + {file = "fixedint-0.1.6.tar.gz", hash = "sha256:703005d090499d41ce7ce2ee7eae8f7a5589a81acdc6b79f1728a56495f2c799"}, +] + [[package]] name = "flatbuffers" version = "24.3.25" @@ -2678,6 +2709,27 @@ msgraph-core = ">=1.0.0" [package.extras] dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] +[[package]] +name = "msrest" +version = "0.7.1" +description = "AutoRest swagger generator Python client runtime." +optional = false +python-versions = ">=3.6" +files = [ + {file = "msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32"}, + {file = "msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9"}, +] + +[package.dependencies] +azure-core = ">=1.24.0" +certifi = ">=2017.4.17" +isodate = ">=0.6.0" +requests = ">=2.16,<3.0" +requests-oauthlib = ">=0.5.0" + +[package.extras] +async = ["aiodns", "aiohttp (>=3.0)"] + [[package]] name = "multidict" version = "6.0.5" @@ -3130,6 +3182,7 @@ description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" files = [ + {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_aarch64.whl", hash = "sha256:004186d5ea6a57758fd6d57052a123c73a4815adf365eb8dd6a85c9eaa7535ff"}, {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d9714f27c1d0f0895cd8915c07a87a1d0029a0aa36acaf9156952ec2a8a12189"}, {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-win_amd64.whl", hash = "sha256:c3401dc8543b52d3a8158007a0c1ab4e9c768fcbd24153a48c86972102197ddd"}, ] @@ -3304,6 +3357,40 @@ files = [ deprecated = ">=1.2.6" importlib-metadata = ">=6.0,<=7.1" +[[package]] +name = "opentelemetry-distro" +version = "0.46b0" +description = "OpenTelemetry Python Distro" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_distro-0.46b0-py3-none-any.whl", hash = "sha256:ac0681ea97a313319212130826813bdc521bb6d07cdb5c4ad4bcede6eba80d3e"}, + {file = "opentelemetry_distro-0.46b0.tar.gz", hash = "sha256:9bfc8a13f1bff2f1e88c3c75bdda8a6241db9c75d4adddb8709cf82b0390f363"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.46b0" +opentelemetry-sdk = ">=1.13,<2.0" + +[package.extras] +otlp = ["opentelemetry-exporter-otlp (==1.25.0)"] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.25.0" +description = "OpenTelemetry Collector Exporters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_exporter_otlp-1.25.0-py3-none-any.whl", hash = "sha256:d67a831757014a3bc3174e4cd629ae1493b7ba8d189e8a007003cacb9f1a6b60"}, + {file = "opentelemetry_exporter_otlp-1.25.0.tar.gz", hash = "sha256:ce03199c1680a845f82e12c0a6a8f61036048c07ec7a0bd943142aca8fa6ced0"}, +] + +[package.dependencies] +opentelemetry-exporter-otlp-proto-grpc = "1.25.0" +opentelemetry-exporter-otlp-proto-http = "1.25.0" + [[package]] name = "opentelemetry-exporter-otlp-proto-common" version = "1.25.0" @@ -3338,6 +3425,26 @@ opentelemetry-exporter-otlp-proto-common = "1.25.0" opentelemetry-proto = "1.25.0" opentelemetry-sdk = ">=1.25.0,<1.26.0" +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.25.0" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.25.0-py3-none-any.whl", hash = "sha256:2eca686ee11b27acd28198b3ea5e5863a53d1266b91cda47c839d95d5e0541a6"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.25.0.tar.gz", hash = "sha256:9f8723859e37c75183ea7afa73a3542f01d0fd274a5b97487ea24cb683d7d684"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.25.0" +opentelemetry-proto = "1.25.0" +opentelemetry-sdk = ">=1.25.0,<1.26.0" +requests = ">=2.7,<3.0" + [[package]] name = "opentelemetry-instrumentation" version = "0.46b0" @@ -6894,4 +7001,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "abbc85df45b3f61d055c1ad24e6860e45b9ffeae63259e25b5c429cf21518474" +content-hash = "007746142001ce93fe42d02bee285a460b170232ad51d6b490047b46dab3ad2a" diff --git a/python/pyproject.toml b/python/pyproject.toml index b965332093df..c48c91d043ed 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "semantic-kernel" -version = "1.1.1" +version = "1.1.2" description = "Semantic Kernel Python SDK" authors = ["Microsoft "] readme = "pip/README.md" @@ -58,6 +58,9 @@ usearch = { version = "^2.9", optional = true} pyarrow = { version = ">=12.0.1,<17.0.0", optional = true} # Groups are for development only (installed through Poetry) +opentelemetry-distro = "^0.46b0" +opentelemetry-exporter-otlp = "^1.25.0" +azure-monitor-opentelemetry-exporter = {version = "^1.0.0b27", allow-prereleases = true} [tool.poetry.group.dev.dependencies] pre-commit = ">=3.7.1" ruff = ">=0.4.5" diff --git a/python/samples/getting_started/10-multiple-results-per-prompt.ipynb b/python/samples/getting_started/10-multiple-results-per-prompt.ipynb index cbb31df9c305..6f5c0f598555 100644 --- a/python/samples/getting_started/10-multiple-results-per-prompt.ipynb +++ b/python/samples/getting_started/10-multiple-results-per-prompt.ipynb @@ -251,7 +251,7 @@ " results = await oai_text_service.get_text_contents(prompt=prompt, settings=oai_text_prompt_execution_settings)\n", "\n", " for i, result in enumerate(results):\n", - " print(f\"Result {i+1}: {result}\")" + " print(f\"Result {i + 1}: {result}\")" ] }, { @@ -276,7 +276,7 @@ " results = await aoai_text_service.get_text_contents(prompt=prompt, settings=oai_text_prompt_execution_settings)\n", "\n", " for i, result in enumerate(results):\n", - " print(f\"Result {i+1}: {result}\")" + " print(f\"Result {i + 1}: {result}\")" ] }, { diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index 28e168f59e1b..807961f4f40b 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator from copy import copy from functools import reduce -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar from openai import AsyncStream from openai.types.chat.chat_completion import ChatCompletion, Choice @@ -46,16 +46,17 @@ from semantic_kernel.filters.filter_types import FilterTypes from semantic_kernel.filters.kernel_filters_extension import _rebuild_auto_function_invocation_context from semantic_kernel.functions.function_result import FunctionResult -from semantic_kernel.utils import model_diagnostics +from semantic_kernel.utils.model_diagnostics import ( + set_completion_error, + set_completion_response, + start_completion_activity, +) if TYPE_CHECKING: from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel -MODEL_PROVIDER_NAME = 'openai' - - logger: logging.Logger = logging.getLogger(__name__) @@ -68,6 +69,8 @@ class InvokeTermination(Exception): class OpenAIChatCompletionBase(OpenAIHandler, ChatCompletionClientBase): """OpenAI Chat completion class.""" + MODEL_PROVIDER_NAME: ClassVar[str] = "openai" + # region Overriding base class methods # most of the methods are overridden from the ChatCompletionClientBase class, otherwise it is mentioned @@ -276,24 +279,40 @@ def _chat_message_content_to_dict(self, message: "ChatMessageContent") -> dict[s async def _send_chat_request(self, settings: OpenAIChatPromptExecutionSettings) -> list["ChatMessageContent"]: """Send the chat request.""" - span = model_diagnostics.start_completion_activity(settings.ai_model_id, MODEL_PROVIDER_NAME, - self._settings_messages_to_prompt(settings.messages), - settings) + span = start_completion_activity( + settings.ai_model_id, + self.MODEL_PROVIDER_NAME, + self._settings_messages_to_prompt(settings.messages), + settings, + ) + + try: + response = await self._send_request(request_settings=settings) + except Exception as exception: + set_completion_error(span, exception) + span.end() + raise - response = await self._send_request(request_settings=settings) response_metadata = self._get_metadata_from_chat_response(response) - - chat_message_contents = [self._create_chat_message_content(response, choice, response_metadata) - for choice in response.choices] - - if span is not None: + + chat_message_contents = [ + self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices + ] + + if span: finish_reasons: list[str] = [] for choice in response.choices: finish_reasons.append(choice.finish_reason) with trace.use_span(span, end_on_exit=True): - model_diagnostics.set_completion_response(span, chat_message_contents, finish_reasons, response.id, - response.usage.prompt_tokens, - response.usage.completion_tokens) + set_completion_response( + span, + chat_message_contents, + finish_reasons, + response.id, + response.usage.prompt_tokens, + response.usage.completion_tokens, + ) + return chat_message_contents async def _send_chat_stream_request( diff --git a/python/semantic_kernel/utils/model_diagnostics.py b/python/semantic_kernel/utils/model_diagnostics.py index d34ed1bde80d..54c1ee7f3330 100644 --- a/python/semantic_kernel/utils/model_diagnostics.py +++ b/python/semantic_kernel/utils/model_diagnostics.py @@ -10,45 +10,38 @@ import os from typing import Optional -from azure.monitor.opentelemetry import configure_azure_monitor from opentelemetry import trace -from opentelemetry.trace import Span +from opentelemetry.trace import Span, StatusCode from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings -from semantic_kernel.contents import ChatHistory from semantic_kernel.contents.chat_message_content import ITEM_TYPES, ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent - -# Activity tags -_SYSTEM = "gen_ai.system" -_OPERATION = "gen_ai.operation.name" -_MODEL = "gen_ai.request.model" -_MAX_TOKEN = "gen_ai.request.max_tokens" -_TEMPERATURE = "gen_ai.request.temperature" -_TOP_P = "gen_ai.request.top_p" -_RESPONSE_ID = "gen_ai.response.id" -_FINISH_REASON = "gen_ai.response.finish_reason" -_PROMPT_TOKEN = "gen_ai.response.prompt_tokens" -_COMPLETION_TOKEN = "gen_ai.response.completion_tokens" - -# Activity events -PROMPT_EVENT_PROMPT = "gen_ai.prompt" -COMPLETION_EVENT_COMPLETION = "gen_ai.completion" - -_enable_diagnostics = os.getenv("SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS", - "false").lower() in ("true", "1", "t") - -_enable_sensitive_events = os.getenv("SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE", - "false").lower() in ("true", "1", "t") - -if _enable_diagnostics or _enable_sensitive_events: - # Configure OpenTelemetry to use Azure Monitor with the - # APPLICATIONINSIGHTS_CONNECTION_STRING environment variable. - connection_string = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") - if connection_string: - configure_azure_monitor(connection_string=connection_string) - -# Sets the global default tracer provider +from semantic_kernel.utils.tracing.const import ( + COMPLETION_EVENT, + COMPLETION_EVENT_COMPLETION, + COMPLETION_TOKEN, + ERROR_TYPE, + FINISH_REASON, + MAX_TOKEN, + MODEL, + OPERATION, + PROMPT_EVENT, + PROMPT_EVENT_PROMPT, + PROMPT_TOKEN, + RESPONSE_ID, + SYSTEM, + TEMPERATURE, + TOP_P, +) + +OTEL_ENABLED_ENV_VAR = "SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS" +OTEL_SENSITIVE_ENABLED_ENV_VAR = "SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE" + + +_enable_diagnostics = os.getenv(OTEL_ENABLED_ENV_VAR, "false").lower() in ("true", "1", "t") +_enable_sensitive_events = os.getenv(OTEL_SENSITIVE_ENABLED_ENV_VAR, "false").lower() in ("true", "1", "t") + +# Creates a tracer from the global tracer provider tracer = trace.get_tracer(__name__) @@ -68,66 +61,86 @@ def are_sensitive_events_enabled() -> bool: return _enable_sensitive_events -def start_completion_activity(model_name: str, model_provider: str, prompt: str, - execution_settings: Optional[PromptExecutionSettings]) -> Optional[Span]: - """Start a text completion activity for a given model.""" +def start_completion_activity( + model_name: str, model_provider: str, prompt: str, execution_settings: Optional[PromptExecutionSettings] +) -> Optional[Span]: + """Start a text or chat completion activity for a given model.""" if not are_model_diagnostics_enabled(): return None - operation_name: str = "chat.completions" if isinstance(prompt, ChatHistory) else "text.completions" + operation_name: str = "chat.completions" span = tracer.start_span(f"{operation_name} {model_name}") # Set attributes on the span - span.set_attributes({ - _OPERATION: operation_name, - _SYSTEM: model_provider, - _MODEL: model_name, - }) + span.set_attributes( + { + OPERATION: operation_name, + SYSTEM: model_provider, + MODEL: model_name, + } + ) if execution_settings is not None: - span.set_attributes({ - _MAX_TOKEN: str(execution_settings.extension_data.get("max_tokens")), - _TEMPERATURE: str(execution_settings.extension_data.get("temperature")), - _TOP_P: str(execution_settings.extension_data.get("top_p")), - }) + attribute = execution_settings.extension_data.get("max_tokens") + if attribute is not None: + span.set_attribute(MAX_TOKEN, attribute) + + attribute = execution_settings.extension_data.get("temperature") + if attribute is not None: + span.set_attribute(TEMPERATURE, attribute) + + attribute = execution_settings.extension_data.get("top_p") + if attribute is not None: + span.set_attribute(TOP_P, attribute) if are_sensitive_events_enabled(): - span.add_event(PROMPT_EVENT_PROMPT, {PROMPT_EVENT_PROMPT: prompt}) + span.add_event(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: prompt}) return span -def set_completion_response(span: Span, completions: list[ChatMessageContent], finish_reasons: list[str], - response_id: str, prompt_tokens: Optional[int] = None, - completion_tokens: Optional[int] = None) -> None: - """Set the text completion response for a given activity.""" +def set_completion_response( + span: Span, + completions: list[ChatMessageContent], + finish_reasons: list[str], + response_id: str, + prompt_tokens: Optional[int] = None, + completion_tokens: Optional[int] = None, +) -> None: + """Set the a text or chat completion response for a given activity.""" if not are_model_diagnostics_enabled(): return if prompt_tokens: - span.set_attribute(_PROMPT_TOKEN, prompt_tokens) + span.set_attribute(PROMPT_TOKEN, prompt_tokens) if completion_tokens: - span.set_attribute(_COMPLETION_TOKEN, completion_tokens) + span.set_attribute(COMPLETION_TOKEN, completion_tokens) if finish_reasons: - span.set_attribute(_FINISH_REASON, ",".join(finish_reasons)) + span.set_attribute(FINISH_REASON, ",".join(finish_reasons)) - span.set_attribute(_RESPONSE_ID, response_id) + span.set_attribute(RESPONSE_ID, response_id) - if are_sensitive_events_enabled() and len(completions) > 0: - span.add_event(COMPLETION_EVENT_COMPLETION, - {COMPLETION_EVENT_COMPLETION: _messages_to_openai_format(completions)}) + if are_sensitive_events_enabled() and completions: + span.add_event(COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: _messages_to_openai_format(completions)}) + + +def set_completion_error(span: Span, error: Exception) -> None: + """Set an error for a text or chat completion .""" + if not are_model_diagnostics_enabled(): + return + + span.set_attribute(ERROR_TYPE, str(type(error))) + + span.set_status(StatusCode.ERROR, str(error)) def _messages_to_openai_format(chat_history: list[ChatMessageContent]) -> str: formatted_messages = [] for message in chat_history: - message_dict = { - "role": message.role, - "content": json.dumps(message.content) - } + message_dict = {"role": message.role, "content": json.dumps(message.content)} if any(isinstance(item, FunctionCallContent) for item in message.items): message_dict["tool_calls"] = _tool_calls_to_openai_format(message.items) formatted_messages.append(json.dumps(message_dict)) @@ -148,4 +161,4 @@ def _tool_calls_to_openai_format(items: list[ITEM_TYPES]) -> str: "type": "function" } tool_calls.append(json.dumps(tool_call)) - return f"[{', '.join(tool_calls)}]" \ No newline at end of file + return f"[{', '.join(tool_calls)}]" diff --git a/python/semantic_kernel/utils/tracing/__init__.py b/python/semantic_kernel/utils/tracing/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/semantic_kernel/utils/tracing/const.py b/python/semantic_kernel/utils/tracing/const.py new file mode 100644 index 000000000000..587d1d0c2aef --- /dev/null +++ b/python/semantic_kernel/utils/tracing/const.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft. All rights reserved. +# +# Constants for tracing activities with semantic conventions. + +# Activity tags +SYSTEM = "gen_ai.system" +OPERATION = "gen_ai.operation.name" +MODEL = "gen_ai.request.model" +MAX_TOKEN = "gen_ai.request.max_tokens" +TEMPERATURE = "gen_ai.request.temperature" +TOP_P = "gen_ai.request.top_p" +RESPONSE_ID = "gen_ai.response.id" +FINISH_REASON = "gen_ai.response.finish_reason" +PROMPT_TOKEN = "gen_ai.response.prompt_tokens" +COMPLETION_TOKEN = "gen_ai.response.completion_tokens" +ADDRESS = "server.address" +PORT = "server.port" +ERROR_TYPE = "error.type" + +# Activity events +PROMPT_EVENT = "gen_ai.content.prompt" +COMPLETION_EVENT = "gen_ai.content.completion" + +# Activity event attributes +PROMPT_EVENT_PROMPT = "gen_ai.prompt" +COMPLETION_EVENT_COMPLETION = "gen_ai.completion" From 4f3783d3ab570f4554c6942eae3869e2ec713b8d Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Thu, 27 Jun 2024 15:04:28 -0700 Subject: [PATCH 04/26] Update poetry info after merge --- python/poetry.lock | 2 +- python/pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/poetry.lock b/python/poetry.lock index ecac679ffde5..31479c48aee2 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -7018,4 +7018,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "1f7da20d986a695f80ec43e38934d0d0231ca8c61ae637e35f252558526423d9" +content-hash = "6609884aa96dafcdb5f2c3d4e3b1f22b599eeb8d9afb24f6bf1f19ce9a65f122" diff --git a/python/pyproject.toml b/python/pyproject.toml index 84c37f1214c3..2c2ef6712445 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -26,6 +26,8 @@ regex = ">=2023.6.3,<2025.0.0" openapi_core = ">=0.18,<0.20" opentelemetry-api = "^1.25.0" opentelemetry-sdk = "^1.25.0" +opentelemetry-distro = "^0.46b0" +opentelemetry-exporter-otlp = "^1.25.0" prance = "^23.6.21.0" pydantic = "^2" pydantic-settings = "^2.2.1" @@ -34,6 +36,7 @@ defusedxml = "^0.7.1" pybars4 = "^0.9.13" jinja2 = "^3.1.3" nest-asyncio = "^1.6.0" +azure-monitor-opentelemetry-exporter = {version = "^1.0.0b27", allow-prereleases = true} # Optional dependencies ipykernel = { version = "^6.21.1", optional = true} @@ -59,9 +62,6 @@ usearch = { version = "^2.9", optional = true} pyarrow = { version = ">=12.0.1,<17.0.0", optional = true} # Groups are for development only (installed through Poetry) -opentelemetry-distro = "^0.46b0" -opentelemetry-exporter-otlp = "^1.25.0" -azure-monitor-opentelemetry-exporter = {version = "^1.0.0b27", allow-prereleases = true} [tool.poetry.group.dev.dependencies] pre-commit = ">=3.7.1" ruff = ">=0.4.5" From e71fab27a42752b1b64b19393876166745300903 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Thu, 27 Jun 2024 16:03:52 -0700 Subject: [PATCH 05/26] Address PR issues --- .../services/open_ai_chat_completion_base.py | 19 ++++++++----------- .../utils/model_diagnostics.py | 9 +++------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index f7851875518a..1a0993108a30 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -42,9 +42,6 @@ from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import ( AutoFunctionInvocationContext, ) -from semantic_kernel.filters.filter_types import FilterTypes -from semantic_kernel.filters.kernel_filters_extension import _rebuild_auto_function_invocation_context -from semantic_kernel.functions.function_result import FunctionResult from semantic_kernel.utils.model_diagnostics import ( set_completion_error, set_completion_response, @@ -294,7 +291,7 @@ async def _send_chat_request(self, settings: OpenAIChatPromptExecutionSettings) span = start_completion_activity( settings.ai_model_id, self.MODEL_PROVIDER_NAME, - self._settings_messages_to_prompt(settings.messages), + self._input_messages_to_prompt(settings.messages), settings, ) @@ -343,13 +340,13 @@ async def _send_chat_stream_request( # endregion # region content creation - def _settings_messages_to_prompt(self, messages: list[dict[str, Any]]) -> str: - entries: list[dict[str, str]] = [] - for message in messages: - entries.append({ - "role": str(message.get("role", "unknown")), - "content": str(message.get("content", "unknown")) - }) + def _input_messages_to_prompt(self, messages: list[dict[str, Any]]) -> str: + """Convert input messages to a prompt string.""" + entries = [ + {"role": str(message.get("role", "unknown")), "content": str(message.get("content", "unknown"))} + for message in messages + ] + return json.dumps(entries) def _create_chat_message_content( diff --git a/python/semantic_kernel/utils/model_diagnostics.py b/python/semantic_kernel/utils/model_diagnostics.py index 54c1ee7f3330..5b33eed9d773 100644 --- a/python/semantic_kernel/utils/model_diagnostics.py +++ b/python/semantic_kernel/utils/model_diagnostics.py @@ -145,7 +145,7 @@ def _messages_to_openai_format(chat_history: list[ChatMessageContent]) -> str: message_dict["tool_calls"] = _tool_calls_to_openai_format(message.items) formatted_messages.append(json.dumps(message_dict)) - return f"[{', \n'.join(formatted_messages)}]" + return "[{}]".format(", \n".join(formatted_messages)) def _tool_calls_to_openai_format(items: list[ITEM_TYPES]) -> str: @@ -154,11 +154,8 @@ def _tool_calls_to_openai_format(items: list[ITEM_TYPES]) -> str: if isinstance(item, FunctionCallContent): tool_call = { "id": item.id, - "function": { - "arguments": json.dumps(item.arguments), - "name": item.function_name - }, - "type": "function" + "function": {"arguments": json.dumps(item.arguments), "name": item.function_name}, + "type": "function", } tool_calls.append(json.dumps(tool_call)) return f"[{', '.join(tool_calls)}]" From 6bf6406d57f01f7ca284d7ad26647e0dffe092ec Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Thu, 27 Jun 2024 20:41:57 -0700 Subject: [PATCH 06/26] Fix lint warning --- python/semantic_kernel/utils/model_diagnostics.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/semantic_kernel/utils/model_diagnostics.py b/python/semantic_kernel/utils/model_diagnostics.py index 5b33eed9d773..36eb9a990e9a 100644 --- a/python/semantic_kernel/utils/model_diagnostics.py +++ b/python/semantic_kernel/utils/model_diagnostics.py @@ -8,7 +8,6 @@ import json import os -from typing import Optional from opentelemetry import trace from opentelemetry.trace import Span, StatusCode @@ -62,8 +61,8 @@ def are_sensitive_events_enabled() -> bool: def start_completion_activity( - model_name: str, model_provider: str, prompt: str, execution_settings: Optional[PromptExecutionSettings] -) -> Optional[Span]: + model_name: str, model_provider: str, prompt: str, execution_settings: PromptExecutionSettings | None +) -> Span | None: """Start a text or chat completion activity for a given model.""" if not are_model_diagnostics_enabled(): return None @@ -105,8 +104,8 @@ def set_completion_response( completions: list[ChatMessageContent], finish_reasons: list[str], response_id: str, - prompt_tokens: Optional[int] = None, - completion_tokens: Optional[int] = None, + prompt_tokens: int | None = None, + completion_tokens: int | None = None, ) -> None: """Set the a text or chat completion response for a given activity.""" if not are_model_diagnostics_enabled(): From 00269b9e5fa80f2e9221636138b2f83e27a1464c Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Thu, 27 Jun 2024 20:56:15 -0700 Subject: [PATCH 07/26] Fix false positive security issue --- python/semantic_kernel/utils/tracing/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/semantic_kernel/utils/tracing/const.py b/python/semantic_kernel/utils/tracing/const.py index 587d1d0c2aef..11d9a59a3970 100644 --- a/python/semantic_kernel/utils/tracing/const.py +++ b/python/semantic_kernel/utils/tracing/const.py @@ -6,13 +6,13 @@ SYSTEM = "gen_ai.system" OPERATION = "gen_ai.operation.name" MODEL = "gen_ai.request.model" -MAX_TOKEN = "gen_ai.request.max_tokens" +MAX_TOKEN = "gen_ai.request.max_tokens" # nosec TEMPERATURE = "gen_ai.request.temperature" TOP_P = "gen_ai.request.top_p" RESPONSE_ID = "gen_ai.response.id" FINISH_REASON = "gen_ai.response.finish_reason" -PROMPT_TOKEN = "gen_ai.response.prompt_tokens" -COMPLETION_TOKEN = "gen_ai.response.completion_tokens" +PROMPT_TOKEN = "gen_ai.response.prompt_tokens" # nosec +COMPLETION_TOKEN = "gen_ai.response.completion_tokens" # nosec ADDRESS = "server.address" PORT = "server.port" ERROR_TYPE = "error.type" From 1fac03f10d2ecaca9b831399f54cd19d95f09c24 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 8 Jul 2024 11:29:24 -0700 Subject: [PATCH 08/26] completion telemetry now in decorator --- .../services/open_ai_chat_completion_base.py | 52 +------- .../decorators.py} | 124 ++++++++++++++---- 2 files changed, 103 insertions(+), 73 deletions(-) rename python/semantic_kernel/utils/{model_diagnostics.py => tracing/decorators.py} (51%) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index 1a0993108a30..372f9e60faf7 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import json import logging from collections.abc import AsyncGenerator from functools import reduce @@ -11,7 +10,6 @@ from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice -from opentelemetry import trace from typing_extensions import deprecated from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase @@ -42,11 +40,7 @@ from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import ( AutoFunctionInvocationContext, ) -from semantic_kernel.utils.model_diagnostics import ( - set_completion_error, - set_completion_response, - start_completion_activity, -) +from semantic_kernel.utils.tracing.decorators import trace_chat_completion if TYPE_CHECKING: from semantic_kernel.functions.kernel_arguments import KernelArguments @@ -74,6 +68,7 @@ def get_prompt_execution_settings_class(self) -> "PromptExecutionSettings": """Create a request settings object.""" return OpenAIChatPromptExecutionSettings + @trace_chat_completion("openai") async def get_chat_message_contents( self, chat_history: ChatHistory, @@ -286,41 +281,13 @@ def _chat_message_content_to_dict(self, message: "ChatMessageContent") -> dict[s # endregion # region internal handlers - async def _send_chat_request(self, settings: OpenAIChatPromptExecutionSettings) -> list["ChatMessageContent"]: + async def _send_chat_request(self, settings: OpenAIChatPromptExecutionSettings) -> list[ChatMessageContent]: """Send the chat request.""" - span = start_completion_activity( - settings.ai_model_id, - self.MODEL_PROVIDER_NAME, - self._input_messages_to_prompt(settings.messages), - settings, - ) - - try: - response = await self._send_request(request_settings=settings) - except Exception as exception: - set_completion_error(span, exception) - span.end() - raise + response = await self._send_request(request_settings=settings) response_metadata = self._get_metadata_from_chat_response(response) - chat_message_contents = [ - self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices - ] - - if span: - finish_reasons: list[str] = [choice.finish_reason for choice in response.choices] - with trace.use_span(span, end_on_exit=True): - set_completion_response( - span, - chat_message_contents, - finish_reasons, - response.id, - response.usage.prompt_tokens, - response.usage.completion_tokens, - ) - - return chat_message_contents + return [self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices] async def _send_chat_stream_request( self, settings: OpenAIChatPromptExecutionSettings @@ -340,15 +307,6 @@ async def _send_chat_stream_request( # endregion # region content creation - def _input_messages_to_prompt(self, messages: list[dict[str, Any]]) -> str: - """Convert input messages to a prompt string.""" - entries = [ - {"role": str(message.get("role", "unknown")), "content": str(message.get("content", "unknown"))} - for message in messages - ] - - return json.dumps(entries) - def _create_chat_message_content( self, response: ChatCompletion, choice: Choice, response_metadata: dict[str, Any] ) -> ChatMessageContent: diff --git a/python/semantic_kernel/utils/model_diagnostics.py b/python/semantic_kernel/utils/tracing/decorators.py similarity index 51% rename from python/semantic_kernel/utils/model_diagnostics.py rename to python/semantic_kernel/utils/tracing/decorators.py index 36eb9a990e9a..f2bdeb3794e0 100644 --- a/python/semantic_kernel/utils/model_diagnostics.py +++ b/python/semantic_kernel/utils/tracing/decorators.py @@ -1,18 +1,21 @@ # Copyright (c) Microsoft. All rights reserved. # -# Model diagnostics to trace model activities with the OTel semantic conventions. +# Code to trace model activities with the OTel semantic conventions. # This code contains experimental features and may change in the future. # To enable these features, set one of the following senvironment variables to true: # SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS # SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE +import functools import json import os +from typing import Any, Callable from opentelemetry import trace from opentelemetry.trace import Span, StatusCode from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ITEM_TYPES, ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent from semantic_kernel.utils.tracing.const import ( @@ -60,8 +63,59 @@ def are_sensitive_events_enabled() -> bool: return _enable_sensitive_events -def start_completion_activity( - model_name: str, model_provider: str, prompt: str, execution_settings: PromptExecutionSettings | None +def trace_chat_completion(model_provider: str) -> Callable: + """Decorator to trace chat completion activities.""" + + def inner_trace_chat_completion(completion_func: Callable) -> Callable: + @functools.wraps(completion_func) + async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageContent]: + chat_history: ChatHistory = kwargs["chat_history"] + settings: PromptExecutionSettings = kwargs["settings"] + + if hasattr(settings, "ai_model_id") and settings.ai_model_id: + model_name = settings.ai_model_id + elif hasattr(args[0], "ai_model_id") and args[0].ai_model_id: + model_name = args[0].ai_model_id + else: + model_name = "unknown" + + span = _start_completion_activity(model_name, model_provider, chat_history, settings) + + try: + completions: list[ChatMessageContent] = await completion_func(*args, **kwargs) + except Exception as exception: + if span: + _set_completion_error(span, exception) + span.end() + raise + + if span: + with trace.use_span(span, end_on_exit=True): + if completions: + first_completion = completions[0] + response_id = first_completion.metadata.get("id", None) + if not response_id: + response_id = ( + first_completion.inner_content.get("id", None) + if first_completion.inner_content + else None + ) + usage = first_completion.metadata.get("usage", None) + prompt_tokens = usage.prompt_tokens if hasattr(usage, "prompt_tokens") else None + completion_tokens = usage.completion_tokens if hasattr(usage, "completion_tokens") else None + _set_completion_response( + span, completions, response_id or "unknown", prompt_tokens, completion_tokens + ) + + return completions + + return wrapper_decorator + + return inner_trace_chat_completion + + +def _start_completion_activity( + model_name: str, model_provider: str, chat_history: ChatHistory, execution_settings: PromptExecutionSettings | None ) -> Span | None: """Start a text or chat completion activity for a given model.""" if not are_model_diagnostics_enabled(): @@ -80,29 +134,29 @@ def start_completion_activity( } ) - if execution_settings is not None: + if execution_settings: attribute = execution_settings.extension_data.get("max_tokens") - if attribute is not None: + if attribute: span.set_attribute(MAX_TOKEN, attribute) attribute = execution_settings.extension_data.get("temperature") - if attribute is not None: + if attribute: span.set_attribute(TEMPERATURE, attribute) attribute = execution_settings.extension_data.get("top_p") - if attribute is not None: + if attribute: span.set_attribute(TOP_P, attribute) if are_sensitive_events_enabled(): - span.add_event(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: prompt}) + formatted_messages = _messages_to_openai_format(chat_history.messages) + span.add_event(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: formatted_messages}) return span -def set_completion_response( +def _set_completion_response( span: Span, completions: list[ChatMessageContent], - finish_reasons: list[str], response_id: str, prompt_tokens: int | None = None, completion_tokens: int | None = None, @@ -111,22 +165,22 @@ def set_completion_response( if not are_model_diagnostics_enabled(): return + span.set_attribute(RESPONSE_ID, response_id) + + finish_reasons: list[str] = [str(content.finish_reason) for content in completions] + span.set_attribute(FINISH_REASON, ",".join(finish_reasons)) + + if are_sensitive_events_enabled() and completions: + span.add_event(COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: _messages_to_openai_format(completions)}) + if prompt_tokens: span.set_attribute(PROMPT_TOKEN, prompt_tokens) if completion_tokens: span.set_attribute(COMPLETION_TOKEN, completion_tokens) - if finish_reasons: - span.set_attribute(FINISH_REASON, ",".join(finish_reasons)) - - span.set_attribute(RESPONSE_ID, response_id) - if are_sensitive_events_enabled() and completions: - span.add_event(COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: _messages_to_openai_format(completions)}) - - -def set_completion_error(span: Span, error: Exception) -> None: +def _set_completion_error(span: Span, error: Exception) -> None: """Set an error for a text or chat completion .""" if not are_model_diagnostics_enabled(): return @@ -136,18 +190,36 @@ def set_completion_error(span: Span, error: Exception) -> None: span.set_status(StatusCode.ERROR, str(error)) -def _messages_to_openai_format(chat_history: list[ChatMessageContent]) -> str: - formatted_messages = [] - for message in chat_history: - message_dict = {"role": message.role, "content": json.dumps(message.content)} - if any(isinstance(item, FunctionCallContent) for item in message.items): - message_dict["tool_calls"] = _tool_calls_to_openai_format(message.items) - formatted_messages.append(json.dumps(message_dict)) +def _messages_to_openai_format(messages: list[ChatMessageContent]) -> str: + """Convert a list of ChatMessageContent to a string in the OpenAI format. + + OpenTelemetry recommends formatting the messages in the OpenAI format + regardless of the actual model being used. + """ + formatted_messages = [ + json.dumps( + { + "role": message.role, + "content": json.dumps(message.content), + **( + {"tool_calls": _tool_calls_to_openai_format(message.items)} + if any(isinstance(item, FunctionCallContent) for item in message.items) + else {} + ), + } + ) + for message in messages + ] return "[{}]".format(", \n".join(formatted_messages)) def _tool_calls_to_openai_format(items: list[ITEM_TYPES]) -> str: + """Convert a list of FunctionCallContent to a string in the OpenAI format. + + OpenTelemetry recommends formatting the messages in the OpenAI format + regardless of the actual model being used. + """ tool_calls: list[str] = [] for item in items: if isinstance(item, FunctionCallContent): From cc13f52324584730375c057b901601a919e411b4 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 8 Jul 2024 11:33:14 -0700 Subject: [PATCH 09/26] Use classvar --- .../ai/open_ai/services/open_ai_chat_completion_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index 372f9e60faf7..3df69a47c2dd 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -68,7 +68,7 @@ def get_prompt_execution_settings_class(self) -> "PromptExecutionSettings": """Create a request settings object.""" return OpenAIChatPromptExecutionSettings - @trace_chat_completion("openai") + @trace_chat_completion(MODEL_PROVIDER_NAME) async def get_chat_message_contents( self, chat_history: ChatHistory, From c6aa839cefbf2219a9d4746a76b398a706ab7b02 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 8 Jul 2024 11:54:33 -0700 Subject: [PATCH 10/26] Proper merge of poetry lock --- python/poetry.lock | 24 ++---------------------- python/pyproject.toml | 6 ++++-- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/python/poetry.lock b/python/poetry.lock index 8f0359c1de9f..3f7fe90c2bf3 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "accelerate" @@ -2403,7 +2403,6 @@ python-versions = ">=3.7" files = [ {file = "milvus_lite-2.4.7-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:c828190118b104b05b8c8e0b5a4147811c86b54b8fb67bc2e726ad10fc0b544e"}, {file = "milvus_lite-2.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e1537633c39879714fb15082be56a4b97f74c905a6e98e302ec01320561081af"}, - {file = "milvus_lite-2.4.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:fcb909d38c83f21478ca9cb500c84264f988c69f62715ae9462e966767fb76dd"}, {file = "milvus_lite-2.4.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f016474d663045787dddf1c3aad13b7d8b61fd329220318f858184918143dcbf"}, ] @@ -3338,25 +3337,6 @@ files = [ deprecated = ">=1.2.6" importlib-metadata = ">=6.0,<=7.1" -[[package]] -name = "opentelemetry-distro" -version = "0.46b0" -description = "OpenTelemetry Python Distro" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_distro-0.46b0-py3-none-any.whl", hash = "sha256:ac0681ea97a313319212130826813bdc521bb6d07cdb5c4ad4bcede6eba80d3e"}, - {file = "opentelemetry_distro-0.46b0.tar.gz", hash = "sha256:9bfc8a13f1bff2f1e88c3c75bdda8a6241db9c75d4adddb8709cf82b0390f363"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.46b0" -opentelemetry-sdk = ">=1.13,<2.0" - -[package.extras] -otlp = ["opentelemetry-exporter-otlp (==1.25.0)"] - [[package]] name = "opentelemetry-exporter-otlp" version = "1.25.0" @@ -6968,4 +6948,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "e8c6f1cee296a7e58fddb6822641685de019be647b813f550d704e3184b9cb08" +content-hash = "a110f00488b7ecd31915d16e4805b9d18086ad3618f2f4e166c7ce44b48a9e47" diff --git a/python/pyproject.toml b/python/pyproject.toml index 2029224ca082..569f19e9656d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -26,17 +26,19 @@ openai = ">=1.0" # openapi and swagger openapi_core = ">=0.18,<0.20" + +# OpenTelemetry opentelemetry-api = "^1.25.0" opentelemetry-sdk = "^1.25.0" -opentelemetry-distro = "^0.46b0" opentelemetry-exporter-otlp = "^1.25.0" +azure-monitor-opentelemetry-exporter = {version = "^1.0.0b27", allow-prereleases = true} + prance = "^23.6.21.0" # templating pybars4 = "^0.9.13" jinja2 = "^3.1.3" nest-asyncio = "^1.6.0" -azure-monitor-opentelemetry-exporter = {version = "^1.0.0b27", allow-prereleases = true} ### Optional dependencies # azure From 8cc76818f864b534fbc206c915b285c4c0ff6045 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 8 Jul 2024 12:57:58 -0700 Subject: [PATCH 11/26] Fix precommit qual issues --- .../ai/open_ai/services/open_ai_text_completion_base.py | 2 +- python/semantic_kernel/utils/tracing/decorators.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py index 6be5147dc6ea..406cf6139ee6 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py @@ -36,7 +36,7 @@ async def get_text_contents( self, prompt: str, settings: "OpenAIPromptExecutionSettings", - ) -> list["TextContent"]: + ) -> list[TextContent]: """Executes a completion request and returns the result. Args: diff --git a/python/semantic_kernel/utils/tracing/decorators.py b/python/semantic_kernel/utils/tracing/decorators.py index f2bdeb3794e0..2394335a0cf3 100644 --- a/python/semantic_kernel/utils/tracing/decorators.py +++ b/python/semantic_kernel/utils/tracing/decorators.py @@ -9,7 +9,8 @@ import functools import json import os -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from opentelemetry import trace from opentelemetry.trace import Span, StatusCode From f875eb3016cf44f67656474e19aaaa90776c2b64 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Tue, 9 Jul 2024 12:35:39 -0700 Subject: [PATCH 12/26] Address PR issues --- python/poetry.lock | 89 +------------------ python/pyproject.toml | 2 - .../services/open_ai_chat_completion_base.py | 4 +- .../services/open_ai_text_completion_base.py | 2 +- .../utils/tracing/decorators.py | 16 ++-- 5 files changed, 10 insertions(+), 103 deletions(-) diff --git a/python/poetry.lock b/python/poetry.lock index 3f7fe90c2bf3..c070d13f3c81 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -342,25 +342,6 @@ cryptography = ">=2.5" msal = ">=1.24.0" msal-extensions = ">=0.3.0" -[[package]] -name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b27" -description = "Microsoft Azure Monitor Opentelemetry Exporter Client Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure-monitor-opentelemetry-exporter-1.0.0b27.tar.gz", hash = "sha256:ee5eb0bb37c29da800cc479084f42181a98d7ad192a27a9b2fdd9cb9957320ad"}, - {file = "azure_monitor_opentelemetry_exporter-1.0.0b27-py2.py3-none-any.whl", hash = "sha256:92f222e11415c6606588be0166b02ba4970159c6bf016160a2023b3713db9f31"}, -] - -[package.dependencies] -azure-core = ">=1.28.0,<2.0.0" -fixedint = "0.1.6" -msrest = ">=0.6.10" -opentelemetry-api = ">=1.21,<2.0" -opentelemetry-sdk = ">=1.21,<2.0" -psutil = ">=5.9,<6.0" - [[package]] name = "azure-search-documents" version = "11.6.0b4" @@ -1221,18 +1202,6 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] -[[package]] -name = "fixedint" -version = "0.1.6" -description = "simple fixed-width integers" -optional = false -python-versions = "*" -files = [ - {file = "fixedint-0.1.6-py2-none-any.whl", hash = "sha256:41953193f08cbe984f584d8513c38fe5eea5fbd392257433b2210391c8a21ead"}, - {file = "fixedint-0.1.6-py3-none-any.whl", hash = "sha256:b8cf9f913735d2904deadda7a6daa9f57100599da1de57a7448ea1be75ae8c9c"}, - {file = "fixedint-0.1.6.tar.gz", hash = "sha256:703005d090499d41ce7ce2ee7eae8f7a5589a81acdc6b79f1728a56495f2c799"}, -] - [[package]] name = "flatbuffers" version = "24.3.25" @@ -2689,27 +2658,6 @@ msgraph-core = ">=1.0.0" [package.extras] dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] -[[package]] -name = "msrest" -version = "0.7.1" -description = "AutoRest swagger generator Python client runtime." -optional = false -python-versions = ">=3.6" -files = [ - {file = "msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32"}, - {file = "msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9"}, -] - -[package.dependencies] -azure-core = ">=1.24.0" -certifi = ">=2017.4.17" -isodate = ">=0.6.0" -requests = ">=2.16,<3.0" -requests-oauthlib = ">=0.5.0" - -[package.extras] -async = ["aiodns", "aiohttp (>=3.0)"] - [[package]] name = "multidict" version = "6.0.5" @@ -3337,21 +3285,6 @@ files = [ deprecated = ">=1.2.6" importlib-metadata = ">=6.0,<=7.1" -[[package]] -name = "opentelemetry-exporter-otlp" -version = "1.25.0" -description = "OpenTelemetry Collector Exporters" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_exporter_otlp-1.25.0-py3-none-any.whl", hash = "sha256:d67a831757014a3bc3174e4cd629ae1493b7ba8d189e8a007003cacb9f1a6b60"}, - {file = "opentelemetry_exporter_otlp-1.25.0.tar.gz", hash = "sha256:ce03199c1680a845f82e12c0a6a8f61036048c07ec7a0bd943142aca8fa6ced0"}, -] - -[package.dependencies] -opentelemetry-exporter-otlp-proto-grpc = "1.25.0" -opentelemetry-exporter-otlp-proto-http = "1.25.0" - [[package]] name = "opentelemetry-exporter-otlp-proto-common" version = "1.25.0" @@ -3386,26 +3319,6 @@ opentelemetry-exporter-otlp-proto-common = "1.25.0" opentelemetry-proto = "1.25.0" opentelemetry-sdk = ">=1.25.0,<1.26.0" -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.25.0" -description = "OpenTelemetry Collector Protobuf over HTTP Exporter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_exporter_otlp_proto_http-1.25.0-py3-none-any.whl", hash = "sha256:2eca686ee11b27acd28198b3ea5e5863a53d1266b91cda47c839d95d5e0541a6"}, - {file = "opentelemetry_exporter_otlp_proto_http-1.25.0.tar.gz", hash = "sha256:9f8723859e37c75183ea7afa73a3542f01d0fd274a5b97487ea24cb683d7d684"}, -] - -[package.dependencies] -deprecated = ">=1.2.6" -googleapis-common-protos = ">=1.52,<2.0" -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.25.0" -opentelemetry-proto = "1.25.0" -opentelemetry-sdk = ">=1.25.0,<1.26.0" -requests = ">=2.7,<3.0" - [[package]] name = "opentelemetry-instrumentation" version = "0.46b0" @@ -6948,4 +6861,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "a110f00488b7ecd31915d16e4805b9d18086ad3618f2f4e166c7ce44b48a9e47" +content-hash = "c7d9bfca776ec892535b6300af4b3ac49032b6d5a251e1ffe50bd7b7afce0a44" diff --git a/python/pyproject.toml b/python/pyproject.toml index 569f19e9656d..48a21e259a77 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -30,8 +30,6 @@ openapi_core = ">=0.18,<0.20" # OpenTelemetry opentelemetry-api = "^1.25.0" opentelemetry-sdk = "^1.25.0" -opentelemetry-exporter-otlp = "^1.25.0" -azure-monitor-opentelemetry-exporter = {version = "^1.0.0b27", allow-prereleases = true} prance = "^23.6.21.0" diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index 3df69a47c2dd..1f7428ff4fec 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -281,7 +281,7 @@ def _chat_message_content_to_dict(self, message: "ChatMessageContent") -> dict[s # endregion # region internal handlers - async def _send_chat_request(self, settings: OpenAIChatPromptExecutionSettings) -> list[ChatMessageContent]: + async def _send_chat_request(self, settings: OpenAIChatPromptExecutionSettings) -> list["ChatMessageContent"]: """Send the chat request.""" response = await self._send_request(request_settings=settings) @@ -309,7 +309,7 @@ async def _send_chat_stream_request( def _create_chat_message_content( self, response: ChatCompletion, choice: Choice, response_metadata: dict[str, Any] - ) -> ChatMessageContent: + ) -> "ChatMessageContent": """Create a chat message content object from a choice.""" metadata = self._get_metadata_from_chat_choice(choice) metadata.update(response_metadata) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py index 406cf6139ee6..6be5147dc6ea 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py @@ -36,7 +36,7 @@ async def get_text_contents( self, prompt: str, settings: "OpenAIPromptExecutionSettings", - ) -> list[TextContent]: + ) -> list["TextContent"]: """Executes a completion request and returns the result. Args: diff --git a/python/semantic_kernel/utils/tracing/decorators.py b/python/semantic_kernel/utils/tracing/decorators.py index 2394335a0cf3..be355113ecca 100644 --- a/python/semantic_kernel/utils/tracing/decorators.py +++ b/python/semantic_kernel/utils/tracing/decorators.py @@ -94,13 +94,9 @@ async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageConten with trace.use_span(span, end_on_exit=True): if completions: first_completion = completions[0] - response_id = first_completion.metadata.get("id", None) - if not response_id: - response_id = ( - first_completion.inner_content.get("id", None) - if first_completion.inner_content - else None - ) + response_id = first_completion.metadata.get("id") or (first_completion.inner_content or {}).get( + "id" + ) usage = first_completion.metadata.get("usage", None) prompt_tokens = usage.prompt_tokens if hasattr(usage, "prompt_tokens") else None completion_tokens = usage.completion_tokens if hasattr(usage, "completion_tokens") else None @@ -122,14 +118,14 @@ def _start_completion_activity( if not are_model_diagnostics_enabled(): return None - operation_name: str = "chat.completions" + OPERATION_NAME: str = "chat.completions" - span = tracer.start_span(f"{operation_name} {model_name}") + span = tracer.start_span(f"{OPERATION_NAME} {model_name}") # Set attributes on the span span.set_attributes( { - OPERATION: operation_name, + OPERATION: OPERATION_NAME, SYSTEM: model_provider, MODEL: model_name, } From 157ff32ad3a97c857981dc7326bc1e7fd060b29d Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 10 Jul 2024 15:17:26 -0700 Subject: [PATCH 13/26] Address PR comments + add trace_text_completion --- .../services/open_ai_text_completion_base.py | 6 +- .../functions/kernel_function_from_prompt.py | 3 +- .../utils/tracing/decorators.py | 169 ++++++++++-------- 3 files changed, 106 insertions(+), 72 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py index 6be5147dc6ea..6ae447095125 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py @@ -2,7 +2,7 @@ import logging from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar from openai import AsyncStream from openai.types import Completion, CompletionChoice @@ -18,6 +18,7 @@ from semantic_kernel.contents.streaming_text_content import StreamingTextContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.exceptions import ServiceInvalidResponseError +from semantic_kernel.utils.tracing.decorators import trace_text_completion if TYPE_CHECKING: from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( @@ -32,6 +33,9 @@ def get_prompt_execution_settings_class(self) -> "PromptExecutionSettings": """Create a request settings object.""" return OpenAITextPromptExecutionSettings + MODEL_PROVIDER_NAME: ClassVar[str] = "openai" + + @trace_text_completion(MODEL_PROVIDER_NAME) async def get_text_contents( self, prompt: str, diff --git a/python/semantic_kernel/functions/kernel_function_from_prompt.py b/python/semantic_kernel/functions/kernel_function_from_prompt.py index c83fba398eae..fb96ab5f3b71 100644 --- a/python/semantic_kernel/functions/kernel_function_from_prompt.py +++ b/python/semantic_kernel/functions/kernel_function_from_prompt.py @@ -187,7 +187,8 @@ async def _invoke_internal(self, context: FunctionInvocationContext) -> None: if isinstance(prompt_render_result.ai_service, TextCompletionClientBase): try: texts = await prompt_render_result.ai_service.get_text_contents( - unescape(prompt_render_result.rendered_prompt), prompt_render_result.execution_settings + prompt=unescape(prompt_render_result.rendered_prompt), + settings=prompt_render_result.execution_settings, ) except Exception as exc: raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc diff --git a/python/semantic_kernel/utils/tracing/decorators.py b/python/semantic_kernel/utils/tracing/decorators.py index be355113ecca..bddec94a9b68 100644 --- a/python/semantic_kernel/utils/tracing/decorators.py +++ b/python/semantic_kernel/utils/tracing/decorators.py @@ -12,13 +12,12 @@ from collections.abc import Callable from typing import Any -from opentelemetry import trace -from opentelemetry.trace import Span, StatusCode +from opentelemetry.trace import Span, StatusCode, get_tracer, use_span from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.contents.chat_history import ChatHistory -from semantic_kernel.contents.chat_message_content import ITEM_TYPES, ChatMessageContent -from semantic_kernel.contents.function_call_content import FunctionCallContent +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.text_content import TextContent from semantic_kernel.utils.tracing.const import ( COMPLETION_EVENT, COMPLETION_EVENT_COMPLETION, @@ -45,7 +44,7 @@ _enable_sensitive_events = os.getenv(OTEL_SENSITIVE_ENABLED_ENV_VAR, "false").lower() in ("true", "1", "t") # Creates a tracer from the global tracer provider -tracer = trace.get_tracer(__name__) +tracer = get_tracer(__name__) def are_model_diagnostics_enabled() -> bool: @@ -70,17 +69,17 @@ def trace_chat_completion(model_provider: str) -> Callable: def inner_trace_chat_completion(completion_func: Callable) -> Callable: @functools.wraps(completion_func) async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageContent]: + OPERATION_NAME: str = "chat.completions" + chat_history: ChatHistory = kwargs["chat_history"] settings: PromptExecutionSettings = kwargs["settings"] - if hasattr(settings, "ai_model_id") and settings.ai_model_id: - model_name = settings.ai_model_id - elif hasattr(args[0], "ai_model_id") and args[0].ai_model_id: - model_name = args[0].ai_model_id - else: - model_name = "unknown" + model_name = getattr(settings, "ai_model_id", None) or getattr(args[0], "ai_model_id", None) or "unknown" - span = _start_completion_activity(model_name, model_provider, chat_history, settings) + formatted_messages = ( + _messages_to_openai_format(chat_history.messages) if are_sensitive_events_enabled() else None + ) + span = _start_completion_activity(OPERATION_NAME, model_name, model_provider, formatted_messages, settings) try: completions: list[ChatMessageContent] = await completion_func(*args, **kwargs) @@ -90,18 +89,29 @@ async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageConten span.end() raise - if span: - with trace.use_span(span, end_on_exit=True): - if completions: - first_completion = completions[0] - response_id = first_completion.metadata.get("id") or (first_completion.inner_content or {}).get( - "id" - ) - usage = first_completion.metadata.get("usage", None) - prompt_tokens = usage.prompt_tokens if hasattr(usage, "prompt_tokens") else None - completion_tokens = usage.completion_tokens if hasattr(usage, "completion_tokens") else None + if span and completions: + with use_span(span, end_on_exit=True): + first_completion = completions[0] + response_id = first_completion.metadata.get("id") or (first_completion.inner_content or {}).get( + "id" + ) + usage = first_completion.metadata.get("usage", None) + prompt_tokens = getattr(usage, "prompt_tokens", None) + completion_tokens = getattr(usage, "completion_tokens", None) + + completion_text: str | None = ( + _messages_to_openai_format(completions) if are_sensitive_events_enabled() else None + ) + + finish_reasons: list[str] = [str(completion.finish_reason) for completion in completions] + _set_completion_response( - span, completions, response_id or "unknown", prompt_tokens, completion_tokens + span, + completion_text, + finish_reasons, + response_id or "unknown", + prompt_tokens, + completion_tokens, ) return completions @@ -111,21 +121,73 @@ async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageConten return inner_trace_chat_completion +def trace_text_completion(model_provider: str) -> Callable: + """Decorator to trace text completion activities.""" + + def inner_trace_text_completion(completion_func: Callable) -> Callable: + @functools.wraps(completion_func) + async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[TextContent]: + OPERATION_NAME: str = "text.completions" + + prompt: str = kwargs["prompt"] + settings: PromptExecutionSettings = kwargs["settings"] + + model_name = getattr(settings, "ai_model_id", None) or getattr(args[0], "ai_model_id", None) or "unknown" + + span = _start_completion_activity(OPERATION_NAME, model_name, model_provider, prompt, settings) + + try: + completions: list[TextContent] = await completion_func(*args, **kwargs) + except Exception as exception: + if span: + _set_completion_error(span, exception) + span.end() + raise + + if span and completions: + with use_span(span, end_on_exit=True): + first_completion = completions[0] + response_id = first_completion.metadata.get("id") or (first_completion.inner_content or {}).get( + "id" + ) + usage = first_completion.metadata.get("usage", None) + prompt_tokens = getattr(usage, "prompt_tokens", None) + completion_tokens = getattr(usage, "completion_tokens", None) + + completion_text: str | None = ( + json.dumps([completion.text for completion in completions]) + if are_sensitive_events_enabled() + else None + ) + + _set_completion_response( + span, completion_text, None, response_id or "unknown", prompt_tokens, completion_tokens + ) + + return completions + + return wrapper_decorator + + return inner_trace_text_completion + + def _start_completion_activity( - model_name: str, model_provider: str, chat_history: ChatHistory, execution_settings: PromptExecutionSettings | None + operation_name: str, + model_name: str, + model_provider: str, + prompt: str | None, + execution_settings: PromptExecutionSettings | None, ) -> Span | None: """Start a text or chat completion activity for a given model.""" if not are_model_diagnostics_enabled(): return None - OPERATION_NAME: str = "chat.completions" - - span = tracer.start_span(f"{OPERATION_NAME} {model_name}") + span = tracer.start_span(f"{operation_name} {model_name}") # Set attributes on the span span.set_attributes( { - OPERATION: OPERATION_NAME, + OPERATION: operation_name, SYSTEM: model_provider, MODEL: model_name, } @@ -145,15 +207,15 @@ def _start_completion_activity( span.set_attribute(TOP_P, attribute) if are_sensitive_events_enabled(): - formatted_messages = _messages_to_openai_format(chat_history.messages) - span.add_event(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: formatted_messages}) + span.add_event(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: prompt}) return span def _set_completion_response( span: Span, - completions: list[ChatMessageContent], + completion_text: str, + finish_reasons: list[str] | None, response_id: str, prompt_tokens: int | None = None, completion_tokens: int | None = None, @@ -164,11 +226,8 @@ def _set_completion_response( span.set_attribute(RESPONSE_ID, response_id) - finish_reasons: list[str] = [str(content.finish_reason) for content in completions] - span.set_attribute(FINISH_REASON, ",".join(finish_reasons)) - - if are_sensitive_events_enabled() and completions: - span.add_event(COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: _messages_to_openai_format(completions)}) + if finish_reasons: + span.set_attribute(FINISH_REASON, ",".join(finish_reasons)) if prompt_tokens: span.set_attribute(PROMPT_TOKEN, prompt_tokens) @@ -176,6 +235,9 @@ def _set_completion_response( if completion_tokens: span.set_attribute(COMPLETION_TOKEN, completion_tokens) + if are_sensitive_events_enabled() and completion_text: + span.add_event(COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: completion_text}) + def _set_completion_error(span: Span, error: Exception) -> None: """Set an error for a text or chat completion .""" @@ -193,37 +255,4 @@ def _messages_to_openai_format(messages: list[ChatMessageContent]) -> str: OpenTelemetry recommends formatting the messages in the OpenAI format regardless of the actual model being used. """ - formatted_messages = [ - json.dumps( - { - "role": message.role, - "content": json.dumps(message.content), - **( - {"tool_calls": _tool_calls_to_openai_format(message.items)} - if any(isinstance(item, FunctionCallContent) for item in message.items) - else {} - ), - } - ) - for message in messages - ] - - return "[{}]".format(", \n".join(formatted_messages)) - - -def _tool_calls_to_openai_format(items: list[ITEM_TYPES]) -> str: - """Convert a list of FunctionCallContent to a string in the OpenAI format. - - OpenTelemetry recommends formatting the messages in the OpenAI format - regardless of the actual model being used. - """ - tool_calls: list[str] = [] - for item in items: - if isinstance(item, FunctionCallContent): - tool_call = { - "id": item.id, - "function": {"arguments": json.dumps(item.arguments), "name": item.function_name}, - "type": "function", - } - tool_calls.append(json.dumps(tool_call)) - return f"[{', '.join(tool_calls)}]" + return json.dumps([message.to_dict() for message in messages]) From 1d94e81c7d792d03d883513ace0fd804f146a1a5 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 10 Jul 2024 16:30:24 -0700 Subject: [PATCH 14/26] Adjust poetry lock --- python/poetry.lock | 95 ++++++++++++++++++++++--------------------- python/pyproject.toml | 4 +- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/python/poetry.lock b/python/poetry.lock index 17eb9b3b55c2..e7d3c431f858 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -2372,6 +2372,7 @@ python-versions = ">=3.7" files = [ {file = "milvus_lite-2.4.7-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:c828190118b104b05b8c8e0b5a4147811c86b54b8fb67bc2e726ad10fc0b544e"}, {file = "milvus_lite-2.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e1537633c39879714fb15082be56a4b97f74c905a6e98e302ec01320561081af"}, + {file = "milvus_lite-2.4.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:fcb909d38c83f21478ca9cb500c84264f988c69f62715ae9462e966767fb76dd"}, {file = "milvus_lite-2.4.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f016474d663045787dddf1c3aad13b7d8b61fd329220318f858184918143dcbf"}, ] @@ -3271,42 +3272,42 @@ openapi-schema-validator = ">=0.6.0,<0.7.0" [[package]] name = "opentelemetry-api" -version = "1.25.0" +version = "1.24.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_api-1.25.0-py3-none-any.whl", hash = "sha256:757fa1aa020a0f8fa139f8959e53dec2051cc26b832e76fa839a6d76ecefd737"}, - {file = "opentelemetry_api-1.25.0.tar.gz", hash = "sha256:77c4985f62f2614e42ce77ee4c9da5fa5f0bc1e1821085e9a47533a9323ae869"}, + {file = "opentelemetry_api-1.24.0-py3-none-any.whl", hash = "sha256:0f2c363d98d10d1ce93330015ca7fd3a65f60be64e05e30f557c61de52c80ca2"}, + {file = "opentelemetry_api-1.24.0.tar.gz", hash = "sha256:42719f10ce7b5a9a73b10a4baf620574fb8ad495a9cbe5c18d76b75d8689c67e"}, ] [package.dependencies] deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<=7.1" +importlib-metadata = ">=6.0,<=7.0" [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.25.0" +version = "1.24.0" description = "OpenTelemetry Protobuf encoding" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.25.0-py3-none-any.whl", hash = "sha256:15637b7d580c2675f70246563363775b4e6de947871e01d0f4e3881d1848d693"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.25.0.tar.gz", hash = "sha256:c93f4e30da4eee02bacd1e004eb82ce4da143a2f8e15b987a9f603e0a85407d3"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.24.0-py3-none-any.whl", hash = "sha256:e51f2c9735054d598ad2df5d3eca830fecfb5b0bda0a2fa742c9c7718e12f641"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.24.0.tar.gz", hash = "sha256:5d31fa1ff976cacc38be1ec4e3279a3f88435c75b38b1f7a099a1faffc302461"}, ] [package.dependencies] -opentelemetry-proto = "1.25.0" +opentelemetry-proto = "1.24.0" [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.25.0" +version = "1.24.0" description = "OpenTelemetry Collector Protobuf over gRPC Exporter" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.25.0-py3-none-any.whl", hash = "sha256:3131028f0c0a155a64c430ca600fd658e8e37043cb13209f0109db5c1a3e4eb4"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.25.0.tar.gz", hash = "sha256:c0b1661415acec5af87625587efa1ccab68b873745ca0ee96b69bb1042087eac"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.24.0-py3-none-any.whl", hash = "sha256:f40d62aa30a0a43cc1657428e59fcf82ad5f7ea8fff75de0f9d9cb6f739e0a3b"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.24.0.tar.gz", hash = "sha256:217c6e30634f2c9797999ea9da29f7300479a94a610139b9df17433f915e7baa"}, ] [package.dependencies] @@ -3314,19 +3315,22 @@ deprecated = ">=1.2.6" googleapis-common-protos = ">=1.52,<2.0" grpcio = ">=1.0.0,<2.0.0" opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.25.0" -opentelemetry-proto = "1.25.0" -opentelemetry-sdk = ">=1.25.0,<1.26.0" +opentelemetry-exporter-otlp-proto-common = "1.24.0" +opentelemetry-proto = "1.24.0" +opentelemetry-sdk = ">=1.24.0,<1.25.0" + +[package.extras] +test = ["pytest-grpc"] [[package]] name = "opentelemetry-instrumentation" -version = "0.46b0" +version = "0.45b0" description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_instrumentation-0.46b0-py3-none-any.whl", hash = "sha256:89cd721b9c18c014ca848ccd11181e6b3fd3f6c7669e35d59c48dc527408c18b"}, - {file = "opentelemetry_instrumentation-0.46b0.tar.gz", hash = "sha256:974e0888fb2a1e01c38fbacc9483d024bb1132aad92d6d24e2e5543887a7adda"}, + {file = "opentelemetry_instrumentation-0.45b0-py3-none-any.whl", hash = "sha256:06c02e2c952c1b076e8eaedf1b82f715e2937ba7eeacab55913dd434fbcec258"}, + {file = "opentelemetry_instrumentation-0.45b0.tar.gz", hash = "sha256:6c47120a7970bbeb458e6a73686ee9ba84b106329a79e4a4a66761f933709c7e"}, ] [package.dependencies] @@ -3336,55 +3340,55 @@ wrapt = ">=1.0.0,<2.0.0" [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.46b0" +version = "0.45b0" description = "ASGI instrumentation for OpenTelemetry" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_instrumentation_asgi-0.46b0-py3-none-any.whl", hash = "sha256:f13c55c852689573057837a9500aeeffc010c4ba59933c322e8f866573374759"}, - {file = "opentelemetry_instrumentation_asgi-0.46b0.tar.gz", hash = "sha256:02559f30cf4b7e2a737ab17eb52aa0779bcf4cc06573064f3e2cb4dcc7d3040a"}, + {file = "opentelemetry_instrumentation_asgi-0.45b0-py3-none-any.whl", hash = "sha256:8be1157ed62f0db24e45fdf7933c530c4338bd025c5d4af7830e903c0756021b"}, + {file = "opentelemetry_instrumentation_asgi-0.45b0.tar.gz", hash = "sha256:97f55620f163fd3d20323e9fd8dc3aacc826c03397213ff36b877e0f4b6b08a6"}, ] [package.dependencies] asgiref = ">=3.0,<4.0" opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.46b0" -opentelemetry-semantic-conventions = "0.46b0" -opentelemetry-util-http = "0.46b0" +opentelemetry-instrumentation = "0.45b0" +opentelemetry-semantic-conventions = "0.45b0" +opentelemetry-util-http = "0.45b0" [package.extras] instruments = ["asgiref (>=3.0,<4.0)"] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.46b0" +version = "0.45b0" description = "OpenTelemetry FastAPI Instrumentation" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_instrumentation_fastapi-0.46b0-py3-none-any.whl", hash = "sha256:e0f5d150c6c36833dd011f0e6ef5ede6d7406c1aed0c7c98b2d3b38a018d1b33"}, - {file = "opentelemetry_instrumentation_fastapi-0.46b0.tar.gz", hash = "sha256:928a883a36fc89f9702f15edce43d1a7104da93d740281e32d50ffd03dbb4365"}, + {file = "opentelemetry_instrumentation_fastapi-0.45b0-py3-none-any.whl", hash = "sha256:77d9c123a363129148f5f66d44094f3d67aaaa2b201396d94782b4a7f9ce4314"}, + {file = "opentelemetry_instrumentation_fastapi-0.45b0.tar.gz", hash = "sha256:5a6b91e1c08a01601845fcfcfdefd0a2aecdb3c356d4a436a3210cb58c21487e"}, ] [package.dependencies] opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.46b0" -opentelemetry-instrumentation-asgi = "0.46b0" -opentelemetry-semantic-conventions = "0.46b0" -opentelemetry-util-http = "0.46b0" +opentelemetry-instrumentation = "0.45b0" +opentelemetry-instrumentation-asgi = "0.45b0" +opentelemetry-semantic-conventions = "0.45b0" +opentelemetry-util-http = "0.45b0" [package.extras] instruments = ["fastapi (>=0.58,<1.0)"] [[package]] name = "opentelemetry-proto" -version = "1.25.0" +version = "1.24.0" description = "OpenTelemetry Python Proto" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_proto-1.25.0-py3-none-any.whl", hash = "sha256:f07e3341c78d835d9b86665903b199893befa5e98866f63d22b00d0b7ca4972f"}, - {file = "opentelemetry_proto-1.25.0.tar.gz", hash = "sha256:35b6ef9dc4a9f7853ecc5006738ad40443701e52c26099e197895cbda8b815a3"}, + {file = "opentelemetry_proto-1.24.0-py3-none-any.whl", hash = "sha256:bcb80e1e78a003040db71ccf83f2ad2019273d1e0828089d183b18a1476527ce"}, + {file = "opentelemetry_proto-1.24.0.tar.gz", hash = "sha256:ff551b8ad63c6cabb1845ce217a6709358dfaba0f75ea1fa21a61ceddc78cab8"}, ] [package.dependencies] @@ -3392,43 +3396,40 @@ protobuf = ">=3.19,<5.0" [[package]] name = "opentelemetry-sdk" -version = "1.25.0" +version = "1.24.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_sdk-1.25.0-py3-none-any.whl", hash = "sha256:d97ff7ec4b351692e9d5a15af570c693b8715ad78b8aafbec5c7100fe966b4c9"}, - {file = "opentelemetry_sdk-1.25.0.tar.gz", hash = "sha256:ce7fc319c57707ef5bf8b74fb9f8ebdb8bfafbe11898410e0d2a761d08a98ec7"}, + {file = "opentelemetry_sdk-1.24.0-py3-none-any.whl", hash = "sha256:fa731e24efe832e98bcd90902085b359dcfef7d9c9c00eb5b9a18587dae3eb59"}, + {file = "opentelemetry_sdk-1.24.0.tar.gz", hash = "sha256:75bc0563affffa827700e0f4f4a68e1e257db0df13372344aebc6f8a64cde2e5"}, ] [package.dependencies] -opentelemetry-api = "1.25.0" -opentelemetry-semantic-conventions = "0.46b0" +opentelemetry-api = "1.24.0" +opentelemetry-semantic-conventions = "0.45b0" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.46b0" +version = "0.45b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_semantic_conventions-0.46b0-py3-none-any.whl", hash = "sha256:6daef4ef9fa51d51855d9f8e0ccd3a1bd59e0e545abe99ac6203804e36ab3e07"}, - {file = "opentelemetry_semantic_conventions-0.46b0.tar.gz", hash = "sha256:fbc982ecbb6a6e90869b15c1673be90bd18c8a56ff1cffc0864e38e2edffaefa"}, + {file = "opentelemetry_semantic_conventions-0.45b0-py3-none-any.whl", hash = "sha256:a4a6fb9a7bacd9167c082aa4681009e9acdbfa28ffb2387af50c2fef3d30c864"}, + {file = "opentelemetry_semantic_conventions-0.45b0.tar.gz", hash = "sha256:7c84215a44ac846bc4b8e32d5e78935c5c43482e491812a0bb8aaf87e4d92118"}, ] -[package.dependencies] -opentelemetry-api = "1.25.0" - [[package]] name = "opentelemetry-util-http" -version = "0.46b0" +version = "0.45b0" description = "Web util for OpenTelemetry" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_util_http-0.46b0-py3-none-any.whl", hash = "sha256:8dc1949ce63caef08db84ae977fdc1848fe6dc38e6bbaad0ae3e6ecd0d451629"}, - {file = "opentelemetry_util_http-0.46b0.tar.gz", hash = "sha256:03b6e222642f9c7eae58d9132343e045b50aca9761fcb53709bd2b663571fdf6"}, + {file = "opentelemetry_util_http-0.45b0-py3-none-any.whl", hash = "sha256:6628868b501b3004e1860f976f410eeb3d3499e009719d818000f24ce17b6e33"}, + {file = "opentelemetry_util_http-0.45b0.tar.gz", hash = "sha256:4ce08b6a7d52dd7c96b7705b5b4f06fdb6aa3eac1233b3b0bfef8a0cab9a92cd"}, ] [[package]] diff --git a/python/pyproject.toml b/python/pyproject.toml index a2963f1865b6..2730b921b92c 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -28,8 +28,8 @@ openai = ">=1.0" openapi_core = ">=0.18,<0.20" # OpenTelemetry -opentelemetry-api = "^1.25.0" -opentelemetry-sdk = "^1.25.0" +opentelemetry-api = "^1.24.0" +opentelemetry-sdk = "^1.24.0" prance = "^23.6.21.0" From 386b12165f88d32ff029a51bd699455b28ef6353 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 10 Jul 2024 16:52:15 -0700 Subject: [PATCH 15/26] Fix mypy warnings --- python/semantic_kernel/utils/tracing/decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/utils/tracing/decorators.py b/python/semantic_kernel/utils/tracing/decorators.py index bddec94a9b68..d460a477bd82 100644 --- a/python/semantic_kernel/utils/tracing/decorators.py +++ b/python/semantic_kernel/utils/tracing/decorators.py @@ -206,7 +206,7 @@ def _start_completion_activity( if attribute: span.set_attribute(TOP_P, attribute) - if are_sensitive_events_enabled(): + if are_sensitive_events_enabled() and prompt: span.add_event(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: prompt}) return span @@ -214,7 +214,7 @@ def _start_completion_activity( def _set_completion_response( span: Span, - completion_text: str, + completion_text: str | None, finish_reasons: list[str] | None, response_id: str, prompt_tokens: int | None = None, From c903017be609c79bf470d197c1d7601f835658e5 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 10 Jul 2024 20:39:25 -0700 Subject: [PATCH 16/26] Sync poetry.lock --- python/poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/poetry.lock b/python/poetry.lock index e7d3c431f858..09d9ea9b9701 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -2372,7 +2372,6 @@ python-versions = ">=3.7" files = [ {file = "milvus_lite-2.4.7-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:c828190118b104b05b8c8e0b5a4147811c86b54b8fb67bc2e726ad10fc0b544e"}, {file = "milvus_lite-2.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e1537633c39879714fb15082be56a4b97f74c905a6e98e302ec01320561081af"}, - {file = "milvus_lite-2.4.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:fcb909d38c83f21478ca9cb500c84264f988c69f62715ae9462e966767fb76dd"}, {file = "milvus_lite-2.4.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f016474d663045787dddf1c3aad13b7d8b61fd329220318f858184918143dcbf"}, ] @@ -3111,6 +3110,7 @@ description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" files = [ + {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_aarch64.whl", hash = "sha256:004186d5ea6a57758fd6d57052a123c73a4815adf365eb8dd6a85c9eaa7535ff"}, {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d9714f27c1d0f0895cd8915c07a87a1d0029a0aa36acaf9156952ec2a8a12189"}, {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-win_amd64.whl", hash = "sha256:c3401dc8543b52d3a8158007a0c1ab4e9c768fcbd24153a48c86972102197ddd"}, ] @@ -6862,4 +6862,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "3d6338982c9871c48bb1ed02967504967163767b0afaf50e96a1b14aa2fe0344" +content-hash = "d22dd422b521e9f1e91fd540a6dbbdbdad72746a42f46602522a312c048eff07" From dd2576083ee4f4f212029015c3033f1ce04073d0 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 15 Jul 2024 11:49:35 -0700 Subject: [PATCH 17/26] Fix ruff warning after merge --- .../ai/open_ai/services/open_ai_text_completion_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py index 0d8a70c49926..505291402279 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py @@ -41,7 +41,6 @@ class OpenAITextCompletionBase(OpenAIHandler, TextCompletionClientBase): def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]: return OpenAITextPromptExecutionSettings - @override @trace_text_completion(MODEL_PROVIDER_NAME) async def get_text_contents( From d95c8d2353c8ea01b93dcbd1792d7645e0040d40 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 15 Jul 2024 14:54:21 -0700 Subject: [PATCH 18/26] Use kwargs for prompt, chat_history and settings --- .../ai/chat_completion_client_base.py | 2 +- .../ai/text_completion_client_base.py | 2 +- ...test_azure_ai_inference_chat_completion.py | 22 ++++---- .../test_mistralai_chat_completion.py | 50 ++++++++----------- .../services/test_ollama_chat_completion.py | 12 +++-- .../services/test_ollama_text_completion.py | 4 +- .../services/test_azure_chat_completion.py | 14 ++++-- .../services/test_azure_text_completion.py | 4 +- 8 files changed, 55 insertions(+), 55 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/chat_completion_client_base.py b/python/semantic_kernel/connectors/ai/chat_completion_client_base.py index e1823d571756..ae24136838c1 100644 --- a/python/semantic_kernel/connectors/ai/chat_completion_client_base.py +++ b/python/semantic_kernel/connectors/ai/chat_completion_client_base.py @@ -50,7 +50,7 @@ async def get_chat_message_content( Returns: A string representing the response from the LLM. """ - results = await self.get_chat_message_contents(chat_history, settings, **kwargs) + results = await self.get_chat_message_contents(chat_history=chat_history, settings=settings, **kwargs) if results: return results[0] # this should not happen, should error out before returning an empty list diff --git a/python/semantic_kernel/connectors/ai/text_completion_client_base.py b/python/semantic_kernel/connectors/ai/text_completion_client_base.py index 2b81baf9cd66..c03d30a6d2e3 100644 --- a/python/semantic_kernel/connectors/ai/text_completion_client_base.py +++ b/python/semantic_kernel/connectors/ai/text_completion_client_base.py @@ -40,7 +40,7 @@ async def get_text_content(self, prompt: str, settings: "PromptExecutionSettings Returns: TextContent: A string or list of strings representing the response(s) from the LLM. """ - result = await self.get_text_contents(prompt, settings) + result = await self.get_text_contents(prompt=prompt, settings=settings) if result: return result[0] # this should not happen, should error out before returning an empty list diff --git a/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_chat_completion.py b/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_chat_completion.py index ccd9ffd2ecc5..a3b2d6e71bcb 100644 --- a/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_chat_completion.py +++ b/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_chat_completion.py @@ -99,7 +99,7 @@ async def test_azure_ai_inference_chat_completion( mock_complete.return_value = mock_azure_ai_inference_chat_completion_response - responses = await azure_ai_inference_service.get_chat_message_contents(chat_history, settings) + responses = await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings) mock_complete.assert_awaited_once_with( messages=[UserMessage(content=user_message_content)], @@ -140,7 +140,7 @@ async def test_azure_ai_inference_chat_completion_with_standard_parameters( mock_complete.return_value = mock_azure_ai_inference_chat_completion_response - responses = await azure_ai_inference_service.get_chat_message_contents(chat_history, settings) + responses = await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings) mock_complete.assert_awaited_once_with( messages=[UserMessage(content=user_message_content)], @@ -180,7 +180,7 @@ async def test_azure_ai_inference_chat_completion_with_extra_parameters( mock_complete.return_value = mock_azure_ai_inference_chat_completion_response - responses = await azure_ai_inference_service.get_chat_message_contents(chat_history, settings) + responses = await azure_ai_inference_service.get_chat_message_contents(chat_history=chat_history, settings=settings) mock_complete.assert_awaited_once_with( messages=[UserMessage(content=user_message_content)], @@ -211,8 +211,8 @@ async def test_azure_ai_inference_chat_completion_with_function_choice_behavior_ function_choice_behavior=FunctionChoiceBehavior.Auto(), ) await azure_ai_inference_service.get_chat_message_contents( - chat_history, - settings, + chat_history=chat_history, + settings=settings, arguments=KernelArguments(), ) @@ -223,8 +223,8 @@ async def test_azure_ai_inference_chat_completion_with_function_choice_behavior_ extra_parameters={"n": 2}, ) await azure_ai_inference_service.get_chat_message_contents( - chat_history, - settings, + chat_history=chat_history, + settings=settings, kernel=kernel, arguments=KernelArguments(), ) @@ -256,8 +256,8 @@ async def test_azure_ai_inference_chat_completion_with_function_choice_behavior( mock_complete.return_value = mock_azure_ai_inference_chat_completion_response_with_tool_call responses = await azure_ai_inference_service.get_chat_message_contents( - chat_history, - settings, + chat_history=chat_history, + settings=settings, kernel=kernel, arguments=KernelArguments(), ) @@ -296,8 +296,8 @@ async def test_azure_ai_inference_chat_completion_with_function_choice_behavior_ mock_complete.return_value = mock_azure_ai_inference_chat_completion_response responses = await azure_ai_inference_service.get_chat_message_contents( - chat_history, - settings, + chat_history=chat_history, + settings=settings, kernel=kernel, arguments=KernelArguments(), ) diff --git a/python/tests/unit/connectors/mistral_ai/services/test_mistralai_chat_completion.py b/python/tests/unit/connectors/mistral_ai/services/test_mistralai_chat_completion.py index ba1b0b51aa7b..1fe0a868a9ff 100644 --- a/python/tests/unit/connectors/mistral_ai/services/test_mistralai_chat_completion.py +++ b/python/tests/unit/connectors/mistral_ai/services/test_mistralai_chat_completion.py @@ -27,9 +27,7 @@ def mock_settings() -> MistralAIChatPromptExecutionSettings: def mock_mistral_ai_client_completion() -> MistralAsyncClient: client = MagicMock(spec=MistralAsyncClient) chat_completion_response = AsyncMock() - choices = [ - MagicMock(finish_reason="stop", message=MagicMock(role="assistant", content="Test")) - ] + choices = [MagicMock(finish_reason="stop", message=MagicMock(role="assistant", content="Test"))] chat_completion_response.choices = choices client.chat.return_value = chat_completion_response return client @@ -41,8 +39,8 @@ def mock_mistral_ai_client_completion_stream() -> MistralAsyncClient: chat_completion_response = MagicMock() choices = [ MagicMock(finish_reason="stop", delta=MagicMock(role="assistant", content="Test")), - MagicMock(finish_reason="stop", delta=MagicMock(role="assistant", content="Test", tool_calls=None)) - ] + MagicMock(finish_reason="stop", delta=MagicMock(role="assistant", content="Test", tool_calls=None)), + ] chat_completion_response.choices = choices chat_completion_response_empty = MagicMock() chat_completion_response_empty.choices = [] @@ -54,9 +52,9 @@ def mock_mistral_ai_client_completion_stream() -> MistralAsyncClient: @pytest.mark.asyncio async def test_complete_chat_contents( - kernel: Kernel, + kernel: Kernel, mock_settings: MistralAIChatPromptExecutionSettings, - mock_mistral_ai_client_completion: MistralAsyncClient + mock_mistral_ai_client_completion: MistralAsyncClient, ): chat_history = MagicMock() arguments = KernelArguments() @@ -65,7 +63,7 @@ async def test_complete_chat_contents( ) content: list[ChatMessageContent] = await chat_completion_base.get_chat_message_contents( - chat_history, mock_settings, kernel=kernel, arguments=arguments + chat_history=chat_history, settings=mock_settings, kernel=kernel, arguments=arguments ) assert content is not None @@ -74,15 +72,16 @@ async def test_complete_chat_contents( async def test_complete_chat_stream_contents( kernel: Kernel, mock_settings: MistralAIChatPromptExecutionSettings, - mock_mistral_ai_client_completion_stream: MistralAsyncClient + mock_mistral_ai_client_completion_stream: MistralAsyncClient, ): chat_history = MagicMock() arguments = KernelArguments() chat_completion_base = MistralAIChatCompletion( - ai_model_id="test_model_id", - service_id="test", api_key="", - async_client=mock_mistral_ai_client_completion_stream + ai_model_id="test_model_id", + service_id="test", + api_key="", + async_client=mock_mistral_ai_client_completion_stream, ) async for content in chat_completion_base.get_streaming_chat_message_contents( @@ -99,14 +98,12 @@ async def test_mistral_ai_sdk_exception(kernel: Kernel, mock_settings: MistralAI client.chat.side_effect = Exception("Test Exception") chat_completion_base = MistralAIChatCompletion( - ai_model_id="test_model_id", - service_id="test", api_key="", - async_client=client + ai_model_id="test_model_id", service_id="test", api_key="", async_client=client ) with pytest.raises(ServiceResponseException): await chat_completion_base.get_chat_message_contents( - chat_history, mock_settings, kernel=kernel, arguments=arguments + chat_history=chat_history, settings=mock_settings, kernel=kernel, arguments=arguments ) @@ -126,7 +123,7 @@ async def test_mistral_ai_sdk_exception_streaming(kernel: Kernel, mock_settings: chat_history, mock_settings, kernel=kernel, arguments=arguments ): assert content is not None - + def test_mistral_ai_chat_completion_init(mistralai_unit_test_env) -> None: # Test successful initialization @@ -162,21 +159,16 @@ def test_prompt_execution_settings_class(mistralai_unit_test_env): @pytest.mark.asyncio -async def test_with_different_execution_settings( - kernel: Kernel, - mock_mistral_ai_client_completion: MagicMock -): +async def test_with_different_execution_settings(kernel: Kernel, mock_mistral_ai_client_completion: MagicMock): chat_history = MagicMock() settings = OpenAIChatPromptExecutionSettings(temperature=0.2, seed=2) arguments = KernelArguments() chat_completion_base = MistralAIChatCompletion( - ai_model_id="test_model_id", - service_id="test", api_key="", - async_client=mock_mistral_ai_client_completion + ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_mistral_ai_client_completion ) await chat_completion_base.get_chat_message_contents( - chat_history, settings, kernel=kernel, arguments=arguments + chat_history=chat_history, settings=settings, kernel=kernel, arguments=arguments ) assert mock_mistral_ai_client_completion.chat.call_args.kwargs["temperature"] == 0.2 assert mock_mistral_ai_client_completion.chat.call_args.kwargs["seed"] == 2 @@ -184,16 +176,16 @@ async def test_with_different_execution_settings( @pytest.mark.asyncio async def test_with_different_execution_settings_stream( - kernel: Kernel, - mock_mistral_ai_client_completion_stream: MagicMock + kernel: Kernel, mock_mistral_ai_client_completion_stream: MagicMock ): chat_history = MagicMock() settings = OpenAIChatPromptExecutionSettings(temperature=0.2, seed=2) arguments = KernelArguments() chat_completion_base = MistralAIChatCompletion( ai_model_id="test_model_id", - service_id="test", api_key="", - async_client=mock_mistral_ai_client_completion_stream + service_id="test", + api_key="", + async_client=mock_mistral_ai_client_completion_stream, ) async for chunk in chat_completion_base.get_streaming_chat_message_contents( diff --git a/python/tests/unit/connectors/ollama/services/test_ollama_chat_completion.py b/python/tests/unit/connectors/ollama/services/test_ollama_chat_completion.py index c83931ecf657..852ae8f57544 100644 --- a/python/tests/unit/connectors/ollama/services/test_ollama_chat_completion.py +++ b/python/tests/unit/connectors/ollama/services/test_ollama_chat_completion.py @@ -24,8 +24,10 @@ async def test_complete_chat(mock_post): chat_history = ChatHistory() chat_history.add_user_message("test_prompt") response = await ollama.get_chat_message_contents( - chat_history, - OllamaChatPromptExecutionSettings(service_id="test_model", ai_model_id="test_model", options={"test": "test"}), + chat_history=chat_history, + settings=OllamaChatPromptExecutionSettings( + service_id="test_model", ai_model_id="test_model", options={"test": "test"} + ), ) assert response[0].content == "test_response" mock_post.assert_called_once_with( @@ -45,8 +47,10 @@ async def test_complete(mock_post): mock_post.return_value = MockResponse(response={"message": {"content": "test_response"}}) ollama = OllamaChatCompletion(ai_model_id="test_model") response = await ollama.get_text_contents( - "test_prompt", - OllamaChatPromptExecutionSettings(service_id="test_model", ai_model_id="test_model", options={"test": "test"}), + prompt="test_prompt", + settings=OllamaChatPromptExecutionSettings( + service_id="test_model", ai_model_id="test_model", options={"test": "test"} + ), ) assert response[0].text == "test_response" diff --git a/python/tests/unit/connectors/ollama/services/test_ollama_text_completion.py b/python/tests/unit/connectors/ollama/services/test_ollama_text_completion.py index 772a3a13e734..b060c05ad69e 100644 --- a/python/tests/unit/connectors/ollama/services/test_ollama_text_completion.py +++ b/python/tests/unit/connectors/ollama/services/test_ollama_text_completion.py @@ -21,8 +21,8 @@ async def test_complete(mock_post): mock_post.return_value = MockResponse(response={"response": "test_response"}) ollama = OllamaTextCompletion(ai_model_id="test_model") response = await ollama.get_text_contents( - "test prompt", - OllamaTextPromptExecutionSettings(options={"test": "test"}), + prompt="test prompt", + settings=OllamaTextPromptExecutionSettings(options={"test": "test"}), ) assert response[0].text == "test_response" diff --git a/python/tests/unit/connectors/open_ai/services/test_azure_chat_completion.py b/python/tests/unit/connectors/open_ai/services/test_azure_chat_completion.py index e18d223f6453..cfe96401ae9a 100644 --- a/python/tests/unit/connectors/open_ai/services/test_azure_chat_completion.py +++ b/python/tests/unit/connectors/open_ai/services/test_azure_chat_completion.py @@ -653,7 +653,7 @@ async def test_content_filtering_raises_correct_exception( with pytest.raises(ContentFilterAIException, match="service encountered a content error") as exc_info: await azure_chat_completion.get_chat_message_contents( - chat_history, complete_prompt_execution_settings, kernel=kernel + chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel ) content_filter_exc = exc_info.value @@ -696,7 +696,7 @@ async def test_content_filtering_without_response_code_raises_with_default_code( with pytest.raises(ContentFilterAIException, match="service encountered a content error"): await azure_chat_completion.get_chat_message_contents( - chat_history, complete_prompt_execution_settings, kernel=kernel + chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel ) @@ -718,7 +718,7 @@ async def test_bad_request_non_content_filter( with pytest.raises(ServiceResponseException, match="service failed to complete the prompt"): await azure_chat_completion.get_chat_message_contents( - chat_history, complete_prompt_execution_settings, kernel=kernel + chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel ) @@ -744,7 +744,9 @@ async def test_no_kernel_provided_throws_error( ServiceInvalidExecutionSettingsError, match="The kernel is required for OpenAI tool calls.", ): - await azure_chat_completion.get_chat_message_contents(chat_history, complete_prompt_execution_settings) + await azure_chat_completion.get_chat_message_contents( + chat_history=chat_history, settings=complete_prompt_execution_settings + ) @pytest.mark.asyncio @@ -769,7 +771,9 @@ async def test_auto_invoke_false_no_kernel_provided_throws_error( ServiceInvalidExecutionSettingsError, match="The kernel is required for OpenAI tool calls.", ): - await azure_chat_completion.get_chat_message_contents(chat_history, complete_prompt_execution_settings) + await azure_chat_completion.get_chat_message_contents( + chat_history=chat_history, settings=complete_prompt_execution_settings + ) @pytest.mark.asyncio diff --git a/python/tests/unit/connectors/open_ai/services/test_azure_text_completion.py b/python/tests/unit/connectors/open_ai/services/test_azure_text_completion.py index d188ac4416e5..f4fb67fe3ed4 100644 --- a/python/tests/unit/connectors/open_ai/services/test_azure_text_completion.py +++ b/python/tests/unit/connectors/open_ai/services/test_azure_text_completion.py @@ -103,7 +103,7 @@ async def test_call_with_parameters( complete_prompt_execution_settings = OpenAITextPromptExecutionSettings() azure_text_completion = AzureTextCompletion() - await azure_text_completion.get_text_contents(prompt, complete_prompt_execution_settings) + await azure_text_completion.get_text_contents(prompt=prompt, settings=complete_prompt_execution_settings) mock_create.assert_awaited_once_with( model=azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"], @@ -135,7 +135,7 @@ async def test_call_with_parameters_logit_bias_not_none( azure_text_completion = AzureTextCompletion() - await azure_text_completion.get_text_contents(prompt, complete_prompt_execution_settings) + await azure_text_completion.get_text_contents(prompt=prompt, settings=complete_prompt_execution_settings) mock_create.assert_awaited_once_with( model=azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"], From 17c2c72e9f7caece06a16bd2b35226f226415d67 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 17 Jul 2024 12:34:40 -0700 Subject: [PATCH 19/26] Fix poetry.lock after merge from main --- python/poetry.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/poetry.lock b/python/poetry.lock index 8898116052c1..d28fc47d7a67 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -3110,6 +3110,7 @@ description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" files = [ + {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_aarch64.whl", hash = "sha256:004186d5ea6a57758fd6d57052a123c73a4815adf365eb8dd6a85c9eaa7535ff"}, {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d9714f27c1d0f0895cd8915c07a87a1d0029a0aa36acaf9156952ec2a8a12189"}, {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-win_amd64.whl", hash = "sha256:c3401dc8543b52d3a8158007a0c1ab4e9c768fcbd24153a48c86972102197ddd"}, ] @@ -6876,4 +6877,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "c16c2c216ef84d091aa8f5327d75ba8a45c713f351953f53c2045bad734528c9" +content-hash = "6a83ed21fe1f70a1a72668944aab60706fd3d5f7b741038348f44cde59edc7b2" From 1f163f1af5ddb81739b18323cd85f1cc7737afd7 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Tue, 23 Jul 2024 13:42:02 -0700 Subject: [PATCH 20/26] Add unit tests --- python/semantic_kernel/utils/tracing/const.py | 2 + .../utils/tracing/decorators.py | 27 ++- python/tests/unit/utils/test_tracing.py | 213 ++++++++++++++++++ 3 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 python/tests/unit/utils/test_tracing.py diff --git a/python/semantic_kernel/utils/tracing/const.py b/python/semantic_kernel/utils/tracing/const.py index 11d9a59a3970..8204885ebea5 100644 --- a/python/semantic_kernel/utils/tracing/const.py +++ b/python/semantic_kernel/utils/tracing/const.py @@ -5,6 +5,8 @@ # Activity tags SYSTEM = "gen_ai.system" OPERATION = "gen_ai.operation.name" +CHAT_COMPLETION_OPERATION = "chat.completions" +TEXT_COMPLETION_OPERATION = "text.completions" MODEL = "gen_ai.request.model" MAX_TOKEN = "gen_ai.request.max_tokens" # nosec TEMPERATURE = "gen_ai.request.temperature" diff --git a/python/semantic_kernel/utils/tracing/decorators.py b/python/semantic_kernel/utils/tracing/decorators.py index d460a477bd82..6ca1322ea888 100644 --- a/python/semantic_kernel/utils/tracing/decorators.py +++ b/python/semantic_kernel/utils/tracing/decorators.py @@ -19,6 +19,7 @@ from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.utils.tracing.const import ( + CHAT_COMPLETION_OPERATION, COMPLETION_EVENT, COMPLETION_EVENT_COMPLETION, COMPLETION_TOKEN, @@ -33,6 +34,7 @@ RESPONSE_ID, SYSTEM, TEMPERATURE, + TEXT_COMPLETION_OPERATION, TOP_P, ) @@ -50,7 +52,7 @@ def are_model_diagnostics_enabled() -> bool: """Check if model diagnostics are enabled. - Model diagnostics are enabled if either EnableModelDiagnostics or EnableSensitiveEvents is set. + Model diagnostics are enabled if either _enable_diagnostics or _enable_sensitive_events is set. """ return _enable_diagnostics or _enable_sensitive_events @@ -58,7 +60,7 @@ def are_model_diagnostics_enabled() -> bool: def are_sensitive_events_enabled() -> bool: """Check if sensitive events are enabled. - Sensitive events are enabled if EnableSensitiveEvents is set. + Sensitive events are enabled if _enable_sensitive_events is set. """ return _enable_sensitive_events @@ -69,8 +71,6 @@ def trace_chat_completion(model_provider: str) -> Callable: def inner_trace_chat_completion(completion_func: Callable) -> Callable: @functools.wraps(completion_func) async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageContent]: - OPERATION_NAME: str = "chat.completions" - chat_history: ChatHistory = kwargs["chat_history"] settings: PromptExecutionSettings = kwargs["settings"] @@ -79,7 +79,9 @@ async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageConten formatted_messages = ( _messages_to_openai_format(chat_history.messages) if are_sensitive_events_enabled() else None ) - span = _start_completion_activity(OPERATION_NAME, model_name, model_provider, formatted_messages, settings) + span = _start_completion_activity( + CHAT_COMPLETION_OPERATION, model_name, model_provider, formatted_messages, settings + ) try: completions: list[ChatMessageContent] = await completion_func(*args, **kwargs) @@ -127,14 +129,12 @@ def trace_text_completion(model_provider: str) -> Callable: def inner_trace_text_completion(completion_func: Callable) -> Callable: @functools.wraps(completion_func) async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[TextContent]: - OPERATION_NAME: str = "text.completions" - prompt: str = kwargs["prompt"] settings: PromptExecutionSettings = kwargs["settings"] model_name = getattr(settings, "ai_model_id", None) or getattr(args[0], "ai_model_id", None) or "unknown" - span = _start_completion_activity(OPERATION_NAME, model_name, model_provider, prompt, settings) + span = _start_completion_activity(TEXT_COMPLETION_OPERATION, model_name, model_provider, prompt, settings) try: completions: list[TextContent] = await completion_func(*args, **kwargs) @@ -161,7 +161,12 @@ async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[TextContent]: ) _set_completion_response( - span, completion_text, None, response_id or "unknown", prompt_tokens, completion_tokens + span, + completion_text, + None, + response_id or "unknown", + prompt_tokens, + completion_tokens, ) return completions @@ -193,6 +198,8 @@ def _start_completion_activity( } ) + # TODO(@glahaye): we'll need to have a way to get these attributes from model + # providers other than OpenAI (for example if the attributes are named differently) if execution_settings: attribute = execution_settings.extension_data.get("max_tokens") if attribute: @@ -246,7 +253,7 @@ def _set_completion_error(span: Span, error: Exception) -> None: span.set_attribute(ERROR_TYPE, str(type(error))) - span.set_status(StatusCode.ERROR, str(error)) + span.set_status(StatusCode.ERROR, repr(error)) def _messages_to_openai_format(messages: list[ChatMessageContent]) -> str: diff --git a/python/tests/unit/utils/test_tracing.py b/python/tests/unit/utils/test_tracing.py new file mode 100644 index 000000000000..4d2f5cf94c86 --- /dev/null +++ b/python/tests/unit/utils/test_tracing.py @@ -0,0 +1,213 @@ +# Copyright (c) Microsoft. All rights reserved. + +from unittest.mock import patch + +import pytest +from openai.types import Completion as TextCompletion +from openai.types import CompletionChoice +from opentelemetry.trace import StatusCode + +from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion +from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base import OpenAIChatCompletionBase +from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion import OpenAITextCompletion +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.text_content import TextContent +from semantic_kernel.contents.utils.author_role import AuthorRole +from semantic_kernel.contents.utils.finish_reason import FinishReason +from semantic_kernel.exceptions.service_exceptions import ServiceResponseException +from semantic_kernel.utils.tracing.const import ( + CHAT_COMPLETION_OPERATION, + COMPLETION_EVENT, + COMPLETION_EVENT_COMPLETION, + ERROR_TYPE, + FINISH_REASON, + MAX_TOKEN, + MODEL, + OPERATION, + PROMPT_EVENT, + PROMPT_EVENT_PROMPT, + RESPONSE_ID, + SYSTEM, + TEMPERATURE, + TEXT_COMPLETION_OPERATION, + TOP_P, +) + +TEST_CONTENT = "Test content" +TEST_RESPONSE_ID = "dummy_id" +TEST_MAX_TOKENS = "1000" +TEST_MODEL = "dummy_model" +TEST_TEMPERATURE = "0.5" +TEST_TOP_P = "0.9" +TEST_CREATED_AT = 1 +TEST_TEXT_PROMPT = "Test prompt" +EXPECTED_CHAT_COMPLETION_EVENT_PAYLOAD = '[{{}"role": "assistant", "content": "{TEST_CONTENT}"}}]' +EXPECTED_TEXT_COMPLETION_EVENT_PAYLOAD = f'["{TEST_CONTENT}"]' + +TEST_CHAT_RESPONSE = [ + ChatMessageContent( + role=AuthorRole.ASSISTANT, + ai_model_id=TEST_MODEL, + content=TEST_CONTENT, + metadata={"id": TEST_RESPONSE_ID}, + finish_reason=FinishReason.STOP, + ) +] + +TEST_TEXT_RESPONSE = TextCompletion( + model=TEST_MODEL, + text=TEST_CONTENT, + id=TEST_RESPONSE_ID, + choices=[CompletionChoice(index=0, text=TEST_CONTENT, finish_reason="stop")], + created=TEST_CREATED_AT, + object="text_completion", +) + +TEST_TEXT_RESPONSE_METADATA = { + "id": TEST_RESPONSE_ID, + "created": TEST_CREATED_AT, + "system_fingerprint": None, + "logprobs": None, + "usage": None, +} + +EXPECTED_TEXT_CONTENT = [ + TextContent( + ai_model_id=TEST_MODEL, + text=TEST_CONTENT, + encoding=None, + metadata=TEST_TEXT_RESPONSE_METADATA, + inner_content=TEST_TEXT_RESPONSE, + ) +] + + +@patch("semantic_kernel.utils.tracing.decorators.are_sensitive_events_enabled", return_value=True) +@patch( + "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.OpenAIChatCompletionBase._send_chat_request", + return_value=TEST_CHAT_RESPONSE, +) +@patch("opentelemetry.trace.INVALID_SPAN") +async def test_trace_chat_completion(mock_span, mock_send_chat_request, mock_sensitive_events_enabled): + chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL) + extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} + + results: list[ChatMessageContent] = await chat_completion.get_chat_message_contents( + chat_history=ChatHistory(), settings=PromptExecutionSettings(extension_data=extension_data) + ) + + assert results == TEST_CHAT_RESPONSE + + mock_span.set_attributes.assert_called_with( + { + OPERATION: CHAT_COMPLETION_OPERATION, + SYSTEM: OpenAIChatCompletionBase.MODEL_PROVIDER_NAME, + MODEL: TEST_MODEL, + } + ) + mock_span.set_attribute.assert_any_call(MAX_TOKEN, TEST_MAX_TOKENS) + mock_span.set_attribute.assert_any_call(TEMPERATURE, TEST_TEMPERATURE) + mock_span.set_attribute.assert_any_call(TOP_P, TEST_TOP_P) + mock_span.add_event.assert_any_call(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: "[]"}) + + mock_span.set_attribute.assert_any_call(RESPONSE_ID, TEST_RESPONSE_ID) + mock_span.set_attribute.assert_any_call(FINISH_REASON, str(FinishReason.STOP)) + mock_span.add_event.assert_any_call( + COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: EXPECTED_CHAT_COMPLETION_EVENT_PAYLOAD} + ) + + +@patch("semantic_kernel.utils.tracing.decorators.are_sensitive_events_enabled", return_value=True) +@patch( + "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base.OpenAITextCompletionBase._send_request", + return_value=TEST_TEXT_RESPONSE, +) +@patch("opentelemetry.trace.INVALID_SPAN") +async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitive_events_enabled): + chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL) + extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} + + results: list[TextContent] = await chat_completion.get_text_contents( + prompt=TEST_TEXT_PROMPT, settings=PromptExecutionSettings(extension_data=extension_data) + ) + + assert results == EXPECTED_TEXT_CONTENT + + mock_span.set_attributes.assert_called_with( + { + OPERATION: TEXT_COMPLETION_OPERATION, + SYSTEM: OpenAIChatCompletionBase.MODEL_PROVIDER_NAME, + MODEL: TEST_MODEL, + } + ) + mock_span.set_attribute.assert_any_call(MAX_TOKEN, TEST_MAX_TOKENS) + mock_span.set_attribute.assert_any_call(TEMPERATURE, TEST_TEMPERATURE) + mock_span.set_attribute.assert_any_call(TOP_P, TEST_TOP_P) + mock_span.add_event.assert_any_call(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: TEST_TEXT_PROMPT}) + + mock_span.set_attribute.assert_any_call(RESPONSE_ID, TEST_RESPONSE_ID) + mock_span.add_event.assert_any_call( + COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: EXPECTED_TEXT_COMPLETION_EVENT_PAYLOAD} + ) + + +@patch("semantic_kernel.utils.tracing.decorators.are_sensitive_events_enabled", return_value=True) +@patch( + "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.OpenAIChatCompletionBase._send_chat_request", + side_effect=ServiceResponseException, +) +@patch("opentelemetry.trace.INVALID_SPAN") +async def test_trace_chat_completion_exception(mock_span, mock_send_chat_request, mock_sensitive_events_enabled): + chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL) + extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} + + with pytest.raises(ServiceResponseException): + await chat_completion.get_chat_message_contents( + chat_history=ChatHistory(), settings=PromptExecutionSettings(extension_data=extension_data) + ) + + mock_span.set_attributes.assert_called_with( + { + OPERATION: CHAT_COMPLETION_OPERATION, + SYSTEM: OpenAIChatCompletionBase.MODEL_PROVIDER_NAME, + MODEL: TEST_MODEL, + } + ) + + exception = ServiceResponseException() + mock_span.set_attribute.assert_any_call(ERROR_TYPE, str(type(exception))) + mock_span.set_status.assert_any_call(StatusCode.ERROR, repr(exception)) + + mock_span.end.assert_any_call() + + +@patch("semantic_kernel.utils.tracing.decorators.are_sensitive_events_enabled", return_value=True) +@patch( + "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base.OpenAITextCompletionBase._send_request", + side_effect=ServiceResponseException, +) +@patch("opentelemetry.trace.INVALID_SPAN") +async def test_trace_text_completion_exception(mock_span, mock_send_chat_request, mock_sensitive_events_enabled): + chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL) + extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} + + with pytest.raises(ServiceResponseException): + await chat_completion.get_text_contents( + prompt=TEST_TEXT_PROMPT, settings=PromptExecutionSettings(extension_data=extension_data) + ) + + mock_span.set_attributes.assert_called_with( + { + OPERATION: TEXT_COMPLETION_OPERATION, + SYSTEM: OpenAIChatCompletionBase.MODEL_PROVIDER_NAME, + MODEL: TEST_MODEL, + } + ) + + exception = ServiceResponseException() + mock_span.set_attribute.assert_any_call(ERROR_TYPE, str(type(exception))) + mock_span.set_status.assert_any_call(StatusCode.ERROR, repr(exception)) + + mock_span.end.assert_any_call() From d73ead700296386149c6a5700d9caf651a96714c Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Tue, 23 Jul 2024 14:05:32 -0700 Subject: [PATCH 21/26] Adapt poetry.lock after merge --- python/poetry.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/poetry.lock b/python/poetry.lock index cba1bea99595..b64153b46b34 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -2376,7 +2376,6 @@ python-versions = ">=3.7" files = [ {file = "milvus_lite-2.4.7-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:c828190118b104b05b8c8e0b5a4147811c86b54b8fb67bc2e726ad10fc0b544e"}, {file = "milvus_lite-2.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e1537633c39879714fb15082be56a4b97f74c905a6e98e302ec01320561081af"}, - {file = "milvus_lite-2.4.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:fcb909d38c83f21478ca9cb500c84264f988c69f62715ae9462e966767fb76dd"}, {file = "milvus_lite-2.4.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f016474d663045787dddf1c3aad13b7d8b61fd329220318f858184918143dcbf"}, ] @@ -6884,4 +6883,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "ea8d2871e94a4986a8ffc59d30758e03b7bf35d8ae3d0c330dd79043b2b25d09" +content-hash = "12b79fd70301ee08410e7c2e21ec0f6e761257573f53029a7d5a62ef7f8458d5" From 6ae2ef680aee23bc43c919c8d216886b20b5a0fb Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 24 Jul 2024 11:28:00 -0700 Subject: [PATCH 22/26] Address PR comments + enable async unit tests --- .../azure_ai_inference_chat_completion.py | 2 +- .../azure_ai_inference_text_embedding.py | 2 +- .../ai/open_ai/services/azure_config_base.py | 2 +- .../services/open_ai_chat_completion_base.py | 2 +- .../open_ai/services/open_ai_config_base.py | 2 +- .../services/open_ai_text_completion_base.py | 2 +- .../connectors/memory/astradb/astra_client.py | 2 +- .../openapi_function_execution_parameters.py | 2 +- .../openapi_plugin/openapi_runner.py | 2 +- .../connectors/utils/document_loader.py | 2 +- .../sessions_python_plugin.py | 2 +- .../utils/{tracing => telemetry}/__init__.py | 0 .../utils/{tracing => telemetry}/const.py | 6 ++-- .../{tracing => telemetry}/decorators.py | 14 ++++---- .../telemetry/user_agent.py} | 0 ...test_azure_ai_inference_chat_completion.py | 2 +- .../test_azure_ai_inference_text_embedding.py | 2 +- .../connectors/utils/test_document_loader.py | 2 +- .../unit/functions/test_kernel_plugins.py | 2 +- .../tests/unit/telemetry/test_user_agent.py | 34 +++++++++---------- python/tests/unit/utils/test_tracing.py | 22 +++++++----- 21 files changed, 55 insertions(+), 51 deletions(-) rename python/semantic_kernel/utils/{tracing => telemetry}/__init__.py (100%) rename python/semantic_kernel/utils/{tracing => telemetry}/const.py (81%) rename python/semantic_kernel/utils/{tracing => telemetry}/decorators.py (97%) rename python/semantic_kernel/{connectors/telemetry.py => utils/telemetry/user_agent.py} (100%) diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py index 754f4752137a..f8a1adf295db 100644 --- a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py @@ -7,7 +7,7 @@ from functools import reduce from typing import TYPE_CHECKING, Any -from semantic_kernel.connectors.telemetry import SEMANTIC_KERNEL_USER_AGENT +from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT if sys.version_info >= (3, 12): from typing import override # pragma: no cover diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_text_embedding.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_text_embedding.py index cfb8f9018c1f..1f3cf3acaa01 100644 --- a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_text_embedding.py +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_text_embedding.py @@ -14,9 +14,9 @@ from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_settings import AzureAIInferenceSettings from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_base import AzureAIInferenceBase from semantic_kernel.connectors.ai.embeddings.embedding_generator_base import EmbeddingGeneratorBase -from semantic_kernel.connectors.telemetry import SEMANTIC_KERNEL_USER_AGENT from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError from semantic_kernel.utils.experimental_decorator import experimental_class +from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT if TYPE_CHECKING: from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/azure_config_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/azure_config_base.py index 6b6aa86d1c2c..980f39c51bab 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/azure_config_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/azure_config_base.py @@ -9,10 +9,10 @@ from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIHandler, OpenAIModelTypes -from semantic_kernel.connectors.telemetry import APP_INFO, prepend_semantic_kernel_to_user_agent from semantic_kernel.const import USER_AGENT from semantic_kernel.exceptions import ServiceInitializationError from semantic_kernel.kernel_pydantic import HttpsUrl +from semantic_kernel.utils.telemetry.user_agent import APP_INFO, prepend_semantic_kernel_to_user_agent logger: logging.Logger = logging.getLogger(__name__) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index 049f888d1150..e71fd85ef265 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -38,7 +38,7 @@ from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import ( AutoFunctionInvocationContext, ) -from semantic_kernel.utils.tracing.decorators import trace_chat_completion +from semantic_kernel.utils.telemetry.decorators import trace_chat_completion if TYPE_CHECKING: from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_config_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_config_base.py index b2463a1633d8..7ead64865445 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_config_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_config_base.py @@ -9,9 +9,9 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIHandler from semantic_kernel.connectors.ai.open_ai.services.open_ai_model_types import OpenAIModelTypes -from semantic_kernel.connectors.telemetry import APP_INFO, prepend_semantic_kernel_to_user_agent from semantic_kernel.const import USER_AGENT from semantic_kernel.exceptions import ServiceInitializationError +from semantic_kernel.utils.telemetry.user_agent import APP_INFO, prepend_semantic_kernel_to_user_agent logger: logging.Logger = logging.getLogger(__name__) diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py index 505291402279..8adc7d5d1fb6 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py @@ -26,7 +26,7 @@ from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase from semantic_kernel.contents.streaming_text_content import StreamingTextContent from semantic_kernel.contents.text_content import TextContent -from semantic_kernel.utils.tracing.decorators import trace_text_completion +from semantic_kernel.utils.telemetry.decorators import trace_text_completion if TYPE_CHECKING: from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings diff --git a/python/semantic_kernel/connectors/memory/astradb/astra_client.py b/python/semantic_kernel/connectors/memory/astradb/astra_client.py index aff8ce5da30f..8129b2b4f552 100644 --- a/python/semantic_kernel/connectors/memory/astradb/astra_client.py +++ b/python/semantic_kernel/connectors/memory/astradb/astra_client.py @@ -5,9 +5,9 @@ import aiohttp from semantic_kernel.connectors.memory.astradb.utils import AsyncSession -from semantic_kernel.connectors.telemetry import APP_INFO from semantic_kernel.exceptions import ServiceResponseException from semantic_kernel.utils.experimental_decorator import experimental_class +from semantic_kernel.utils.telemetry.user_agent import APP_INFO ASTRA_CALLER_IDENTITY: str SEMANTIC_KERNEL_VERSION = APP_INFO.get("Semantic-Kernel-Version") diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py index 1468e200ab45..dd014a97e55c 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py @@ -28,7 +28,7 @@ class OpenAPIFunctionExecutionParameters(KernelBaseModel): def model_post_init(self, __context: Any) -> None: """Post initialization method for the model.""" - from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT + from semantic_kernel.utils.telemetry.user_agent import HTTP_USER_AGENT if self.server_url_override: parsed_url = urlparse(self.server_url_override) diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_runner.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_runner.py index 951a2c4d69fc..09869445dc05 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_runner.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_runner.py @@ -17,10 +17,10 @@ ) from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload import RestApiOperationPayload from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_run_options import RestApiOperationRunOptions -from semantic_kernel.connectors.telemetry import APP_INFO, prepend_semantic_kernel_to_user_agent from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.utils.experimental_decorator import experimental_class +from semantic_kernel.utils.telemetry.user_agent import APP_INFO, prepend_semantic_kernel_to_user_agent logger: logging.Logger = logging.getLogger(__name__) diff --git a/python/semantic_kernel/connectors/utils/document_loader.py b/python/semantic_kernel/connectors/utils/document_loader.py index 74a0190b8bb1..5984c3b9bfce 100644 --- a/python/semantic_kernel/connectors/utils/document_loader.py +++ b/python/semantic_kernel/connectors/utils/document_loader.py @@ -6,8 +6,8 @@ from httpx import AsyncClient, HTTPStatusError, RequestError -from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT from semantic_kernel.exceptions import ServiceInvalidRequestError +from semantic_kernel.utils.telemetry.user_agent import HTTP_USER_AGENT logger: logging.Logger = logging.getLogger(__name__) diff --git a/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py b/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py index 63cf86a27c08..70849ce6827d 100644 --- a/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py +++ b/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py @@ -10,7 +10,6 @@ from httpx import AsyncClient, HTTPStatusError from pydantic import ValidationError -from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT, version_info from semantic_kernel.const import USER_AGENT from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_settings import ( ACASessionsSettings, @@ -20,6 +19,7 @@ from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException, FunctionInitializationError from semantic_kernel.functions.kernel_function_decorator import kernel_function from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseModel +from semantic_kernel.utils.telemetry.user_agent import HTTP_USER_AGENT, version_info logger = logging.getLogger(__name__) diff --git a/python/semantic_kernel/utils/tracing/__init__.py b/python/semantic_kernel/utils/telemetry/__init__.py similarity index 100% rename from python/semantic_kernel/utils/tracing/__init__.py rename to python/semantic_kernel/utils/telemetry/__init__.py diff --git a/python/semantic_kernel/utils/tracing/const.py b/python/semantic_kernel/utils/telemetry/const.py similarity index 81% rename from python/semantic_kernel/utils/tracing/const.py rename to python/semantic_kernel/utils/telemetry/const.py index 8204885ebea5..5c74f708b986 100644 --- a/python/semantic_kernel/utils/tracing/const.py +++ b/python/semantic_kernel/utils/telemetry/const.py @@ -8,13 +8,13 @@ CHAT_COMPLETION_OPERATION = "chat.completions" TEXT_COMPLETION_OPERATION = "text.completions" MODEL = "gen_ai.request.model" -MAX_TOKEN = "gen_ai.request.max_tokens" # nosec +MAX_TOKENS = "gen_ai.request.max_tokens" # nosec TEMPERATURE = "gen_ai.request.temperature" TOP_P = "gen_ai.request.top_p" RESPONSE_ID = "gen_ai.response.id" FINISH_REASON = "gen_ai.response.finish_reason" -PROMPT_TOKEN = "gen_ai.response.prompt_tokens" # nosec -COMPLETION_TOKEN = "gen_ai.response.completion_tokens" # nosec +PROMPT_TOKENS = "gen_ai.response.prompt_tokens" # nosec +COMPLETION_TOKENS = "gen_ai.response.completion_tokens" # nosec ADDRESS = "server.address" PORT = "server.port" ERROR_TYPE = "error.type" diff --git a/python/semantic_kernel/utils/tracing/decorators.py b/python/semantic_kernel/utils/telemetry/decorators.py similarity index 97% rename from python/semantic_kernel/utils/tracing/decorators.py rename to python/semantic_kernel/utils/telemetry/decorators.py index 6ca1322ea888..366168ae3938 100644 --- a/python/semantic_kernel/utils/tracing/decorators.py +++ b/python/semantic_kernel/utils/telemetry/decorators.py @@ -18,19 +18,19 @@ from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.text_content import TextContent -from semantic_kernel.utils.tracing.const import ( +from semantic_kernel.utils.telemetry.const import ( CHAT_COMPLETION_OPERATION, COMPLETION_EVENT, COMPLETION_EVENT_COMPLETION, - COMPLETION_TOKEN, + COMPLETION_TOKENS, ERROR_TYPE, FINISH_REASON, - MAX_TOKEN, + MAX_TOKENS, MODEL, OPERATION, PROMPT_EVENT, PROMPT_EVENT_PROMPT, - PROMPT_TOKEN, + PROMPT_TOKENS, RESPONSE_ID, SYSTEM, TEMPERATURE, @@ -203,7 +203,7 @@ def _start_completion_activity( if execution_settings: attribute = execution_settings.extension_data.get("max_tokens") if attribute: - span.set_attribute(MAX_TOKEN, attribute) + span.set_attribute(MAX_TOKENS, attribute) attribute = execution_settings.extension_data.get("temperature") if attribute: @@ -237,10 +237,10 @@ def _set_completion_response( span.set_attribute(FINISH_REASON, ",".join(finish_reasons)) if prompt_tokens: - span.set_attribute(PROMPT_TOKEN, prompt_tokens) + span.set_attribute(PROMPT_TOKENS, prompt_tokens) if completion_tokens: - span.set_attribute(COMPLETION_TOKEN, completion_tokens) + span.set_attribute(COMPLETION_TOKENS, completion_tokens) if are_sensitive_events_enabled() and completion_text: span.add_event(COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: completion_text}) diff --git a/python/semantic_kernel/connectors/telemetry.py b/python/semantic_kernel/utils/telemetry/user_agent.py similarity index 100% rename from python/semantic_kernel/connectors/telemetry.py rename to python/semantic_kernel/utils/telemetry/user_agent.py diff --git a/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_chat_completion.py b/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_chat_completion.py index 6a440669f4c9..4a9426c611b7 100644 --- a/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_chat_completion.py +++ b/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_chat_completion.py @@ -13,7 +13,6 @@ ) from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_settings import AzureAIInferenceSettings from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior -from semantic_kernel.connectors.telemetry import SEMANTIC_KERNEL_USER_AGENT from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.utils.finish_reason import FinishReason from semantic_kernel.exceptions.service_exceptions import ( @@ -21,6 +20,7 @@ ServiceInvalidExecutionSettingsError, ) from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT # region init diff --git a/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_text_embedding.py b/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_text_embedding.py index ab846219c247..e65e7fb6cc3c 100644 --- a/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_text_embedding.py +++ b/python/tests/unit/connectors/azure_ai_inference/services/test_azure_ai_inference_text_embedding.py @@ -11,8 +11,8 @@ AzureAIInferenceTextEmbedding, ) from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_settings import AzureAIInferenceSettings -from semantic_kernel.connectors.telemetry import SEMANTIC_KERNEL_USER_AGENT from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError +from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT def test_azure_ai_inference_text_embedding_init(azure_ai_inference_unit_test_env, model_id) -> None: diff --git a/python/tests/unit/connectors/utils/test_document_loader.py b/python/tests/unit/connectors/utils/test_document_loader.py index a7ca87e6cd18..349f4c697483 100644 --- a/python/tests/unit/connectors/utils/test_document_loader.py +++ b/python/tests/unit/connectors/utils/test_document_loader.py @@ -5,9 +5,9 @@ import pytest from httpx import AsyncClient, HTTPStatusError, RequestError -from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT from semantic_kernel.connectors.utils.document_loader import DocumentLoader from semantic_kernel.exceptions import ServiceInvalidRequestError +from semantic_kernel.utils.telemetry.user_agent import HTTP_USER_AGENT @pytest.fixture diff --git a/python/tests/unit/functions/test_kernel_plugins.py b/python/tests/unit/functions/test_kernel_plugins.py index 627357c23526..a30f16f6b2c4 100644 --- a/python/tests/unit/functions/test_kernel_plugins.py +++ b/python/tests/unit/functions/test_kernel_plugins.py @@ -13,7 +13,6 @@ from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( OpenAIFunctionExecutionParameters, ) -from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT from semantic_kernel.exceptions.function_exceptions import PluginInitializationError from semantic_kernel.functions import kernel_function from semantic_kernel.functions.kernel_function import KernelFunction @@ -22,6 +21,7 @@ from semantic_kernel.functions.kernel_plugin import KernelPlugin from semantic_kernel.prompt_template.input_variable import InputVariable from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig +from semantic_kernel.utils.telemetry.user_agent import HTTP_USER_AGENT @pytest.fixture diff --git a/python/tests/unit/telemetry/test_user_agent.py b/python/tests/unit/telemetry/test_user_agent.py index 8572ca01c3d0..bf46cebb9bbf 100644 --- a/python/tests/unit/telemetry/test_user_agent.py +++ b/python/tests/unit/telemetry/test_user_agent.py @@ -2,23 +2,23 @@ import importlib -from semantic_kernel.connectors.telemetry import ( +from semantic_kernel.const import USER_AGENT +from semantic_kernel.utils.telemetry.user_agent import ( HTTP_USER_AGENT, TELEMETRY_DISABLED_ENV_VAR, prepend_semantic_kernel_to_user_agent, ) -from semantic_kernel.const import USER_AGENT def test_append_to_existing_user_agent(monkeypatch): monkeypatch.setenv(TELEMETRY_DISABLED_ENV_VAR, "false") monkeypatch.setattr("importlib.metadata.version", lambda _: "1.0.0") - monkeypatch.setattr("semantic_kernel.connectors.telemetry.version_info", "1.0.0") + monkeypatch.setattr("semantic_kernel.utils.telemetry.user_agent.version_info", "1.0.0") # need to reload the module to get the updated version number - import semantic_kernel.connectors.telemetry + import semantic_kernel.utils.telemetry.user_agent - importlib.reload(semantic_kernel.connectors.telemetry) + importlib.reload(semantic_kernel.utils.telemetry.user_agent) headers = {USER_AGENT: "existing-agent"} expected = {USER_AGENT: f"{HTTP_USER_AGENT}/1.0.0 existing-agent"} @@ -29,12 +29,12 @@ def test_append_to_existing_user_agent(monkeypatch): def test_create_new_user_agent(monkeypatch): monkeypatch.setenv(TELEMETRY_DISABLED_ENV_VAR, "false") monkeypatch.setattr("importlib.metadata.version", lambda _: "1.0.0") - monkeypatch.setattr("semantic_kernel.connectors.telemetry.version_info", "1.0.0") + monkeypatch.setattr("semantic_kernel.utils.telemetry.user_agent.version_info", "1.0.0") # need to reload the module to get the updated version number - import semantic_kernel.connectors.telemetry + import semantic_kernel.utils.telemetry.user_agent - importlib.reload(semantic_kernel.connectors.telemetry) + importlib.reload(semantic_kernel.utils.telemetry.user_agent) headers = {} expected = {USER_AGENT: f"{HTTP_USER_AGENT}/1.0.0"} @@ -45,7 +45,7 @@ def test_create_new_user_agent(monkeypatch): def test_telemetry_disabled(monkeypatch): monkeypatch.setenv(TELEMETRY_DISABLED_ENV_VAR, "true") monkeypatch.setattr("importlib.metadata.version", lambda _: "1.0.0") - monkeypatch.setattr("semantic_kernel.connectors.telemetry.version_info", "1.0.0") + monkeypatch.setattr("semantic_kernel.utils.telemetry.user_agent.version_info", "1.0.0") headers = {} result = prepend_semantic_kernel_to_user_agent(headers) @@ -55,25 +55,25 @@ def test_telemetry_disabled(monkeypatch): def test_app_info_when_telemetry_enabled(monkeypatch): monkeypatch.setenv(TELEMETRY_DISABLED_ENV_VAR, "false") monkeypatch.setattr("importlib.metadata.version", lambda _: "1.0.0") - monkeypatch.setattr("semantic_kernel.connectors.telemetry.version_info", "1.0.0") + monkeypatch.setattr("semantic_kernel.utils.telemetry.user_agent.version_info", "1.0.0") # need to reload the module to get the updated APP_INFO - import semantic_kernel.connectors.telemetry + import semantic_kernel.utils.telemetry.user_agent - importlib.reload(semantic_kernel.connectors.telemetry) + importlib.reload(semantic_kernel.utils.telemetry.user_agent) expected = {"semantic-kernel-version": "python/1.0.0"} - assert expected == semantic_kernel.connectors.telemetry.APP_INFO + assert expected == semantic_kernel.utils.telemetry.user_agent.APP_INFO def test_app_info_when_telemetry_disabled(monkeypatch): monkeypatch.setenv(TELEMETRY_DISABLED_ENV_VAR, "true") monkeypatch.setattr("importlib.metadata.version", lambda _: "1.0.0") - monkeypatch.setattr("semantic_kernel.connectors.telemetry.version_info", "1.0.0") + monkeypatch.setattr("semantic_kernel.utils.telemetry.user_agent.version_info", "1.0.0") # need to reload the module to get the updated APP_INFO - import semantic_kernel.connectors.telemetry + import semantic_kernel.utils.telemetry.user_agent - importlib.reload(semantic_kernel.connectors.telemetry) + importlib.reload(semantic_kernel.connectors.user_agent) - assert semantic_kernel.connectors.telemetry.APP_INFO is None + assert semantic_kernel.utils.telemetry.user_agent.APP_INFO is None diff --git a/python/tests/unit/utils/test_tracing.py b/python/tests/unit/utils/test_tracing.py index 4d2f5cf94c86..01e751a4fdd2 100644 --- a/python/tests/unit/utils/test_tracing.py +++ b/python/tests/unit/utils/test_tracing.py @@ -17,13 +17,13 @@ from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.contents.utils.finish_reason import FinishReason from semantic_kernel.exceptions.service_exceptions import ServiceResponseException -from semantic_kernel.utils.tracing.const import ( +from semantic_kernel.utils.telemetry.const import ( CHAT_COMPLETION_OPERATION, COMPLETION_EVENT, COMPLETION_EVENT_COMPLETION, ERROR_TYPE, FINISH_REASON, - MAX_TOKEN, + MAX_TOKENS, MODEL, OPERATION, PROMPT_EVENT, @@ -43,7 +43,7 @@ TEST_TOP_P = "0.9" TEST_CREATED_AT = 1 TEST_TEXT_PROMPT = "Test prompt" -EXPECTED_CHAT_COMPLETION_EVENT_PAYLOAD = '[{{}"role": "assistant", "content": "{TEST_CONTENT}"}}]' +EXPECTED_CHAT_COMPLETION_EVENT_PAYLOAD = f'[{{"role": "assistant", "content": "{TEST_CONTENT}"}}]' EXPECTED_TEXT_COMPLETION_EVENT_PAYLOAD = f'["{TEST_CONTENT}"]' TEST_CHAT_RESPONSE = [ @@ -84,7 +84,8 @@ ] -@patch("semantic_kernel.utils.tracing.decorators.are_sensitive_events_enabled", return_value=True) +@pytest.mark.asyncio +@patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) @patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.OpenAIChatCompletionBase._send_chat_request", return_value=TEST_CHAT_RESPONSE, @@ -107,7 +108,7 @@ async def test_trace_chat_completion(mock_span, mock_send_chat_request, mock_sen MODEL: TEST_MODEL, } ) - mock_span.set_attribute.assert_any_call(MAX_TOKEN, TEST_MAX_TOKENS) + mock_span.set_attribute.assert_any_call(MAX_TOKENS, TEST_MAX_TOKENS) mock_span.set_attribute.assert_any_call(TEMPERATURE, TEST_TEMPERATURE) mock_span.set_attribute.assert_any_call(TOP_P, TEST_TOP_P) mock_span.add_event.assert_any_call(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: "[]"}) @@ -119,7 +120,8 @@ async def test_trace_chat_completion(mock_span, mock_send_chat_request, mock_sen ) -@patch("semantic_kernel.utils.tracing.decorators.are_sensitive_events_enabled", return_value=True) +@pytest.mark.asyncio +@patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) @patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base.OpenAITextCompletionBase._send_request", return_value=TEST_TEXT_RESPONSE, @@ -142,7 +144,7 @@ async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitiv MODEL: TEST_MODEL, } ) - mock_span.set_attribute.assert_any_call(MAX_TOKEN, TEST_MAX_TOKENS) + mock_span.set_attribute.assert_any_call(MAX_TOKENS, TEST_MAX_TOKENS) mock_span.set_attribute.assert_any_call(TEMPERATURE, TEST_TEMPERATURE) mock_span.set_attribute.assert_any_call(TOP_P, TEST_TOP_P) mock_span.add_event.assert_any_call(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: TEST_TEXT_PROMPT}) @@ -153,7 +155,8 @@ async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitiv ) -@patch("semantic_kernel.utils.tracing.decorators.are_sensitive_events_enabled", return_value=True) +@pytest.mark.asyncio +@patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) @patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.OpenAIChatCompletionBase._send_chat_request", side_effect=ServiceResponseException, @@ -183,7 +186,8 @@ async def test_trace_chat_completion_exception(mock_span, mock_send_chat_request mock_span.end.assert_any_call() -@patch("semantic_kernel.utils.tracing.decorators.are_sensitive_events_enabled", return_value=True) +@pytest.mark.asyncio +@patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) @patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base.OpenAITextCompletionBase._send_request", side_effect=ServiceResponseException, From c001002006cd8d70fa0509dd509d6820c3cfcb6b Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 24 Jul 2024 12:10:24 -0700 Subject: [PATCH 23/26] Fix unit tests --- python/tests/unit/telemetry/test_user_agent.py | 2 +- python/tests/unit/utils/test_tracing.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/python/tests/unit/telemetry/test_user_agent.py b/python/tests/unit/telemetry/test_user_agent.py index bf46cebb9bbf..72059dac2fdc 100644 --- a/python/tests/unit/telemetry/test_user_agent.py +++ b/python/tests/unit/telemetry/test_user_agent.py @@ -74,6 +74,6 @@ def test_app_info_when_telemetry_disabled(monkeypatch): # need to reload the module to get the updated APP_INFO import semantic_kernel.utils.telemetry.user_agent - importlib.reload(semantic_kernel.connectors.user_agent) + importlib.reload(semantic_kernel.utils.telemetry.user_agent) assert semantic_kernel.utils.telemetry.user_agent.APP_INFO is None diff --git a/python/tests/unit/utils/test_tracing.py b/python/tests/unit/utils/test_tracing.py index 01e751a4fdd2..e8069add7804 100644 --- a/python/tests/unit/utils/test_tracing.py +++ b/python/tests/unit/utils/test_tracing.py @@ -91,7 +91,9 @@ return_value=TEST_CHAT_RESPONSE, ) @patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_chat_completion(mock_span, mock_send_chat_request, mock_sensitive_events_enabled): +async def test_trace_chat_completion( + mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env +): chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL) extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} @@ -127,7 +129,7 @@ async def test_trace_chat_completion(mock_span, mock_send_chat_request, mock_sen return_value=TEST_TEXT_RESPONSE, ) @patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitive_events_enabled): +async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitive_events_enabled, openai_unit_test_env): chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL) extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} @@ -162,7 +164,9 @@ async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitiv side_effect=ServiceResponseException, ) @patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_chat_completion_exception(mock_span, mock_send_chat_request, mock_sensitive_events_enabled): +async def test_trace_chat_completion_exception( + mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env +): chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL) extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} @@ -193,7 +197,9 @@ async def test_trace_chat_completion_exception(mock_span, mock_send_chat_request side_effect=ServiceResponseException, ) @patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_text_completion_exception(mock_span, mock_send_chat_request, mock_sensitive_events_enabled): +async def test_trace_text_completion_exception( + mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env +): chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL) extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} From 1a7361414ef2dedd534073dd2422c53563d3eece Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 24 Jul 2024 14:44:15 -0700 Subject: [PATCH 24/26] Overriding .env file in unit tests --- python/tests/unit/utils/test_tracing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/tests/unit/utils/test_tracing.py b/python/tests/unit/utils/test_tracing.py index e8069add7804..d7d319a69749 100644 --- a/python/tests/unit/utils/test_tracing.py +++ b/python/tests/unit/utils/test_tracing.py @@ -94,7 +94,7 @@ async def test_trace_chat_completion( mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env ): - chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL) + chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} results: list[ChatMessageContent] = await chat_completion.get_chat_message_contents( @@ -130,7 +130,7 @@ async def test_trace_chat_completion( ) @patch("opentelemetry.trace.INVALID_SPAN") async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitive_events_enabled, openai_unit_test_env): - chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL) + chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} results: list[TextContent] = await chat_completion.get_text_contents( @@ -167,7 +167,7 @@ async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitiv async def test_trace_chat_completion_exception( mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env ): - chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL) + chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} with pytest.raises(ServiceResponseException): @@ -200,7 +200,7 @@ async def test_trace_chat_completion_exception( async def test_trace_text_completion_exception( mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env ): - chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL) + chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} with pytest.raises(ServiceResponseException): From dabe4c2c19bab53ee7510b435840654c4cc7478c Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Thu, 25 Jul 2024 11:25:58 -0700 Subject: [PATCH 25/26] Fix unit tests --- python/tests/unit/utils/test_tracing.py | 26 +++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/python/tests/unit/utils/test_tracing.py b/python/tests/unit/utils/test_tracing.py index d7d319a69749..5d2c2f9e4bf6 100644 --- a/python/tests/unit/utils/test_tracing.py +++ b/python/tests/unit/utils/test_tracing.py @@ -85,6 +85,7 @@ @pytest.mark.asyncio +@patch("semantic_kernel.utils.telemetry.decorators.are_model_diagnostics_enabled", return_value=True) @patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) @patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.OpenAIChatCompletionBase._send_chat_request", @@ -92,7 +93,11 @@ ) @patch("opentelemetry.trace.INVALID_SPAN") async def test_trace_chat_completion( - mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env + mock_span, + mock_send_chat_request, + mock_sensitive_events_enabled, + mock_model_diagnostics_enabled, + openai_unit_test_env, ): chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} @@ -123,13 +128,16 @@ async def test_trace_chat_completion( @pytest.mark.asyncio +@patch("semantic_kernel.utils.telemetry.decorators.are_model_diagnostics_enabled", return_value=True) @patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) @patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base.OpenAITextCompletionBase._send_request", return_value=TEST_TEXT_RESPONSE, ) @patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitive_events_enabled, openai_unit_test_env): +async def test_trace_text_completion( + mock_span, mock_send_request, mock_sensitive_events_enabled, mock_model_diagnostics_enabled, openai_unit_test_env +): chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} @@ -158,6 +166,7 @@ async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitiv @pytest.mark.asyncio +@patch("semantic_kernel.utils.telemetry.decorators.are_model_diagnostics_enabled", return_value=True) @patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) @patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.OpenAIChatCompletionBase._send_chat_request", @@ -165,7 +174,11 @@ async def test_trace_text_completion(mock_span, mock_send_request, mock_sensitiv ) @patch("opentelemetry.trace.INVALID_SPAN") async def test_trace_chat_completion_exception( - mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env + mock_span, + mock_send_chat_request, + mock_sensitive_events_enabled, + mock_model_diagnostics_enabled, + openai_unit_test_env, ): chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} @@ -191,6 +204,7 @@ async def test_trace_chat_completion_exception( @pytest.mark.asyncio +@patch("semantic_kernel.utils.telemetry.decorators.are_model_diagnostics_enabled", return_value=True) @patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) @patch( "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base.OpenAITextCompletionBase._send_request", @@ -198,7 +212,11 @@ async def test_trace_chat_completion_exception( ) @patch("opentelemetry.trace.INVALID_SPAN") async def test_trace_text_completion_exception( - mock_span, mock_send_chat_request, mock_sensitive_events_enabled, openai_unit_test_env + mock_span, + mock_send_chat_request, + mock_sensitive_events_enabled, + mock_model_diagnostics_enabled, + openai_unit_test_env, ): chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} From f2b0f5d4e4e69a563b61842ff31dec3f5804267f Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Thu, 25 Jul 2024 11:48:18 -0700 Subject: [PATCH 26/26] Fix poetry.lock after merge --- python/poetry.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/poetry.lock b/python/poetry.lock index 83c52ee44f02..fa66ec67ab43 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -3248,6 +3248,7 @@ description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" files = [ + {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_aarch64.whl", hash = "sha256:004186d5ea6a57758fd6d57052a123c73a4815adf365eb8dd6a85c9eaa7535ff"}, {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d9714f27c1d0f0895cd8915c07a87a1d0029a0aa36acaf9156952ec2a8a12189"}, {file = "nvidia_nvjitlink_cu12-12.5.40-py3-none-win_amd64.whl", hash = "sha256:c3401dc8543b52d3a8158007a0c1ab4e9c768fcbd24153a48c86972102197ddd"}, ] @@ -7059,4 +7060,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "034b0c11304ba2e3e6668e7529c7b38fb74361aab5580077fd4ce24199095c3c" +content-hash = "c29fb1fca8d1da50daf3538331cce8f45bbdc9949d0699feaced0fe049787251"