Python telemetry (#223)

* initial work on telemetry

* moved tool operation const

* missing quotes

* working otel with samples

* updated readme and other assets

* added tests

* added tests

* small updates

* updated genaiattributes docs

* updated tests

* additional warning

* cleanup of tests
This commit is contained in:
Eduard van Valkenburg
2025-07-28 09:33:42 +02:00
committed by GitHub
Unverified
parent 3ee9dddfa2
commit 0ce8eb1e2f
27 changed files with 2197 additions and 153 deletions
@@ -4,7 +4,7 @@ import json
from collections.abc import AsyncIterable, Mapping, MutableSequence, Sequence
from datetime import datetime
from itertools import chain
from typing import Any, cast
from typing import Any, ClassVar, cast
from openai import AsyncOpenAI, AsyncStream
from openai.types import CompletionUsage
@@ -29,21 +29,19 @@ from .._types import (
UsageDetails,
)
from ..exceptions import ServiceInitializationError, ServiceInvalidResponseError
from ..telemetry import use_telemetry
from ._shared import OpenAIConfigBase, OpenAIHandler, OpenAIModelTypes, OpenAISettings
__all__ = ["OpenAIChatClient"]
# region OpenAIChatClientBase
# Implements agent_framework.ChatClient protocol, through ChatClientBase
# region Base Client
@use_telemetry
@use_tool_calling
class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
"""OpenAI Chat completion class."""
# region Overriding base class methods
# most of the methods are overridden from the ChatClientBase class, otherwise it is mentioned
MODEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc]
async def _inner_get_response(
self,
@@ -100,8 +98,6 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
for choice in chunk.choices
)
# endregion
# region content creation
def _create_chat_message_content(
@@ -220,34 +216,34 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
# Flatten the list of lists into a single list
return list(chain.from_iterable(list_of_list))
# endregion
# region Parsers
def _openai_chat_message_parser(self, message: ChatMessage) -> list[dict[str, Any]]:
"""Parse a chat message into the openai format."""
all_messages: list[dict[str, Any]] = []
args: dict[str, Any] = {
"role": message.role.value if isinstance(message.role, ChatRole) else message.role,
}
if message.additional_properties:
args["metadata"] = message.additional_properties
for content in message.contents:
args: dict[str, Any] = {
"role": message.role.value if isinstance(message.role, ChatRole) else message.role,
}
if message.additional_properties:
args["metadata"] = message.additional_properties
match content:
case FunctionResultContent():
new_args = args.copy()
new_args["tool_call_id"] = content.call_id
new_args["content"] = content.result
all_messages.append(new_args)
case FunctionCallContent():
function_call = self._openai_content_parser(content)
if "tool_calls" not in args:
args["tool_calls"] = []
args["tool_calls"].append(function_call) # type: ignore
if all_messages and "tool_calls" in all_messages[-1]:
# If the last message already has tool calls, append to it
all_messages[-1]["tool_calls"].append(self._openai_content_parser(content))
else:
args["tool_calls"] = [self._openai_content_parser(content)] # type: ignore
case FunctionResultContent():
args["tool_call_id"] = content.call_id
args["content"] = content.result
case _:
if "content" not in args:
args["content"] = []
# this is a list to allow multi-modal content
args["content"].append(self._openai_content_parser(content)) # type: ignore
if "content" in args or "tool_calls" in args:
all_messages.append(args)
if "content" in args or "tool_calls" in args:
all_messages.append(args)
return all_messages
def _openai_content_parser(self, content: AIContents) -> dict[str, Any]:
@@ -268,10 +264,16 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
case _:
return content.model_dump(exclude_none=True)
def service_url(self) -> str | None:
"""Get the URL of the service.
# endregion
Override this in the subclass to return the proper URL.
If the service does not have a URL, return None.
"""
return str(self.client.base_url) if self.client else None
# region OpenAIChatClient
# region Public client
class OpenAIChatClient(OpenAIConfigBase, OpenAIChatClientBase):