1212GEN_AI_SYSTEM_MESSAGE = "gen_ai.system.message"
1313GEN_AI_USER_MESSAGE = "gen_ai.user.message"
1414GEN_AI_ASSISTANT_MESSAGE = "gen_ai.assistant.message"
15+ TRACELOOP_ENTITY_INPUT = "traceloop.entity.input"
16+ TRACELOOP_ENTITY_OUTPUT = "traceloop.entity.output"
1517
1618_logger = logging .getLogger (__name__ )
1719
20+
1821class LLOHandler :
1922 """
2023 Utility class for handling Large Language Objects (LLO) in OpenTelemetry spans.
@@ -24,6 +27,7 @@ class LLOHandler:
2427 2. Extracts and transforms these attributes into OpenTelemetry Gen AI Events
2528 3. Filters LLO from spans
2629 """
30+
2731 def __init__ (self , logger_provider : LoggerProvider ):
2832 """
2933 Initialize an LLOHandler with the specified logger provider.
@@ -37,12 +41,15 @@ def __init__(self, logger_provider: LoggerProvider):
3741 self ._event_logger_provider = EventLoggerProvider (logger_provider = self ._logger_provider )
3842 self ._event_logger = self ._event_logger_provider .get_event_logger ("gen_ai.events" )
3943
40- self ._exact_match_patterns = []
44+ self ._exact_match_patterns = [
45+ "traceloop.entity.input" ,
46+ "traceloop.entity.output" ,
47+ ]
4148 self ._regex_match_patterns = [
42- r"^gen_ai\.prompt\.\d+\.content$"
49+ r"^gen_ai\.prompt\.\d+\.content$" ,
50+ r"^gen_ai\.completion\.\d+\.content$" ,
4351 ]
4452
45-
4653 def process_spans (self , spans : Sequence [ReadableSpan ]) -> List [ReadableSpan ]:
4754 """
4855 Performs LLO processing for each span:
@@ -66,7 +73,7 @@ def process_spans(self, spans: Sequence[ReadableSpan]) -> List[ReadableSpan]:
6673 maxlen = span .attributes .maxlen ,
6774 attributes = updated_attributes ,
6875 immutable = span .attributes ._immutable ,
69- max_value_len = span .attributes .max_value_len
76+ max_value_len = span .attributes .max_value_len ,
7077 )
7178 else :
7279 span ._attributes = updated_attributes
@@ -75,7 +82,6 @@ def process_spans(self, spans: Sequence[ReadableSpan]) -> List[ReadableSpan]:
7582
7683 return modified_spans
7784
78-
7985 def _emit_llo_attributes (self , span : ReadableSpan , attributes : Dict [str , Any ]) -> None :
8086 """
8187 Collects the Gen AI Events for each LLO attribute in the span and emits them
@@ -90,12 +96,13 @@ def _emit_llo_attributes(self, span: ReadableSpan, attributes: Dict[str, Any]) -
9096 """
9197 all_events = []
9298 all_events .extend (self ._extract_gen_ai_prompt_events (span , attributes ))
99+ all_events .extend (self ._extract_gen_ai_completion_events (span , attributes ))
100+ all_events .extend (self ._extract_traceloop_events (span , attributes ))
93101
94102 for event in all_events :
95103 self ._event_logger .emit (event )
96104 _logger .debug (f"Emitted Gen AI Event: { event .name } " )
97105
98-
99106 def _filter_attributes (self , attributes : Dict [str , Any ]) -> Dict [str , Any ]:
100107 """
101108 Filter out attributes that contain LLO from the span's attributes. This
@@ -116,7 +123,6 @@ def _filter_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
116123
117124 return filtered_attributes
118125
119-
120126 def _is_llo_attribute (self , key : str ) -> bool :
121127 """
122128 Determine if a span attribute contains an LLO based on its key name.
@@ -131,12 +137,10 @@ def _is_llo_attribute(self, key: str) -> bool:
131137 Returns:
132138 bool: True if the key matches an LLO pattern, False otherwise
133139 """
134- return (
135- any (pattern == key for pattern in self ._exact_match_patterns ) or
136- any (re .match (pattern , key ) for pattern in self ._regex_match_patterns )
140+ return any (pattern == key for pattern in self ._exact_match_patterns ) or any (
141+ re .match (pattern , key ) for pattern in self ._regex_match_patterns
137142 )
138143
139-
140144 def _extract_gen_ai_prompt_events (self , span : ReadableSpan , attributes : Dict [str , Any ]) -> List [Event ]:
141145 """
142146 Extract gen_ai prompt events from attributes. Each item `gen_ai.prompt.{n}.content`
@@ -173,15 +177,9 @@ def _extract_gen_ai_prompt_events(self, span: ReadableSpan, attributes: Dict[str
173177 role_key = f"gen_ai.prompt.{ index } .role"
174178 role = attributes .get (role_key , "unknown" )
175179
176- event_attributes = {
177- "gen_ai.system" : gen_ai_system ,
178- "original_attribute" : key
179- }
180+ event_attributes = {"gen_ai.system" : gen_ai_system , "original_attribute" : key }
180181
181- body = {
182- "content" : value ,
183- "role" : role
184- }
182+ body = {"content" : value , "role" : role }
185183
186184 event = None
187185 if role == "system" :
@@ -190,46 +188,104 @@ def _extract_gen_ai_prompt_events(self, span: ReadableSpan, attributes: Dict[str
190188 span_ctx = span_ctx ,
191189 timestamp = prompt_timestamp ,
192190 attributes = event_attributes ,
193- body = body
191+ body = body ,
194192 )
195193 elif role == "user" :
196194 event = self ._get_gen_ai_event (
197195 name = GEN_AI_USER_MESSAGE ,
198196 span_ctx = span_ctx ,
199197 timestamp = prompt_timestamp ,
200198 attributes = event_attributes ,
201- body = body
199+ body = body ,
202200 )
203201 elif role == "assistant" :
204202 event = self ._get_gen_ai_event (
205203 name = GEN_AI_ASSISTANT_MESSAGE ,
206204 span_ctx = span_ctx ,
207205 timestamp = prompt_timestamp ,
208206 attributes = event_attributes ,
209- body = body
207+ body = body ,
210208 )
211209 elif role in ["function" , "unknown" ]:
212210 event = self ._get_gen_ai_event (
213211 name = f"gen_ai.{ gen_ai_system } .message" ,
214212 span_ctx = span_ctx ,
215213 timestamp = prompt_timestamp ,
216214 attributes = event_attributes ,
217- body = body
215+ body = body ,
218216 )
219217
220218 if event :
221219 events .append (event )
222220
223221 return events
224222
225- def _get_gen_ai_event (
226- self ,
227- name ,
228- span_ctx ,
229- timestamp ,
230- attributes ,
231- body
232- ):
223+ def _extract_gen_ai_completion_events (self , span : ReadableSpan , attributes : Dict [str , Any ]) -> List [Event ]:
224+ events = []
225+ span_ctx = span .context
226+ gen_ai_system = span .attributes .get ("gen_ai.system" , "unknown" )
227+
228+ completion_timestamp = span .end_time
229+
230+ completion_content_pattern = re .compile (r"^gen_ai\.completion\.(\d+)\.content$" )
231+
232+ for key , value in attributes .items ():
233+ match = completion_content_pattern .match (key )
234+ if not match :
235+ continue
236+
237+ index = match .group (1 )
238+ role_key = f"gen_ai.completion.{ index } .role"
239+ role = attributes .get (role_key , "unknown" )
240+
241+ event_attributes = {"gen_ai.system" : gen_ai_system , "original_attribute" : key }
242+
243+ body = {"content" : value , "role" : role }
244+
245+ event = None
246+ if role == "assistant" :
247+ event = self ._get_gen_ai_event (
248+ name = GEN_AI_ASSISTANT_MESSAGE ,
249+ span_ctx = span_ctx ,
250+ timestamp = completion_timestamp ,
251+ attributes = event_attributes ,
252+ body = body ,
253+ )
254+ else :
255+ event = self ._get_gen_ai_event (
256+ name = f"gen_ai.{ gen_ai_system } .message" ,
257+ span_ctx = span_ctx ,
258+ timestamp = completion_timestamp ,
259+ attributes = event_attributes ,
260+ body = body ,
261+ )
262+
263+ if event :
264+ events .append (event )
265+
266+ return events
267+
268+ def _extract_traceloop_events (self , span : ReadableSpan , attributes : Dict [str , Any ]) -> List [Event ]:
269+ events = []
270+ span_ctx = span .context
271+ gen_ai_system = span .attributes .get ("traceloop.entity.name" , "unknown" )
272+
273+ traceloop_attrs = [(TRACELOOP_ENTITY_INPUT , span .start_time ), (TRACELOOP_ENTITY_OUTPUT , span .end_time )]
274+
275+ for attr_key , timestamp in traceloop_attrs :
276+ if attr_key in attributes :
277+ event = self ._get_gen_ai_event (
278+ name = f"gen_ai.{ gen_ai_system } .message" ,
279+ span_ctx = span_ctx ,
280+ timestamp = timestamp ,
281+ attributes = {"gen_ai.system" : gen_ai_system , "original_attribute" : attr_key },
282+ body = {"content" : attributes [attr_key ]},
283+ )
284+ events .append (event )
285+
286+ return events
287+
288+ def _get_gen_ai_event (self , name , span_ctx , timestamp , attributes , body ):
233289 """
234290 Create and return a Gen AI Event with the provided parameters.
235291
@@ -250,5 +306,5 @@ def _get_gen_ai_event(
250306 body = body ,
251307 trace_id = span_ctx .trace_id ,
252308 span_id = span_ctx .span_id ,
253- trace_flags = span_ctx .trace_flags
309+ trace_flags = span_ctx .trace_flags ,
254310 )
0 commit comments