项目文件夹

文件
2026-07-13 13:32:05 +08:00

310 行
9.8 KiB
Python

from typing import Callable, List
from functools import wraps
from deepeval.openai.extractors import (
safe_extract_output_parameters,
safe_extract_input_parameters,
InputParameters,
OutputParameters,
)
from deepeval.test_case.llm_test_case import ToolCall
from deepeval.tracing.context import (
current_span_context,
current_trace_context,
update_current_span,
update_llm_span,
)
from deepeval.tracing import observe
from deepeval.tracing.trace_context import current_llm_context
from deepeval.tracing.types import LlmSpan
from deepeval.tracing.integrations import Integration, Provider
from deepeval.tracing.tracing import trace_manager
# Store original methods for safety and potential unpatching
_ORIGINAL_METHODS = {}
_OPENAI_PATCHED = False
def patch_openai_classes():
"""Monkey patch OpenAI resource classes directly."""
global _OPENAI_PATCHED
# Single guard - if already patched, return immediately
if _OPENAI_PATCHED:
return
try:
from openai.resources.chat.completions import (
Completions,
AsyncCompletions,
)
# Store original methods before patching
if hasattr(Completions, "create"):
_ORIGINAL_METHODS["Completions.create"] = Completions.create
Completions.create = _create_sync_wrapper(
Completions.create, is_completion_method=True
)
if hasattr(Completions, "parse"):
_ORIGINAL_METHODS["Completions.parse"] = Completions.parse
Completions.parse = _create_sync_wrapper(
Completions.parse, is_completion_method=True
)
if hasattr(AsyncCompletions, "create"):
_ORIGINAL_METHODS["AsyncCompletions.create"] = (
AsyncCompletions.create
)
AsyncCompletions.create = _create_async_wrapper(
AsyncCompletions.create, is_completion_method=True
)
if hasattr(AsyncCompletions, "parse"):
_ORIGINAL_METHODS["AsyncCompletions.parse"] = AsyncCompletions.parse
AsyncCompletions.parse = _create_async_wrapper(
AsyncCompletions.parse, is_completion_method=True
)
except ImportError:
pass
try:
from openai.resources.responses import Responses, AsyncResponses
if hasattr(Responses, "create"):
_ORIGINAL_METHODS["Responses.create"] = Responses.create
Responses.create = _create_sync_wrapper(
Responses.create, is_completion_method=False
)
if hasattr(AsyncResponses, "create"):
_ORIGINAL_METHODS["AsyncResponses.create"] = AsyncResponses.create
AsyncResponses.create = _create_async_wrapper(
AsyncResponses.create, is_completion_method=False
)
except ImportError:
pass
# Set flag at the END after successful patching
_OPENAI_PATCHED = True
def _create_sync_wrapper(original_method, is_completion_method: bool):
"""Create a wrapper for sync methods - called ONCE during patching."""
@wraps(original_method)
def method_wrapper(self, *args, **kwargs):
bound_method = original_method.__get__(self, type(self))
patched = _patch_sync_openai_client_method(
orig_method=bound_method, is_completion_method=is_completion_method
)
return patched(*args, **kwargs)
return method_wrapper
def _create_async_wrapper(original_method, is_completion_method: bool):
"""Create a wrapper for async methods - called ONCE during patching."""
@wraps(original_method)
async def method_wrapper(self, *args, **kwargs):
bound_method = original_method.__get__(self, type(self))
patched = _patch_async_openai_client_method(
orig_method=bound_method, is_completion_method=is_completion_method
)
return await patched(*args, **kwargs)
return method_wrapper
def _patch_async_openai_client_method(
orig_method: Callable,
is_completion_method: bool = False,
):
@wraps(orig_method)
async def patched_async_openai_method(*args, **kwargs):
input_parameters: InputParameters = safe_extract_input_parameters(
is_completion_method, kwargs
)
llm_context = current_llm_context.get()
@observe(
type="llm",
model=input_parameters.model,
metrics=llm_context.metrics,
metric_collection=llm_context.metric_collection,
)
async def llm_generation(*args, **kwargs):
response = await orig_method(*args, **kwargs)
output_parameters = safe_extract_output_parameters(
is_completion_method, response, input_parameters
)
_update_all_attributes(
input_parameters,
output_parameters,
llm_context.expected_tools,
llm_context.expected_output,
llm_context.context,
llm_context.retrieval_context,
)
return response
return await llm_generation(*args, **kwargs)
return patched_async_openai_method
def _patch_sync_openai_client_method(
orig_method: Callable,
is_completion_method: bool = False,
):
@wraps(orig_method)
def patched_sync_openai_method(*args, **kwargs):
input_parameters: InputParameters = safe_extract_input_parameters(
is_completion_method, kwargs
)
llm_context = current_llm_context.get()
@observe(
type="llm",
model=input_parameters.model,
metrics=llm_context.metrics,
metric_collection=llm_context.metric_collection,
)
def llm_generation(*args, **kwargs):
response = orig_method(*args, **kwargs)
output_parameters = safe_extract_output_parameters(
is_completion_method, response, input_parameters
)
_update_all_attributes(
input_parameters,
output_parameters,
llm_context.expected_tools,
llm_context.expected_output,
llm_context.context,
llm_context.retrieval_context,
)
return response
return llm_generation(*args, **kwargs)
return patched_sync_openai_method
def _update_all_attributes(
input_parameters: InputParameters,
output_parameters: OutputParameters,
expected_tools: List[ToolCall],
expected_output: str,
context: List[str],
retrieval_context: List[str],
):
"""Update span and trace attributes with input/output parameters."""
update_current_span(
input=input_parameters.messages,
output=output_parameters.output or output_parameters.tools_called,
tools_called=output_parameters.tools_called,
# attributes to be added
expected_output=expected_output,
expected_tools=expected_tools,
context=context,
retrieval_context=retrieval_context,
)
llm_context = current_llm_context.get()
update_llm_span(
input_token_count=output_parameters.prompt_tokens,
output_token_count=output_parameters.completion_tokens,
prompt=llm_context.prompt,
)
current_span = current_span_context.get()
if isinstance(current_span, LlmSpan):
current_span.integration = Integration.OPEN_AI.value
current_span.provider = Provider.OPEN_AI.value
if current_span.parent_uuid:
parent_span = trace_manager.get_span_by_uuid(
current_span.parent_uuid
)
if parent_span and not parent_span.integration:
parent_span.integration = Integration.OPEN_AI.value
__update_input_and_output_of_current_trace(
input_parameters, output_parameters
)
def __update_input_and_output_of_current_trace(
input_parameters: InputParameters, output_parameters: OutputParameters
):
current_trace = current_trace_context.get()
if current_trace:
if current_trace.input is None:
current_trace.input = (
input_parameters.input or input_parameters.messages
)
if current_trace.output is None:
current_trace.output = output_parameters.output
return
def unpatch_openai_classes():
"""Restore OpenAI resource classes to their original state."""
global _OPENAI_PATCHED
# If not patched, nothing to do
if not _OPENAI_PATCHED:
return
try:
from openai.resources.chat.completions import (
Completions,
AsyncCompletions,
)
# Restore original methods for Completions
if "Completions.create" in _ORIGINAL_METHODS:
Completions.create = _ORIGINAL_METHODS["Completions.create"]
if "Completions.parse" in _ORIGINAL_METHODS:
Completions.parse = _ORIGINAL_METHODS["Completions.parse"]
# Restore original methods for AsyncCompletions
if "AsyncCompletions.create" in _ORIGINAL_METHODS:
AsyncCompletions.create = _ORIGINAL_METHODS[
"AsyncCompletions.create"
]
if "AsyncCompletions.parse" in _ORIGINAL_METHODS:
AsyncCompletions.parse = _ORIGINAL_METHODS["AsyncCompletions.parse"]
except ImportError:
pass
try:
from openai.resources.responses import Responses, AsyncResponses
# Restore original methods for Responses
if "Responses.create" in _ORIGINAL_METHODS:
Responses.create = _ORIGINAL_METHODS["Responses.create"]
# Restore original methods for AsyncResponses
if "AsyncResponses.create" in _ORIGINAL_METHODS:
AsyncResponses.create = _ORIGINAL_METHODS["AsyncResponses.create"]
except ImportError:
pass
# Reset the patched flag
_OPENAI_PATCHED = False