ioa-observe-sdk 1.0.19__py3-none-any.whl → 1.0.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,10 +16,10 @@ from ioa_observe.sdk.decorators.helpers import (
16
16
  _is_async_generator,
17
17
  )
18
18
 
19
-
20
19
  from langgraph.graph.state import CompiledStateGraph
21
20
  from opentelemetry import trace
22
21
  from opentelemetry import context as context_api
22
+ from opentelemetry.context import get_value, attach, set_value
23
23
  from pydantic_core import PydanticSerializationError
24
24
  from typing_extensions import ParamSpec
25
25
 
@@ -113,7 +113,49 @@ def _setup_span(
113
113
  with get_tracer() as tracer:
114
114
  span = tracer.start_span(span_name)
115
115
  ctx = trace.set_span_in_context(span)
116
+
117
+ # Preserve existing context values before attaching new context
118
+ session_id = get_value("session.id")
119
+ current_traceparent = get_value("current_traceparent")
120
+ agent_id = get_value("agent_id")
121
+ application_id_ctx = get_value("application_id")
122
+ association_properties = get_value("association_properties")
123
+ managed_prompt = get_value("managed_prompt")
124
+ prompt_key = get_value("prompt_key")
125
+ prompt_version = get_value("prompt_version")
126
+ prompt_version_name = get_value("prompt_version_name")
127
+ prompt_version_hash = get_value("prompt_version_hash")
128
+ prompt_template = get_value("prompt_template")
129
+ prompt_template_variables = get_value("prompt_template_variables")
130
+
116
131
  ctx_token = context_api.attach(ctx)
132
+
133
+ # Re-attach preserved context values to the new context
134
+ if session_id is not None:
135
+ attach(set_value("session.id", session_id))
136
+ if current_traceparent is not None:
137
+ attach(set_value("current_traceparent", current_traceparent))
138
+ if agent_id is not None:
139
+ attach(set_value("agent_id", agent_id))
140
+ if application_id_ctx is not None:
141
+ attach(set_value("application_id", application_id_ctx))
142
+ if association_properties is not None:
143
+ attach(set_value("association_properties", association_properties))
144
+ if managed_prompt is not None:
145
+ attach(set_value("managed_prompt", managed_prompt))
146
+ if prompt_key is not None:
147
+ attach(set_value("prompt_key", prompt_key))
148
+ if prompt_version is not None:
149
+ attach(set_value("prompt_version", prompt_version))
150
+ if prompt_version_name is not None:
151
+ attach(set_value("prompt_version_name", prompt_version_name))
152
+ if prompt_version_hash is not None:
153
+ attach(set_value("prompt_version_hash", prompt_version_hash))
154
+ if prompt_template is not None:
155
+ attach(set_value("prompt_template", prompt_template))
156
+ if prompt_template_variables is not None:
157
+ attach(set_value("prompt_template_variables", prompt_template_variables))
158
+
117
159
  if tlp_span_kind == ObserveSpanKindValues.AGENT:
118
160
  with trace.get_tracer(__name__).start_span(
119
161
  "agent_start_event", context=trace.set_span_in_context(span)
@@ -127,9 +169,6 @@ def _setup_span(
127
169
  },
128
170
  )
129
171
  # start_span.end() # end the span immediately
130
- # session_id = get_value("session.id")
131
- # if session_id is not None:
132
- # span.set_attribute("session.id", session_id)
133
172
  if tlp_span_kind in [
134
173
  ObserveSpanKindValues.TASK,
135
174
  ObserveSpanKindValues.TOOL,
@@ -16,7 +16,7 @@ from ioa_observe.sdk import TracerWrapper
16
16
  from ioa_observe.sdk.client import kv_store
17
17
  from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
18
18
 
19
- _instruments = ("slim-bindings >= 0.4",)
19
+ _instruments = ("slim-bindings >= 0.4.0",)
20
20
  _global_tracer = None
21
21
  _kv_lock = threading.RLock() # Add thread-safety for kv_store operations
22
22
 
@@ -39,7 +39,9 @@ class SLIMInstrumentor(BaseInstrumentor):
39
39
  )
40
40
 
41
41
  # Instrument `publish` method - handles multiple signatures
42
+ # In v0.6.0+, publish moved from Slim class to Session objects
42
43
  if hasattr(slim_bindings.Slim, "publish"):
44
+ # Legacy v0.5.x app-level publish method
43
45
  original_publish = slim_bindings.Slim.publish
44
46
 
45
47
  @functools.wraps(original_publish)
@@ -166,7 +168,7 @@ class SLIMInstrumentor(BaseInstrumentor):
166
168
 
167
169
  slim_bindings.Slim.publish_to = instrumented_publish_to
168
170
 
169
- # Instrument `request_reply` (new v0.4.0+ method)
171
+ # Instrument `request_reply` (v0.4.0+ to v0.5.x method, removed in v0.6.0)
170
172
  if hasattr(slim_bindings.Slim, "request_reply"):
171
173
  original_request_reply = slim_bindings.Slim.request_reply
172
174
 
@@ -290,35 +292,261 @@ class SLIMInstrumentor(BaseInstrumentor):
290
292
 
291
293
  slim_bindings.Slim.set_route = instrumented_set_route
292
294
 
293
- # Instrument `receive`
294
- original_receive = slim_bindings.Slim.receive
295
-
296
- @functools.wraps(original_receive)
297
- async def instrumented_receive(
298
- self, session=None, timeout=None, *args, **kwargs
299
- ):
300
- # Handle both old and new API patterns
301
- if session is not None or timeout is not None:
302
- # New API pattern with session parameter
303
- kwargs_with_params = kwargs.copy()
304
- if session is not None:
305
- kwargs_with_params["session"] = session
306
- if timeout is not None:
307
- kwargs_with_params["timeout"] = timeout
308
- recv_session, raw_message = await original_receive(
309
- self, **kwargs_with_params
310
- )
295
+ # Instrument `receive` - only if it exists (removed in v0.6.0)
296
+ if hasattr(slim_bindings.Slim, "receive"):
297
+ original_receive = slim_bindings.Slim.receive
298
+
299
+ @functools.wraps(original_receive)
300
+ async def instrumented_receive(
301
+ self, session=None, timeout=None, *args, **kwargs
302
+ ):
303
+ # Handle both old and new API patterns
304
+ if session is not None or timeout is not None:
305
+ # New API pattern with session parameter
306
+ kwargs_with_params = kwargs.copy()
307
+ if session is not None:
308
+ kwargs_with_params["session"] = session
309
+ if timeout is not None:
310
+ kwargs_with_params["timeout"] = timeout
311
+ recv_session, raw_message = await original_receive(
312
+ self, **kwargs_with_params
313
+ )
314
+ else:
315
+ # Legacy API pattern
316
+ recv_session, raw_message = await original_receive(
317
+ self, *args, **kwargs
318
+ )
319
+
320
+ if raw_message is None:
321
+ return recv_session, raw_message
322
+
323
+ try:
324
+ message_dict = json.loads(raw_message.decode())
325
+ headers = message_dict.get("headers", {})
326
+
327
+ # Extract traceparent and session info from headers
328
+ traceparent = headers.get("traceparent")
329
+ session_id = headers.get("session_id")
330
+
331
+ # Create carrier for context propagation
332
+ carrier = {}
333
+ for key in ["traceparent", "Traceparent", "baggage", "Baggage"]:
334
+ if key.lower() in [k.lower() for k in headers.keys()]:
335
+ for k in headers.keys():
336
+ if k.lower() == key.lower():
337
+ carrier[key.lower()] = headers[k]
338
+
339
+ # Restore trace context
340
+ if carrier and traceparent:
341
+ ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
342
+ ctx = W3CBaggagePropagator().extract(
343
+ carrier=carrier, context=ctx
344
+ )
345
+
346
+ # Activate the restored context
347
+ token = context.attach(ctx)
348
+
349
+ try:
350
+ # Set execution ID with the restored context
351
+ if session_id and session_id != "None":
352
+ set_session_id(session_id, traceparent=traceparent)
353
+
354
+ # Store in kv_store with thread safety
355
+ with _kv_lock:
356
+ kv_store.set(f"execution.{traceparent}", session_id)
357
+
358
+ # DON'T detach the context yet - we need it to persist for the callback
359
+ # The context will be cleaned up later or by the garbage collector
360
+
361
+ except Exception as e:
362
+ # Only detach on error
363
+ context.detach(token)
364
+ raise e
365
+ elif traceparent and session_id and session_id != "None":
366
+ # Even without carrier context, set session ID if we have the data
367
+ set_session_id(session_id, traceparent=traceparent)
368
+
369
+ # Fallback: check stored execution ID if not found in headers
370
+ if traceparent and (not session_id or session_id == "None"):
371
+ with _kv_lock:
372
+ stored_session_id = kv_store.get(f"execution.{traceparent}")
373
+ if stored_session_id:
374
+ session_id = stored_session_id
375
+ set_session_id(session_id, traceparent=traceparent)
376
+
377
+ # Process and clean the message
378
+ message_to_return = message_dict.copy()
379
+ if "headers" in message_to_return:
380
+ headers_copy = message_to_return["headers"].copy()
381
+ # Remove tracing-specific headers but keep other headers
382
+ headers_copy.pop("traceparent", None)
383
+ headers_copy.pop("session_id", None)
384
+ headers_copy.pop("slim_session_id", None)
385
+ if headers_copy:
386
+ message_to_return["headers"] = headers_copy
387
+ else:
388
+ message_to_return.pop("headers", None)
389
+
390
+ # Return processed message
391
+ if len(message_to_return) == 1 and "payload" in message_to_return:
392
+ payload = message_to_return["payload"]
393
+ if isinstance(payload, str):
394
+ try:
395
+ payload_dict = json.loads(payload)
396
+ return recv_session, json.dumps(payload_dict).encode(
397
+ "utf-8"
398
+ )
399
+ except json.JSONDecodeError:
400
+ return recv_session, payload.encode(
401
+ "utf-8"
402
+ ) if isinstance(payload, str) else payload
403
+ return recv_session, json.dumps(payload).encode(
404
+ "utf-8"
405
+ ) if isinstance(payload, (dict, list)) else payload
406
+ else:
407
+ return recv_session, json.dumps(message_to_return).encode(
408
+ "utf-8"
409
+ )
410
+
411
+ except Exception as e:
412
+ print(f"Error processing message: {e}")
413
+ return recv_session, raw_message
414
+
415
+ slim_bindings.Slim.receive = instrumented_receive
416
+
417
+ # Instrument `connect` - only if it exists
418
+ if hasattr(slim_bindings.Slim, "connect"):
419
+ original_connect = slim_bindings.Slim.connect
420
+
421
+ @functools.wraps(original_connect)
422
+ async def instrumented_connect(self, *args, **kwargs):
423
+ if _global_tracer:
424
+ with _global_tracer.start_as_current_span("slim.connect"):
425
+ return await original_connect(self, *args, **kwargs)
426
+ else:
427
+ return await original_connect(self, *args, **kwargs)
428
+
429
+ slim_bindings.Slim.connect = instrumented_connect
430
+
431
+ # Instrument `create_session` (new v0.4.0+ method)
432
+ if hasattr(slim_bindings.Slim, "create_session"):
433
+ original_create_session = slim_bindings.Slim.create_session
434
+
435
+ @functools.wraps(original_create_session)
436
+ async def instrumented_create_session(self, config, *args, **kwargs):
437
+ if _global_tracer:
438
+ with _global_tracer.start_as_current_span(
439
+ "slim.create_session"
440
+ ) as span:
441
+ session_info = await original_create_session(
442
+ self, config, *args, **kwargs
443
+ )
444
+
445
+ # Add session attributes to span
446
+ if hasattr(session_info, "id"):
447
+ span.set_attribute("slim.session.id", str(session_info.id))
448
+
449
+ return session_info
450
+ else:
451
+ return await original_create_session(self, config, *args, **kwargs)
452
+
453
+ slim_bindings.Slim.create_session = instrumented_create_session
454
+
455
+ # Instrument new v0.6.0+ session-level methods
456
+ # These methods are available on Session objects, not the Slim app
457
+ self._instrument_session_methods(slim_bindings)
458
+
459
+ # Instrument new v0.6.0+ app-level methods
460
+ # listen_for_session replaces app.receive() for new sessions in v0.6.0+
461
+ if hasattr(slim_bindings.Slim, "listen_for_session"):
462
+ original_listen_for_session = slim_bindings.Slim.listen_for_session
463
+
464
+ @functools.wraps(original_listen_for_session)
465
+ async def instrumented_listen_for_session(self, *args, **kwargs):
466
+ if _global_tracer:
467
+ with _global_tracer.start_as_current_span(
468
+ "slim.listen_for_session"
469
+ ):
470
+ session = await original_listen_for_session(
471
+ self, *args, **kwargs
472
+ )
473
+
474
+ return session
475
+ else:
476
+ return await original_listen_for_session(self, *args, **kwargs)
477
+
478
+ slim_bindings.Slim.listen_for_session = instrumented_listen_for_session
479
+
480
+ def _instrument_session_methods(self, slim_bindings):
481
+ # In v0.6.0+, we need to instrument session classes dynamically
482
+ # Try to find session-related classes in the slim_bindings module
483
+ session_classes = []
484
+
485
+ # Look for common session class names
486
+ for attr_name in ["Session", "P2PSession", "GroupSession"]:
487
+ if hasattr(slim_bindings, attr_name):
488
+ session_class = getattr(slim_bindings, attr_name)
489
+ session_classes.append((attr_name, session_class))
490
+
491
+ # Also look for any class that has session-like methods
492
+ for attr_name in dir(slim_bindings):
493
+ attr = getattr(slim_bindings, attr_name)
494
+ if isinstance(attr, type) and (
495
+ hasattr(attr, "get_message") or hasattr(attr, "publish")
496
+ ):
497
+ session_classes.append((attr_name, attr))
498
+
499
+ # Instrument session methods for found classes
500
+ for _, session_class in session_classes:
501
+ # Instrument get_message (v0.6.0+ replacement for receive)
502
+ if hasattr(session_class, "get_message"):
503
+ self._instrument_session_get_message(session_class)
504
+
505
+ # Instrument session publish methods
506
+ if hasattr(session_class, "publish"):
507
+ self._instrument_session_publish(session_class, "publish")
508
+
509
+ if hasattr(session_class, "publish_to"):
510
+ self._instrument_session_publish(session_class, "publish_to")
511
+
512
+ def _instrument_session_get_message(self, session_class):
513
+ """Instrument session.get_message method (v0.6.0+)"""
514
+ original_get_message = session_class.get_message
515
+
516
+ @functools.wraps(original_get_message)
517
+ async def instrumented_get_message(self, timeout=None, *args, **kwargs):
518
+ # Handle the message reception similar to the old receive method
519
+ if timeout is not None:
520
+ kwargs["timeout"] = timeout
521
+
522
+ result = await original_get_message(self, **kwargs)
523
+
524
+ # Handle different return types from get_message
525
+ if result is None:
526
+ return result
527
+
528
+ # Check if get_message returns a tuple (context, message) or just message
529
+ if isinstance(result, tuple) and len(result) == 2:
530
+ message_context, raw_message = result
311
531
  else:
312
- # Legacy API pattern
313
- recv_session, raw_message = await original_receive(
314
- self, *args, **kwargs
315
- )
532
+ raw_message = result
533
+ message_context = None
316
534
 
317
535
  if raw_message is None:
318
- return recv_session, raw_message
536
+ return result
319
537
 
320
538
  try:
321
- message_dict = json.loads(raw_message.decode())
539
+ # Handle different message types
540
+ if isinstance(raw_message, bytes):
541
+ message_dict = json.loads(raw_message.decode())
542
+ elif isinstance(raw_message, str):
543
+ message_dict = json.loads(raw_message)
544
+ elif isinstance(raw_message, dict):
545
+ message_dict = raw_message
546
+ else:
547
+ # Unknown type, return as-is
548
+ return result
549
+
322
550
  headers = message_dict.get("headers", {})
323
551
 
324
552
  # Extract traceparent and session info from headers
@@ -349,8 +577,16 @@ class SLIMInstrumentor(BaseInstrumentor):
349
577
  # Store in kv_store with thread safety
350
578
  with _kv_lock:
351
579
  kv_store.set(f"execution.{traceparent}", session_id)
352
- finally:
580
+
581
+ # DON'T detach the context yet - we need it to persist for the callback
582
+
583
+ except Exception as e:
584
+ # Only detach on error
353
585
  context.detach(token)
586
+ raise e
587
+ elif traceparent and session_id and session_id != "None":
588
+ # Even without carrier context, set session ID if we have the data
589
+ set_session_id(session_id, traceparent=traceparent)
354
590
 
355
591
  # Fallback: check stored execution ID if not found in headers
356
592
  if traceparent and (not session_id or session_id == "None"):
@@ -373,68 +609,257 @@ class SLIMInstrumentor(BaseInstrumentor):
373
609
  else:
374
610
  message_to_return.pop("headers", None)
375
611
 
376
- # Return processed message
612
+ # Return processed message, maintaining original return format
377
613
  if len(message_to_return) == 1 and "payload" in message_to_return:
378
614
  payload = message_to_return["payload"]
379
615
  if isinstance(payload, str):
380
616
  try:
381
617
  payload_dict = json.loads(payload)
382
- return recv_session, json.dumps(payload_dict).encode(
383
- "utf-8"
384
- )
618
+ processed_message = json.dumps(payload_dict).encode("utf-8")
385
619
  except json.JSONDecodeError:
386
- return recv_session, payload.encode("utf-8") if isinstance(
387
- payload, str
388
- ) else payload
389
- return recv_session, json.dumps(payload).encode(
390
- "utf-8"
391
- ) if isinstance(payload, (dict, list)) else payload
620
+ processed_message = (
621
+ payload.encode("utf-8")
622
+ if isinstance(payload, str)
623
+ else payload
624
+ )
625
+ else:
626
+ processed_message = (
627
+ json.dumps(payload).encode("utf-8")
628
+ if isinstance(payload, (dict, list))
629
+ else payload
630
+ )
631
+ else:
632
+ processed_message = json.dumps(message_to_return).encode("utf-8")
633
+
634
+ # Return in the same format as received
635
+ if isinstance(result, tuple) and len(result) == 2:
636
+ return (message_context, processed_message)
392
637
  else:
393
- return recv_session, json.dumps(message_to_return).encode("utf-8")
638
+ return processed_message
394
639
 
395
640
  except Exception as e:
396
641
  print(f"Error processing message: {e}")
397
- return recv_session, raw_message
642
+ return result
398
643
 
399
- slim_bindings.Slim.receive = instrumented_receive
644
+ session_class.get_message = instrumented_get_message
400
645
 
401
- # Instrument `connect`
402
- original_connect = slim_bindings.Slim.connect
646
+ def _instrument_session_publish(self, session_class, method_name):
647
+ """Instrument session publish methods"""
648
+ original_method = getattr(session_class, method_name)
403
649
 
404
- @functools.wraps(original_connect)
405
- async def instrumented_connect(self, *args, **kwargs):
650
+ @functools.wraps(original_method)
651
+ async def instrumented_session_publish(self, *args, **kwargs):
406
652
  if _global_tracer:
407
- with _global_tracer.start_as_current_span("slim.connect"):
408
- return await original_connect(self, *args, **kwargs)
653
+ with _global_tracer.start_as_current_span(
654
+ f"session.{method_name}"
655
+ ) as span:
656
+ traceparent = get_current_traceparent()
657
+
658
+ # Add session context to span
659
+ if hasattr(self, "id"):
660
+ span.set_attribute("slim.session.id", str(self.id))
661
+
662
+ # Handle message wrapping
663
+ if args:
664
+ # Thread-safe access to kv_store
665
+ session_id = None
666
+ if traceparent:
667
+ with _kv_lock:
668
+ session_id = kv_store.get(f"execution.{traceparent}")
669
+ if session_id:
670
+ kv_store.set(f"execution.{traceparent}", session_id)
671
+
672
+ headers = {
673
+ "session_id": session_id if session_id else None,
674
+ "traceparent": traceparent,
675
+ "slim_session_id": str(self.id)
676
+ if hasattr(self, "id")
677
+ else None,
678
+ }
679
+
680
+ # Set baggage context
681
+ if traceparent and session_id:
682
+ baggage.set_baggage(f"execution.{traceparent}", session_id)
683
+
684
+ # Wrap the message (first argument for publish, second for publish_to)
685
+ message_idx = 1 if method_name == "publish_to" else 0
686
+ if len(args) > message_idx:
687
+ args_list = list(args)
688
+ message = args_list[message_idx]
689
+ wrapped_message = (
690
+ SLIMInstrumentor._wrap_message_with_headers(
691
+ None, message, headers
692
+ )
693
+ )
694
+
695
+ # Convert wrapped message back to bytes if needed
696
+ if isinstance(wrapped_message, dict):
697
+ message_to_send = json.dumps(wrapped_message).encode(
698
+ "utf-8"
699
+ )
700
+ else:
701
+ message_to_send = wrapped_message
702
+
703
+ args_list[message_idx] = message_to_send
704
+ args = tuple(args_list)
705
+
706
+ return await original_method(self, *args, **kwargs)
409
707
  else:
410
- return await original_connect(self, *args, **kwargs)
708
+ # Handle message wrapping even without tracing
709
+ if args:
710
+ traceparent = get_current_traceparent()
711
+ session_id = None
712
+ if traceparent:
713
+ with _kv_lock:
714
+ session_id = kv_store.get(f"execution.{traceparent}")
715
+
716
+ if traceparent or session_id:
717
+ headers = {
718
+ "session_id": session_id if session_id else None,
719
+ "traceparent": traceparent,
720
+ "slim_session_id": str(self.id)
721
+ if hasattr(self, "id")
722
+ else None,
723
+ }
411
724
 
412
- slim_bindings.Slim.connect = instrumented_connect
725
+ # Wrap the message (first argument for publish, second for publish_to)
726
+ message_idx = 1 if method_name == "publish_to" else 0
727
+ if len(args) > message_idx:
728
+ args_list = list(args)
729
+ message = args_list[message_idx]
730
+ wrapped_message = (
731
+ SLIMInstrumentor._wrap_message_with_headers(
732
+ None, message, headers
733
+ )
734
+ )
413
735
 
414
- # Instrument `create_session` (new v0.4.0+ method)
415
- if hasattr(slim_bindings.Slim, "create_session"):
416
- original_create_session = slim_bindings.Slim.create_session
736
+ if isinstance(wrapped_message, dict):
737
+ message_to_send = json.dumps(wrapped_message).encode(
738
+ "utf-8"
739
+ )
740
+ else:
741
+ message_to_send = wrapped_message
417
742
 
418
- @functools.wraps(original_create_session)
419
- async def instrumented_create_session(self, config, *args, **kwargs):
420
- if _global_tracer:
421
- with _global_tracer.start_as_current_span(
422
- "slim.create_session"
423
- ) as span:
424
- session_info = await original_create_session(
425
- self, config, *args, **kwargs
426
- )
743
+ args_list[message_idx] = message_to_send
744
+ args = tuple(args_list)
427
745
 
428
- # Add session attributes to span
429
- if hasattr(session_info, "id"):
430
- span.set_attribute("slim.session.id", str(session_info.id))
746
+ return await original_method(self, *args, **kwargs)
431
747
 
432
- return session_info
433
- else:
434
- return await original_create_session(self, config, *args, **kwargs)
748
+ setattr(session_class, method_name, instrumented_session_publish)
435
749
 
436
- slim_bindings.Slim.create_session = instrumented_create_session
750
+ def _instrument_session_method_if_exists(self, slim_bindings, method_name):
751
+ """Helper to instrument a session method if it exists"""
752
+
753
+ # Look for session classes that might have this method
754
+ for attr_name in dir(slim_bindings):
755
+ attr = getattr(slim_bindings, attr_name)
756
+ if hasattr(attr, method_name):
757
+ original_method = getattr(attr, method_name)
758
+
759
+ if callable(original_method):
760
+ instrumented_method = self._create_session_method_wrapper(
761
+ method_name, original_method
762
+ )
763
+ setattr(attr, method_name, instrumented_method)
764
+
765
+ def _create_session_method_wrapper(self, method_name, original_method):
766
+ """Create an instrumented wrapper for session methods"""
767
+
768
+ @functools.wraps(original_method)
769
+ async def instrumented_session_method(self, *args, **kwargs):
770
+ if _global_tracer:
771
+ with _global_tracer.start_as_current_span(f"session.{method_name}"):
772
+ traceparent = get_current_traceparent()
773
+
774
+ # Handle message wrapping for publish methods
775
+ if method_name in ["publish", "publish_to"] and args:
776
+ # Thread-safe access to kv_store
777
+ session_id = None
778
+ if traceparent:
779
+ with _kv_lock:
780
+ session_id = kv_store.get(f"execution.{traceparent}")
781
+ if session_id:
782
+ kv_store.set(f"execution.{traceparent}", session_id)
783
+
784
+ headers = {
785
+ "session_id": session_id if session_id else None,
786
+ "traceparent": traceparent,
787
+ "slim_session_id": str(self.id)
788
+ if hasattr(self, "id")
789
+ else None,
790
+ }
791
+
792
+ # Set baggage context
793
+ if traceparent and session_id:
794
+ baggage.set_baggage(f"execution.{traceparent}", session_id)
795
+
796
+ # Wrap the message (first argument for publish, second for publish_to)
797
+ message_idx = 1 if method_name == "publish_to" else 0
798
+ if len(args) > message_idx:
799
+ args_list = list(args)
800
+ message = args_list[message_idx]
801
+ wrapped_message = SLIMInstrumentor._wrap_message_with_headers(
802
+ None,
803
+ message,
804
+ headers, # Pass None for self since it's a static method
805
+ )
806
+
807
+ # Convert wrapped message back to bytes if needed
808
+ if isinstance(wrapped_message, dict):
809
+ message_to_send = json.dumps(wrapped_message).encode(
810
+ "utf-8"
811
+ )
812
+ else:
813
+ message_to_send = wrapped_message
814
+
815
+ args_list[message_idx] = message_to_send
816
+ args = tuple(args_list)
437
817
 
818
+ return await original_method(self, *args, **kwargs)
819
+ else:
820
+ # Handle message wrapping even without tracing
821
+ if method_name in ["publish", "publish_to"] and args:
822
+ traceparent = get_current_traceparent()
823
+ session_id = None
824
+ if traceparent:
825
+ with _kv_lock:
826
+ session_id = kv_store.get(f"execution.{traceparent}")
827
+
828
+ if traceparent or session_id:
829
+ headers = {
830
+ "session_id": session_id if session_id else None,
831
+ "traceparent": traceparent,
832
+ "slim_session_id": str(self.id)
833
+ if hasattr(self, "id")
834
+ else None,
835
+ }
836
+
837
+ # Wrap the message
838
+ message_idx = 1 if method_name == "publish_to" else 0
839
+ if len(args) > message_idx:
840
+ args_list = list(args)
841
+ message = args_list[message_idx]
842
+ wrapped_message = (
843
+ SLIMInstrumentor._wrap_message_with_headers(
844
+ None, message, headers
845
+ )
846
+ )
847
+
848
+ if isinstance(wrapped_message, dict):
849
+ message_to_send = json.dumps(wrapped_message).encode(
850
+ "utf-8"
851
+ )
852
+ else:
853
+ message_to_send = wrapped_message
854
+
855
+ args_list[message_idx] = message_to_send
856
+ args = tuple(args_list)
857
+
858
+ return await original_method(self, *args, **kwargs)
859
+
860
+ return instrumented_session_method
861
+
862
+ @staticmethod
438
863
  def _wrap_message_with_headers(self, message, headers):
439
864
  """Helper method to wrap messages with headers consistently"""
440
865
  if isinstance(message, bytes):
@@ -497,12 +922,13 @@ class SLIMInstrumentor(BaseInstrumentor):
497
922
  methods_to_restore = [
498
923
  "publish",
499
924
  "publish_to",
500
- "request_reply",
925
+ "request_reply", # v0.4.0-v0.5.x only
501
926
  "receive",
502
927
  "connect",
503
928
  "create_session",
504
929
  "invite",
505
930
  "set_route",
931
+ "listen_for_session", # v0.6.0+
506
932
  ]
507
933
 
508
934
  for method_name in methods_to_restore:
@@ -512,3 +938,21 @@ class SLIMInstrumentor(BaseInstrumentor):
512
938
  setattr(
513
939
  slim_bindings.Slim, method_name, original_method.__wrapped__
514
940
  )
941
+
942
+ # Also try to restore session-level methods (v0.6.0+)
943
+ # This is best-effort since session classes may vary
944
+ session_methods_to_restore = [
945
+ "publish",
946
+ "publish_to",
947
+ "get_message",
948
+ "invite",
949
+ "remove",
950
+ ]
951
+
952
+ for attr_name in dir(slim_bindings):
953
+ attr = getattr(slim_bindings, attr_name)
954
+ for method_name in session_methods_to_restore:
955
+ if hasattr(attr, method_name):
956
+ original_method = getattr(attr, method_name)
957
+ if hasattr(original_method, "__wrapped__"):
958
+ setattr(attr, method_name, original_method.__wrapped__)
@@ -1,20 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ioa-observe-sdk
3
- Version: 1.0.19
3
+ Version: 1.0.21
4
4
  Summary: IOA Observability SDK
5
+ License-Expression: Apache-2.0
5
6
  Requires-Python: >=3.10
6
7
  Description-Content-Type: text/markdown
7
8
  License-File: LICENSE.md
8
9
  Requires-Dist: colorama==0.4.6
9
10
  Requires-Dist: requests>=2.32.3
10
- Requires-Dist: opentelemetry-api==1.33.1
11
+ Requires-Dist: opentelemetry-api==1.37.0
11
12
  Requires-Dist: opentelemetry-distro
12
- Requires-Dist: opentelemetry-exporter-otlp==1.33.1
13
- Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.33.1
14
- Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.33.1
15
- Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.33.1
13
+ Requires-Dist: opentelemetry-exporter-otlp==1.37.0
14
+ Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.37.0
15
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.37.0
16
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.37.0
16
17
  Requires-Dist: opentelemetry-instrumentation
17
- Requires-Dist: opentelemetry-instrumentation-logging==0.54b1
18
+ Requires-Dist: opentelemetry-instrumentation-logging==0.58b0
18
19
  Requires-Dist: opentelemetry-instrumentation-openai==0.43.1
19
20
  Requires-Dist: opentelemetry-instrumentation-llamaindex==0.43.1
20
21
  Requires-Dist: opentelemetry-instrumentation-ollama==0.43.1
@@ -26,18 +27,18 @@ Requires-Dist: opentelemetry-instrumentation-crewai==0.43.1
26
27
  Requires-Dist: opentelemetry-instrumentation-google-generativeai==0.43.1
27
28
  Requires-Dist: opentelemetry-instrumentation-groq==0.43.1
28
29
  Requires-Dist: opentelemetry-instrumentation-mistralai==0.43.1
29
- Requires-Dist: opentelemetry-instrumentation-requests==0.54b1
30
+ Requires-Dist: opentelemetry-instrumentation-requests==0.58b0
30
31
  Requires-Dist: opentelemetry-instrumentation-sagemaker==0.43.1
31
- Requires-Dist: opentelemetry-instrumentation-threading==0.54b1
32
+ Requires-Dist: opentelemetry-instrumentation-threading==0.58b0
32
33
  Requires-Dist: opentelemetry-instrumentation-together==0.43.1
33
34
  Requires-Dist: opentelemetry-instrumentation-transformers==0.43.1
34
- Requires-Dist: opentelemetry-instrumentation-urllib3==0.54b1
35
+ Requires-Dist: opentelemetry-instrumentation-urllib3==0.58b0
35
36
  Requires-Dist: opentelemetry-instrumentation-vertexai==0.43.1
36
- Requires-Dist: opentelemetry-proto==1.33.1
37
- Requires-Dist: opentelemetry-sdk==1.33.1
38
- Requires-Dist: opentelemetry-semantic-conventions==0.54b1
37
+ Requires-Dist: opentelemetry-proto==1.37.0
38
+ Requires-Dist: opentelemetry-sdk==1.37.0
39
+ Requires-Dist: opentelemetry-semantic-conventions==0.58b0
39
40
  Requires-Dist: opentelemetry-semantic-conventions-ai>=0.4.11
40
- Requires-Dist: opentelemetry-util-http==0.54b1
41
+ Requires-Dist: opentelemetry-util-http==0.58b0
41
42
  Requires-Dist: langgraph>=0.3.2
42
43
  Requires-Dist: langchain>=0.3.19
43
44
  Requires-Dist: langchain-openai>=0.3.8
@@ -10,13 +10,13 @@ ioa_observe/sdk/config/__init__.py,sha256=8aVNaw0yRNLFPxlf97iOZLlJVcV81ivSDnudH2
10
10
  ioa_observe/sdk/connectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  ioa_observe/sdk/connectors/slim.py,sha256=NwbKEV7d5NIOqmG8zKqtgGigSJl7kf3QJ65z2gxpsY8,8498
12
12
  ioa_observe/sdk/decorators/__init__.py,sha256=qCpJAv98eLKs3I5EMXJVTV0s49Nc6QDSOHNh5rW5vLg,4268
13
- ioa_observe/sdk/decorators/base.py,sha256=eYep1xHXscdxIGisXgXwSGbkCrdpqqT_2s5BMP07ldg,30646
13
+ ioa_observe/sdk/decorators/base.py,sha256=pnoj73UrpvaZmOPbcpGRRYCwg7LBAgO8PiBDh5ntn0c,32694
14
14
  ioa_observe/sdk/decorators/helpers.py,sha256=I9HXMBivkZpGDtPe9Ad_UU35p_m_wEPate4r_fU0oOA,2705
15
15
  ioa_observe/sdk/decorators/util.py,sha256=IebvH9gwZN1en3LblYJUh4bAV2STl6xmp8WpZzBDH2g,30068
16
16
  ioa_observe/sdk/instrumentations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  ioa_observe/sdk/instrumentations/a2a.py,sha256=ZpqvPl4u-yheQzSdBfxnZhWFZ8ntbKni_uaW3IDyjqw,6309
18
18
  ioa_observe/sdk/instrumentations/mcp.py,sha256=vRM3ofnn7AMmry2RrfyZnZVPEutLWiDMghx2TSnm0Wk,18569
19
- ioa_observe/sdk/instrumentations/slim.py,sha256=UM_XCuzetOzeDFQ-bFY0Q93OLffH7OsvbgNaS58E0RA,22632
19
+ ioa_observe/sdk/instrumentations/slim.py,sha256=r_vIuYUo-IhSRuYWymqfS26swDZ8_CFDH6p_QbduLKU,43757
20
20
  ioa_observe/sdk/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  ioa_observe/sdk/logging/logging.py,sha256=HZxW9s8Due7jgiNkdI38cIjv5rC9D-Flta3RQMOnpow,2891
22
22
  ioa_observe/sdk/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -41,8 +41,8 @@ ioa_observe/sdk/utils/const.py,sha256=d67dUTAH9UpWvUV9GLBUqn1Sc2knJ55dy-e6YoLrvS
41
41
  ioa_observe/sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
42
42
  ioa_observe/sdk/utils/json_encoder.py,sha256=g4NQ0tTqgWssY6I1D7r4zo0G6PiUo61jhofTAw5-jno,639
43
43
  ioa_observe/sdk/utils/package_check.py,sha256=1d1MjxhwoEZIx9dumirT2pRsEWgn-m-SI4npDeEalew,576
44
- ioa_observe_sdk-1.0.19.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
45
- ioa_observe_sdk-1.0.19.dist-info/METADATA,sha256=ynlTiy3cP00dX-e_PjBgTP3wB8FoT20ztUbOLeSKQEg,7027
46
- ioa_observe_sdk-1.0.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- ioa_observe_sdk-1.0.19.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
48
- ioa_observe_sdk-1.0.19.dist-info/RECORD,,
44
+ ioa_observe_sdk-1.0.21.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
45
+ ioa_observe_sdk-1.0.21.dist-info/METADATA,sha256=M6yTeHdBIWvp72-hLTpv8AztbhAxUwA8hVoduyRqxHU,7058
46
+ ioa_observe_sdk-1.0.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ ioa_observe_sdk-1.0.21.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
48
+ ioa_observe_sdk-1.0.21.dist-info/RECORD,,