langtrace-python-sdk 3.8.3__py3-none-any.whl → 3.8.5__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.
- langtrace_python_sdk/instrumentation/__init__.py +2 -0
- langtrace_python_sdk/instrumentation/agno/instrumentation.py +6 -3
- langtrace_python_sdk/instrumentation/agno/patch.py +277 -229
- langtrace_python_sdk/instrumentation/openai_agents/__init__.py +5 -0
- langtrace_python_sdk/instrumentation/openai_agents/instrumentation.py +52 -0
- langtrace_python_sdk/instrumentation/openai_agents/patch.py +533 -0
- langtrace_python_sdk/instrumentation/phidata/instrumentation.py +6 -3
- langtrace_python_sdk/langtrace.py +4 -3
- langtrace_python_sdk/utils/llm.py +151 -1
- langtrace_python_sdk/version.py +1 -1
- {langtrace_python_sdk-3.8.3.dist-info → langtrace_python_sdk-3.8.5.dist-info}/METADATA +3 -2
- {langtrace_python_sdk-3.8.3.dist-info → langtrace_python_sdk-3.8.5.dist-info}/RECORD +15 -12
- {langtrace_python_sdk-3.8.3.dist-info → langtrace_python_sdk-3.8.5.dist-info}/WHEEL +0 -0
- {langtrace_python_sdk-3.8.3.dist-info → langtrace_python_sdk-3.8.5.dist-info}/entry_points.txt +0 -0
- {langtrace_python_sdk-3.8.3.dist-info → langtrace_python_sdk-3.8.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
"""
|
2
|
+
Copyright (c) 2025 Scale3 Labs
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
Unless required by applicable law or agreed to in writing, software
|
8
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
See the License for the specific language governing permissions and
|
11
|
+
limitations under the License.
|
12
|
+
"""
|
13
|
+
|
14
|
+
import importlib.metadata
|
15
|
+
import logging
|
16
|
+
from typing import Any, Collection, Optional
|
17
|
+
|
18
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
19
|
+
from opentelemetry.trace import TracerProvider, get_tracer
|
20
|
+
from wrapt import wrap_function_wrapper
|
21
|
+
|
22
|
+
from langtrace_python_sdk.instrumentation.openai_agents.patch import \
|
23
|
+
get_new_response
|
24
|
+
|
25
|
+
logging.basicConfig(level=logging.FATAL)
|
26
|
+
|
27
|
+
|
28
|
+
class OpenAIAgentsInstrumentation(BaseInstrumentor): # type: ignore
|
29
|
+
|
30
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
31
|
+
return ["openai-agents >= 0.0.3", "trace-attributes >= 4.0.5"]
|
32
|
+
|
33
|
+
def _instrument(self, **kwargs: Any) -> None:
|
34
|
+
tracer_provider: Optional[TracerProvider] = kwargs.get("tracer_provider")
|
35
|
+
tracer = get_tracer(__name__, "", tracer_provider)
|
36
|
+
version: str = importlib.metadata.version("openai")
|
37
|
+
|
38
|
+
# TODO(Karthik): This is adding a lot of noise to the trace.
|
39
|
+
# wrap_function_wrapper(
|
40
|
+
# "agents.run",
|
41
|
+
# "Runner._get_handoffs",
|
42
|
+
# get_handoffs(version, tracer),
|
43
|
+
# )
|
44
|
+
|
45
|
+
wrap_function_wrapper(
|
46
|
+
"agents.run",
|
47
|
+
"Runner._get_new_response",
|
48
|
+
get_new_response(version, tracer),
|
49
|
+
)
|
50
|
+
|
51
|
+
def _uninstrument(self, **kwargs: Any) -> None:
|
52
|
+
pass
|
@@ -0,0 +1,533 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any, Callable, List
|
3
|
+
|
4
|
+
from agents.exceptions import (InputGuardrailTripwireTriggered,
|
5
|
+
OutputGuardrailTripwireTriggered)
|
6
|
+
from agents.run import Runner
|
7
|
+
from importlib_metadata import version as v
|
8
|
+
from langtrace.trace_attributes import FrameworkSpanAttributes, SpanAttributes
|
9
|
+
from opentelemetry import baggage, trace
|
10
|
+
from opentelemetry.trace import SpanKind, Tracer
|
11
|
+
from opentelemetry.trace.status import Status, StatusCode
|
12
|
+
|
13
|
+
from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME
|
14
|
+
from langtrace_python_sdk.constants.instrumentation.common import (
|
15
|
+
LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, SERVICE_PROVIDERS)
|
16
|
+
from langtrace_python_sdk.utils.llm import (set_event_completion,
|
17
|
+
set_span_attributes,
|
18
|
+
set_usage_attributes)
|
19
|
+
|
20
|
+
|
21
|
+
def extract_agent_details(agent_or_handoff):
|
22
|
+
"""Extract relevant details from an agent/handoff and its handoffs."""
|
23
|
+
try:
|
24
|
+
if agent_or_handoff is None:
|
25
|
+
return None
|
26
|
+
|
27
|
+
# Handle both Agent and Handoff types
|
28
|
+
if hasattr(agent_or_handoff, 'agent'): # This is a Handoff
|
29
|
+
agent = agent_or_handoff.agent
|
30
|
+
else: # This is an Agent
|
31
|
+
agent = agent_or_handoff
|
32
|
+
|
33
|
+
if agent is None:
|
34
|
+
return None
|
35
|
+
|
36
|
+
agent_details = {
|
37
|
+
"name": getattr(agent, 'name', None),
|
38
|
+
"instructions": getattr(agent, 'instructions', None),
|
39
|
+
"handoff_description": getattr(agent, 'handoff_description', None),
|
40
|
+
"handoffs": []
|
41
|
+
}
|
42
|
+
|
43
|
+
if hasattr(agent, 'handoffs') and agent.handoffs:
|
44
|
+
for handoff_item in agent.handoffs:
|
45
|
+
handoff_details = extract_agent_details(handoff_item)
|
46
|
+
if handoff_details:
|
47
|
+
agent_details["handoffs"].append(handoff_details)
|
48
|
+
|
49
|
+
return agent_details
|
50
|
+
except Exception: # Catch all exceptions and fail silently
|
51
|
+
return None
|
52
|
+
|
53
|
+
|
54
|
+
def extract_handoff_details(handoff):
|
55
|
+
"""Extract relevant details from a Handoff object."""
|
56
|
+
try:
|
57
|
+
if handoff is None:
|
58
|
+
return None
|
59
|
+
|
60
|
+
return {
|
61
|
+
"tool_name": getattr(handoff, 'tool_name', None),
|
62
|
+
"tool_description": getattr(handoff, 'tool_description', None),
|
63
|
+
"agent_name": getattr(handoff, 'agent_name', None),
|
64
|
+
"input_json_schema": getattr(handoff, 'input_json_schema', {}),
|
65
|
+
"strict_json_schema": getattr(handoff, 'strict_json_schema', False)
|
66
|
+
}
|
67
|
+
except Exception: # Catch all exceptions and fail silently
|
68
|
+
return None
|
69
|
+
|
70
|
+
|
71
|
+
def get_handoffs(version: str, tracer: Tracer) -> Callable:
|
72
|
+
"""Wrap the `prompt` method of the `TLM` class to trace it."""
|
73
|
+
|
74
|
+
def traced_method(
|
75
|
+
wrapped: Callable,
|
76
|
+
instance: Any,
|
77
|
+
args: List[Any],
|
78
|
+
kwargs: Any,
|
79
|
+
) -> Any:
|
80
|
+
try:
|
81
|
+
service_provider = SERVICE_PROVIDERS["OPENAI"]
|
82
|
+
extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
|
83
|
+
span_attributes = {
|
84
|
+
"langtrace.sdk.name": "langtrace-python-sdk",
|
85
|
+
"langtrace.service.name": service_provider,
|
86
|
+
"langtrace.service.type": "framework",
|
87
|
+
"langtrace.service.version": version,
|
88
|
+
"langtrace.version": v(LANGTRACE_SDK_NAME),
|
89
|
+
**(extra_attributes if extra_attributes is not None else {}),
|
90
|
+
}
|
91
|
+
|
92
|
+
# Process agents from args
|
93
|
+
agents_list = []
|
94
|
+
if args:
|
95
|
+
for arg in args:
|
96
|
+
try:
|
97
|
+
if arg is not None:
|
98
|
+
if hasattr(arg, 'name') or hasattr(arg, 'agent'):
|
99
|
+
agent_details = extract_agent_details(arg)
|
100
|
+
if agent_details:
|
101
|
+
agents_list.append(agent_details)
|
102
|
+
elif isinstance(arg, (list, tuple)):
|
103
|
+
for item in arg:
|
104
|
+
if item is not None and (hasattr(item, 'name') or hasattr(item, 'agent')):
|
105
|
+
agent_details = extract_agent_details(item)
|
106
|
+
if agent_details:
|
107
|
+
agents_list.append(agent_details)
|
108
|
+
except Exception:
|
109
|
+
continue # Skip any errors in processing individual arguments
|
110
|
+
|
111
|
+
if agents_list:
|
112
|
+
try:
|
113
|
+
span_attributes["openai_agents.agents"] = json.dumps(agents_list)
|
114
|
+
except Exception:
|
115
|
+
pass # Silently fail if JSON serialization fails
|
116
|
+
|
117
|
+
attributes = FrameworkSpanAttributes(**span_attributes)
|
118
|
+
|
119
|
+
with tracer.start_as_current_span(
|
120
|
+
name=f"openai_agents.available_handoffs", kind=SpanKind.CLIENT
|
121
|
+
) as span:
|
122
|
+
try:
|
123
|
+
set_span_attributes(span, attributes)
|
124
|
+
result = wrapped(*args, **kwargs)
|
125
|
+
|
126
|
+
# Process handoff results
|
127
|
+
if result is not None:
|
128
|
+
handoffs_list = []
|
129
|
+
try:
|
130
|
+
if isinstance(result, (list, tuple)):
|
131
|
+
for handoff in result:
|
132
|
+
if handoff is not None and hasattr(handoff, 'tool_name'):
|
133
|
+
handoff_details = extract_handoff_details(handoff)
|
134
|
+
if handoff_details:
|
135
|
+
handoffs_list.append(handoff_details)
|
136
|
+
elif hasattr(result, 'tool_name'):
|
137
|
+
handoff_details = extract_handoff_details(result)
|
138
|
+
if handoff_details:
|
139
|
+
handoffs_list.append(handoff_details)
|
140
|
+
|
141
|
+
if handoffs_list:
|
142
|
+
try:
|
143
|
+
span.set_attribute("openai_agents.handoffs", json.dumps(handoffs_list))
|
144
|
+
span.set_status(Status(StatusCode.OK))
|
145
|
+
except Exception:
|
146
|
+
pass # Silently fail if JSON serialization fails
|
147
|
+
except Exception:
|
148
|
+
pass # Silently fail if handoff processing fails
|
149
|
+
|
150
|
+
return result
|
151
|
+
|
152
|
+
except Exception as err:
|
153
|
+
try:
|
154
|
+
span.record_exception(err)
|
155
|
+
span.set_status(Status(StatusCode.ERROR, str(err)))
|
156
|
+
except Exception:
|
157
|
+
pass # Silently fail if error recording fails
|
158
|
+
raise # Re-raise the original error since it's from the wrapped function
|
159
|
+
|
160
|
+
except Exception as outer_err:
|
161
|
+
# If anything fails in our instrumentation wrapper, catch it and return control to the wrapped function
|
162
|
+
try:
|
163
|
+
return wrapped(*args, **kwargs)
|
164
|
+
except Exception as wrapped_err:
|
165
|
+
raise wrapped_err # Only raise errors from the wrapped function
|
166
|
+
|
167
|
+
return traced_method
|
168
|
+
|
169
|
+
|
170
|
+
def extract_response_input_details(input_item):
|
171
|
+
"""Extract relevant details from a response input item."""
|
172
|
+
try:
|
173
|
+
if input_item is None:
|
174
|
+
return None
|
175
|
+
|
176
|
+
details = {
|
177
|
+
"type": input_item.__class__.__name__,
|
178
|
+
}
|
179
|
+
|
180
|
+
# Extract common attributes that might be present
|
181
|
+
for attr in ['content', 'role', 'name', 'tool_name', 'tool_call_id']:
|
182
|
+
if hasattr(input_item, attr):
|
183
|
+
value = getattr(input_item, attr)
|
184
|
+
if value is not None:
|
185
|
+
details[attr] = value
|
186
|
+
|
187
|
+
return details
|
188
|
+
except Exception:
|
189
|
+
return None
|
190
|
+
|
191
|
+
|
192
|
+
def extract_model_response(response):
|
193
|
+
"""Extract relevant details from a ModelResponse."""
|
194
|
+
try:
|
195
|
+
if response is None:
|
196
|
+
return None
|
197
|
+
|
198
|
+
response_dict = {
|
199
|
+
"referenceable_id": getattr(response, "referenceable_id", None),
|
200
|
+
"usage": {
|
201
|
+
"requests": getattr(response.usage, "requests", 0),
|
202
|
+
"input_tokens": getattr(response.usage, "input_tokens", 0),
|
203
|
+
"output_tokens": getattr(response.usage, "output_tokens", 0),
|
204
|
+
"total_tokens": getattr(response.usage, "total_tokens", 0)
|
205
|
+
},
|
206
|
+
"output": []
|
207
|
+
}
|
208
|
+
|
209
|
+
# Extract output messages or function calls
|
210
|
+
if response.output:
|
211
|
+
for output_item in response.output:
|
212
|
+
if hasattr(output_item, 'type'):
|
213
|
+
if output_item.type == 'function_call':
|
214
|
+
# Handle function call
|
215
|
+
function_call = {
|
216
|
+
"id": getattr(output_item, "id", None),
|
217
|
+
"type": "function_call",
|
218
|
+
"status": getattr(output_item, "status", None),
|
219
|
+
"call_id": getattr(output_item, "call_id", None),
|
220
|
+
"name": getattr(output_item, "name", None),
|
221
|
+
"arguments": getattr(output_item, "arguments", "{}")
|
222
|
+
}
|
223
|
+
response_dict["output"].append(function_call)
|
224
|
+
# Set response type as function_call and capture the function name
|
225
|
+
response_dict["response_type"] = "function_call"
|
226
|
+
response_dict["function"] = getattr(output_item, "name", None)
|
227
|
+
elif output_item.type == 'message':
|
228
|
+
# Handle regular message
|
229
|
+
message = {
|
230
|
+
"id": getattr(output_item, "id", None),
|
231
|
+
"role": getattr(output_item, "role", None),
|
232
|
+
"status": getattr(output_item, "status", None),
|
233
|
+
"type": "message",
|
234
|
+
"content": []
|
235
|
+
}
|
236
|
+
|
237
|
+
# Extract content (text, annotations, etc.)
|
238
|
+
if hasattr(output_item, 'content') and output_item.content:
|
239
|
+
for content_item in output_item.content:
|
240
|
+
content_dict = {
|
241
|
+
"type": getattr(content_item, "type", None),
|
242
|
+
"text": getattr(content_item, "text", None),
|
243
|
+
"annotations": getattr(content_item, "annotations", [])
|
244
|
+
}
|
245
|
+
message["content"].append(content_dict)
|
246
|
+
|
247
|
+
response_dict["output"].append(message)
|
248
|
+
# Set response type as response for messages
|
249
|
+
if "response_type" not in response_dict:
|
250
|
+
response_dict["response_type"] = "response"
|
251
|
+
|
252
|
+
return response_dict
|
253
|
+
except Exception:
|
254
|
+
return None
|
255
|
+
|
256
|
+
|
257
|
+
def extract_message_history(messages):
|
258
|
+
"""Extract relevant details from message history."""
|
259
|
+
try:
|
260
|
+
if not messages or not isinstance(messages, list):
|
261
|
+
return []
|
262
|
+
|
263
|
+
history = []
|
264
|
+
for msg in messages:
|
265
|
+
if not isinstance(msg, dict):
|
266
|
+
continue
|
267
|
+
|
268
|
+
message = {}
|
269
|
+
# Extract common fields
|
270
|
+
for key in ['content', 'role', 'id', 'type', 'status']:
|
271
|
+
if key in msg:
|
272
|
+
message[key] = msg[key]
|
273
|
+
|
274
|
+
# Extract function call specific fields
|
275
|
+
if msg.get('type') == 'function_call':
|
276
|
+
for key in ['arguments', 'call_id', 'name']:
|
277
|
+
if key in msg:
|
278
|
+
message[key] = msg[key]
|
279
|
+
|
280
|
+
# Extract function output
|
281
|
+
if msg.get('type') == 'function_call_output':
|
282
|
+
for key in ['call_id', 'output']:
|
283
|
+
if key in msg:
|
284
|
+
message[key] = msg[key]
|
285
|
+
|
286
|
+
if message:
|
287
|
+
history.append(message)
|
288
|
+
|
289
|
+
return history
|
290
|
+
except Exception:
|
291
|
+
return []
|
292
|
+
|
293
|
+
|
294
|
+
def extract_run_context(context):
|
295
|
+
"""Extract relevant details from RunContextWrapper."""
|
296
|
+
try:
|
297
|
+
if context is None:
|
298
|
+
return None
|
299
|
+
|
300
|
+
return {
|
301
|
+
"usage": {
|
302
|
+
"requests": getattr(context.usage, "requests", 0),
|
303
|
+
"input_tokens": getattr(context.usage, "input_tokens", 0),
|
304
|
+
"output_tokens": getattr(context.usage, "output_tokens", 0),
|
305
|
+
"total_tokens": getattr(context.usage, "total_tokens", 0)
|
306
|
+
}
|
307
|
+
}
|
308
|
+
except Exception:
|
309
|
+
return None
|
310
|
+
|
311
|
+
|
312
|
+
def extract_run_config(config):
|
313
|
+
"""Extract relevant details from RunConfig."""
|
314
|
+
try:
|
315
|
+
if config is None:
|
316
|
+
return None
|
317
|
+
|
318
|
+
return {
|
319
|
+
"workflow_name": getattr(config, "workflow_name", None),
|
320
|
+
"trace_id": getattr(config, "trace_id", None),
|
321
|
+
"group_id": getattr(config, "group_id", None),
|
322
|
+
"tracing_disabled": getattr(config, "tracing_disabled", False),
|
323
|
+
"trace_include_sensitive_data": getattr(config, "trace_include_sensitive_data", True)
|
324
|
+
}
|
325
|
+
except Exception:
|
326
|
+
return None
|
327
|
+
|
328
|
+
|
329
|
+
def get_new_response(version: str, tracer: Tracer) -> Callable:
|
330
|
+
"""Wrap the _get_new_response method to trace inputs and outputs."""
|
331
|
+
|
332
|
+
async def traced_method(
|
333
|
+
wrapped: Callable,
|
334
|
+
instance: Any,
|
335
|
+
args: List[Any],
|
336
|
+
kwargs: Any,
|
337
|
+
) -> Any:
|
338
|
+
try:
|
339
|
+
service_provider = SERVICE_PROVIDERS["OPENAI"]
|
340
|
+
extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
|
341
|
+
span_attributes = {
|
342
|
+
"langtrace.sdk.name": "langtrace-python-sdk",
|
343
|
+
"langtrace.service.name": service_provider,
|
344
|
+
"langtrace.service.type": "framework",
|
345
|
+
"langtrace.service.version": version,
|
346
|
+
"langtrace.version": v(LANGTRACE_SDK_NAME),
|
347
|
+
**(extra_attributes if extra_attributes is not None else {}),
|
348
|
+
}
|
349
|
+
|
350
|
+
# Process input arguments
|
351
|
+
try:
|
352
|
+
if args and len(args) >= 7: # Check if we have all expected arguments
|
353
|
+
agent = args[0]
|
354
|
+
agent_name = getattr(agent, 'name', None) if agent else None
|
355
|
+
input_details = {
|
356
|
+
"agent": extract_agent_details(agent),
|
357
|
+
"instructions": args[1],
|
358
|
+
"message_history": extract_message_history(args[2]),
|
359
|
+
"run_context": extract_run_context(args[5]),
|
360
|
+
"run_config": extract_run_config(args[6])
|
361
|
+
}
|
362
|
+
span_attributes["openai_agents.inputs"] = json.dumps(input_details)
|
363
|
+
|
364
|
+
# Set standard LLM prompts attribute
|
365
|
+
if args[2]: # message_history exists
|
366
|
+
messages = []
|
367
|
+
for msg in args[2]:
|
368
|
+
if isinstance(msg, dict):
|
369
|
+
messages.append({
|
370
|
+
"role": msg.get("role", "user"),
|
371
|
+
"content": msg.get("content", "")
|
372
|
+
})
|
373
|
+
if messages:
|
374
|
+
span_attributes[SpanAttributes.LLM_PROMPTS] = json.dumps(messages)
|
375
|
+
except Exception:
|
376
|
+
pass # Silently fail if input processing fails
|
377
|
+
|
378
|
+
attributes = FrameworkSpanAttributes(**span_attributes)
|
379
|
+
# Determine span name based on agent name
|
380
|
+
agent_name = getattr(args[0], 'name', None) if args and len(args) > 0 else None
|
381
|
+
span_name = (f"openai_agents.{agent_name}" if agent_name
|
382
|
+
else "openai_agents.agent_response")
|
383
|
+
|
384
|
+
with tracer.start_as_current_span(
|
385
|
+
name=span_name,
|
386
|
+
kind=SpanKind.CLIENT
|
387
|
+
) as span:
|
388
|
+
try:
|
389
|
+
set_span_attributes(span, attributes)
|
390
|
+
|
391
|
+
# Get the model from _get_model
|
392
|
+
agent = args[0] if args and len(args) > 0 else None
|
393
|
+
run_config = args[6] if args and len(args) > 6 else None
|
394
|
+
if agent and run_config:
|
395
|
+
try:
|
396
|
+
model = Runner._get_model(agent, run_config)
|
397
|
+
if hasattr(model, 'model'):
|
398
|
+
model_name = None
|
399
|
+
if isinstance(model.model, str):
|
400
|
+
model_name = model.model
|
401
|
+
elif hasattr(model.model, 'model_name'):
|
402
|
+
model_name = model.model.model_name
|
403
|
+
if model_name:
|
404
|
+
span.set_attribute(SpanAttributes.LLM_REQUEST_MODEL, model_name)
|
405
|
+
except Exception:
|
406
|
+
pass # Silently fail if model extraction fails
|
407
|
+
|
408
|
+
result = await wrapped(*args, **kwargs)
|
409
|
+
|
410
|
+
# Process response output
|
411
|
+
if result is not None:
|
412
|
+
try:
|
413
|
+
response_dict = extract_model_response(result)
|
414
|
+
if response_dict:
|
415
|
+
# Set the main response output
|
416
|
+
span.set_attribute("openai_agents.outputs", json.dumps(response_dict))
|
417
|
+
|
418
|
+
# Set response type and function details as separate attributes for easier querying
|
419
|
+
span.set_attribute("openai_agents.response_type", response_dict.get("response_type", "response"))
|
420
|
+
if response_dict.get("response_type") == "function_call" and "function" in response_dict:
|
421
|
+
span.set_attribute("openai_agents.function", response_dict["function"])
|
422
|
+
|
423
|
+
# Set usage attributes using the llm utility function
|
424
|
+
if "usage" in response_dict:
|
425
|
+
set_usage_attributes(span, response_dict["usage"])
|
426
|
+
|
427
|
+
# Set standard LLM completion event
|
428
|
+
if "output" in response_dict:
|
429
|
+
completions = []
|
430
|
+
for output in response_dict["output"]:
|
431
|
+
if output["type"] == "message":
|
432
|
+
content = " ".join([item.get("text", "") for item in output.get("content", [])])
|
433
|
+
completions.append({
|
434
|
+
"role": output.get("role", "assistant"),
|
435
|
+
"content": content
|
436
|
+
})
|
437
|
+
elif output["type"] == "function_call":
|
438
|
+
completions.append({
|
439
|
+
"role": "assistant",
|
440
|
+
"function_call": {
|
441
|
+
"name": output.get("name"),
|
442
|
+
"arguments": output.get("arguments", "{}")
|
443
|
+
}
|
444
|
+
})
|
445
|
+
if completions:
|
446
|
+
set_event_completion(span, completions)
|
447
|
+
|
448
|
+
span.set_status(Status(StatusCode.OK))
|
449
|
+
except Exception:
|
450
|
+
pass # Silently fail if response processing fails
|
451
|
+
|
452
|
+
return result
|
453
|
+
|
454
|
+
except InputGuardrailTripwireTriggered as err:
|
455
|
+
# Handle guardrail tripwire specifically
|
456
|
+
guardrail_name = err.guardrail_result.guardrail.__class__.__name__
|
457
|
+
error_msg = f"Input guardrail {guardrail_name} triggered tripwire"
|
458
|
+
|
459
|
+
# Set error attributes and status on current span
|
460
|
+
span.set_attribute("error.type", "input_guardrail_tripwire")
|
461
|
+
span.set_attribute("error.guardrail", guardrail_name)
|
462
|
+
span.record_exception(err)
|
463
|
+
span.set_status(Status(StatusCode.ERROR, error_msg))
|
464
|
+
|
465
|
+
# Get the current context and root span
|
466
|
+
ctx = trace.get_current_span().get_span_context()
|
467
|
+
if ctx:
|
468
|
+
root_span = trace.get_tracer(__name__).start_span(
|
469
|
+
"root_span",
|
470
|
+
context=ctx,
|
471
|
+
kind=SpanKind.CLIENT
|
472
|
+
)
|
473
|
+
root_span.set_attribute("error.type", "input_guardrail_tripwire")
|
474
|
+
root_span.set_attribute("error.guardrail", guardrail_name)
|
475
|
+
root_span.record_exception(err)
|
476
|
+
root_span.set_status(Status(StatusCode.ERROR, error_msg))
|
477
|
+
root_span.end()
|
478
|
+
|
479
|
+
raise
|
480
|
+
except OutputGuardrailTripwireTriggered as err:
|
481
|
+
# Handle guardrail tripwire specifically
|
482
|
+
guardrail_name = err.guardrail_result.guardrail.__class__.__name__
|
483
|
+
error_msg = f"Output guardrail {guardrail_name} triggered tripwire"
|
484
|
+
|
485
|
+
# Set error attributes and status on current span
|
486
|
+
span.set_attribute("error.type", "output_guardrail_tripwire")
|
487
|
+
span.set_attribute("error.guardrail", guardrail_name)
|
488
|
+
span.record_exception(err)
|
489
|
+
span.set_status(Status(StatusCode.ERROR, error_msg))
|
490
|
+
|
491
|
+
# Get the current context and root span
|
492
|
+
ctx = trace.get_current_span().get_span_context()
|
493
|
+
if ctx:
|
494
|
+
root_span = trace.get_tracer(__name__).start_span(
|
495
|
+
"root_span",
|
496
|
+
context=ctx,
|
497
|
+
kind=SpanKind.CLIENT
|
498
|
+
)
|
499
|
+
root_span.set_attribute("error.type", "output_guardrail_tripwire")
|
500
|
+
root_span.set_attribute("error.guardrail", guardrail_name)
|
501
|
+
root_span.record_exception(err)
|
502
|
+
root_span.set_status(Status(StatusCode.ERROR, error_msg))
|
503
|
+
root_span.end()
|
504
|
+
|
505
|
+
raise
|
506
|
+
except Exception as err:
|
507
|
+
error_msg = str(err)
|
508
|
+
|
509
|
+
# Set error status on current span
|
510
|
+
span.record_exception(err)
|
511
|
+
span.set_status(Status(StatusCode.ERROR, error_msg))
|
512
|
+
|
513
|
+
# Get the current context and root span
|
514
|
+
ctx = trace.get_current_span().get_span_context()
|
515
|
+
if ctx:
|
516
|
+
root_span = trace.get_tracer(__name__).start_span(
|
517
|
+
"root_span",
|
518
|
+
context=ctx,
|
519
|
+
kind=SpanKind.CLIENT
|
520
|
+
)
|
521
|
+
root_span.record_exception(err)
|
522
|
+
root_span.set_status(Status(StatusCode.ERROR, error_msg))
|
523
|
+
root_span.end()
|
524
|
+
|
525
|
+
raise
|
526
|
+
|
527
|
+
except Exception as outer_err:
|
528
|
+
try:
|
529
|
+
return await wrapped(*args, **kwargs)
|
530
|
+
except Exception as wrapped_err:
|
531
|
+
raise wrapped_err
|
532
|
+
|
533
|
+
return traced_method
|
@@ -1,5 +1,5 @@
|
|
1
1
|
"""
|
2
|
-
Copyright (c)
|
2
|
+
Copyright (c) 2025 Scale3 Labs
|
3
3
|
|
4
4
|
Licensed under the Apache License, Version 2.0 (the "License");
|
5
5
|
you may not use this file except in compliance with the License.
|
@@ -14,13 +14,16 @@ See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
15
15
|
"""
|
16
16
|
|
17
|
+
from typing import Collection
|
18
|
+
|
19
|
+
from importlib_metadata import version as v
|
17
20
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
18
21
|
from opentelemetry.trace import get_tracer
|
19
22
|
from wrapt import wrap_function_wrapper as _W
|
20
|
-
|
21
|
-
from importlib_metadata import version as v
|
23
|
+
|
22
24
|
from .patch import patch_agent, patch_memory
|
23
25
|
|
26
|
+
|
24
27
|
class PhiDataInstrumentation(BaseInstrumentor):
|
25
28
|
def instrumentation_dependencies(self) -> Collection[str]:
|
26
29
|
return ["phidata >= 2.7.10"] # Adjust version as needed
|
@@ -50,9 +50,9 @@ from langtrace_python_sdk.instrumentation import (
|
|
50
50
|
LangchainCoreInstrumentation, LangchainInstrumentation,
|
51
51
|
LanggraphInstrumentation, LiteLLMInstrumentation,
|
52
52
|
LlamaindexInstrumentation, MilvusInstrumentation, MistralInstrumentation,
|
53
|
-
OllamaInstrumentor,
|
54
|
-
PineconeInstrumentation, PyMongoInstrumentation,
|
55
|
-
VertexAIInstrumentation, WeaviateInstrumentation)
|
53
|
+
OllamaInstrumentor, OpenAIAgentsInstrumentation, OpenAIInstrumentation,
|
54
|
+
PhiDataInstrumentation, PineconeInstrumentation, PyMongoInstrumentation,
|
55
|
+
QdrantInstrumentation, VertexAIInstrumentation, WeaviateInstrumentation)
|
56
56
|
from langtrace_python_sdk.types import (DisableInstrumentations,
|
57
57
|
InstrumentationMethods)
|
58
58
|
from langtrace_python_sdk.utils import (check_if_sdk_is_outdated,
|
@@ -291,6 +291,7 @@ def init(
|
|
291
291
|
"pymilvus": MilvusInstrumentation(),
|
292
292
|
"crewai-tools": CrewaiToolsInstrumentation(),
|
293
293
|
"cleanlab-tlm": CleanLabInstrumentation(),
|
294
|
+
"openai-agents": OpenAIAgentsInstrumentation(),
|
294
295
|
}
|
295
296
|
|
296
297
|
init_instrumentations(config.disable_instrumentations, all_instrumentations)
|