mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Improved telemetry setup (#421)
* test with stack and simplified names * quick demo of agent decorator * moved builder to protocol to enhance functionality * undid chatclientAgent -> agent rename * one more * reverted AIAgent rename * final reverts * fixed foundry import * revert changes * streamlined otel and fcc decorators * cleanup of telemetry * further refinement * lots of updates * fixed typing * fix for mypy * added input and output atttributes * fix import * initial work on baking in otel * major update to telemetry * final fixes after rename * fix * fix test * updated tests * fix for tests * fixes for tests * updated based on comments * removed agent decorator * fix for Python: ServiceResponseException when using multiple tools Fixes #649 * addressed comments * fix tests * fix tests * fix tools tests * fix for conversation_id in assistants client * fix responses test * fix tests and mypy * updated test * foundry fix --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
6aa746d891
commit
82ca4065cb
@@ -20,8 +20,8 @@ from openai.types.beta.threads.run_submit_tool_outputs_params import ToolOutput
|
||||
from openai.types.beta.threads.runs import RunStep
|
||||
from pydantic import Field, PrivateAttr, SecretStr, ValidationError
|
||||
|
||||
from .._clients import BaseChatClient, use_tool_calling
|
||||
from .._tools import AIFunction, HostedCodeInterpreterTool, HostedFileSearchTool
|
||||
from .._clients import BaseChatClient
|
||||
from .._tools import AIFunction, HostedCodeInterpreterTool, HostedFileSearchTool, use_function_invocation
|
||||
from .._types import (
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
@@ -50,8 +50,8 @@ else:
|
||||
__all__ = ["OpenAIAssistantsClient"]
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
@use_telemetry
|
||||
@use_tool_calling
|
||||
class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
"""OpenAI Assistants client."""
|
||||
|
||||
@@ -166,7 +166,9 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
|
||||
# Get the thread ID
|
||||
thread_id: str | None = (
|
||||
chat_options.conversation_id if chat_options.conversation_id is not None else self.thread_id
|
||||
chat_options.conversation_id
|
||||
if chat_options.conversation_id is not None
|
||||
else run_options.get("conversation_id", self.thread_id)
|
||||
)
|
||||
|
||||
if thread_id is None and tool_results is not None:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Mapping, MutableMapping, MutableSequence, Sequence
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
@@ -15,9 +16,9 @@ from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
|
||||
from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall
|
||||
from pydantic import BaseModel, SecretStr, ValidationError
|
||||
|
||||
from .._clients import BaseChatClient, use_tool_calling
|
||||
from .._clients import BaseChatClient
|
||||
from .._logging import get_logger
|
||||
from .._tools import AIFunction, HostedWebSearchTool, ToolProtocol
|
||||
from .._tools import AIFunction, HostedWebSearchTool, ToolProtocol, use_function_invocation
|
||||
from .._types import (
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
@@ -41,14 +42,17 @@ from ..telemetry import use_telemetry
|
||||
from ._exceptions import OpenAIContentFilterException
|
||||
from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings, prepare_function_call_results
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import] # pragma: no cover
|
||||
|
||||
__all__ = ["OpenAIChatClient"]
|
||||
|
||||
logger = get_logger("agent_framework.openai")
|
||||
|
||||
|
||||
# region Base Client
|
||||
@use_telemetry
|
||||
@use_tool_calling
|
||||
class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
"""OpenAI Chat completion class."""
|
||||
|
||||
@@ -233,11 +237,26 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
)
|
||||
|
||||
def _usage_details_from_openai(self, usage: CompletionUsage) -> UsageDetails:
|
||||
return UsageDetails(
|
||||
prompt_tokens=usage.prompt_tokens,
|
||||
completion_tokens=usage.completion_tokens,
|
||||
total_tokens=usage.total_tokens,
|
||||
details = UsageDetails(
|
||||
input_token_count=usage.prompt_tokens,
|
||||
output_token_count=usage.completion_tokens,
|
||||
total_token_count=usage.total_tokens,
|
||||
)
|
||||
if usage.completion_tokens_details:
|
||||
if tokens := usage.completion_tokens_details.accepted_prediction_tokens:
|
||||
details["completion/accepted_prediction_tokens"] = tokens
|
||||
if tokens := usage.completion_tokens_details.audio_tokens:
|
||||
details["completion/audio_tokens"] = tokens
|
||||
if tokens := usage.completion_tokens_details.reasoning_tokens:
|
||||
details["completion/reasoning_tokens"] = tokens
|
||||
if tokens := usage.completion_tokens_details.rejected_prediction_tokens:
|
||||
details["completion/rejected_prediction_tokens"] = tokens
|
||||
if usage.prompt_tokens_details:
|
||||
if tokens := usage.prompt_tokens_details.audio_tokens:
|
||||
details["prompt/audio_tokens"] = tokens
|
||||
if tokens := usage.prompt_tokens_details.cached_tokens:
|
||||
details["prompt/cached_tokens"] = tokens
|
||||
return details
|
||||
|
||||
def _parse_text_from_choice(self, choice: Choice | ChunkChoice) -> TextContent | None:
|
||||
"""Parse the choice into a TextContent object."""
|
||||
@@ -362,13 +381,14 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
case _:
|
||||
return content.model_dump(exclude_none=True)
|
||||
|
||||
def service_url(self) -> str | None:
|
||||
@override
|
||||
def service_url(self) -> str:
|
||||
"""Get the URL of the service.
|
||||
|
||||
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
|
||||
return str(self.client.base_url) if self.client else "Unknown"
|
||||
|
||||
|
||||
# region Public client
|
||||
@@ -376,6 +396,8 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
TOpenAIChatClient = TypeVar("TOpenAIChatClient", bound="OpenAIChatClient")
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
@use_telemetry
|
||||
class OpenAIChatClient(OpenAIConfigMixin, OpenAIBaseChatClient):
|
||||
"""OpenAI Chat completion class."""
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from openai.types.responses.web_search_tool_param import UserLocation as WebSear
|
||||
from openai.types.responses.web_search_tool_param import WebSearchToolParam
|
||||
from pydantic import BaseModel, SecretStr, ValidationError
|
||||
|
||||
from .._clients import BaseChatClient, use_tool_calling
|
||||
from .._clients import BaseChatClient
|
||||
from .._logging import get_logger
|
||||
from .._tools import (
|
||||
AIFunction,
|
||||
@@ -34,6 +34,7 @@ from .._tools import (
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
use_function_invocation,
|
||||
)
|
||||
from .._types import (
|
||||
ChatMessage,
|
||||
@@ -406,7 +407,7 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
tool_args["file_ids"] = []
|
||||
for tool_input in tool.inputs:
|
||||
if isinstance(tool_input, HostedFileContent):
|
||||
tool_args["file_ids"].append(tool_input.file_id)
|
||||
tool_args["file_ids"].append(tool_input.file_id) # type: ignore[attr-defined]
|
||||
if not tool_args["file_ids"]:
|
||||
tool_args.pop("file_ids")
|
||||
response_tools.append(
|
||||
@@ -1040,8 +1041,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
TOpenAIResponsesClient = TypeVar("TOpenAIResponsesClient", bound="OpenAIResponsesClient")
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
@use_telemetry
|
||||
@use_tool_calling
|
||||
class OpenAIResponsesClient(OpenAIConfigMixin, OpenAIBaseResponsesClient):
|
||||
"""OpenAI Responses client class."""
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ def prepare_function_call_results(content: Contents | Any | list[Contents | Any]
|
||||
results.extend(res)
|
||||
else:
|
||||
results.append(res)
|
||||
return results[0] if len(results) == 1 else results
|
||||
return results[0] if len(results) == 1 else json.dumps(results)
|
||||
if isinstance(content, BaseModel):
|
||||
return content.model_dump_json(exclude_none=True, exclude={"raw_representation", "additional_properties"})
|
||||
# fallback
|
||||
@@ -127,7 +127,7 @@ class OpenAIBase(AFBaseModel):
|
||||
class OpenAIConfigMixin(OpenAIBase):
|
||||
"""Internal class for configuring a connection to an OpenAI service."""
|
||||
|
||||
MODEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc]
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc]
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def __init__(
|
||||
|
||||
Reference in New Issue
Block a user