mseep-agentops 0.4.18__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.
Files changed (94) hide show
  1. agentops/__init__.py +488 -0
  2. agentops/client/__init__.py +5 -0
  3. agentops/client/api/__init__.py +71 -0
  4. agentops/client/api/base.py +162 -0
  5. agentops/client/api/types.py +21 -0
  6. agentops/client/api/versions/__init__.py +10 -0
  7. agentops/client/api/versions/v3.py +65 -0
  8. agentops/client/api/versions/v4.py +104 -0
  9. agentops/client/client.py +211 -0
  10. agentops/client/http/__init__.py +0 -0
  11. agentops/client/http/http_adapter.py +116 -0
  12. agentops/client/http/http_client.py +215 -0
  13. agentops/config.py +268 -0
  14. agentops/enums.py +36 -0
  15. agentops/exceptions.py +38 -0
  16. agentops/helpers/__init__.py +44 -0
  17. agentops/helpers/dashboard.py +54 -0
  18. agentops/helpers/deprecation.py +50 -0
  19. agentops/helpers/env.py +52 -0
  20. agentops/helpers/serialization.py +137 -0
  21. agentops/helpers/system.py +178 -0
  22. agentops/helpers/time.py +11 -0
  23. agentops/helpers/version.py +36 -0
  24. agentops/instrumentation/__init__.py +598 -0
  25. agentops/instrumentation/common/__init__.py +82 -0
  26. agentops/instrumentation/common/attributes.py +278 -0
  27. agentops/instrumentation/common/instrumentor.py +147 -0
  28. agentops/instrumentation/common/metrics.py +100 -0
  29. agentops/instrumentation/common/objects.py +26 -0
  30. agentops/instrumentation/common/span_management.py +176 -0
  31. agentops/instrumentation/common/streaming.py +218 -0
  32. agentops/instrumentation/common/token_counting.py +177 -0
  33. agentops/instrumentation/common/version.py +71 -0
  34. agentops/instrumentation/common/wrappers.py +235 -0
  35. agentops/legacy/__init__.py +277 -0
  36. agentops/legacy/event.py +156 -0
  37. agentops/logging/__init__.py +4 -0
  38. agentops/logging/config.py +86 -0
  39. agentops/logging/formatters.py +34 -0
  40. agentops/logging/instrument_logging.py +91 -0
  41. agentops/sdk/__init__.py +27 -0
  42. agentops/sdk/attributes.py +151 -0
  43. agentops/sdk/core.py +607 -0
  44. agentops/sdk/decorators/__init__.py +51 -0
  45. agentops/sdk/decorators/factory.py +486 -0
  46. agentops/sdk/decorators/utility.py +216 -0
  47. agentops/sdk/exporters.py +87 -0
  48. agentops/sdk/processors.py +71 -0
  49. agentops/sdk/types.py +21 -0
  50. agentops/semconv/__init__.py +36 -0
  51. agentops/semconv/agent.py +29 -0
  52. agentops/semconv/core.py +19 -0
  53. agentops/semconv/enum.py +11 -0
  54. agentops/semconv/instrumentation.py +13 -0
  55. agentops/semconv/langchain.py +63 -0
  56. agentops/semconv/message.py +61 -0
  57. agentops/semconv/meters.py +24 -0
  58. agentops/semconv/resource.py +52 -0
  59. agentops/semconv/span_attributes.py +118 -0
  60. agentops/semconv/span_kinds.py +50 -0
  61. agentops/semconv/status.py +11 -0
  62. agentops/semconv/tool.py +15 -0
  63. agentops/semconv/workflow.py +69 -0
  64. agentops/validation.py +357 -0
  65. mseep_agentops-0.4.18.dist-info/METADATA +49 -0
  66. mseep_agentops-0.4.18.dist-info/RECORD +94 -0
  67. mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
  68. mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
  69. mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
  70. tests/__init__.py +0 -0
  71. tests/conftest.py +10 -0
  72. tests/unit/__init__.py +0 -0
  73. tests/unit/client/__init__.py +1 -0
  74. tests/unit/client/test_http_adapter.py +221 -0
  75. tests/unit/client/test_http_client.py +206 -0
  76. tests/unit/conftest.py +54 -0
  77. tests/unit/sdk/__init__.py +1 -0
  78. tests/unit/sdk/instrumentation_tester.py +207 -0
  79. tests/unit/sdk/test_attributes.py +392 -0
  80. tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
  81. tests/unit/sdk/test_decorators.py +763 -0
  82. tests/unit/sdk/test_exporters.py +241 -0
  83. tests/unit/sdk/test_factory.py +1188 -0
  84. tests/unit/sdk/test_internal_span_processor.py +397 -0
  85. tests/unit/sdk/test_resource_attributes.py +35 -0
  86. tests/unit/test_config.py +82 -0
  87. tests/unit/test_context_manager.py +777 -0
  88. tests/unit/test_events.py +27 -0
  89. tests/unit/test_host_env.py +54 -0
  90. tests/unit/test_init_py.py +501 -0
  91. tests/unit/test_serialization.py +433 -0
  92. tests/unit/test_session.py +676 -0
  93. tests/unit/test_user_agent.py +34 -0
  94. tests/unit/test_validation.py +405 -0
@@ -0,0 +1,486 @@
1
+ import inspect
2
+ import functools
3
+ import asyncio
4
+ from typing import Any, Dict, Callable, Optional, Union
5
+
6
+
7
+ import wrapt # type: ignore
8
+
9
+ from agentops.logging import logger
10
+ from agentops.sdk.core import TraceContext, tracer
11
+ from agentops.semconv.span_kinds import SpanKind
12
+ from agentops.semconv import SpanAttributes, CoreAttributes
13
+
14
+ from agentops.sdk.decorators.utility import (
15
+ _create_as_current_span,
16
+ _process_async_generator,
17
+ _process_sync_generator,
18
+ _record_entity_input,
19
+ _record_entity_output,
20
+ _extract_request_data,
21
+ _extract_response_data,
22
+ )
23
+
24
+
25
+ def create_entity_decorator(entity_kind: str) -> Callable[..., Any]:
26
+ """
27
+ Factory that creates decorators for instrumenting functions and classes.
28
+ Handles different entity kinds (e.g., SESSION, TASK, HTTP) and function types (sync, async, generator).
29
+ """
30
+
31
+ def decorator(
32
+ wrapped: Optional[Callable[..., Any]] = None,
33
+ *,
34
+ name: Optional[str] = None,
35
+ version: Optional[Any] = None,
36
+ tags: Optional[Union[list, dict]] = None,
37
+ cost=None,
38
+ spec=None,
39
+ capture_request: bool = True,
40
+ capture_response: bool = True,
41
+ ) -> Callable[..., Any]:
42
+ if wrapped is None:
43
+ return functools.partial(
44
+ decorator,
45
+ name=name,
46
+ version=version,
47
+ tags=tags,
48
+ cost=cost,
49
+ spec=spec,
50
+ capture_request=capture_request,
51
+ capture_response=capture_response,
52
+ )
53
+
54
+ if inspect.isclass(wrapped):
55
+ # Class decoration wraps __init__ and aenter/aexit for context management.
56
+ # For SpanKind.SESSION, this creates a span for __init__ or async context, not instance lifetime.
57
+ class WrappedClass(wrapped):
58
+ def __init__(self, *args: Any, **kwargs: Any):
59
+ op_name = name or wrapped.__name__
60
+ self._agentops_span_context_manager = _create_as_current_span(op_name, entity_kind, version)
61
+ self._agentops_active_span = self._agentops_span_context_manager.__enter__()
62
+ try:
63
+ _record_entity_input(self._agentops_active_span, args, kwargs)
64
+ except Exception as e:
65
+ logger.warning(f"Failed to record entity input for class {op_name}: {e}")
66
+ super().__init__(*args, **kwargs)
67
+
68
+ def __del__(self):
69
+ """Ensure span is properly ended when object is destroyed."""
70
+ if hasattr(self, "_agentops_span_context_manager") and self._agentops_span_context_manager:
71
+ try:
72
+ self._agentops_span_context_manager.__exit__(None, None, None)
73
+ except Exception:
74
+ pass
75
+
76
+ async def __aenter__(self) -> "WrappedClass":
77
+ if hasattr(self, "_agentops_active_span") and self._agentops_active_span is not None:
78
+ return self
79
+ op_name = name or wrapped.__name__
80
+ self._agentops_span_context_manager = _create_as_current_span(op_name, entity_kind, version)
81
+ self._agentops_active_span = self._agentops_span_context_manager.__enter__()
82
+ return self
83
+
84
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
85
+ if hasattr(self, "_agentops_active_span") and hasattr(self, "_agentops_span_context_manager"):
86
+ try:
87
+ _record_entity_output(self._agentops_active_span, self)
88
+ except Exception as e:
89
+ logger.warning(f"Failed to record entity output for class instance: {e}")
90
+ self._agentops_span_context_manager.__exit__(exc_type, exc_val, exc_tb)
91
+ self._agentops_span_context_manager = None
92
+ self._agentops_active_span = None
93
+
94
+ WrappedClass.__name__ = wrapped.__name__
95
+ WrappedClass.__qualname__ = wrapped.__qualname__
96
+ WrappedClass.__module__ = wrapped.__module__
97
+ WrappedClass.__doc__ = wrapped.__doc__
98
+ return WrappedClass
99
+
100
+ @wrapt.decorator
101
+ def wrapper(
102
+ wrapped_func: Callable[..., Any], instance: Optional[Any], args: tuple, kwargs: Dict[str, Any]
103
+ ) -> Any:
104
+ if not tracer.initialized:
105
+ return wrapped_func(*args, **kwargs)
106
+
107
+ operation_name = name or wrapped_func.__name__
108
+ is_async = asyncio.iscoroutinefunction(wrapped_func)
109
+ is_generator = inspect.isgeneratorfunction(wrapped_func)
110
+ is_async_generator = inspect.isasyncgenfunction(wrapped_func)
111
+
112
+ # Special handling for HTTP entity kind
113
+ if entity_kind == SpanKind.HTTP:
114
+ if is_generator or is_async_generator:
115
+ logger.warning(
116
+ f"@track_endpoint on generator '{operation_name}' is not supported. Use @trace instead."
117
+ )
118
+ return wrapped_func(*args, **kwargs)
119
+
120
+ if is_async:
121
+
122
+ async def _wrapped_http_async() -> Any:
123
+ trace_context: Optional[TraceContext] = None
124
+ try:
125
+ # Create main session span
126
+ trace_context = tracer.start_trace(trace_name=operation_name, tags=tags)
127
+ if not trace_context:
128
+ logger.error(
129
+ f"Failed to start trace for @track_endpoint '{operation_name}'. Executing without trace."
130
+ )
131
+ return await wrapped_func(*args, **kwargs)
132
+
133
+ # Create HTTP request span
134
+ if capture_request:
135
+ with _create_as_current_span(
136
+ f"{operation_name}.request",
137
+ SpanKind.HTTP,
138
+ version=version,
139
+ attributes={SpanAttributes.HTTP_METHOD: "REQUEST"}
140
+ if SpanAttributes.HTTP_METHOD
141
+ else None,
142
+ ) as request_span:
143
+ try:
144
+ request_data = _extract_request_data()
145
+ if request_data:
146
+ # Set HTTP attributes
147
+ if hasattr(SpanAttributes, "HTTP_METHOD") and request_data.get("method"):
148
+ request_span.set_attribute(
149
+ SpanAttributes.HTTP_METHOD, request_data["method"]
150
+ )
151
+ if hasattr(SpanAttributes, "HTTP_URL") and request_data.get("url"):
152
+ request_span.set_attribute(SpanAttributes.HTTP_URL, request_data["url"])
153
+
154
+ # Record the full request data
155
+ _record_entity_input(request_span, (request_data,), {})
156
+ except Exception as e:
157
+ logger.warning(f"Failed to record HTTP request for '{operation_name}': {e}")
158
+
159
+ # Execute the main function
160
+ result = await wrapped_func(*args, **kwargs)
161
+
162
+ # Create HTTP response span
163
+ if capture_response:
164
+ with _create_as_current_span(
165
+ f"{operation_name}.response",
166
+ SpanKind.HTTP,
167
+ version=version,
168
+ attributes={SpanAttributes.HTTP_METHOD: "RESPONSE"}
169
+ if SpanAttributes.HTTP_METHOD
170
+ else None,
171
+ ) as response_span:
172
+ try:
173
+ response_data = _extract_response_data(result)
174
+ if response_data:
175
+ # Set HTTP attributes
176
+ if hasattr(SpanAttributes, "HTTP_STATUS_CODE") and response_data.get(
177
+ "status_code"
178
+ ):
179
+ response_span.set_attribute(
180
+ SpanAttributes.HTTP_STATUS_CODE, response_data["status_code"]
181
+ )
182
+
183
+ # Record the full response data
184
+ _record_entity_output(response_span, response_data)
185
+ except Exception as e:
186
+ logger.warning(f"Failed to record HTTP response for '{operation_name}': {e}")
187
+
188
+ tracer.end_trace(trace_context, "Success")
189
+ return result
190
+ except Exception:
191
+ if trace_context:
192
+ tracer.end_trace(trace_context, "Indeterminate")
193
+ raise
194
+ finally:
195
+ if trace_context and trace_context.span.is_recording():
196
+ logger.warning(
197
+ f"Trace for @track_endpoint '{operation_name}' not explicitly ended. Ending as 'Unknown'."
198
+ )
199
+ tracer.end_trace(trace_context, "Unknown")
200
+
201
+ return _wrapped_http_async()
202
+ else: # Sync function for HTTP
203
+ trace_context: Optional[TraceContext] = None
204
+ try:
205
+ # Create main session span
206
+ trace_context = tracer.start_trace(trace_name=operation_name, tags=tags)
207
+ if not trace_context:
208
+ logger.error(
209
+ f"Failed to start trace for @track_endpoint '{operation_name}'. Executing without trace."
210
+ )
211
+ return wrapped_func(*args, **kwargs)
212
+
213
+ # Create HTTP request span
214
+ if capture_request:
215
+ with _create_as_current_span(
216
+ f"{operation_name}.request",
217
+ SpanKind.HTTP,
218
+ version=version,
219
+ attributes={SpanAttributes.HTTP_METHOD: "REQUEST"}
220
+ if SpanAttributes.HTTP_METHOD
221
+ else None,
222
+ ) as request_span:
223
+ try:
224
+ request_data = _extract_request_data()
225
+ if request_data:
226
+ # Set HTTP attributes
227
+ if hasattr(SpanAttributes, "HTTP_METHOD") and request_data.get("method"):
228
+ request_span.set_attribute(
229
+ SpanAttributes.HTTP_METHOD, request_data["method"]
230
+ )
231
+ if hasattr(SpanAttributes, "HTTP_URL") and request_data.get("url"):
232
+ request_span.set_attribute(SpanAttributes.HTTP_URL, request_data["url"])
233
+
234
+ # Record the full request data
235
+ _record_entity_input(request_span, (request_data,), {})
236
+ except Exception as e:
237
+ logger.warning(f"Failed to record HTTP request for '{operation_name}': {e}")
238
+
239
+ # Execute the main function
240
+ result = wrapped_func(*args, **kwargs)
241
+
242
+ # Create HTTP response span
243
+ if capture_response:
244
+ with _create_as_current_span(
245
+ f"{operation_name}.response",
246
+ SpanKind.HTTP,
247
+ version=version,
248
+ attributes={SpanAttributes.HTTP_METHOD: "RESPONSE"}
249
+ if SpanAttributes.HTTP_METHOD
250
+ else None,
251
+ ) as response_span:
252
+ try:
253
+ response_data = _extract_response_data(result)
254
+ if response_data:
255
+ # Set HTTP attributes
256
+ if hasattr(SpanAttributes, "HTTP_STATUS_CODE") and response_data.get(
257
+ "status_code"
258
+ ):
259
+ response_span.set_attribute(
260
+ SpanAttributes.HTTP_STATUS_CODE, response_data["status_code"]
261
+ )
262
+
263
+ # Record the full response data
264
+ _record_entity_output(response_span, response_data)
265
+ except Exception as e:
266
+ logger.warning(f"Failed to record HTTP response for '{operation_name}': {e}")
267
+
268
+ tracer.end_trace(trace_context, "Success")
269
+ return result
270
+ except Exception:
271
+ if trace_context:
272
+ tracer.end_trace(trace_context, "Indeterminate")
273
+ raise
274
+ finally:
275
+ if trace_context and trace_context.span.is_recording():
276
+ logger.warning(
277
+ f"Trace for @track_endpoint '{operation_name}' not explicitly ended. Ending as 'Unknown'."
278
+ )
279
+ tracer.end_trace(trace_context, "Unknown")
280
+
281
+ elif entity_kind == SpanKind.SESSION:
282
+ if is_generator or is_async_generator:
283
+ logger.warning(
284
+ f"@agentops.trace on generator '{operation_name}' creates a single span, not a full trace."
285
+ )
286
+ # Fallthrough to existing generator logic which creates a single span.
287
+
288
+ # !! was previously not implemented, checking with @dwij if this was intentional or if my implementation should go in
289
+ if is_generator:
290
+ span, _, token = tracer.make_span(
291
+ operation_name,
292
+ entity_kind,
293
+ version=version,
294
+ attributes={CoreAttributes.TAGS: tags} if tags else None,
295
+ )
296
+ try:
297
+ _record_entity_input(span, args, kwargs, entity_kind=entity_kind)
298
+ except Exception as e:
299
+ logger.warning(f"Input recording failed for '{operation_name}': {e}")
300
+ result = wrapped_func(*args, **kwargs)
301
+ return _process_sync_generator(span, result)
302
+ elif is_async_generator:
303
+ span, _, token = tracer.make_span(
304
+ operation_name,
305
+ entity_kind,
306
+ version=version,
307
+ attributes={CoreAttributes.TAGS: tags} if tags else None,
308
+ )
309
+ try:
310
+ _record_entity_input(span, args, kwargs, entity_kind=entity_kind)
311
+ except Exception as e:
312
+ logger.warning(f"Input recording failed for '{operation_name}': {e}")
313
+ result = wrapped_func(*args, **kwargs)
314
+ return _process_async_generator(span, token, result)
315
+ elif is_async:
316
+
317
+ async def _wrapped_session_async() -> Any:
318
+ trace_context: Optional[TraceContext] = None
319
+ try:
320
+ trace_context = tracer.start_trace(trace_name=operation_name, tags=tags)
321
+ if not trace_context:
322
+ logger.error(
323
+ f"Failed to start trace for @trace '{operation_name}'. Executing without trace."
324
+ )
325
+ return await wrapped_func(*args, **kwargs)
326
+ try:
327
+ _record_entity_input(trace_context.span, args, kwargs)
328
+ except Exception as e:
329
+ logger.warning(f"Input recording failed for @trace '{operation_name}': {e}")
330
+ result = await wrapped_func(*args, **kwargs)
331
+ try:
332
+ _record_entity_output(trace_context.span, result)
333
+ except Exception as e:
334
+ logger.warning(f"Output recording failed for @trace '{operation_name}': {e}")
335
+ tracer.end_trace(trace_context, "Success")
336
+ return result
337
+ except Exception:
338
+ if trace_context:
339
+ tracer.end_trace(trace_context, "Indeterminate")
340
+ raise
341
+ finally:
342
+ if trace_context and trace_context.span.is_recording():
343
+ logger.warning(
344
+ f"Trace for @trace '{operation_name}' not explicitly ended. Ending as 'Unknown'."
345
+ )
346
+ tracer.end_trace(trace_context, "Unknown")
347
+
348
+ return _wrapped_session_async()
349
+ else: # Sync function for SpanKind.SESSION
350
+ trace_context: Optional[TraceContext] = None
351
+ try:
352
+ trace_context = tracer.start_trace(trace_name=operation_name, tags=tags)
353
+ if not trace_context:
354
+ logger.error(
355
+ f"Failed to start trace for @trace '{operation_name}'. Executing without trace."
356
+ )
357
+ return wrapped_func(*args, **kwargs)
358
+ try:
359
+ _record_entity_input(trace_context.span, args, kwargs)
360
+ except Exception as e:
361
+ logger.warning(f"Input recording failed for @trace '{operation_name}': {e}")
362
+ result = wrapped_func(*args, **kwargs)
363
+ try:
364
+ _record_entity_output(trace_context.span, result)
365
+ except Exception as e:
366
+ logger.warning(f"Output recording failed for @trace '{operation_name}': {e}")
367
+ tracer.end_trace(trace_context, "Success")
368
+ return result
369
+ except Exception:
370
+ if trace_context:
371
+ tracer.end_trace(trace_context, "Indeterminate")
372
+ raise
373
+ finally:
374
+ if trace_context and trace_context.span.is_recording():
375
+ logger.warning(
376
+ f"Trace for @trace '{operation_name}' not explicitly ended. Ending as 'Unknown'."
377
+ )
378
+ tracer.end_trace(trace_context, "Unknown")
379
+
380
+ # Logic for non-SESSION kinds or generators under @trace (as per fallthrough)
381
+ elif is_generator:
382
+ span, _, token = tracer.make_span(
383
+ operation_name,
384
+ entity_kind,
385
+ version=version,
386
+ attributes={CoreAttributes.TAGS: tags} if tags else None,
387
+ )
388
+ try:
389
+ _record_entity_input(span, args, kwargs, entity_kind=entity_kind)
390
+ # Set cost attribute if tool
391
+ if entity_kind == "tool" and cost is not None:
392
+ span.set_attribute(SpanAttributes.LLM_USAGE_TOOL_COST, cost)
393
+ # Set spec attribute if guardrail
394
+ if entity_kind == "guardrail" and (spec == "input" or spec == "output"):
395
+ span.set_attribute(SpanAttributes.AGENTOPS_DECORATOR_SPEC.format(entity_kind=entity_kind), spec)
396
+ except Exception as e:
397
+ logger.warning(f"Input recording failed for '{operation_name}': {e}")
398
+ result = wrapped_func(*args, **kwargs)
399
+ return _process_sync_generator(span, result)
400
+ elif is_async_generator:
401
+ span, _, token = tracer.make_span(
402
+ operation_name,
403
+ entity_kind,
404
+ version=version,
405
+ attributes={CoreAttributes.TAGS: tags} if tags else None,
406
+ )
407
+ try:
408
+ _record_entity_input(span, args, kwargs, entity_kind=entity_kind)
409
+ # Set cost attribute if tool
410
+ if entity_kind == "tool" and cost is not None:
411
+ span.set_attribute(SpanAttributes.LLM_USAGE_TOOL_COST, cost)
412
+ # Set spec attribute if guardrail
413
+ if entity_kind == "guardrail" and (spec == "input" or spec == "output"):
414
+ span.set_attribute(SpanAttributes.AGENTOPS_DECORATOR_SPEC.format(entity_kind=entity_kind), spec)
415
+ except Exception as e:
416
+ logger.warning(f"Input recording failed for '{operation_name}': {e}")
417
+ result = wrapped_func(*args, **kwargs)
418
+ return _process_async_generator(span, token, result)
419
+ elif is_async:
420
+
421
+ async def _wrapped_async() -> Any:
422
+ with _create_as_current_span(
423
+ operation_name,
424
+ entity_kind,
425
+ version=version,
426
+ attributes={CoreAttributes.TAGS: tags} if tags else None,
427
+ ) as span:
428
+ try:
429
+ _record_entity_input(span, args, kwargs, entity_kind=entity_kind)
430
+ # Set cost attribute if tool
431
+ if entity_kind == "tool" and cost is not None:
432
+ span.set_attribute(SpanAttributes.LLM_USAGE_TOOL_COST, cost)
433
+ # Set spec attribute if guardrail
434
+ if entity_kind == "guardrail" and (spec == "input" or spec == "output"):
435
+ span.set_attribute(
436
+ SpanAttributes.AGENTOPS_DECORATOR_SPEC.format(entity_kind=entity_kind), spec
437
+ )
438
+ except Exception as e:
439
+ logger.warning(f"Input recording failed for '{operation_name}': {e}")
440
+ try:
441
+ result = await wrapped_func(*args, **kwargs)
442
+ try:
443
+ _record_entity_output(span, result, entity_kind=entity_kind)
444
+ except Exception as e:
445
+ logger.warning(f"Output recording failed for '{operation_name}': {e}")
446
+ return result
447
+ except Exception as e:
448
+ logger.error(f"Error in async function execution: {e}")
449
+ span.record_exception(e)
450
+ raise
451
+
452
+ return _wrapped_async()
453
+ else: # Sync function for non-SESSION kinds
454
+ with _create_as_current_span(
455
+ operation_name,
456
+ entity_kind,
457
+ version=version,
458
+ attributes={CoreAttributes.TAGS: tags} if tags else None,
459
+ ) as span:
460
+ try:
461
+ _record_entity_input(span, args, kwargs, entity_kind=entity_kind)
462
+ # Set cost attribute if tool
463
+ if entity_kind == "tool" and cost is not None:
464
+ span.set_attribute(SpanAttributes.LLM_USAGE_TOOL_COST, cost)
465
+ # Set spec attribute if guardrail
466
+ if entity_kind == "guardrail" and (spec == "input" or spec == "output"):
467
+ span.set_attribute(
468
+ SpanAttributes.AGENTOPS_DECORATOR_SPEC.format(entity_kind=entity_kind), spec
469
+ )
470
+ except Exception as e:
471
+ logger.warning(f"Input recording failed for '{operation_name}': {e}")
472
+ try:
473
+ result = wrapped_func(*args, **kwargs)
474
+ try:
475
+ _record_entity_output(span, result, entity_kind=entity_kind)
476
+ except Exception as e:
477
+ logger.warning(f"Output recording failed for '{operation_name}': {e}")
478
+ return result
479
+ except Exception as e:
480
+ logger.error(f"Error in sync function execution: {e}")
481
+ span.record_exception(e)
482
+ raise
483
+
484
+ return wrapper(wrapped)
485
+
486
+ return decorator