Skip to content

Commit f77031d

Browse files
fenilfalduDwij1704
andauthored
Agent decorator fix (#976)
* refactored the factory implementation of decorator to handle the async calls * code refactored * added some test * ruff linters * removed the __del__ method * removed the __del__ in unit tests * apply ruff linter and formatter fixes --------- Co-authored-by: Dwij <96073160+Dwij1704@users.noreply.github.com>
1 parent a3d41f3 commit f77031d

File tree

2 files changed

+49
-5
lines changed

2 files changed

+49
-5
lines changed

agentops/sdk/decorators/factory.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ def decorator(wrapped=None, *, name=None, version=None):
3838
# Create a proxy class that wraps the original class
3939
class WrappedClass(wrapped):
4040
def __init__(self, *args, **kwargs):
41-
# Start span when instance is created
4241
operation_name = name or wrapped.__name__
4342
self._agentops_span_context_manager = _create_as_current_span(operation_name, entity_kind, version)
4443
self._agentops_active_span = self._agentops_span_context_manager.__enter__()
@@ -51,15 +50,34 @@ def __init__(self, *args, **kwargs):
5150
# Call the original __init__
5251
super().__init__(*args, **kwargs)
5352

54-
def __del__(self):
55-
# End span when instance is destroyed
53+
async def __aenter__(self):
54+
# Added for async context manager support
55+
# This allows using the class with 'async with' statement
56+
57+
# If span is already created in __init__, just return self
58+
if hasattr(self, "_agentops_active_span") and self._agentops_active_span is not None:
59+
return self
60+
61+
# Otherwise create span (for backward compatibility)
62+
operation_name = name or wrapped.__name__
63+
self._agentops_span_context_manager = _create_as_current_span(operation_name, entity_kind, version)
64+
self._agentops_active_span = self._agentops_span_context_manager.__enter__()
65+
return self
66+
67+
async def __aexit__(self, exc_type, exc_val, exc_tb):
68+
# Added for proper async cleanup
69+
# This ensures spans are properly closed when using 'async with'
70+
5671
if hasattr(self, "_agentops_active_span") and hasattr(self, "_agentops_span_context_manager"):
5772
try:
5873
_record_entity_output(self._agentops_active_span, self)
5974
except Exception as e:
6075
logger.warning(f"Failed to record entity output: {e}")
6176

62-
self._agentops_span_context_manager.__exit__(None, None, None)
77+
self._agentops_span_context_manager.__exit__(exc_type, exc_val, exc_tb)
78+
# Clear the span references after cleanup
79+
self._agentops_span_context_manager = None
80+
self._agentops_active_span = None
6381

6482
# Preserve metadata of the original class
6583
WrappedClass.__name__ = wrapped.__name__
@@ -136,11 +154,13 @@ async def _wrapped_async():
136154
with _create_as_current_span(operation_name, entity_kind, version) as span:
137155
try:
138156
_record_entity_input(span, args, kwargs)
157+
139158
except Exception as e:
140159
logger.warning(f"Failed to record entity input: {e}")
141160

142161
try:
143162
result = wrapped(*args, **kwargs)
163+
144164
try:
145165
_record_entity_output(span, result)
146166
except Exception as e:

tests/unit/sdk/test_decorators.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from typing import AsyncGenerator
22
import asyncio
3-
3+
import pytest
44

55
from agentops.sdk.decorators import agent, operation, session, workflow, task
66
from agentops.semconv import SpanKind
77
from agentops.semconv.span_attributes import SpanAttributes
88
from tests.unit.sdk.instrumentation_tester import InstrumentationTester
9+
from agentops.sdk.decorators.factory import create_entity_decorator
910

1011

1112
class TestSpanNesting:
@@ -600,3 +601,26 @@ def test_workflow_session():
600601
assert transform_task.parent is not None
601602
assert workflow_span.context is not None
602603
assert transform_task.parent.span_id == workflow_span.context.span_id
604+
605+
606+
@pytest.mark.asyncio
607+
async def test_async_context_manager():
608+
"""
609+
Tests async context manager functionality (__aenter__, __aexit__).
610+
"""
611+
612+
# Create a simple decorated class
613+
@create_entity_decorator("test")
614+
class TestClass:
615+
def __init__(self):
616+
self.value = 42
617+
618+
# Cover __aenter__ and __aexit__ (normal exit)
619+
async with TestClass() as instance:
620+
assert hasattr(instance, "_agentops_active_span")
621+
assert instance._agentops_active_span is not None
622+
623+
# Cover __aenter__ and __aexit__ (exceptional exit)
624+
with pytest.raises(ValueError):
625+
async with TestClass() as instance:
626+
raise ValueError("Trigger exception for __aexit__ coverage")

0 commit comments

Comments
 (0)