openlit 1.34.28__py3-none-any.whl → 1.34.30__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openlit/instrumentation/crewai/__init__.py +86 -24
- openlit/instrumentation/crewai/async_crewai.py +89 -0
- openlit/instrumentation/crewai/crewai.py +79 -131
- openlit/instrumentation/crewai/utils.py +512 -0
- openlit/instrumentation/litellm/utils.py +18 -9
- openlit/instrumentation/openai/utils.py +58 -23
- openlit/instrumentation/openai_agents/__init__.py +46 -26
- openlit/instrumentation/openai_agents/processor.py +452 -0
- openlit/semcov/__init__.py +31 -2
- {openlit-1.34.28.dist-info → openlit-1.34.30.dist-info}/METADATA +2 -1
- {openlit-1.34.28.dist-info → openlit-1.34.30.dist-info}/RECORD +13 -11
- openlit/instrumentation/openai_agents/openai_agents.py +0 -65
- {openlit-1.34.28.dist-info → openlit-1.34.30.dist-info}/LICENSE +0 -0
- {openlit-1.34.28.dist-info → openlit-1.34.30.dist-info}/WHEEL +0 -0
@@ -22,6 +22,16 @@ from openlit.__helpers import (
|
|
22
22
|
)
|
23
23
|
from openlit.semcov import SemanticConvention
|
24
24
|
|
25
|
+
def handle_not_given(value, default=None):
|
26
|
+
"""
|
27
|
+
Handle OpenAI's NotGiven values and None values by converting them to appropriate defaults.
|
28
|
+
"""
|
29
|
+
if hasattr(value, '__class__') and value.__class__.__name__ == 'NotGiven':
|
30
|
+
return default
|
31
|
+
if value is None:
|
32
|
+
return default
|
33
|
+
return value
|
34
|
+
|
25
35
|
def format_content(messages):
|
26
36
|
"""
|
27
37
|
Format the messages into a string for span events.
|
@@ -248,9 +258,15 @@ def common_response_logic(scope, pricing_info, environment, application_name, me
|
|
248
258
|
environment, application_name, is_stream, scope._tbt, scope._ttft, version)
|
249
259
|
|
250
260
|
# Span Attributes for Request parameters specific to responses API
|
251
|
-
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TEMPERATURE, scope._kwargs.get("temperature", 1.0))
|
252
|
-
scope._span.set_attribute(
|
253
|
-
|
261
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TEMPERATURE, handle_not_given(scope._kwargs.get("temperature"), 1.0))
|
262
|
+
scope._span.set_attribute(
|
263
|
+
SemanticConvention.GEN_AI_REQUEST_TOP_P,
|
264
|
+
handle_not_given(scope._kwargs.get("top_p"), 1.0)
|
265
|
+
)
|
266
|
+
scope._span.set_attribute(
|
267
|
+
SemanticConvention.GEN_AI_REQUEST_MAX_TOKENS,
|
268
|
+
handle_not_given(scope._kwargs.get("max_output_tokens"), -1)
|
269
|
+
)
|
254
270
|
|
255
271
|
# Reasoning parameters
|
256
272
|
reasoning = scope._kwargs.get("reasoning", {})
|
@@ -427,20 +443,30 @@ def common_chat_logic(scope, pricing_info, environment, application_name, metric
|
|
427
443
|
cost = get_chat_model_cost(request_model, pricing_info, input_tokens, output_tokens)
|
428
444
|
|
429
445
|
# Common Span Attributes
|
430
|
-
common_span_attributes(
|
431
|
-
|
432
|
-
|
433
|
-
|
446
|
+
common_span_attributes(
|
447
|
+
scope,
|
448
|
+
SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT,
|
449
|
+
SemanticConvention.GEN_AI_SYSTEM_OPENAI,
|
450
|
+
scope._server_address, scope._server_port, request_model,
|
451
|
+
scope._response_model, environment, application_name,
|
452
|
+
is_stream, scope._tbt, scope._ttft, version
|
453
|
+
)
|
434
454
|
|
435
455
|
# Span Attributes for Request parameters
|
436
|
-
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_SEED, str(scope._kwargs.get("seed", "")))
|
437
|
-
scope._span.set_attribute(
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
scope._span.set_attribute(SemanticConvention.
|
442
|
-
scope._span.set_attribute(
|
443
|
-
|
456
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_SEED, str(handle_not_given(scope._kwargs.get("seed"), "")))
|
457
|
+
scope._span.set_attribute(
|
458
|
+
SemanticConvention.GEN_AI_REQUEST_FREQUENCY_PENALTY,
|
459
|
+
handle_not_given(scope._kwargs.get("frequency_penalty"), 0.0)
|
460
|
+
)
|
461
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MAX_TOKENS, handle_not_given(scope._kwargs.get("max_tokens"), -1))
|
462
|
+
scope._span.set_attribute(
|
463
|
+
SemanticConvention.GEN_AI_REQUEST_PRESENCE_PENALTY,
|
464
|
+
handle_not_given(scope._kwargs.get("presence_penalty"), 0.0)
|
465
|
+
)
|
466
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_STOP_SEQUENCES, handle_not_given(scope._kwargs.get("stop"), []))
|
467
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TEMPERATURE, handle_not_given(scope._kwargs.get("temperature"), 1.0))
|
468
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_TOP_P, handle_not_given(scope._kwargs.get("top_p"), 1.0))
|
469
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_USER, handle_not_given(scope._kwargs.get("user"), ""))
|
444
470
|
|
445
471
|
# Span Attributes for Response parameters
|
446
472
|
scope._span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_ID, scope._response_id)
|
@@ -571,8 +597,11 @@ def common_embedding_logic(scope, request_model, pricing_info, environment, appl
|
|
571
597
|
environment, application_name, False, scope._tbt, scope._ttft, version)
|
572
598
|
|
573
599
|
# Span Attributes for Request parameters
|
574
|
-
scope._span.set_attribute(
|
575
|
-
|
600
|
+
scope._span.set_attribute(
|
601
|
+
SemanticConvention.GEN_AI_REQUEST_ENCODING_FORMATS,
|
602
|
+
[handle_not_given(scope._kwargs.get("encoding_format"), "float")]
|
603
|
+
)
|
604
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_USER, handle_not_given(scope._kwargs.get("user"), ""))
|
576
605
|
|
577
606
|
# Span Attributes for Cost and Tokens
|
578
607
|
scope._span.set_attribute(SemanticConvention.GEN_AI_USAGE_INPUT_TOKENS, scope._input_tokens)
|
@@ -611,9 +640,12 @@ def common_image_logic(scope, request_model, pricing_info, environment, applicat
|
|
611
640
|
environment, application_name, False, scope._tbt, scope._ttft, version)
|
612
641
|
|
613
642
|
# Span Attributes for Request parameters
|
614
|
-
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_IMAGE_SIZE, scope._kwargs.get("size", "1024x1024"))
|
615
|
-
scope._span.set_attribute(
|
616
|
-
|
643
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_IMAGE_SIZE, handle_not_given(scope._kwargs.get("size"), "1024x1024"))
|
644
|
+
scope._span.set_attribute(
|
645
|
+
SemanticConvention.GEN_AI_REQUEST_IMAGE_QUALITY,
|
646
|
+
handle_not_given(scope._kwargs.get("quality"), "standard")
|
647
|
+
)
|
648
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_USER, handle_not_given(scope._kwargs.get("user"), ""))
|
617
649
|
|
618
650
|
# Extract response data
|
619
651
|
response_dict = scope._response_dict
|
@@ -709,9 +741,12 @@ def common_audio_logic(scope, request_model, pricing_info, environment, applicat
|
|
709
741
|
environment, application_name, False, scope._tbt, scope._ttft, version)
|
710
742
|
|
711
743
|
# Span Attributes for Request parameters
|
712
|
-
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_AUDIO_VOICE, scope._kwargs.get("voice", "alloy"))
|
713
|
-
scope._span.set_attribute(
|
714
|
-
|
744
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_AUDIO_VOICE, handle_not_given(scope._kwargs.get("voice"), "alloy"))
|
745
|
+
scope._span.set_attribute(
|
746
|
+
SemanticConvention.GEN_AI_REQUEST_AUDIO_RESPONSE_FORMAT,
|
747
|
+
handle_not_given(scope._kwargs.get("response_format"), "mp3")
|
748
|
+
)
|
749
|
+
scope._span.set_attribute(SemanticConvention.GEN_AI_REQUEST_AUDIO_SPEED, handle_not_given(scope._kwargs.get("speed"), 1.0))
|
715
750
|
|
716
751
|
# Span Attributes for Cost
|
717
752
|
scope._span.set_attribute(SemanticConvention.GEN_AI_USAGE_COST, cost)
|
@@ -1,42 +1,62 @@
|
|
1
|
-
"""
|
1
|
+
"""
|
2
|
+
OpenLIT OpenAI Agents Instrumentation
|
3
|
+
"""
|
2
4
|
|
3
5
|
from typing import Collection
|
4
6
|
import importlib.metadata
|
5
7
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
6
|
-
from wrapt import wrap_function_wrapper
|
7
8
|
|
8
|
-
from openlit.instrumentation.openai_agents.
|
9
|
-
create_agent
|
10
|
-
)
|
9
|
+
from openlit.instrumentation.openai_agents.processor import OpenLITTracingProcessor
|
11
10
|
|
12
|
-
_instruments = (
|
11
|
+
_instruments = ("openai-agents >= 0.0.3",)
|
13
12
|
|
14
13
|
class OpenAIAgentsInstrumentor(BaseInstrumentor):
|
15
|
-
"""
|
16
|
-
An instrumentor for openai-agents's client library.
|
17
|
-
"""
|
14
|
+
"""OpenLIT instrumentor for OpenAI Agents using native tracing system"""
|
18
15
|
|
19
16
|
def instrumentation_dependencies(self) -> Collection[str]:
|
20
17
|
return _instruments
|
21
18
|
|
22
19
|
def _instrument(self, **kwargs):
|
23
|
-
|
24
|
-
environment = kwargs.get(
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
disable_metrics = kwargs.get(
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
20
|
+
version = importlib.metadata.version("openai-agents")
|
21
|
+
environment = kwargs.get("environment", "default")
|
22
|
+
application_name = kwargs.get("application_name", "default")
|
23
|
+
tracer = kwargs.get("tracer")
|
24
|
+
pricing_info = kwargs.get("pricing_info", {})
|
25
|
+
capture_message_content = kwargs.get("capture_message_content", False)
|
26
|
+
metrics = kwargs.get("metrics_dict")
|
27
|
+
disable_metrics = kwargs.get("disable_metrics")
|
28
|
+
detailed_tracing = kwargs.get("detailed_tracing", False)
|
29
|
+
|
30
|
+
# Create our processor with OpenLIT enhancements
|
31
|
+
processor = OpenLITTracingProcessor(
|
32
|
+
tracer=tracer,
|
33
|
+
version=version,
|
34
|
+
environment=environment,
|
35
|
+
application_name=application_name,
|
36
|
+
pricing_info=pricing_info,
|
37
|
+
capture_message_content=capture_message_content,
|
38
|
+
metrics=metrics,
|
39
|
+
disable_metrics=disable_metrics,
|
40
|
+
detailed_tracing=detailed_tracing
|
38
41
|
)
|
39
42
|
|
43
|
+
# Integrate with OpenAI Agents' native tracing system
|
44
|
+
try:
|
45
|
+
from agents import set_trace_processors
|
46
|
+
# Replace existing processors with our enhanced processor
|
47
|
+
set_trace_processors([processor])
|
48
|
+
except ImportError:
|
49
|
+
# Fallback: Add our processor to existing ones
|
50
|
+
try:
|
51
|
+
from agents import add_trace_processor
|
52
|
+
add_trace_processor(processor)
|
53
|
+
except ImportError:
|
54
|
+
pass # Agents package may not have tracing
|
55
|
+
|
40
56
|
def _uninstrument(self, **kwargs):
|
41
|
-
#
|
42
|
-
|
57
|
+
# Clear our processors
|
58
|
+
try:
|
59
|
+
from agents import set_trace_processors
|
60
|
+
set_trace_processors([])
|
61
|
+
except ImportError:
|
62
|
+
pass
|
@@ -0,0 +1,452 @@
|
|
1
|
+
"""
|
2
|
+
OpenLIT OpenAI Agents Instrumentation - Native TracingProcessor Implementation
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
from typing import Any, Dict, TYPE_CHECKING
|
7
|
+
|
8
|
+
from opentelemetry.trace import SpanKind, Status, StatusCode, set_span_in_context
|
9
|
+
|
10
|
+
from openlit.__helpers import (
|
11
|
+
common_framework_span_attributes,
|
12
|
+
handle_exception,
|
13
|
+
get_chat_model_cost
|
14
|
+
)
|
15
|
+
from openlit.semcov import SemanticConvention
|
16
|
+
|
17
|
+
# Try to import agents framework components with fallback
|
18
|
+
try:
|
19
|
+
from agents import TracingProcessor
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from agents import Trace, Span
|
22
|
+
TRACING_AVAILABLE = True
|
23
|
+
except ImportError:
|
24
|
+
# Create dummy class for when agents is not available
|
25
|
+
class TracingProcessor:
|
26
|
+
"""Dummy TracingProcessor class for when agents is not available"""
|
27
|
+
|
28
|
+
def force_flush(self):
|
29
|
+
"""Dummy force_flush method"""
|
30
|
+
return None
|
31
|
+
|
32
|
+
def shutdown(self):
|
33
|
+
"""Dummy shutdown method"""
|
34
|
+
return None
|
35
|
+
|
36
|
+
if TYPE_CHECKING:
|
37
|
+
# Type hints only - these don't exist at runtime when agents unavailable
|
38
|
+
Trace = Any
|
39
|
+
Span = Any
|
40
|
+
|
41
|
+
TRACING_AVAILABLE = False
|
42
|
+
|
43
|
+
|
44
|
+
class OpenLITTracingProcessor(TracingProcessor):
|
45
|
+
"""
|
46
|
+
OpenAI Agents tracing processor that integrates with OpenLIT observability.
|
47
|
+
|
48
|
+
This processor enhances OpenAI Agents' native tracing system with OpenLIT's
|
49
|
+
comprehensive observability features including business intelligence,
|
50
|
+
cost tracking, and performance metrics.
|
51
|
+
"""
|
52
|
+
|
53
|
+
def __init__(self, tracer, version, environment, application_name,
|
54
|
+
pricing_info, capture_message_content, metrics,
|
55
|
+
disable_metrics, detailed_tracing, **kwargs):
|
56
|
+
"""Initialize the OpenLIT tracing processor."""
|
57
|
+
super().__init__()
|
58
|
+
|
59
|
+
# Core configuration
|
60
|
+
self.tracer = tracer
|
61
|
+
self.version = version
|
62
|
+
self.environment = environment
|
63
|
+
self.application_name = application_name
|
64
|
+
self.pricing_info = pricing_info
|
65
|
+
self.capture_message_content = capture_message_content
|
66
|
+
self.metrics = metrics
|
67
|
+
self.disable_metrics = disable_metrics
|
68
|
+
self.detailed_tracing = detailed_tracing
|
69
|
+
|
70
|
+
# Internal tracking
|
71
|
+
self.active_spans = {}
|
72
|
+
self.span_stack = []
|
73
|
+
|
74
|
+
def start_trace(self, trace_id: str, name: str, **kwargs):
|
75
|
+
"""
|
76
|
+
Start a new trace with OpenLIT enhancements.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
trace_id: Unique trace identifier
|
80
|
+
name: Trace name
|
81
|
+
**kwargs: Additional trace metadata
|
82
|
+
"""
|
83
|
+
try:
|
84
|
+
# Generate span name using OpenTelemetry conventions
|
85
|
+
span_name = self._get_span_name(name, **kwargs)
|
86
|
+
|
87
|
+
# Start root span with OpenLIT context
|
88
|
+
span = self.tracer.start_as_current_span(
|
89
|
+
span_name,
|
90
|
+
kind=SpanKind.CLIENT,
|
91
|
+
attributes={
|
92
|
+
SemanticConvention.GEN_AI_SYSTEM: "openai_agents",
|
93
|
+
SemanticConvention.GEN_AI_OPERATION:
|
94
|
+
SemanticConvention.GEN_AI_OPERATION_TYPE_WORKFLOW,
|
95
|
+
"trace.id": trace_id,
|
96
|
+
"trace.name": name,
|
97
|
+
}
|
98
|
+
)
|
99
|
+
|
100
|
+
# Create scope for common attributes
|
101
|
+
scope = type("GenericScope", (), {})()
|
102
|
+
scope._span = span # pylint: disable=protected-access
|
103
|
+
scope._start_time = time.time() # pylint: disable=protected-access
|
104
|
+
scope._end_time = None # pylint: disable=protected-access
|
105
|
+
|
106
|
+
# Apply common framework attributes
|
107
|
+
common_framework_span_attributes(
|
108
|
+
scope,
|
109
|
+
"openai_agents",
|
110
|
+
"api.openai.com",
|
111
|
+
443,
|
112
|
+
self.environment,
|
113
|
+
self.application_name,
|
114
|
+
self.version,
|
115
|
+
name
|
116
|
+
)
|
117
|
+
|
118
|
+
# Track active span
|
119
|
+
self.active_spans[trace_id] = span
|
120
|
+
self.span_stack.append(span)
|
121
|
+
|
122
|
+
return span
|
123
|
+
|
124
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
125
|
+
# Graceful degradation
|
126
|
+
handle_exception(None, e)
|
127
|
+
return None
|
128
|
+
|
129
|
+
def end_trace(self, trace_id: str, **kwargs):
|
130
|
+
"""
|
131
|
+
End an active trace.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
trace_id: Trace identifier to end
|
135
|
+
**kwargs: Additional metadata
|
136
|
+
"""
|
137
|
+
try:
|
138
|
+
span = self.active_spans.get(trace_id)
|
139
|
+
if span:
|
140
|
+
# Set final attributes and status
|
141
|
+
span.set_status(Status(StatusCode.OK))
|
142
|
+
|
143
|
+
# End span
|
144
|
+
span.end()
|
145
|
+
|
146
|
+
# Cleanup tracking
|
147
|
+
if trace_id in self.active_spans:
|
148
|
+
del self.active_spans[trace_id]
|
149
|
+
if span in self.span_stack:
|
150
|
+
self.span_stack.remove(span)
|
151
|
+
|
152
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
153
|
+
handle_exception(span if span else None, e)
|
154
|
+
|
155
|
+
def _get_span_name(self, operation_name: str, **metadata) -> str:
|
156
|
+
"""
|
157
|
+
Generate OpenTelemetry-compliant span names.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
operation_name: Base operation name
|
161
|
+
**metadata: Additional context for naming
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
Formatted span name following semantic conventions
|
165
|
+
"""
|
166
|
+
# Extract context for naming
|
167
|
+
agent_name = metadata.get('agent_name', '')
|
168
|
+
model_name = metadata.get('model_name', '')
|
169
|
+
tool_name = metadata.get('tool_name', '')
|
170
|
+
workflow_name = metadata.get('workflow_name', '')
|
171
|
+
|
172
|
+
# Apply OpenTelemetry semantic conventions for GenAI agents
|
173
|
+
if 'agent' in operation_name.lower():
|
174
|
+
if agent_name:
|
175
|
+
return f"invoke_agent {agent_name}"
|
176
|
+
return "invoke_agent"
|
177
|
+
if 'chat' in operation_name.lower():
|
178
|
+
if model_name:
|
179
|
+
return f"chat {model_name}"
|
180
|
+
return "chat response"
|
181
|
+
if 'tool' in operation_name.lower():
|
182
|
+
if tool_name:
|
183
|
+
return f"execute_tool {tool_name}"
|
184
|
+
return "execute_tool"
|
185
|
+
if 'handoff' in operation_name.lower():
|
186
|
+
target_agent = metadata.get('target_agent', 'unknown')
|
187
|
+
return f"invoke_agent {target_agent}"
|
188
|
+
if 'workflow' in operation_name.lower():
|
189
|
+
if workflow_name:
|
190
|
+
return f"workflow {workflow_name}"
|
191
|
+
return "workflow"
|
192
|
+
|
193
|
+
# Default case
|
194
|
+
return operation_name
|
195
|
+
|
196
|
+
def span_start(self, span_data, trace_id: str):
|
197
|
+
"""
|
198
|
+
Handle span start events from OpenAI Agents.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
span_data: Span data from agents framework
|
202
|
+
trace_id: Associated trace identifier
|
203
|
+
"""
|
204
|
+
try:
|
205
|
+
# Extract span information
|
206
|
+
span_name = getattr(span_data, 'name', 'unknown_operation')
|
207
|
+
span_type = getattr(span_data, 'type', 'unknown')
|
208
|
+
|
209
|
+
# Generate enhanced span name
|
210
|
+
enhanced_name = self._get_span_name(
|
211
|
+
span_name,
|
212
|
+
agent_name=getattr(span_data, 'agent_name', None),
|
213
|
+
model_name=getattr(span_data, 'model_name', None),
|
214
|
+
tool_name=getattr(span_data, 'tool_name', None)
|
215
|
+
)
|
216
|
+
|
217
|
+
# Determine span operation type
|
218
|
+
operation_type = self._get_operation_type(span_type, span_name)
|
219
|
+
|
220
|
+
# Start span with proper context
|
221
|
+
parent_span = self.span_stack[-1] if self.span_stack else None
|
222
|
+
context = set_span_in_context(parent_span) if parent_span else None
|
223
|
+
|
224
|
+
span = self.tracer.start_as_current_span(
|
225
|
+
enhanced_name,
|
226
|
+
kind=SpanKind.CLIENT,
|
227
|
+
context=context,
|
228
|
+
attributes={
|
229
|
+
SemanticConvention.GEN_AI_SYSTEM: "openai_agents",
|
230
|
+
SemanticConvention.GEN_AI_OPERATION: operation_type,
|
231
|
+
"span.type": span_type,
|
232
|
+
"span.id": getattr(span_data, 'span_id', ''),
|
233
|
+
}
|
234
|
+
)
|
235
|
+
|
236
|
+
# Process specific span types
|
237
|
+
self._process_span_attributes(span, span_data, span_type)
|
238
|
+
|
239
|
+
# Track span
|
240
|
+
span_id = getattr(span_data, 'span_id', len(self.span_stack))
|
241
|
+
self.active_spans[f"{trace_id}:{span_id}"] = span
|
242
|
+
self.span_stack.append(span)
|
243
|
+
|
244
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
245
|
+
handle_exception(None, e)
|
246
|
+
|
247
|
+
def _get_operation_type(self, span_type: str, span_name: str) -> str:
|
248
|
+
"""Get operation type based on span characteristics."""
|
249
|
+
type_mapping = {
|
250
|
+
'agent': SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
|
251
|
+
'generation': SemanticConvention.GEN_AI_OPERATION_CHAT,
|
252
|
+
'function': SemanticConvention.GEN_AI_OPERATION_CHAT,
|
253
|
+
'tool': SemanticConvention.GEN_AI_OPERATION_CHAT,
|
254
|
+
'handoff': SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
|
255
|
+
}
|
256
|
+
|
257
|
+
# Check span type first
|
258
|
+
for key, operation in type_mapping.items():
|
259
|
+
if key in span_type.lower():
|
260
|
+
return operation
|
261
|
+
|
262
|
+
# Check span name
|
263
|
+
for key, operation in type_mapping.items():
|
264
|
+
if key in span_name.lower():
|
265
|
+
return operation
|
266
|
+
|
267
|
+
return SemanticConvention.GEN_AI_OPERATION_CHAT
|
268
|
+
|
269
|
+
def _process_span_attributes(self, span, span_data, span_type: str):
|
270
|
+
"""Process and set span attributes based on span type."""
|
271
|
+
try:
|
272
|
+
# Common attributes
|
273
|
+
if hasattr(span_data, 'agent_name'):
|
274
|
+
span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME,
|
275
|
+
span_data.agent_name)
|
276
|
+
|
277
|
+
if hasattr(span_data, 'model_name'):
|
278
|
+
span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MODEL,
|
279
|
+
span_data.model_name)
|
280
|
+
|
281
|
+
# Agent-specific attributes
|
282
|
+
if span_type == 'agent':
|
283
|
+
self._process_agent_span(span, span_data)
|
284
|
+
|
285
|
+
# Generation-specific attributes
|
286
|
+
elif span_type == 'generation':
|
287
|
+
self._process_generation_span(span, span_data)
|
288
|
+
|
289
|
+
# Function/Tool-specific attributes
|
290
|
+
elif span_type in ['function', 'tool']:
|
291
|
+
self._process_function_span(span, span_data)
|
292
|
+
|
293
|
+
# Handoff-specific attributes
|
294
|
+
elif span_type == 'handoff':
|
295
|
+
self._process_handoff_span(span, span_data)
|
296
|
+
|
297
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
298
|
+
handle_exception(span, e)
|
299
|
+
|
300
|
+
def _process_agent_span(self, span, agent_span):
|
301
|
+
"""Process agent span data (unused parameter)."""
|
302
|
+
# Agent-specific processing
|
303
|
+
if hasattr(agent_span, 'instructions'):
|
304
|
+
span.set_attribute(SemanticConvention.GEN_AI_AGENT_DESCRIPTION,
|
305
|
+
str(agent_span.instructions)[:500])
|
306
|
+
|
307
|
+
if hasattr(agent_span, 'model'):
|
308
|
+
span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MODEL,
|
309
|
+
agent_span.model)
|
310
|
+
|
311
|
+
def _process_generation_span(self, span, generation_span):
|
312
|
+
"""Process generation span data."""
|
313
|
+
# Set generation-specific attributes
|
314
|
+
if hasattr(generation_span, 'prompt'):
|
315
|
+
span.set_attribute(SemanticConvention.GEN_AI_PROMPT,
|
316
|
+
str(generation_span.prompt)[:1000])
|
317
|
+
|
318
|
+
if hasattr(generation_span, 'completion'):
|
319
|
+
span.set_attribute(SemanticConvention.GEN_AI_COMPLETION,
|
320
|
+
str(generation_span.completion)[:1000])
|
321
|
+
|
322
|
+
if hasattr(generation_span, 'usage'):
|
323
|
+
usage = generation_span.usage
|
324
|
+
if hasattr(usage, 'prompt_tokens'):
|
325
|
+
span.set_attribute(SemanticConvention.GEN_AI_USAGE_PROMPT_TOKENS,
|
326
|
+
usage.prompt_tokens)
|
327
|
+
if hasattr(usage, 'completion_tokens'):
|
328
|
+
span.set_attribute(
|
329
|
+
SemanticConvention.GEN_AI_USAGE_COMPLETION_TOKENS,
|
330
|
+
usage.completion_tokens
|
331
|
+
)
|
332
|
+
|
333
|
+
def _process_function_span(self, span, function_span):
|
334
|
+
"""Process function/tool span data."""
|
335
|
+
if hasattr(function_span, 'function_name'):
|
336
|
+
span.set_attribute(SemanticConvention.GEN_AI_TOOL_NAME,
|
337
|
+
function_span.function_name)
|
338
|
+
|
339
|
+
if hasattr(function_span, 'arguments'):
|
340
|
+
span.set_attribute("gen_ai.tool.arguments",
|
341
|
+
str(function_span.arguments)[:500])
|
342
|
+
|
343
|
+
if hasattr(function_span, 'result'):
|
344
|
+
span.set_attribute("gen_ai.tool.result",
|
345
|
+
str(function_span.result)[:500])
|
346
|
+
|
347
|
+
def _process_handoff_span(self, span, handoff_span):
|
348
|
+
"""Process handoff span data."""
|
349
|
+
if hasattr(handoff_span, 'target_agent'):
|
350
|
+
span.set_attribute("gen_ai.handoff.target_agent",
|
351
|
+
handoff_span.target_agent)
|
352
|
+
|
353
|
+
if hasattr(handoff_span, 'reason'):
|
354
|
+
span.set_attribute("gen_ai.handoff.reason",
|
355
|
+
str(handoff_span.reason)[:200])
|
356
|
+
|
357
|
+
def span_end(self, span_data, trace_id: str):
|
358
|
+
"""Handle span end events."""
|
359
|
+
try:
|
360
|
+
span_id = getattr(span_data, 'span_id', '')
|
361
|
+
span_key = f"{trace_id}:{span_id}"
|
362
|
+
|
363
|
+
span = self.active_spans.get(span_key)
|
364
|
+
if span:
|
365
|
+
# Set final status
|
366
|
+
if hasattr(span_data, 'error') and span_data.error:
|
367
|
+
span.set_status(Status(StatusCode.ERROR,
|
368
|
+
str(span_data.error)))
|
369
|
+
else:
|
370
|
+
span.set_status(Status(StatusCode.OK))
|
371
|
+
|
372
|
+
# End span
|
373
|
+
span.end()
|
374
|
+
|
375
|
+
# Cleanup
|
376
|
+
if span_key in self.active_spans:
|
377
|
+
del self.active_spans[span_key]
|
378
|
+
if span in self.span_stack:
|
379
|
+
self.span_stack.remove(span)
|
380
|
+
|
381
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
382
|
+
handle_exception(span if 'span' in locals() else None, e)
|
383
|
+
|
384
|
+
def force_flush(self):
|
385
|
+
"""Force flush all pending spans."""
|
386
|
+
try:
|
387
|
+
# End any remaining spans
|
388
|
+
for span in list(self.active_spans.values()):
|
389
|
+
span.end()
|
390
|
+
|
391
|
+
self.active_spans.clear()
|
392
|
+
self.span_stack.clear()
|
393
|
+
|
394
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
395
|
+
handle_exception(None, e)
|
396
|
+
|
397
|
+
def shutdown(self):
|
398
|
+
"""Shutdown the processor."""
|
399
|
+
self.force_flush()
|
400
|
+
|
401
|
+
def _extract_model_info(self, span_data) -> Dict[str, Any]:
|
402
|
+
"""Extract model information from span data."""
|
403
|
+
model_info = {}
|
404
|
+
|
405
|
+
if hasattr(span_data, 'model'):
|
406
|
+
model_info['model'] = span_data.model
|
407
|
+
if hasattr(span_data, 'model_name'):
|
408
|
+
model_info['model'] = span_data.model_name
|
409
|
+
|
410
|
+
return model_info
|
411
|
+
|
412
|
+
def _calculate_cost(self, model: str, prompt_tokens: int,
|
413
|
+
completion_tokens: int) -> float:
|
414
|
+
"""Calculate cost based on token usage."""
|
415
|
+
try:
|
416
|
+
return get_chat_model_cost(
|
417
|
+
model, self.pricing_info, prompt_tokens, completion_tokens
|
418
|
+
)
|
419
|
+
except Exception: # pylint: disable=broad-exception-caught
|
420
|
+
return 0.0
|
421
|
+
|
422
|
+
# Abstract method implementations required by OpenAI Agents framework
|
423
|
+
def on_trace_start(self, trace):
|
424
|
+
"""Called when a trace starts - required by OpenAI Agents framework"""
|
425
|
+
try:
|
426
|
+
self.start_trace(getattr(trace, 'trace_id', 'unknown'),
|
427
|
+
getattr(trace, 'name', 'workflow'))
|
428
|
+
except Exception: # pylint: disable=broad-exception-caught
|
429
|
+
pass
|
430
|
+
|
431
|
+
def on_trace_end(self, trace):
|
432
|
+
"""Called when a trace ends - required by OpenAI Agents framework"""
|
433
|
+
try:
|
434
|
+
self.end_trace(getattr(trace, 'trace_id', 'unknown'))
|
435
|
+
except Exception: # pylint: disable=broad-exception-caught
|
436
|
+
pass
|
437
|
+
|
438
|
+
def on_span_start(self, span):
|
439
|
+
"""Called when a span starts - required by OpenAI Agents framework"""
|
440
|
+
try:
|
441
|
+
trace_id = getattr(span, 'trace_id', 'unknown')
|
442
|
+
self.span_start(span, trace_id)
|
443
|
+
except Exception: # pylint: disable=broad-exception-caught
|
444
|
+
pass
|
445
|
+
|
446
|
+
def on_span_end(self, span):
|
447
|
+
"""Called when a span ends - required by OpenAI Agents framework"""
|
448
|
+
try:
|
449
|
+
trace_id = getattr(span, 'trace_id', 'unknown')
|
450
|
+
self.span_end(span, trace_id)
|
451
|
+
except Exception: # pylint: disable=broad-exception-caught
|
452
|
+
pass
|