66
77from agentops .logging import logger
88from agentops .sdk .core import TracingCore
9+ from agentops .semconv .span_kinds import SpanKind
910
1011from .utility import (
1112 _create_as_current_span ,
@@ -28,10 +29,10 @@ def create_entity_decorator(entity_kind: str):
2829 A decorator with optional arguments for name and version
2930 """
3031
31- def decorator (wrapped = None , * , name = None , version = None ):
32+ def decorator (wrapped = None , * , name = None , version = None , tags = None ):
3233 # Handle case where decorator is called with parameters
3334 if wrapped is None :
34- return functools .partial (decorator , name = name , version = version )
35+ return functools .partial (decorator , name = name , version = version , tags = tags )
3536
3637 # Handle class decoration
3738 if inspect .isclass (wrapped ):
@@ -91,7 +92,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
9192 @wrapt .decorator
9293 def wrapper (wrapped , instance , args , kwargs ):
9394 # Skip instrumentation if tracer not initialized
94- if not TracingCore .get_instance ()._initialized :
95+ if not TracingCore .get_instance ().initialized :
9596 return wrapped (* args , ** kwargs )
9697
9798 # Use provided name or function name
@@ -102,8 +103,100 @@ def wrapper(wrapped, instance, args, kwargs):
102103 is_generator = inspect .isgeneratorfunction (wrapped )
103104 is_async_generator = inspect .isasyncgenfunction (wrapped )
104105
106+ # If it's a SESSION kind, we use start_trace/end_trace
107+ if entity_kind == SpanKind .SESSION :
108+ if is_generator or is_async_generator :
109+ # Using start_trace/end_trace for generators decorated with @session might be complex
110+ # due to the nature of yielding. For now, log a warning and fall back to existing generator handling OR disallow.
111+ # Let's keep existing generator handling for now, which creates a single span.
112+ # A true "session per generator invocation" would require more complex handling.
113+ logger .warning (
114+ f"@agentops.session decorator used on a generator function '{ operation_name } '. \
115+ This will create a single span for the generator's instantiation, not a long-running trace for its entire execution."
116+ )
117+ # Fallthrough to existing generator logic below for non-session spans
118+ pass # Explicitly fall through
119+
120+ elif is_async :
121+
122+ async def _wrapped_session_async ():
123+ trace_context = None
124+ try :
125+ trace_context = TracingCore .get_instance ().start_trace (trace_name = operation_name , tags = tags )
126+ if not trace_context :
127+ logger .error (
128+ f"Failed to start trace for @session '{ operation_name } '. Executing function without AgentOps trace."
129+ )
130+ return await wrapped (* args , ** kwargs )
131+
132+ # Record input if possible (span is in trace_context.span)
133+ try :
134+ _record_entity_input (trace_context .span , args , kwargs )
135+ except Exception as e :
136+ logger .warning (f"Failed to record entity input for @session '{ operation_name } ': { e } " )
137+
138+ result = await wrapped (* args , ** kwargs )
139+
140+ try :
141+ _record_entity_output (trace_context .span , result )
142+ except Exception as e :
143+ logger .warning (f"Failed to record entity output for @session '{ operation_name } ': { e } " )
144+
145+ TracingCore .get_instance ().end_trace (trace_context , "Success" )
146+ return result
147+ except Exception :
148+ if trace_context :
149+ TracingCore .get_instance ().end_trace (trace_context , "Failure" )
150+ # record_exception on trace_context.span might be an option too
151+ # trace_context.span.record_exception(e) # If we want it on the span directly
152+ raise
153+ finally :
154+ # Ensure trace is ended if not already (e.g. early exit without exception but before success end_trace)
155+ if trace_context and trace_context .span .is_recording ():
156+ logger .warning (
157+ f"Trace for @session '{ operation_name } ' was not explicitly ended. Ending as 'Unknown'."
158+ )
159+ TracingCore .get_instance ().end_trace (trace_context , "Unknown" )
160+
161+ return _wrapped_session_async ()
162+ else : # Sync function for SpanKind.SESSION
163+ trace_context = None
164+ try :
165+ trace_context = TracingCore .get_instance ().start_trace (trace_name = operation_name , tags = tags )
166+ if not trace_context :
167+ logger .error (
168+ f"Failed to start trace for @session '{ operation_name } '. Executing function without AgentOps trace."
169+ )
170+ return wrapped (* args , ** kwargs )
171+
172+ try :
173+ _record_entity_input (trace_context .span , args , kwargs )
174+ except Exception as e :
175+ logger .warning (f"Failed to record entity input for @session '{ operation_name } ': { e } " )
176+
177+ result = wrapped (* args , ** kwargs )
178+
179+ try :
180+ _record_entity_output (trace_context .span , result )
181+ except Exception as e :
182+ logger .warning (f"Failed to record entity output for @session '{ operation_name } ': { e } " )
183+
184+ TracingCore .get_instance ().end_trace (trace_context , "Success" )
185+ return result
186+ except Exception :
187+ if trace_context :
188+ TracingCore .get_instance ().end_trace (trace_context , "Failure" )
189+ raise
190+ finally :
191+ if trace_context and trace_context .span .is_recording ():
192+ logger .warning (
193+ f"Trace for @session '{ operation_name } ' was not explicitly ended. Ending as 'Unknown'."
194+ )
195+ TracingCore .get_instance ().end_trace (trace_context , "Unknown" )
196+
197+ # Existing logic for non-SESSION kinds or generators under @session (as per above warning)
105198 # Handle generator functions
106- if is_generator :
199+ if is_generator : # This 'if' will also catch generators decorated with @session due to fallthrough
107200 # Use the old approach for generators
108201 span , ctx , token = _make_span (operation_name , entity_kind , version )
109202 try :
@@ -115,7 +208,7 @@ def wrapper(wrapped, instance, args, kwargs):
115208 return _process_sync_generator (span , result )
116209
117210 # Handle async generator functions
118- elif is_async_generator :
211+ elif is_async_generator : # This 'elif' will also catch async generators decorated with @session
119212 # Use the old approach for async generators
120213 span , ctx , token = _make_span (operation_name , entity_kind , version )
121214 try :
@@ -126,8 +219,8 @@ def wrapper(wrapped, instance, args, kwargs):
126219 result = wrapped (* args , ** kwargs )
127220 return _process_async_generator (span , token , result )
128221
129- # Handle async functions
130- elif is_async :
222+ # Handle async functions (non-SESSION)
223+ elif is_async : # This is for entity_kind != SpanKind.SESSION
131224
132225 async def _wrapped_async ():
133226 with _create_as_current_span (operation_name , entity_kind , version ) as span :
@@ -149,8 +242,8 @@ async def _wrapped_async():
149242
150243 return _wrapped_async ()
151244
152- # Handle sync functions
153- else :
245+ # Handle sync functions (non-SESSION)
246+ else : # This is for entity_kind != SpanKind.SESSION
154247 with _create_as_current_span (operation_name , entity_kind , version ) as span :
155248 try :
156249 _record_entity_input (span , args , kwargs )
0 commit comments