openlit 1.34.29__py3-none-any.whl → 1.34.31__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/__helpers.py +235 -86
- openlit/__init__.py +16 -13
- openlit/_instrumentors.py +2 -1
- openlit/evals/all.py +50 -21
- openlit/evals/bias_detection.py +47 -20
- openlit/evals/hallucination.py +53 -22
- openlit/evals/toxicity.py +50 -21
- openlit/evals/utils.py +54 -30
- openlit/guard/all.py +61 -19
- openlit/guard/prompt_injection.py +34 -14
- openlit/guard/restrict_topic.py +46 -15
- openlit/guard/sensitive_topic.py +34 -14
- openlit/guard/utils.py +58 -22
- openlit/instrumentation/ag2/__init__.py +24 -8
- openlit/instrumentation/ag2/ag2.py +34 -13
- openlit/instrumentation/ag2/async_ag2.py +34 -13
- openlit/instrumentation/ag2/utils.py +133 -30
- openlit/instrumentation/ai21/__init__.py +43 -14
- openlit/instrumentation/ai21/ai21.py +47 -21
- openlit/instrumentation/ai21/async_ai21.py +47 -21
- openlit/instrumentation/ai21/utils.py +299 -78
- openlit/instrumentation/anthropic/__init__.py +21 -4
- openlit/instrumentation/anthropic/anthropic.py +28 -17
- openlit/instrumentation/anthropic/async_anthropic.py +28 -17
- openlit/instrumentation/anthropic/utils.py +145 -35
- openlit/instrumentation/assemblyai/__init__.py +11 -2
- openlit/instrumentation/assemblyai/assemblyai.py +15 -4
- openlit/instrumentation/assemblyai/utils.py +120 -25
- openlit/instrumentation/astra/__init__.py +43 -10
- openlit/instrumentation/astra/astra.py +28 -5
- openlit/instrumentation/astra/async_astra.py +28 -5
- openlit/instrumentation/astra/utils.py +151 -55
- openlit/instrumentation/azure_ai_inference/__init__.py +43 -10
- openlit/instrumentation/azure_ai_inference/async_azure_ai_inference.py +53 -21
- openlit/instrumentation/azure_ai_inference/azure_ai_inference.py +53 -21
- openlit/instrumentation/azure_ai_inference/utils.py +307 -83
- openlit/instrumentation/bedrock/__init__.py +21 -4
- openlit/instrumentation/bedrock/bedrock.py +63 -25
- openlit/instrumentation/bedrock/utils.py +139 -30
- openlit/instrumentation/chroma/__init__.py +89 -16
- openlit/instrumentation/chroma/chroma.py +28 -6
- openlit/instrumentation/chroma/utils.py +167 -51
- openlit/instrumentation/cohere/__init__.py +63 -18
- openlit/instrumentation/cohere/async_cohere.py +63 -24
- openlit/instrumentation/cohere/cohere.py +63 -24
- openlit/instrumentation/cohere/utils.py +286 -73
- openlit/instrumentation/controlflow/__init__.py +35 -9
- openlit/instrumentation/controlflow/controlflow.py +66 -33
- openlit/instrumentation/crawl4ai/__init__.py +25 -10
- openlit/instrumentation/crawl4ai/async_crawl4ai.py +78 -31
- openlit/instrumentation/crawl4ai/crawl4ai.py +78 -31
- openlit/instrumentation/crewai/__init__.py +111 -24
- openlit/instrumentation/crewai/async_crewai.py +114 -0
- openlit/instrumentation/crewai/crewai.py +104 -131
- openlit/instrumentation/crewai/utils.py +615 -0
- openlit/instrumentation/dynamiq/__init__.py +46 -12
- openlit/instrumentation/dynamiq/dynamiq.py +74 -33
- openlit/instrumentation/elevenlabs/__init__.py +23 -4
- openlit/instrumentation/elevenlabs/async_elevenlabs.py +16 -4
- openlit/instrumentation/elevenlabs/elevenlabs.py +16 -4
- openlit/instrumentation/elevenlabs/utils.py +128 -25
- openlit/instrumentation/embedchain/__init__.py +11 -2
- openlit/instrumentation/embedchain/embedchain.py +68 -35
- openlit/instrumentation/firecrawl/__init__.py +24 -7
- openlit/instrumentation/firecrawl/firecrawl.py +46 -20
- openlit/instrumentation/google_ai_studio/__init__.py +45 -10
- openlit/instrumentation/google_ai_studio/async_google_ai_studio.py +67 -44
- openlit/instrumentation/google_ai_studio/google_ai_studio.py +67 -44
- openlit/instrumentation/google_ai_studio/utils.py +180 -67
- openlit/instrumentation/gpt4all/__init__.py +22 -7
- openlit/instrumentation/gpt4all/gpt4all.py +67 -29
- openlit/instrumentation/gpt4all/utils.py +285 -61
- openlit/instrumentation/gpu/__init__.py +128 -47
- openlit/instrumentation/groq/__init__.py +21 -4
- openlit/instrumentation/groq/async_groq.py +33 -21
- openlit/instrumentation/groq/groq.py +33 -21
- openlit/instrumentation/groq/utils.py +192 -55
- openlit/instrumentation/haystack/__init__.py +70 -24
- openlit/instrumentation/haystack/async_haystack.py +28 -6
- openlit/instrumentation/haystack/haystack.py +28 -6
- openlit/instrumentation/haystack/utils.py +196 -74
- openlit/instrumentation/julep/__init__.py +69 -19
- openlit/instrumentation/julep/async_julep.py +53 -27
- openlit/instrumentation/julep/julep.py +53 -28
- openlit/instrumentation/langchain/__init__.py +74 -63
- openlit/instrumentation/langchain/callback_handler.py +1100 -0
- openlit/instrumentation/langchain_community/__init__.py +13 -2
- openlit/instrumentation/langchain_community/async_langchain_community.py +23 -5
- openlit/instrumentation/langchain_community/langchain_community.py +23 -5
- openlit/instrumentation/langchain_community/utils.py +35 -9
- openlit/instrumentation/letta/__init__.py +68 -15
- openlit/instrumentation/letta/letta.py +99 -54
- openlit/instrumentation/litellm/__init__.py +43 -14
- openlit/instrumentation/litellm/async_litellm.py +51 -26
- openlit/instrumentation/litellm/litellm.py +51 -26
- openlit/instrumentation/litellm/utils.py +312 -101
- openlit/instrumentation/llamaindex/__init__.py +267 -90
- openlit/instrumentation/llamaindex/async_llamaindex.py +28 -6
- openlit/instrumentation/llamaindex/llamaindex.py +28 -6
- openlit/instrumentation/llamaindex/utils.py +204 -91
- openlit/instrumentation/mem0/__init__.py +11 -2
- openlit/instrumentation/mem0/mem0.py +50 -29
- openlit/instrumentation/milvus/__init__.py +10 -2
- openlit/instrumentation/milvus/milvus.py +31 -6
- openlit/instrumentation/milvus/utils.py +166 -67
- openlit/instrumentation/mistral/__init__.py +63 -18
- openlit/instrumentation/mistral/async_mistral.py +63 -24
- openlit/instrumentation/mistral/mistral.py +63 -24
- openlit/instrumentation/mistral/utils.py +277 -69
- openlit/instrumentation/multion/__init__.py +69 -19
- openlit/instrumentation/multion/async_multion.py +57 -26
- openlit/instrumentation/multion/multion.py +57 -26
- openlit/instrumentation/ollama/__init__.py +39 -18
- openlit/instrumentation/ollama/async_ollama.py +57 -26
- openlit/instrumentation/ollama/ollama.py +57 -26
- openlit/instrumentation/ollama/utils.py +226 -50
- openlit/instrumentation/openai/__init__.py +156 -32
- openlit/instrumentation/openai/async_openai.py +147 -67
- openlit/instrumentation/openai/openai.py +150 -67
- openlit/instrumentation/openai/utils.py +660 -186
- openlit/instrumentation/openai_agents/__init__.py +6 -2
- openlit/instrumentation/openai_agents/processor.py +409 -537
- openlit/instrumentation/phidata/__init__.py +13 -5
- openlit/instrumentation/phidata/phidata.py +67 -32
- openlit/instrumentation/pinecone/__init__.py +48 -9
- openlit/instrumentation/pinecone/async_pinecone.py +27 -5
- openlit/instrumentation/pinecone/pinecone.py +27 -5
- openlit/instrumentation/pinecone/utils.py +153 -47
- openlit/instrumentation/premai/__init__.py +22 -7
- openlit/instrumentation/premai/premai.py +51 -26
- openlit/instrumentation/premai/utils.py +246 -59
- openlit/instrumentation/pydantic_ai/__init__.py +49 -22
- openlit/instrumentation/pydantic_ai/pydantic_ai.py +69 -16
- openlit/instrumentation/pydantic_ai/utils.py +89 -24
- openlit/instrumentation/qdrant/__init__.py +19 -4
- openlit/instrumentation/qdrant/async_qdrant.py +33 -7
- openlit/instrumentation/qdrant/qdrant.py +33 -7
- openlit/instrumentation/qdrant/utils.py +228 -93
- openlit/instrumentation/reka/__init__.py +23 -10
- openlit/instrumentation/reka/async_reka.py +17 -11
- openlit/instrumentation/reka/reka.py +17 -11
- openlit/instrumentation/reka/utils.py +138 -36
- openlit/instrumentation/together/__init__.py +44 -12
- openlit/instrumentation/together/async_together.py +50 -27
- openlit/instrumentation/together/together.py +50 -27
- openlit/instrumentation/together/utils.py +301 -71
- openlit/instrumentation/transformers/__init__.py +2 -1
- openlit/instrumentation/transformers/transformers.py +13 -3
- openlit/instrumentation/transformers/utils.py +139 -36
- openlit/instrumentation/vertexai/__init__.py +81 -16
- openlit/instrumentation/vertexai/async_vertexai.py +33 -15
- openlit/instrumentation/vertexai/utils.py +123 -27
- openlit/instrumentation/vertexai/vertexai.py +33 -15
- openlit/instrumentation/vllm/__init__.py +12 -5
- openlit/instrumentation/vllm/utils.py +121 -31
- openlit/instrumentation/vllm/vllm.py +16 -10
- openlit/otel/events.py +35 -10
- openlit/otel/metrics.py +32 -24
- openlit/otel/tracing.py +24 -9
- openlit/semcov/__init__.py +101 -7
- {openlit-1.34.29.dist-info → openlit-1.34.31.dist-info}/METADATA +2 -1
- openlit-1.34.31.dist-info/RECORD +166 -0
- openlit/instrumentation/langchain/async_langchain.py +0 -102
- openlit/instrumentation/langchain/langchain.py +0 -102
- openlit/instrumentation/langchain/utils.py +0 -252
- openlit-1.34.29.dist-info/RECORD +0 -166
- {openlit-1.34.29.dist-info → openlit-1.34.31.dist-info}/LICENSE +0 -0
- {openlit-1.34.29.dist-info → openlit-1.34.31.dist-info}/WHEEL +0 -0
@@ -2,26 +2,22 @@
|
|
2
2
|
OpenLIT OpenAI Agents Instrumentation - Native TracingProcessor Implementation
|
3
3
|
"""
|
4
4
|
|
5
|
-
import json
|
6
5
|
import time
|
7
|
-
from
|
8
|
-
from typing import Any, Dict, Optional, TYPE_CHECKING
|
6
|
+
from typing import Any, Dict, TYPE_CHECKING
|
9
7
|
|
10
|
-
from opentelemetry import context as context_api
|
11
8
|
from opentelemetry.trace import SpanKind, Status, StatusCode, set_span_in_context
|
12
|
-
from opentelemetry.context import detach
|
13
9
|
|
14
10
|
from openlit.__helpers import (
|
15
11
|
common_framework_span_attributes,
|
16
12
|
handle_exception,
|
17
|
-
|
18
|
-
get_chat_model_cost
|
13
|
+
get_chat_model_cost,
|
19
14
|
)
|
20
15
|
from openlit.semcov import SemanticConvention
|
21
16
|
|
22
17
|
# Try to import agents framework components with fallback
|
23
18
|
try:
|
24
19
|
from agents import TracingProcessor
|
20
|
+
|
25
21
|
if TYPE_CHECKING:
|
26
22
|
from agents import Trace, Span
|
27
23
|
TRACING_AVAILABLE = True
|
@@ -48,553 +44,429 @@ except ImportError:
|
|
48
44
|
|
49
45
|
class OpenLITTracingProcessor(TracingProcessor):
|
50
46
|
"""
|
51
|
-
|
52
|
-
|
47
|
+
OpenAI Agents tracing processor that integrates with OpenLIT observability.
|
48
|
+
|
49
|
+
This processor enhances OpenAI Agents' native tracing system with OpenLIT's
|
50
|
+
comprehensive observability features including business intelligence,
|
51
|
+
cost tracking, and performance metrics.
|
53
52
|
"""
|
54
53
|
|
55
|
-
def __init__(
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
#
|
72
|
-
self.
|
73
|
-
self.
|
74
|
-
self.
|
75
|
-
self.
|
76
|
-
|
77
|
-
|
78
|
-
self.
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
# Track span and context
|
144
|
-
self._otel_spans[span.span_id] = otel_span
|
145
|
-
self._tokens[span.span_id] = context_api.attach(set_span_in_context(otel_span))
|
146
|
-
self._span_start_times[span.span_id] = time.time()
|
147
|
-
|
148
|
-
def on_span_end(self, span: "Span[Any]") -> None:
|
149
|
-
"""Called when a span is finished - adds business intelligence and ends span"""
|
150
|
-
if not TRACING_AVAILABLE or span.span_id not in self._otel_spans:
|
151
|
-
return
|
152
|
-
|
153
|
-
otel_span = self._otel_spans[span.span_id]
|
54
|
+
def __init__(
|
55
|
+
self,
|
56
|
+
tracer,
|
57
|
+
version,
|
58
|
+
environment,
|
59
|
+
application_name,
|
60
|
+
pricing_info,
|
61
|
+
capture_message_content,
|
62
|
+
metrics,
|
63
|
+
disable_metrics,
|
64
|
+
detailed_tracing,
|
65
|
+
**kwargs,
|
66
|
+
):
|
67
|
+
"""Initialize the OpenLIT tracing processor."""
|
68
|
+
super().__init__()
|
69
|
+
|
70
|
+
# Core configuration
|
71
|
+
self.tracer = tracer
|
72
|
+
self.version = version
|
73
|
+
self.environment = environment
|
74
|
+
self.application_name = application_name
|
75
|
+
self.pricing_info = pricing_info
|
76
|
+
self.capture_message_content = capture_message_content
|
77
|
+
self.metrics = metrics
|
78
|
+
self.disable_metrics = disable_metrics
|
79
|
+
self.detailed_tracing = detailed_tracing
|
80
|
+
|
81
|
+
# Internal tracking
|
82
|
+
self.active_spans = {}
|
83
|
+
self.span_stack = []
|
84
|
+
|
85
|
+
def start_trace(self, trace_id: str, name: str, **kwargs):
|
86
|
+
"""
|
87
|
+
Start a new trace with OpenLIT enhancements.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
trace_id: Unique trace identifier
|
91
|
+
name: Trace name
|
92
|
+
**kwargs: Additional trace metadata
|
93
|
+
"""
|
94
|
+
try:
|
95
|
+
# Generate span name using OpenTelemetry conventions
|
96
|
+
span_name = self._get_span_name(name, **kwargs)
|
97
|
+
|
98
|
+
# Start root span with OpenLIT context
|
99
|
+
span = self.tracer.start_as_current_span(
|
100
|
+
span_name,
|
101
|
+
kind=SpanKind.CLIENT,
|
102
|
+
attributes={
|
103
|
+
SemanticConvention.GEN_AI_SYSTEM: "openai_agents",
|
104
|
+
SemanticConvention.GEN_AI_OPERATION: SemanticConvention.GEN_AI_OPERATION_TYPE_WORKFLOW,
|
105
|
+
"trace.id": trace_id,
|
106
|
+
"trace.name": name,
|
107
|
+
},
|
108
|
+
)
|
109
|
+
|
110
|
+
# Create scope for common attributes
|
111
|
+
scope = type("GenericScope", (), {})()
|
112
|
+
scope._span = span # pylint: disable=protected-access
|
113
|
+
scope._start_time = time.time() # pylint: disable=protected-access
|
114
|
+
scope._end_time = None # pylint: disable=protected-access
|
115
|
+
|
116
|
+
# Apply common framework attributes
|
117
|
+
common_framework_span_attributes(
|
118
|
+
scope,
|
119
|
+
"openai_agents",
|
120
|
+
"api.openai.com",
|
121
|
+
443,
|
122
|
+
self.environment,
|
123
|
+
self.application_name,
|
124
|
+
self.version,
|
125
|
+
name,
|
126
|
+
)
|
127
|
+
|
128
|
+
# Track active span
|
129
|
+
self.active_spans[trace_id] = span
|
130
|
+
self.span_stack.append(span)
|
131
|
+
|
132
|
+
return span
|
133
|
+
|
134
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
135
|
+
# Graceful degradation
|
136
|
+
handle_exception(None, e)
|
137
|
+
return None
|
138
|
+
|
139
|
+
def end_trace(self, trace_id: str, **kwargs):
|
140
|
+
"""
|
141
|
+
End an active trace.
|
154
142
|
|
143
|
+
Args:
|
144
|
+
trace_id: Trace identifier to end
|
145
|
+
**kwargs: Additional metadata
|
146
|
+
"""
|
155
147
|
try:
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
self.
|
169
|
-
|
170
|
-
|
148
|
+
span = self.active_spans.get(trace_id)
|
149
|
+
if span:
|
150
|
+
# Set final attributes and status
|
151
|
+
span.set_status(Status(StatusCode.OK))
|
152
|
+
|
153
|
+
# End span
|
154
|
+
span.end()
|
155
|
+
|
156
|
+
# Cleanup tracking
|
157
|
+
if trace_id in self.active_spans:
|
158
|
+
del self.active_spans[trace_id]
|
159
|
+
if span in self.span_stack:
|
160
|
+
self.span_stack.remove(span)
|
161
|
+
|
162
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
163
|
+
handle_exception(span if span else None, e)
|
164
|
+
|
165
|
+
def _get_span_name(self, operation_name: str, **metadata) -> str:
|
166
|
+
"""
|
167
|
+
Generate OpenTelemetry-compliant span names.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
operation_name: Base operation name
|
171
|
+
**metadata: Additional context for naming
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
Formatted span name following semantic conventions
|
175
|
+
"""
|
176
|
+
# Extract context for naming
|
177
|
+
agent_name = metadata.get("agent_name", "")
|
178
|
+
model_name = metadata.get("model_name", "")
|
179
|
+
tool_name = metadata.get("tool_name", "")
|
180
|
+
workflow_name = metadata.get("workflow_name", "")
|
181
|
+
|
182
|
+
# Apply OpenTelemetry semantic conventions for GenAI agents
|
183
|
+
if "agent" in operation_name.lower():
|
184
|
+
if agent_name:
|
185
|
+
return f"invoke_agent {agent_name}"
|
186
|
+
return "invoke_agent"
|
187
|
+
if "chat" in operation_name.lower():
|
188
|
+
if model_name:
|
189
|
+
return f"chat {model_name}"
|
190
|
+
return "chat response"
|
191
|
+
if "tool" in operation_name.lower():
|
192
|
+
if tool_name:
|
193
|
+
return f"execute_tool {tool_name}"
|
194
|
+
return "execute_tool"
|
195
|
+
if "handoff" in operation_name.lower():
|
196
|
+
target_agent = metadata.get("target_agent", "unknown")
|
197
|
+
return f"invoke_agent {target_agent}"
|
198
|
+
if "workflow" in operation_name.lower():
|
199
|
+
if workflow_name:
|
200
|
+
return f"workflow {workflow_name}"
|
201
|
+
return "workflow"
|
202
|
+
|
203
|
+
# Default case
|
204
|
+
return operation_name
|
205
|
+
|
206
|
+
def span_start(self, span_data, trace_id: str):
|
207
|
+
"""
|
208
|
+
Handle span start events from OpenAI Agents.
|
209
|
+
|
210
|
+
Args:
|
211
|
+
span_data: Span data from agents framework
|
212
|
+
trace_id: Associated trace identifier
|
213
|
+
"""
|
214
|
+
try:
|
215
|
+
# Extract span information
|
216
|
+
span_name = getattr(span_data, "name", "unknown_operation")
|
217
|
+
span_type = getattr(span_data, "type", "unknown")
|
218
|
+
|
219
|
+
# Generate enhanced span name
|
220
|
+
enhanced_name = self._get_span_name(
|
221
|
+
span_name,
|
222
|
+
agent_name=getattr(span_data, "agent_name", None),
|
223
|
+
model_name=getattr(span_data, "model_name", None),
|
224
|
+
tool_name=getattr(span_data, "tool_name", None),
|
225
|
+
)
|
226
|
+
|
227
|
+
# Determine span operation type
|
228
|
+
operation_type = self._get_operation_type(span_type, span_name)
|
229
|
+
|
230
|
+
# Start span with proper context
|
231
|
+
parent_span = self.span_stack[-1] if self.span_stack else None
|
232
|
+
context = set_span_in_context(parent_span) if parent_span else None
|
233
|
+
|
234
|
+
span = self.tracer.start_as_current_span(
|
235
|
+
enhanced_name,
|
236
|
+
kind=SpanKind.CLIENT,
|
237
|
+
context=context,
|
238
|
+
attributes={
|
239
|
+
SemanticConvention.GEN_AI_SYSTEM: "openai_agents",
|
240
|
+
SemanticConvention.GEN_AI_OPERATION: operation_type,
|
241
|
+
"span.type": span_type,
|
242
|
+
"span.id": getattr(span_data, "span_id", ""),
|
243
|
+
},
|
244
|
+
)
|
245
|
+
|
246
|
+
# Process specific span types
|
247
|
+
self._process_span_attributes(span, span_data, span_type)
|
248
|
+
|
249
|
+
# Track span
|
250
|
+
span_id = getattr(span_data, "span_id", len(self.span_stack))
|
251
|
+
self.active_spans[f"{trace_id}:{span_id}"] = span
|
252
|
+
self.span_stack.append(span)
|
253
|
+
|
254
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
255
|
+
handle_exception(None, e)
|
256
|
+
|
257
|
+
def _get_operation_type(self, span_type: str, span_name: str) -> str:
|
258
|
+
"""Get operation type based on span characteristics."""
|
259
|
+
type_mapping = {
|
260
|
+
"agent": SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
|
261
|
+
"generation": SemanticConvention.GEN_AI_OPERATION_CHAT,
|
262
|
+
"function": SemanticConvention.GEN_AI_OPERATION_CHAT,
|
263
|
+
"tool": SemanticConvention.GEN_AI_OPERATION_CHAT,
|
264
|
+
"handoff": SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
|
265
|
+
}
|
266
|
+
|
267
|
+
# Check span type first
|
268
|
+
for key, operation in type_mapping.items():
|
269
|
+
if key in span_type.lower():
|
270
|
+
return operation
|
271
|
+
|
272
|
+
# Check span name
|
273
|
+
for key, operation in type_mapping.items():
|
274
|
+
if key in span_name.lower():
|
275
|
+
return operation
|
276
|
+
|
277
|
+
return SemanticConvention.GEN_AI_OPERATION_CHAT
|
278
|
+
|
279
|
+
def _process_span_attributes(self, span, span_data, span_type: str):
|
280
|
+
"""Process and set span attributes based on span type."""
|
281
|
+
try:
|
282
|
+
# Common attributes
|
283
|
+
if hasattr(span_data, "agent_name"):
|
284
|
+
span.set_attribute(
|
285
|
+
SemanticConvention.GEN_AI_AGENT_NAME, span_data.agent_name
|
171
286
|
)
|
172
287
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
otel_span.end()
|
288
|
+
if hasattr(span_data, "model_name"):
|
289
|
+
span.set_attribute(
|
290
|
+
SemanticConvention.GEN_AI_REQUEST_MODEL, span_data.model_name
|
291
|
+
)
|
178
292
|
|
179
|
-
#
|
180
|
-
if
|
181
|
-
|
182
|
-
|
293
|
+
# Agent-specific attributes
|
294
|
+
if span_type == "agent":
|
295
|
+
self._process_agent_span(span, span_data)
|
296
|
+
|
297
|
+
# Generation-specific attributes
|
298
|
+
elif span_type == "generation":
|
299
|
+
self._process_generation_span(span, span_data)
|
300
|
+
|
301
|
+
# Function/Tool-specific attributes
|
302
|
+
elif span_type in ["function", "tool"]:
|
303
|
+
self._process_function_span(span, span_data)
|
304
|
+
|
305
|
+
# Handoff-specific attributes
|
306
|
+
elif span_type == "handoff":
|
307
|
+
self._process_handoff_span(span, span_data)
|
308
|
+
|
309
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
310
|
+
handle_exception(span, e)
|
311
|
+
|
312
|
+
def _process_agent_span(self, span, agent_span):
|
313
|
+
"""Process agent span data (unused parameter)."""
|
314
|
+
# Agent-specific processing
|
315
|
+
if hasattr(agent_span, "instructions"):
|
316
|
+
span.set_attribute(
|
317
|
+
SemanticConvention.GEN_AI_AGENT_DESCRIPTION,
|
318
|
+
str(agent_span.instructions)[:500],
|
319
|
+
)
|
320
|
+
|
321
|
+
if hasattr(agent_span, "model"):
|
322
|
+
span.set_attribute(
|
323
|
+
SemanticConvention.GEN_AI_REQUEST_MODEL, agent_span.model
|
324
|
+
)
|
325
|
+
|
326
|
+
def _process_generation_span(self, span, generation_span):
|
327
|
+
"""Process generation span data."""
|
328
|
+
# Set generation-specific attributes
|
329
|
+
if hasattr(generation_span, "prompt"):
|
330
|
+
span.set_attribute(
|
331
|
+
SemanticConvention.GEN_AI_PROMPT, str(generation_span.prompt)[:1000]
|
332
|
+
)
|
333
|
+
|
334
|
+
if hasattr(generation_span, "completion"):
|
335
|
+
span.set_attribute(
|
336
|
+
SemanticConvention.GEN_AI_COMPLETION,
|
337
|
+
str(generation_span.completion)[:1000],
|
338
|
+
)
|
339
|
+
|
340
|
+
if hasattr(generation_span, "usage"):
|
341
|
+
usage = generation_span.usage
|
342
|
+
if hasattr(usage, "prompt_tokens"):
|
343
|
+
span.set_attribute(
|
344
|
+
SemanticConvention.GEN_AI_USAGE_PROMPT_TOKENS, usage.prompt_tokens
|
345
|
+
)
|
346
|
+
if hasattr(usage, "completion_tokens"):
|
347
|
+
span.set_attribute(
|
348
|
+
SemanticConvention.GEN_AI_USAGE_COMPLETION_TOKENS,
|
349
|
+
usage.completion_tokens,
|
350
|
+
)
|
183
351
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
352
|
+
def _process_function_span(self, span, function_span):
|
353
|
+
"""Process function/tool span data."""
|
354
|
+
if hasattr(function_span, "function_name"):
|
355
|
+
span.set_attribute(
|
356
|
+
SemanticConvention.GEN_AI_TOOL_NAME, function_span.function_name
|
357
|
+
)
|
189
358
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
359
|
+
if hasattr(function_span, "arguments"):
|
360
|
+
span.set_attribute(
|
361
|
+
"gen_ai.tool.arguments", str(function_span.arguments)[:500]
|
362
|
+
)
|
194
363
|
|
195
|
-
|
364
|
+
if hasattr(function_span, "result"):
|
365
|
+
span.set_attribute("gen_ai.tool.result", str(function_span.result)[:500])
|
196
366
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
except Exception as e:
|
202
|
-
handle_exception(root_span, e)
|
203
|
-
finally:
|
204
|
-
root_span.end()
|
205
|
-
|
206
|
-
# Cleanup
|
207
|
-
if trace.trace_id in self._root_spans:
|
208
|
-
del self._root_spans[trace.trace_id]
|
209
|
-
if trace.trace_id in self._span_start_times:
|
210
|
-
del self._span_start_times[trace.trace_id]
|
211
|
-
|
212
|
-
def _get_span_name(self, span: "Span[Any]") -> str:
|
213
|
-
"""Get semantic span name using {operation_type} {operation_name} format"""
|
214
|
-
data = span.span_data
|
215
|
-
operation_type = self._get_operation_type(data)
|
216
|
-
|
217
|
-
# Extract operation name based on span type
|
218
|
-
operation_name = "unknown"
|
219
|
-
|
220
|
-
# Special handling for handoffs
|
221
|
-
if hasattr(data, '__class__') and data.__class__.__name__ == 'HandoffSpanData':
|
222
|
-
if hasattr(data, 'to_agent') and data.to_agent:
|
223
|
-
operation_name = f"to {data.to_agent}"
|
224
|
-
else:
|
225
|
-
operation_name = "handoff"
|
226
|
-
|
227
|
-
# Use agent name for agent spans
|
228
|
-
elif hasattr(data, '__class__') and data.__class__.__name__ == 'AgentSpanData':
|
229
|
-
# Try multiple possible attribute names for agent name
|
230
|
-
agent_name = None
|
231
|
-
|
232
|
-
for attr in ['agent_name', 'name', 'agent', 'agent_id']:
|
233
|
-
if hasattr(data, attr):
|
234
|
-
agent_name = getattr(data, attr)
|
235
|
-
if agent_name and isinstance(agent_name, str):
|
236
|
-
break
|
237
|
-
|
238
|
-
# If still no agent name, try looking in context or other attributes
|
239
|
-
if not agent_name:
|
240
|
-
# Try context or other nested attributes
|
241
|
-
if hasattr(data, 'context') and hasattr(data.context, 'agent'):
|
242
|
-
agent_name = getattr(data.context.agent, 'name', None)
|
243
|
-
elif hasattr(data, 'metadata') and hasattr(data.metadata, 'agent_name'):
|
244
|
-
agent_name = data.metadata.agent_name
|
367
|
+
def _process_handoff_span(self, span, handoff_span):
|
368
|
+
"""Process handoff span data."""
|
369
|
+
if hasattr(handoff_span, "target_agent"):
|
370
|
+
span.set_attribute("gen_ai.handoff.target_agent", handoff_span.target_agent)
|
245
371
|
|
246
|
-
|
247
|
-
|
248
|
-
else:
|
249
|
-
# If no agent name found, use a more descriptive fallback
|
250
|
-
operation_name = "execution"
|
251
|
-
|
252
|
-
# Use name if available for other spans
|
253
|
-
elif hasattr(data, 'name') and isinstance(data.name, str):
|
254
|
-
operation_name = data.name
|
255
|
-
|
256
|
-
# Fallback to type-based names
|
257
|
-
else:
|
258
|
-
operation_name = getattr(data, 'type', 'operation')
|
259
|
-
|
260
|
-
# Return formatted name: {operation_type} {operation_name}
|
261
|
-
return f"{operation_type} {operation_name}"
|
262
|
-
|
263
|
-
def _get_operation_type(self, data: Any) -> str:
|
264
|
-
"""Map span data to operation types"""
|
265
|
-
class_name = data.__class__.__name__ if hasattr(data, '__class__') else str(type(data))
|
266
|
-
|
267
|
-
mapping = {
|
268
|
-
'AgentSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
|
269
|
-
'GenerationSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT,
|
270
|
-
'FunctionSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_TOOLS,
|
271
|
-
'HandoffSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_AGENT,
|
272
|
-
'ResponseSpanData': SemanticConvention.GEN_AI_OPERATION_TYPE_CHAT,
|
273
|
-
}
|
372
|
+
if hasattr(handoff_span, "reason"):
|
373
|
+
span.set_attribute("gen_ai.handoff.reason", str(handoff_span.reason)[:200])
|
274
374
|
|
275
|
-
|
276
|
-
|
277
|
-
def _set_common_framework_attributes(self, span: Any, operation_type: str) -> None:
|
278
|
-
"""Set common framework attributes using semantic conventions"""
|
279
|
-
# Create scope object for common_framework_span_attributes
|
280
|
-
scope = type("GenericScope", (), {})()
|
281
|
-
scope._span = span
|
282
|
-
scope._start_time = time.time()
|
283
|
-
scope._end_time = time.time()
|
284
|
-
|
285
|
-
# Use common framework attributes helper
|
286
|
-
# For framework operations, use localhost like other agent frameworks (AG2, Pydantic AI)
|
287
|
-
common_framework_span_attributes(
|
288
|
-
scope, SemanticConvention.GEN_AI_SYSTEM_OPENAI_AGENTS,
|
289
|
-
"localhost", 80, self._environment, self._application_name,
|
290
|
-
self._version, operation_type, None
|
291
|
-
)
|
292
|
-
|
293
|
-
def _set_common_attributes(self, span: Any, trace_id: str) -> None:
|
294
|
-
"""Set common framework attributes for root spans"""
|
295
|
-
self._set_common_framework_attributes(span, SemanticConvention.GEN_AI_OPERATION_TYPE_FRAMEWORK)
|
296
|
-
|
297
|
-
def _set_span_attributes(self, span: Any, agent_span: "Span[Any]") -> None:
|
298
|
-
"""Set span-specific attributes based on span data using semantic conventions"""
|
299
|
-
data = agent_span.span_data
|
300
|
-
|
301
|
-
# Agent-specific attributes using semantic conventions
|
302
|
-
if hasattr(data, 'agent_name') and data.agent_name:
|
303
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, data.agent_name)
|
304
|
-
elif hasattr(data, 'name') and data.name:
|
305
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, data.name)
|
306
|
-
|
307
|
-
# Enhanced model information extraction
|
308
|
-
model = self._extract_model_info(data, agent_span)
|
309
|
-
if model:
|
310
|
-
span.set_attribute(SemanticConvention.GEN_AI_REQUEST_MODEL, str(model))
|
311
|
-
|
312
|
-
# Enhanced input/output capture with MIME types (OpenLIT enhancement)
|
313
|
-
if self._capture_message_content:
|
314
|
-
self._capture_input_output(span, data)
|
315
|
-
|
316
|
-
# Enhanced token usage details (inspired by OpenInference)
|
317
|
-
self._capture_detailed_token_usage(span, data)
|
318
|
-
|
319
|
-
# Model invocation parameters as JSON (new feature from OpenInference)
|
320
|
-
self._capture_model_parameters(span, data)
|
321
|
-
|
322
|
-
# Tool/function information for tool calls
|
323
|
-
if hasattr(data, '__class__') and 'Function' in data.__class__.__name__:
|
324
|
-
if hasattr(data, 'function_name'):
|
325
|
-
span.set_attribute(SemanticConvention.GEN_AI_TOOL_NAME, data.function_name)
|
326
|
-
if hasattr(data, 'arguments'):
|
327
|
-
span.set_attribute(SemanticConvention.GEN_AI_TOOL_ARGS, str(data.arguments))
|
328
|
-
|
329
|
-
# Enhanced handoff information extraction
|
330
|
-
if hasattr(data, '__class__') and 'Handoff' in data.__class__.__name__:
|
331
|
-
target_agent = self._extract_handoff_target(data, agent_span)
|
332
|
-
if target_agent:
|
333
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, target_agent)
|
334
|
-
else:
|
335
|
-
# Fallback for handoff spans without clear target
|
336
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_NAME, "agent handoff")
|
337
|
-
|
338
|
-
# Request/response IDs if available
|
339
|
-
if hasattr(data, 'request_id'):
|
340
|
-
span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_ID, data.request_id)
|
341
|
-
elif hasattr(data, 'response_id'):
|
342
|
-
span.set_attribute(SemanticConvention.GEN_AI_RESPONSE_ID, data.response_id)
|
343
|
-
|
344
|
-
def _extract_model_info(self, data: Any, agent_span: "Span[Any]") -> Optional[str]:
|
345
|
-
"""Extract model information from span data or agent configuration"""
|
346
|
-
# Try direct model attributes first
|
347
|
-
model_attrs = ['model', 'model_name', 'model_id', 'llm_model', 'openai_model']
|
348
|
-
|
349
|
-
model = self._check_model_attrs(data, model_attrs)
|
350
|
-
if model:
|
351
|
-
return model
|
352
|
-
|
353
|
-
# Try nested configuration objects
|
354
|
-
config_attrs = ['config', 'configuration', 'client_config', 'llm_config']
|
355
|
-
model = self._check_config_model_attrs(data, config_attrs, model_attrs)
|
356
|
-
if model:
|
357
|
-
return model
|
358
|
-
|
359
|
-
# Try looking in the agent span itself
|
360
|
-
if hasattr(agent_span, 'model'):
|
361
|
-
return str(agent_span.model)
|
362
|
-
|
363
|
-
# Try agent_config if available
|
364
|
-
if hasattr(agent_span, 'agent_config'):
|
365
|
-
model = self._check_model_attrs(agent_span.agent_config, model_attrs)
|
366
|
-
if model:
|
367
|
-
return model
|
368
|
-
|
369
|
-
# Default fallback
|
370
|
-
return "gpt-4o"
|
371
|
-
|
372
|
-
def _check_model_attrs(self, obj: Any, model_attrs: list) -> Optional[str]:
|
373
|
-
"""Helper method to check model attributes on an object"""
|
374
|
-
for attr in model_attrs:
|
375
|
-
if not hasattr(obj, attr):
|
376
|
-
continue
|
377
|
-
model_value = getattr(obj, attr)
|
378
|
-
if model_value and isinstance(model_value, str):
|
379
|
-
return model_value
|
380
|
-
return None
|
381
|
-
|
382
|
-
def _check_config_model_attrs(self, data: Any, config_attrs: list, model_attrs: list) -> Optional[str]:
|
383
|
-
"""Helper method to check model attributes in nested configuration objects"""
|
384
|
-
for config_attr in config_attrs:
|
385
|
-
if not hasattr(data, config_attr):
|
386
|
-
continue
|
387
|
-
config = getattr(data, config_attr)
|
388
|
-
if not config:
|
389
|
-
continue
|
390
|
-
model = self._check_model_attrs(config, model_attrs)
|
391
|
-
if model:
|
392
|
-
return model
|
393
|
-
return None
|
394
|
-
|
395
|
-
def _extract_handoff_target(self, data: Any, agent_span: "Span[Any]") -> Optional[str]:
|
396
|
-
"""Extract handoff target information with enhanced logic"""
|
397
|
-
# Try direct target attributes
|
398
|
-
target_attrs = ['to_agent', 'target_agent', 'destination_agent', 'next_agent']
|
399
|
-
for attr in target_attrs:
|
400
|
-
if hasattr(data, attr):
|
401
|
-
target = getattr(data, attr)
|
402
|
-
if target and isinstance(target, str):
|
403
|
-
return f"to {target}"
|
404
|
-
|
405
|
-
# Try from_agent for better handoff description
|
406
|
-
from_attrs = ['from_agent', 'source_agent', 'previous_agent']
|
407
|
-
for attr in from_attrs:
|
408
|
-
if hasattr(data, attr):
|
409
|
-
source = getattr(data, attr)
|
410
|
-
if source and isinstance(source, str):
|
411
|
-
return f"from {source}"
|
412
|
-
|
413
|
-
# Try nested objects
|
414
|
-
if hasattr(data, 'handoff_info'):
|
415
|
-
info = data.handoff_info
|
416
|
-
for attr in target_attrs + from_attrs:
|
417
|
-
if hasattr(info, attr):
|
418
|
-
value = getattr(info, attr)
|
419
|
-
if value and isinstance(value, str):
|
420
|
-
prefix = "to" if attr in target_attrs else "from"
|
421
|
-
return f"{prefix} {value}"
|
422
|
-
|
423
|
-
return None
|
424
|
-
|
425
|
-
def _capture_input_output(self, span: Any, data: Any) -> None:
|
426
|
-
"""Capture input/output content with MIME type detection (OpenLIT enhancement)"""
|
375
|
+
def span_end(self, span_data, trace_id: str):
|
376
|
+
"""Handle span end events."""
|
427
377
|
try:
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
span.set_attribute("gen_ai.content.prompt.mime_type", "text/plain")
|
437
|
-
|
438
|
-
# Capture output/response content
|
439
|
-
if hasattr(data, 'response') and data.response is not None:
|
440
|
-
content = str(data.response)
|
441
|
-
span.set_attribute(SemanticConvention.GEN_AI_CONTENT_COMPLETION, content)
|
442
|
-
# Set MIME type based on content structure
|
443
|
-
if content.startswith('{') or content.startswith('['):
|
444
|
-
span.set_attribute("gen_ai.content.completion.mime_type", "application/json")
|
378
|
+
span_id = getattr(span_data, "span_id", "")
|
379
|
+
span_key = f"{trace_id}:{span_id}"
|
380
|
+
|
381
|
+
span = self.active_spans.get(span_key)
|
382
|
+
if span:
|
383
|
+
# Set final status
|
384
|
+
if hasattr(span_data, "error") and span_data.error:
|
385
|
+
span.set_status(Status(StatusCode.ERROR, str(span_data.error)))
|
445
386
|
else:
|
446
|
-
span.
|
387
|
+
span.set_status(Status(StatusCode.OK))
|
388
|
+
|
389
|
+
# End span
|
390
|
+
span.end()
|
391
|
+
|
392
|
+
# Cleanup
|
393
|
+
if span_key in self.active_spans:
|
394
|
+
del self.active_spans[span_key]
|
395
|
+
if span in self.span_stack:
|
396
|
+
self.span_stack.remove(span)
|
447
397
|
|
448
|
-
except Exception:
|
449
|
-
|
398
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
399
|
+
handle_exception(span if "span" in locals() else None, e)
|
450
400
|
|
451
|
-
def
|
452
|
-
"""
|
401
|
+
def force_flush(self):
|
402
|
+
"""Force flush all pending spans."""
|
453
403
|
try:
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
def
|
480
|
-
|
404
|
+
# End any remaining spans
|
405
|
+
for span in list(self.active_spans.values()):
|
406
|
+
span.end()
|
407
|
+
|
408
|
+
self.active_spans.clear()
|
409
|
+
self.span_stack.clear()
|
410
|
+
|
411
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
412
|
+
handle_exception(None, e)
|
413
|
+
|
414
|
+
def shutdown(self):
|
415
|
+
"""Shutdown the processor."""
|
416
|
+
self.force_flush()
|
417
|
+
|
418
|
+
def _extract_model_info(self, span_data) -> Dict[str, Any]:
|
419
|
+
"""Extract model information from span data."""
|
420
|
+
model_info = {}
|
421
|
+
|
422
|
+
if hasattr(span_data, "model"):
|
423
|
+
model_info["model"] = span_data.model
|
424
|
+
if hasattr(span_data, "model_name"):
|
425
|
+
model_info["model"] = span_data.model_name
|
426
|
+
|
427
|
+
return model_info
|
428
|
+
|
429
|
+
def _calculate_cost(
|
430
|
+
self, model: str, prompt_tokens: int, completion_tokens: int
|
431
|
+
) -> float:
|
432
|
+
"""Calculate cost based on token usage."""
|
433
|
+
try:
|
434
|
+
return get_chat_model_cost(
|
435
|
+
model, self.pricing_info, prompt_tokens, completion_tokens
|
436
|
+
)
|
437
|
+
except Exception: # pylint: disable=broad-exception-caught
|
438
|
+
return 0.0
|
439
|
+
|
440
|
+
# Abstract method implementations required by OpenAI Agents framework
|
441
|
+
def on_trace_start(self, trace):
|
442
|
+
"""Called when a trace starts - required by OpenAI Agents framework"""
|
481
443
|
try:
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
if 'usage' in response_dict:
|
507
|
-
params['usage'] = response_dict['usage']
|
508
|
-
except Exception:
|
509
|
-
pass
|
510
|
-
|
511
|
-
# Set as JSON if we found any parameters
|
512
|
-
if params:
|
513
|
-
span.set_attribute("gen_ai.request.parameters", json.dumps(params))
|
514
|
-
|
515
|
-
except Exception:
|
516
|
-
pass # Ignore export errors
|
517
|
-
|
518
|
-
def _process_span_completion(self, span: Any, agent_span: "Span[Any]") -> None:
|
519
|
-
"""Process span completion with enhanced business intelligence"""
|
520
|
-
data = agent_span.span_data
|
521
|
-
|
522
|
-
# Process response data if available
|
523
|
-
self._process_response_data(span, data)
|
524
|
-
|
525
|
-
# Extract and set token usage for business intelligence
|
526
|
-
self._extract_token_usage(span, data)
|
527
|
-
|
528
|
-
def _extract_token_usage(self, span: Any, data: Any) -> None:
|
529
|
-
"""Extract token usage and calculate costs (OpenLIT's business intelligence)"""
|
444
|
+
self.start_trace(
|
445
|
+
getattr(trace, "trace_id", "unknown"),
|
446
|
+
getattr(trace, "name", "workflow"),
|
447
|
+
)
|
448
|
+
except Exception: # pylint: disable=broad-exception-caught
|
449
|
+
pass
|
450
|
+
|
451
|
+
def on_trace_end(self, trace):
|
452
|
+
"""Called when a trace ends - required by OpenAI Agents framework"""
|
453
|
+
try:
|
454
|
+
self.end_trace(getattr(trace, "trace_id", "unknown"))
|
455
|
+
except Exception: # pylint: disable=broad-exception-caught
|
456
|
+
pass
|
457
|
+
|
458
|
+
def on_span_start(self, span):
|
459
|
+
"""Called when a span starts - required by OpenAI Agents framework"""
|
460
|
+
try:
|
461
|
+
trace_id = getattr(span, "trace_id", "unknown")
|
462
|
+
self.span_start(span, trace_id)
|
463
|
+
except Exception: # pylint: disable=broad-exception-caught
|
464
|
+
pass
|
465
|
+
|
466
|
+
def on_span_end(self, span):
|
467
|
+
"""Called when a span ends - required by OpenAI Agents framework"""
|
530
468
|
try:
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
# Check direct usage attributes
|
536
|
-
if hasattr(data, 'usage'):
|
537
|
-
usage = data.usage
|
538
|
-
input_tokens = getattr(usage, 'input_tokens', 0) or getattr(usage, 'prompt_tokens', 0)
|
539
|
-
output_tokens = getattr(usage, 'output_tokens', 0) or getattr(usage, 'completion_tokens', 0)
|
540
|
-
|
541
|
-
# Check response object
|
542
|
-
elif hasattr(data, 'response') and hasattr(data.response, 'usage'):
|
543
|
-
usage = data.response.usage
|
544
|
-
input_tokens = getattr(usage, 'input_tokens', 0) or getattr(usage, 'prompt_tokens', 0)
|
545
|
-
output_tokens = getattr(usage, 'output_tokens', 0) or getattr(usage, 'completion_tokens', 0)
|
546
|
-
|
547
|
-
# Set token attributes
|
548
|
-
span.set_attribute(SemanticConvention.GEN_AI_USAGE_INPUT_TOKENS, input_tokens)
|
549
|
-
span.set_attribute(SemanticConvention.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens)
|
550
|
-
span.set_attribute(SemanticConvention.GEN_AI_CLIENT_TOKEN_USAGE, input_tokens + output_tokens)
|
551
|
-
|
552
|
-
# Calculate cost (OpenLIT's business intelligence advantage)
|
553
|
-
model = getattr(data, 'model', 'gpt-4o')
|
554
|
-
cost = get_chat_model_cost(model, self._pricing_info, input_tokens, output_tokens)
|
555
|
-
span.set_attribute(SemanticConvention.GEN_AI_USAGE_COST, cost)
|
556
|
-
|
557
|
-
except Exception:
|
558
|
-
pass # Ignore errors in token usage extraction
|
559
|
-
|
560
|
-
def _process_response_data(self, span: Any, data: Any) -> None:
|
561
|
-
"""Process response data with content capture"""
|
562
|
-
if self._capture_message_content:
|
563
|
-
self._capture_input_output(span, data)
|
564
|
-
|
565
|
-
def _process_trace_completion(self, span: Any, trace: "Trace") -> None:
|
566
|
-
"""Process trace completion with business intelligence aggregation"""
|
567
|
-
# Add trace-level metadata
|
568
|
-
span.set_attribute(SemanticConvention.GEN_AI_OPERATION_NAME, "workflow")
|
569
|
-
|
570
|
-
# Calculate total duration
|
571
|
-
if trace.trace_id in self._span_start_times:
|
572
|
-
start_time = self._span_start_times[trace.trace_id]
|
573
|
-
duration = time.time() - start_time
|
574
|
-
span.set_attribute(SemanticConvention.GEN_AI_CLIENT_OPERATION_DURATION, duration)
|
575
|
-
|
576
|
-
def _parse_timestamp(self, timestamp: Any) -> float:
|
577
|
-
"""Parse timestamp from various formats"""
|
578
|
-
if isinstance(timestamp, (int, float)):
|
579
|
-
return float(timestamp)
|
580
|
-
elif isinstance(timestamp, str):
|
581
|
-
try:
|
582
|
-
# Try parsing ISO format
|
583
|
-
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
584
|
-
return dt.timestamp()
|
585
|
-
except ValueError:
|
586
|
-
return time.time()
|
587
|
-
else:
|
588
|
-
return time.time()
|
589
|
-
|
590
|
-
def _as_utc_nano(self, timestamp: float) -> int:
|
591
|
-
"""Convert timestamp to UTC nanoseconds for OpenTelemetry"""
|
592
|
-
return int(timestamp * 1_000_000_000)
|
593
|
-
|
594
|
-
def force_flush(self) -> bool:
|
595
|
-
"""Force flush any pending spans (required by TracingProcessor)"""
|
596
|
-
return True
|
597
|
-
|
598
|
-
def shutdown(self) -> bool:
|
599
|
-
"""Shutdown the processor (required by TracingProcessor)"""
|
600
|
-
return True
|
469
|
+
trace_id = getattr(span, "trace_id", "unknown")
|
470
|
+
self.span_end(span, trace_id)
|
471
|
+
except Exception: # pylint: disable=broad-exception-caught
|
472
|
+
pass
|