@@ -244,29 +244,47 @@ pub fn enrich_ai_event_data(
244244
245245/// Infer AI operation type mapping to a span.
246246///
247- /// This function maps span.op values to gen_ai.operation.type based on the provided
248- /// operation type map configuration.
247+ /// This function sets the gen_ai.operation.type attribute based on the value of either
248+ /// gen_ai.operation.name or span.op based on the provided operation type map configuration.
249249fn infer_ai_operation_type ( span : & mut Span , operation_type_map : & AiOperationTypeMap ) {
250250 let data = span. data . get_or_insert_with ( SpanData :: default) ;
251+ let op_type = data
252+ . gen_ai_operation_name
253+ . value ( )
254+ . or ( span. op . value ( ) )
255+ . and_then ( |op| operation_type_map. get_operation_type ( op) ) ;
251256
252- if let Some ( op) = span. op . value ( )
253- && let Some ( operation_type) = operation_type_map. get_operation_type ( op)
254- {
257+ if let Some ( operation_type) = op_type {
255258 data. gen_ai_operation_type
256259 . set_value ( Some ( operation_type. to_owned ( ) ) ) ;
257260 }
258261}
259262
260263/// Returns true if the span is an AI span.
261- /// AI spans are spans with op starting with "ai." (legacy) or "gen_ai." (new).
264+ /// AI spans are spans with either a gen_ai.operation.name attribute or op starting with "ai."
265+ /// (legacy) or "gen_ai." (new).
262266fn is_ai_span ( span : & Span ) -> bool {
263- span. op
267+ let has_ai_op = span
268+ . data
269+ . value ( )
270+ . and_then ( |data| data. gen_ai_operation_name . value ( ) )
271+ . is_some ( ) ;
272+
273+ let is_ai_span_op = span
274+ . op
264275 . value ( )
265- . is_some_and ( |op| op. starts_with ( "ai." ) || op. starts_with ( "gen_ai." ) )
276+ . is_some_and ( |op| op. starts_with ( "ai." ) || op. starts_with ( "gen_ai." ) ) ;
277+
278+ has_ai_op || is_ai_span_op
266279}
267280
268281#[ cfg( test) ]
269282mod tests {
283+ use std:: collections:: HashMap ;
284+
285+ use relay_pattern:: Pattern ;
286+ use relay_protocol:: get_value;
287+
270288 use super :: * ;
271289
272290 #[ test]
@@ -364,4 +382,132 @@ mod tests {
364382 }
365383 " ) ;
366384 }
385+
386+ /// Test that the AI operation type is inferred from a gen_ai.operation.name attribute.
387+ #[ test]
388+ fn test_infer_ai_operation_type_from_gen_ai_operation_name ( ) {
389+ let operation_types = HashMap :: from ( [
390+ ( Pattern :: new ( "*" ) . unwrap ( ) , "ai_client" . to_owned ( ) ) ,
391+ ( Pattern :: new ( "invoke_agent" ) . unwrap ( ) , "agent" . to_owned ( ) ) ,
392+ (
393+ Pattern :: new ( "gen_ai.invoke_agent" ) . unwrap ( ) ,
394+ "agent" . to_owned ( ) ,
395+ ) ,
396+ ] ) ;
397+
398+ let operation_type_map = AiOperationTypeMap {
399+ version : 1 ,
400+ operation_types,
401+ } ;
402+
403+ let span = r#"{
404+ "data": {
405+ "gen_ai.operation.name": "invoke_agent"
406+ }
407+ }"# ;
408+ let mut span = Annotated :: from_json ( span) . unwrap ( ) ;
409+ infer_ai_operation_type ( span. value_mut ( ) . as_mut ( ) . unwrap ( ) , & operation_type_map) ;
410+ assert_eq ! (
411+ get_value!( span. data. gen_ai_operation_type!) . as_str( ) ,
412+ "agent"
413+ ) ;
414+ }
415+
416+ /// Test that the AI operation type is inferred from a span.op attribute.
417+ #[ test]
418+ fn test_infer_ai_operation_type_from_span_op ( ) {
419+ let operation_types = HashMap :: from ( [
420+ ( Pattern :: new ( "*" ) . unwrap ( ) , "ai_client" . to_owned ( ) ) ,
421+ ( Pattern :: new ( "invoke_agent" ) . unwrap ( ) , "agent" . to_owned ( ) ) ,
422+ (
423+ Pattern :: new ( "gen_ai.invoke_agent" ) . unwrap ( ) ,
424+ "agent" . to_owned ( ) ,
425+ ) ,
426+ ] ) ;
427+ let operation_type_map = AiOperationTypeMap {
428+ version : 1 ,
429+ operation_types,
430+ } ;
431+
432+ let span = r#"{
433+ "op": "gen_ai.invoke_agent"
434+ }"# ;
435+ let mut span = Annotated :: from_json ( span) . unwrap ( ) ;
436+ infer_ai_operation_type ( span. value_mut ( ) . as_mut ( ) . unwrap ( ) , & operation_type_map) ;
437+ assert_eq ! (
438+ get_value!( span. data. gen_ai_operation_type!) . as_str( ) ,
439+ "agent"
440+ ) ;
441+ }
442+
443+ /// Test that the AI operation type is inferred from a fallback.
444+ #[ test]
445+ fn test_infer_ai_operation_type_from_fallback ( ) {
446+ let operation_types = HashMap :: from ( [
447+ ( Pattern :: new ( "*" ) . unwrap ( ) , "ai_client" . to_owned ( ) ) ,
448+ ( Pattern :: new ( "invoke_agent" ) . unwrap ( ) , "agent" . to_owned ( ) ) ,
449+ (
450+ Pattern :: new ( "gen_ai.invoke_agent" ) . unwrap ( ) ,
451+ "agent" . to_owned ( ) ,
452+ ) ,
453+ ] ) ;
454+
455+ let operation_type_map = AiOperationTypeMap {
456+ version : 1 ,
457+ operation_types,
458+ } ;
459+
460+ let span = r#"{
461+ "data": {
462+ "gen_ai.operation.name": "embeddings"
463+ }
464+ }"# ;
465+ let mut span = Annotated :: from_json ( span) . unwrap ( ) ;
466+ infer_ai_operation_type ( span. value_mut ( ) . as_mut ( ) . unwrap ( ) , & operation_type_map) ;
467+ assert_eq ! (
468+ get_value!( span. data. gen_ai_operation_type!) . as_str( ) ,
469+ "ai_client"
470+ ) ;
471+ }
472+
473+ /// Test that an AI span is detected from a gen_ai.operation.name attribute.
474+ #[ test]
475+ fn test_is_ai_span_from_gen_ai_operation_name ( ) {
476+ let span = r#"{
477+ "data": {
478+ "gen_ai.operation.name": "chat"
479+ }
480+ }"# ;
481+ let span: Span = Annotated :: from_json ( span) . unwrap ( ) . into_value ( ) . unwrap ( ) ;
482+ assert ! ( is_ai_span( & span) ) ;
483+ }
484+
485+ /// Test that an AI span is detected from a span.op starting with "ai.".
486+ #[ test]
487+ fn test_is_ai_span_from_span_op_ai ( ) {
488+ let span = r#"{
489+ "op": "ai.chat"
490+ }"# ;
491+ let span: Span = Annotated :: from_json ( span) . unwrap ( ) . into_value ( ) . unwrap ( ) ;
492+ assert ! ( is_ai_span( & span) ) ;
493+ }
494+
495+ /// Test that an AI span is detected from a span.op starting with "gen_ai.".
496+ #[ test]
497+ fn test_is_ai_span_from_span_op_gen_ai ( ) {
498+ let span = r#"{
499+ "op": "gen_ai.chat"
500+ }"# ;
501+ let span: Span = Annotated :: from_json ( span) . unwrap ( ) . into_value ( ) . unwrap ( ) ;
502+ assert ! ( is_ai_span( & span) ) ;
503+ }
504+
505+ /// Test that a non-AI span is detected.
506+ #[ test]
507+ fn test_is_ai_span_negative ( ) {
508+ let span = r#"{
509+ }"# ;
510+ let span: Span = Annotated :: from_json ( span) . unwrap ( ) . into_value ( ) . unwrap ( ) ;
511+ assert ! ( !is_ai_span( & span) ) ;
512+ }
367513}
0 commit comments