Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agentops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def configure(**kwargs):

_client.configure(**kwargs)


# For backwards compatibility and testing


Expand All @@ -132,8 +133,7 @@ def get_client() -> Client:
return _client



from agentops.legacy import * # type: ignore
from agentops.legacy import * # type: ignore

__all__ = [
"init",
Expand Down
1 change: 1 addition & 0 deletions agentops/helpers/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
def is_coroutine_or_generator(fn: Any) -> bool:
"""Check if a function is asynchronous (coroutine or async generator)"""
import inspect

return inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn)
6 changes: 3 additions & 3 deletions agentops/legacy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def end_session(self):
self.span.end()



def start_session(
tags: Union[Dict[str, Any], List[str], None] = None,
) -> Session:
Expand All @@ -60,15 +59,15 @@ def start_session(
Returns:
A Session object that should be passed to end_session
"""
from agentops import Client
if not Client().initialized:
Client().init()

from agentops.sdk.decorators.utility import _make_span

attributes = {}
if tags:
attributes["tags"] = tags
span, context, token = _make_span('session', span_kind=SpanKind.SESSION, attributes=attributes)
span, context, token = _make_span("session", span_kind=SpanKind.SESSION, attributes=attributes)
return Session(span, token)


Expand All @@ -85,6 +84,7 @@ def end_session(session: Session) -> None:
session: The session object returned by start_session
"""
from agentops.sdk.decorators.utility import _finalize_span

_finalize_span(session.span, session.token)


Expand Down
2 changes: 2 additions & 0 deletions agentops/sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

# Import core components
from agentops.sdk.core import TracingCore

# Import decorators
from agentops.sdk.decorators import agent, operation, session, task, workflow

# from agentops.sdk.traced import TracedObject # Merged into TracedObject
from agentops.sdk.types import TracingConfig

Expand Down
8 changes: 1 addition & 7 deletions agentops/sdk/decorators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@
session = create_entity_decorator(SpanKind.SESSION)
operation = task

__all__ = [
'agent',
'task',
'workflow',
'session',
'operation'
]
__all__ = ["agent", "task", "workflow", "session", "operation"]

# Create decorators task, workflow, session, agent
58 changes: 32 additions & 26 deletions agentops/sdk/decorators/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,33 @@
from agentops.logging import logger
from agentops.sdk.core import TracingCore

from .utility import (_create_as_current_span, _finalize_span, _make_span,
_process_async_generator, _process_sync_generator,
_record_entity_input, _record_entity_output)
from .utility import (
_create_as_current_span,
_finalize_span,
_make_span,
_process_async_generator,
_process_sync_generator,
_record_entity_input,
_record_entity_output,
)


def create_entity_decorator(entity_kind: str):
"""
Factory function that creates decorators for specific entity kinds.

Args:
entity_kind: The type of operation being performed (SpanKind.*)

Returns:
A decorator with optional arguments for name and version
"""

def decorator(wrapped=None, *, name=None, version=None):
# Handle case where decorator is called with parameters
if wrapped is None:
return functools.partial(decorator, name=name, version=version)

# Handle class decoration
if inspect.isclass(wrapped):
# Create a proxy class that wraps the original class
Expand All @@ -37,33 +44,33 @@ def __init__(self, *args, **kwargs):
operation_name = name or wrapped.__name__
self._agentops_span_context_manager = _create_as_current_span(operation_name, entity_kind, version)
self._agentops_active_span = self._agentops_span_context_manager.__enter__()

try:
_record_entity_input(self._agentops_active_span, args, kwargs)
except Exception as e:
logger.warning(f"Failed to record entity input: {e}")

# Call the original __init__
super().__init__(*args, **kwargs)

def __del__(self):
# End span when instance is destroyed
if hasattr(self, '_agentops_active_span') and hasattr(self, '_agentops_span_context_manager'):
if hasattr(self, "_agentops_active_span") and hasattr(self, "_agentops_span_context_manager"):
try:
_record_entity_output(self._agentops_active_span, self)
except Exception as e:
logger.warning(f"Failed to record entity output: {e}")

self._agentops_span_context_manager.__exit__(None, None, None)

# Preserve metadata of the original class
WrappedClass.__name__ = wrapped.__name__
WrappedClass.__qualname__ = wrapped.__qualname__
WrappedClass.__module__ = wrapped.__module__
WrappedClass.__doc__ = wrapped.__doc__

return WrappedClass

# Create the actual decorator wrapper function for functions
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
Expand All @@ -87,10 +94,10 @@ def wrapper(wrapped, instance, args, kwargs):
_record_entity_input(span, args, kwargs)
except Exception as e:
logger.warning(f"Failed to record entity input: {e}")

result = wrapped(*args, **kwargs)
return _process_sync_generator(span, result)

# Handle async generator functions
elif is_async_generator:
# Use the old approach for async generators
Expand All @@ -99,19 +106,20 @@ def wrapper(wrapped, instance, args, kwargs):
_record_entity_input(span, args, kwargs)
except Exception as e:
logger.warning(f"Failed to record entity input: {e}")

result = wrapped(*args, **kwargs)
return _process_async_generator(span, token, result)

# Handle async functions
elif is_async:

async def _wrapped_async():
with _create_as_current_span(operation_name, entity_kind, version) as span:
try:
_record_entity_input(span, args, kwargs)
except Exception as e:
logger.warning(f"Failed to record entity input: {e}")

try:
result = await wrapped(*args, **kwargs)
try:
Expand All @@ -122,17 +130,17 @@ async def _wrapped_async():
except Exception as e:
span.record_exception(e)
raise

return _wrapped_async()

# Handle sync functions
else:
with _create_as_current_span(operation_name, entity_kind, version) as span:
try:
_record_entity_input(span, args, kwargs)
except Exception as e:
logger.warning(f"Failed to record entity input: {e}")

try:
result = wrapped(*args, **kwargs)
try:
Expand All @@ -145,8 +153,6 @@ async def _wrapped_async():
raise

# Return the wrapper for functions, we already returned WrappedClass for classes
return wrapper(wrapped) # type: ignore

return decorator

return wrapper(wrapped) # type: ignore

return decorator
43 changes: 21 additions & 22 deletions agentops/sdk/decorators/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def set_workflow_name(workflow_name: str) -> None:
def set_entity_path(entity_path: str) -> None:
attach(set_value("entity_path", entity_path))


# Helper functions for content management


Expand Down Expand Up @@ -73,17 +74,14 @@ def _get_current_span_info():
"span_id": f"{ctx.span_id:x}" if hasattr(ctx, "span_id") else "None",
"trace_id": f"{ctx.trace_id:x}" if hasattr(ctx, "trace_id") else "None",
"name": getattr(current_span, "name", "Unknown"),
"is_recording": getattr(current_span, "is_recording", False)
"is_recording": getattr(current_span, "is_recording", False),
}
return {"name": "No current span"}


@contextmanager
def _create_as_current_span(
operation_name: str,
span_kind: str,
version: Optional[int] = None,
attributes: Optional[Dict[str, Any]] = None
operation_name: str, span_kind: str, version: Optional[int] = None, attributes: Optional[Dict[str, Any]] = None
) -> Generator[Span, None, None]:
"""
Create and yield an instrumentation span as the current span using proper context management.
Expand All @@ -104,7 +102,7 @@ def _create_as_current_span(
# Log before we do anything
before_span = _get_current_span_info()
logger.debug(f"[DEBUG] BEFORE {operation_name}.{span_kind} - Current context: {before_span}")

# Create span with proper naming convention
span_name = f"{operation_name}.{span_kind}"

Expand All @@ -125,26 +123,25 @@ def _create_as_current_span(

# Get current context explicitly to debug it
current_context = context_api.get_current()

# Use OpenTelemetry's context manager to properly handle span lifecycle
with tracer.start_as_current_span(span_name, attributes=attributes, context=current_context) as span:
# Log after span creation
if hasattr(span, "get_span_context"):
span_ctx = span.get_span_context()
logger.debug(f"[DEBUG] CREATED {span_name} - span_id: {span_ctx.span_id:x}, parent: {before_span.get('span_id', 'None')}")

logger.debug(
f"[DEBUG] CREATED {span_name} - span_id: {span_ctx.span_id:x}, parent: {before_span.get('span_id', 'None')}"
)

yield span

# Log after we're done
after_span = _get_current_span_info()
logger.debug(f"[DEBUG] AFTER {operation_name}.{span_kind} - Returned to context: {after_span}")


def _make_span(
operation_name: str,
span_kind: str,
version: Optional[int] = None,
attributes: Optional[Dict[str, Any]] = None
operation_name: str, span_kind: str, version: Optional[int] = None, attributes: Optional[Dict[str, Any]] = None
) -> tuple:
"""
Create a span without context management for manual span lifecycle control.
Expand All @@ -167,7 +164,7 @@ def _make_span(
# Log before we do anything
before_span = _get_current_span_info()
logger.debug(f"[DEBUG] BEFORE _make_span {operation_name}.{span_kind} - Current context: {before_span}")

# Create span with proper naming convention
span_name = f"{operation_name}.{span_kind}"

Expand All @@ -188,18 +185,20 @@ def _make_span(

# Get current context explicitly
current_context = context_api.get_current()

# Create the span with explicit context
span = tracer.start_span(span_name, context=current_context, attributes=attributes)

# Set as current context and get token for later detachment
ctx = trace.set_span_in_context(span)
token = context_api.attach(ctx)

# Log after span creation
if hasattr(span, "get_span_context"):
span_ctx = span.get_span_context()
logger.debug(f"[DEBUG] CREATED _make_span {span_name} - span_id: {span_ctx.span_id:x}, parent: {before_span.get('span_id', 'None')}")
logger.debug(
f"[DEBUG] CREATED _make_span {span_name} - span_id: {span_ctx.span_id:x}, parent: {before_span.get('span_id', 'None')}"
)

return span, ctx, token

Expand Down Expand Up @@ -236,15 +235,15 @@ def _finalize_span(span: trace.Span, token: Any) -> None:
if hasattr(span, "get_span_context") and hasattr(span.get_span_context(), "span_id"):
span_id = f"{span.get_span_context().span_id:x}"
logger.debug(f"[DEBUG] ENDING span {getattr(span, 'name', 'unknown')} - span_id: {span_id}")

span.end()

# Debug info before detaching
current_after_end = _get_current_span_info()
logger.debug(f"[DEBUG] AFTER span.end() - Current context: {current_after_end}")

context_api.detach(token)

# Debug info after detaching
final_context = _get_current_span_info()
logger.debug(f"[DEBUG] AFTER detach - Final context: {final_context}")
2 changes: 1 addition & 1 deletion agentops/semconv/span_kinds.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class SpanKind:

# Workflow kinds
WORKFLOW_STEP = "workflow.step" # Step in a workflow
WORKFLOW = 'workflow'
WORKFLOW = "workflow"
SESSION = "session"
TASK = "task"
OPERATION = "operation"
Expand Down
Loading
Loading