ioa-observe-sdk 1.0.6__py3-none-any.whl → 1.0.8__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.
@@ -19,15 +19,22 @@ F = TypeVar("F", bound=Callable[P, Union[R, Awaitable[R]]])
19
19
 
20
20
  def task(
21
21
  name: Optional[str] = None,
22
+ description: Optional[str] = None,
22
23
  version: Optional[int] = None,
23
24
  method_name: Optional[str] = None,
24
25
  tlp_span_kind: Optional[ObserveSpanKindValues] = ObserveSpanKindValues.TASK,
25
26
  ) -> Callable[[F], F]:
26
27
  if method_name is None:
27
- return entity_method(name=name, version=version, tlp_span_kind=tlp_span_kind)
28
+ return entity_method(
29
+ name=name,
30
+ description=description,
31
+ version=version,
32
+ tlp_span_kind=tlp_span_kind,
33
+ )
28
34
  else:
29
35
  return entity_class(
30
36
  name=name,
37
+ description=description,
31
38
  version=version,
32
39
  method_name=method_name,
33
40
  tlp_span_kind=tlp_span_kind,
@@ -36,21 +43,21 @@ def task(
36
43
 
37
44
  def workflow(
38
45
  name: Optional[str] = None,
46
+ description: Optional[str] = None,
39
47
  version: Optional[int] = None,
40
48
  method_name: Optional[str] = None,
41
49
  tlp_span_kind: Optional[
42
50
  Union[ObserveSpanKindValues, str]
43
51
  ] = ObserveSpanKindValues.WORKFLOW,
44
52
  ) -> Callable[[F], F]:
45
- if method_name is None:
46
- return entity_method(name=name, version=version, tlp_span_kind=tlp_span_kind)
47
- else:
48
- return entity_class(
49
- name=name,
50
- version=version,
51
- method_name=method_name,
52
- tlp_span_kind=tlp_span_kind,
53
- )
53
+ # Always use entity_class for class decorators
54
+ return entity_class(
55
+ name=name,
56
+ description=description,
57
+ version=version,
58
+ method_name=method_name,
59
+ tlp_span_kind=tlp_span_kind,
60
+ )
54
61
 
55
62
 
56
63
  def graph(
@@ -60,6 +67,7 @@ def graph(
60
67
  ) -> Callable[[F], F]:
61
68
  return workflow(
62
69
  name=name,
70
+ description=None,
63
71
  version=version,
64
72
  method_name=method_name,
65
73
  tlp_span_kind="graph",
@@ -68,11 +76,13 @@ def graph(
68
76
 
69
77
  def agent(
70
78
  name: Optional[str] = None,
79
+ description: Optional[str] = None,
71
80
  version: Optional[int] = None,
72
81
  method_name: Optional[str] = None,
73
82
  ) -> Callable[[F], F]:
74
83
  return workflow(
75
84
  name=name,
85
+ description=description,
76
86
  version=version,
77
87
  method_name=method_name,
78
88
  tlp_span_kind=ObserveSpanKindValues.AGENT,
@@ -81,11 +91,13 @@ def agent(
81
91
 
82
92
  def tool(
83
93
  name: Optional[str] = None,
94
+ description: Optional[str] = None,
84
95
  version: Optional[int] = None,
85
96
  method_name: Optional[str] = None,
86
97
  ) -> Callable[[F], F]:
87
98
  return task(
88
99
  name=name,
100
+ description=description,
89
101
  version=version,
90
102
  method_name=method_name,
91
103
  tlp_span_kind=ObserveSpanKindValues.TOOL,
@@ -2,6 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import json
5
+ import time
5
6
  import traceback
6
7
  from functools import wraps
7
8
  import os
@@ -91,6 +92,7 @@ def _setup_span(
91
92
  entity_name,
92
93
  tlp_span_kind: Optional[ObserveSpanKindValues] = None,
93
94
  version: Optional[int] = None,
95
+ description: Optional[str] = None,
94
96
  ):
95
97
  """Sets up the OpenTelemetry span and context"""
96
98
  if tlp_span_kind in [
@@ -100,8 +102,8 @@ def _setup_span(
100
102
  ]:
101
103
  set_workflow_name(entity_name)
102
104
  # if tlp_span_kind == "graph":
103
- # execution_id = entity_name + "_" + str(uuid.uuid4())
104
- # set_execution_id(execution_id)
105
+ # session_id = entity_name + "_" + str(uuid.uuid4())
106
+ # set_session_id(session_id)
105
107
  if tlp_span_kind == "graph":
106
108
  span_name = f"{entity_name}.{tlp_span_kind}"
107
109
  else:
@@ -122,13 +124,14 @@ def _setup_span(
122
124
  "agent_start_event",
123
125
  {
124
126
  "agent_name": entity_name,
127
+ "description": description if description else "",
125
128
  "type": tlp_span_kind.value,
126
129
  },
127
130
  )
128
131
  # start_span.end() # end the span immediately
129
- # execution_id = get_value("execution.id")
130
- # if execution_id is not None:
131
- # span.set_attribute("execution.id", execution_id)
132
+ # session_id = get_value("session.id")
133
+ # if session_id is not None:
134
+ # span.set_attribute("session.id", session_id)
132
135
  if tlp_span_kind in [
133
136
  ObserveSpanKindValues.TASK,
134
137
  ObserveSpanKindValues.TOOL,
@@ -145,9 +148,8 @@ def _setup_span(
145
148
  if version:
146
149
  span.set_attribute(OBSERVE_ENTITY_VERSION, version)
147
150
 
148
- # if execution_id:
149
- # print(f"Execution ID: {execution_id}")
150
- # span.set_attribute("execution.id", execution_id)
151
+ if tlp_span_kind == ObserveSpanKindValues.AGENT:
152
+ span.set_attribute("agent_chain_start_time", time.time())
151
153
 
152
154
  return span, ctx, ctx_token
153
155
 
@@ -243,12 +245,27 @@ def _handle_span_output(span, tlp_span_kind, res, cls=None):
243
245
 
244
246
  def _cleanup_span(span, ctx_token):
245
247
  """End the span process and detach the context token"""
248
+
249
+ # Calculate agent chain completion time before ending span
250
+ span_kind = span.attributes.get(OBSERVE_SPAN_KIND)
251
+ if span_kind == ObserveSpanKindValues.AGENT.value:
252
+ start_time = span.attributes.get("agent_chain_start_time")
253
+ if start_time is not None:
254
+ import time
255
+
256
+ completion_time = time.time() - start_time
257
+
258
+ # Emit the metric
259
+ TracerWrapper().agent_chain_completion_time_histogram.record(
260
+ completion_time, attributes=span.attributes
261
+ )
246
262
  span.end()
247
263
  context_api.detach(ctx_token)
248
264
 
249
265
 
250
266
  def entity_method(
251
267
  name: Optional[str] = None,
268
+ description: Optional[str] = None,
252
269
  version: Optional[int] = None,
253
270
  tlp_span_kind: Optional[ObserveSpanKindValues] = ObserveSpanKindValues.TASK,
254
271
  ) -> Callable[[F], F]:
@@ -266,7 +283,10 @@ def entity_method(
266
283
  return
267
284
 
268
285
  span, ctx, ctx_token = _setup_span(
269
- entity_name, tlp_span_kind, version
286
+ entity_name,
287
+ tlp_span_kind,
288
+ version,
289
+ description if description else None,
270
290
  )
271
291
  _handle_span_input(span, args, kwargs, cls=JSONEncoder)
272
292
 
@@ -284,7 +304,10 @@ def entity_method(
284
304
  return await fn(*args, **kwargs)
285
305
 
286
306
  span, ctx, ctx_token = _setup_span(
287
- entity_name, tlp_span_kind, version
307
+ entity_name,
308
+ tlp_span_kind,
309
+ version,
310
+ description if description else None,
288
311
  )
289
312
  _handle_span_input(span, args, kwargs, cls=JSONEncoder)
290
313
  success = False
@@ -359,6 +382,8 @@ def entity_method(
359
382
  _handle_agent_failure_event(str(e), span, tlp_span_kind)
360
383
  raise e
361
384
  finally:
385
+ if tlp_span_kind == ObserveSpanKindValues.AGENT:
386
+ TracerWrapper().record_agent_execution(entity_name, success)
362
387
  _cleanup_span(span, ctx_token)
363
388
  return res
364
389
 
@@ -370,10 +395,15 @@ def entity_method(
370
395
  if not TracerWrapper.verify_initialized():
371
396
  return fn(*args, **kwargs)
372
397
 
373
- span, ctx, ctx_token = _setup_span(entity_name, tlp_span_kind, version)
398
+ span, ctx, ctx_token = _setup_span(
399
+ entity_name,
400
+ tlp_span_kind,
401
+ version,
402
+ description if description else None,
403
+ )
374
404
 
375
405
  _handle_span_input(span, args, kwargs, cls=JSONEncoder)
376
- _handle_agent_span(span, entity_name, tlp_span_kind)
406
+ _handle_agent_span(span, entity_name, description, tlp_span_kind)
377
407
  success = False
378
408
 
379
409
  # Record heartbeat for agent
@@ -459,6 +489,8 @@ def entity_method(
459
489
  _handle_agent_failure_event(str(e), span, tlp_span_kind)
460
490
  raise e
461
491
  finally:
492
+ if tlp_span_kind == ObserveSpanKindValues.AGENT:
493
+ TracerWrapper().record_agent_execution(entity_name, success)
462
494
  _cleanup_span(span, ctx_token)
463
495
  return res
464
496
 
@@ -469,26 +501,51 @@ def entity_method(
469
501
 
470
502
  def entity_class(
471
503
  name: Optional[str],
504
+ description: Optional[str],
472
505
  version: Optional[int],
473
- method_name: str,
506
+ method_name: Optional[str],
474
507
  tlp_span_kind: Optional[ObserveSpanKindValues] = ObserveSpanKindValues.TASK,
475
508
  ):
476
509
  def decorator(cls):
477
510
  task_name = name if name else camel_to_snake(cls.__qualname__)
478
- method = getattr(cls, method_name)
479
- setattr(
480
- cls,
481
- method_name,
482
- entity_method(name=task_name, version=version, tlp_span_kind=tlp_span_kind)(
483
- method
484
- ),
485
- )
511
+
512
+ methods_to_wrap = []
513
+
514
+ if method_name:
515
+ # Specific method specified - existing behavior
516
+ methods_to_wrap = [method_name]
517
+ else:
518
+ # No method specified - wrap all public methods
519
+ for attr_name in dir(cls):
520
+ if (
521
+ not attr_name.startswith("_") # Skip private/built-in methods
522
+ and attr_name != "mro" # Skip class method
523
+ and hasattr(cls, attr_name)
524
+ and callable(getattr(cls, attr_name))
525
+ and not isinstance(
526
+ getattr(cls, attr_name), (classmethod, staticmethod, property)
527
+ )
528
+ ):
529
+ methods_to_wrap.append(attr_name)
530
+
531
+ # Wrap all detected methods
532
+ for method_to_wrap in methods_to_wrap:
533
+ if hasattr(cls, method_to_wrap):
534
+ method = getattr(cls, method_to_wrap)
535
+ wrapped_method = entity_method(
536
+ name=f"{task_name}.{method_to_wrap}",
537
+ description=description,
538
+ version=version,
539
+ tlp_span_kind=tlp_span_kind,
540
+ )(method)
541
+ setattr(cls, method_to_wrap, wrapped_method)
542
+
486
543
  return cls
487
544
 
488
545
  return decorator
489
546
 
490
547
 
491
- def _handle_agent_span(span, entity_name, tlp_span_kind):
548
+ def _handle_agent_span(span, entity_name, description, tlp_span_kind):
492
549
  if tlp_span_kind == ObserveSpanKindValues.AGENT:
493
550
  try:
494
551
  set_agent_id_event(entity_name)
@@ -496,6 +553,7 @@ def _handle_agent_span(span, entity_name, tlp_span_kind):
496
553
  "agent_start_event",
497
554
  {
498
555
  "agent_name": entity_name,
556
+ "description": description if description else "",
499
557
  "type": tlp_span_kind.value
500
558
  if tlp_span_kind != "graph"
501
559
  else "graph",
@@ -581,7 +639,8 @@ def _handle_graph_response(span, res, tlp_span_kind):
581
639
  }
582
640
 
583
641
  # Convert to JSON string
584
- s_graph_json = json.dumps(graph_dict, indent=2)
642
+ s_graph_json = json.dumps(graph_dict)
643
+ # convert to JSON and set as attribute
585
644
  span.set_attribute("gen_ai.ioa.graph", s_graph_json)
586
645
 
587
646
  # get graph dynamism
@@ -21,6 +21,12 @@ def determine_workflow_type(workflow_obj: Any) -> Union[None, dict]:
21
21
  for key, value in workflow_obj.items()
22
22
  ):
23
23
  return build_agent_dict_topology(workflow_obj)
24
+
25
+ # Check if it's a list of agent names
26
+ elif isinstance(workflow_obj, list) and all(
27
+ isinstance(item, str) for item in workflow_obj
28
+ ):
29
+ return build_agent_dict_topology(workflow_obj)
24
30
  # Try LlamaIndex built-in workflow types first
25
31
  result = determine_llama_index_workflow_type(workflow_obj)
26
32
  if result:
@@ -42,17 +48,14 @@ def determine_workflow_type(workflow_obj: Any) -> Union[None, dict]:
42
48
  return None
43
49
 
44
50
 
45
- # This function generates a graph topology dict for the workflow.
46
51
  def determine_llama_index_workflow_type(workflow_obj: Any) -> Union[None, dict]:
47
52
  """Generates a graph topology dict for the llama-index compatible workflow."""
48
- if isinstance(workflow_obj, Workflow) and "agent.Workflow" in str(
49
- workflow_obj.__class__.__bases__
50
- ):
51
- return generate_topology_dict(workflow_obj)
52
- elif isinstance(
53
- workflow_obj, AgentWorkflow
54
- ) and "multi_agent_workflow.AgentWorkflow" in str(workflow_obj.__class__.__bases__):
53
+ # Check for AgentWorkflow first (more specific)
54
+ if isinstance(workflow_obj, AgentWorkflow):
55
55
  return get_multi_agent_workflow_graph_as_json(workflow_obj)
56
+ # Check for general Workflow (less specific)
57
+ elif isinstance(workflow_obj, Workflow):
58
+ return generate_topology_dict(workflow_obj)
56
59
  else:
57
60
  return None
58
61
 
@@ -171,7 +174,6 @@ def generate_topology_dict(workflow: Workflow) -> Union[None, dict]:
171
174
  else:
172
175
  # No next step? Ignore or log
173
176
  pass
174
-
175
177
  return {"nodes": nodes, "edges": edges}
176
178
 
177
179
 
@@ -282,8 +284,13 @@ def get_multi_agent_workflow_graph_as_json(agent_workflow_instance):
282
284
 
283
285
 
284
286
  def detect_custom_agent_workflow(obj: Any) -> Union[None, dict]:
285
- """Detects and generates topology for custom agent workflows not built with LlamaIndex."""
287
+ # Check if object has a workflow attribute that's a LlamaIndex workflow
288
+ if hasattr(obj, "workflow") and isinstance(obj.workflow, (Workflow, AgentWorkflow)):
289
+ return determine_llama_index_workflow_type(obj.workflow)
290
+
291
+ # Detects and generates topology for custom agent workflows not built with LlamaIndex."""
286
292
  # Check if this is a dictionary of agents returned from a get_agents method
293
+
287
294
  if isinstance(obj, dict) and all(
288
295
  hasattr(agent, "invoke") for agent in obj.values()
289
296
  ):
@@ -0,0 +1,111 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from typing import Collection
5
+ import functools
6
+ import threading
7
+
8
+ from opentelemetry import baggage
9
+ from opentelemetry.baggage.propagation import W3CBaggagePropagator
10
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
11
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
12
+
13
+ from ioa_observe.sdk import TracerWrapper
14
+ from ioa_observe.sdk.client import kv_store
15
+ from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
16
+
17
+ _instruments = ("a2a-sdk >= 0.2.5",)
18
+ _global_tracer = None
19
+ _kv_lock = threading.RLock() # Add thread-safety for kv_store operations
20
+
21
+
22
+ class A2AInstrumentor(BaseInstrumentor):
23
+ def __init__(self):
24
+ super().__init__()
25
+ global _global_tracer
26
+ _global_tracer = TracerWrapper().get_tracer()
27
+
28
+ def instrumentation_dependencies(self) -> Collection[str]:
29
+ return _instruments
30
+
31
+ def _instrument(self, **kwargs):
32
+ import importlib
33
+
34
+ if importlib.util.find_spec("a2a") is None:
35
+ raise ImportError("No module named 'a2a-sdk'. Please install it first.")
36
+
37
+ # Instrument `publish`
38
+ from a2a.client import A2AClient
39
+
40
+ original_send_message = A2AClient.send_message
41
+
42
+ @functools.wraps(original_send_message)
43
+ async def instrumented_send_message(
44
+ self, request, *, http_kwargs=None, context=None
45
+ ):
46
+ with _global_tracer.start_as_current_span("a2a.send_message"):
47
+ traceparent = get_current_traceparent()
48
+ session_id = None
49
+ if traceparent:
50
+ session_id = kv_store.get(f"execution.{traceparent}")
51
+ if session_id:
52
+ kv_store.set(f"execution.{traceparent}", session_id)
53
+ # Inject headers into http_kwargs
54
+ if http_kwargs is None:
55
+ http_kwargs = {}
56
+ headers = http_kwargs.get("headers", {})
57
+ headers["traceparent"] = traceparent
58
+ if session_id:
59
+ headers["session_id"] = session_id
60
+ baggage.set_baggage(f"execution.{traceparent}", session_id)
61
+ http_kwargs["headers"] = headers
62
+ return await original_send_message(self, request, http_kwargs=http_kwargs)
63
+
64
+ from a2a.client import A2AClient
65
+
66
+ A2AClient.send_message = instrumented_send_message
67
+
68
+ from a2a.server.request_handlers import DefaultRequestHandler
69
+
70
+ original_server_on_message_send = DefaultRequestHandler.on_message_send
71
+
72
+ @functools.wraps(original_server_on_message_send)
73
+ async def instrumented_execute(self, params, context):
74
+ # Extract headers from context (assume context.request.headers)
75
+
76
+ traceparent = context.state.get("headers", {}).get("traceparent")
77
+ session_id = context.state.get("headers", {}).get("session_id")
78
+ carrier = {
79
+ k.lower(): v
80
+ for k, v in context.state.get("headers", {}).items()
81
+ if k.lower() in ["traceparent", "baggage"]
82
+ }
83
+ if carrier and traceparent:
84
+ ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
85
+ ctx = W3CBaggagePropagator().extract(carrier=carrier, context=ctx)
86
+ if session_id and session_id != "None":
87
+ set_session_id(session_id, traceparent=traceparent)
88
+ kv_store.set(f"execution.{traceparent}", session_id)
89
+ return await original_server_on_message_send(self, params, context)
90
+
91
+ from a2a.server.request_handlers import DefaultRequestHandler
92
+
93
+ DefaultRequestHandler.on_message_send = instrumented_execute
94
+
95
+ def _uninstrument(self, **kwargs):
96
+ import importlib
97
+
98
+ if importlib.util.find_spec("a2a") is None:
99
+ raise ImportError("No module named 'a2a-sdk'. Please install it first.")
100
+
101
+ # Uninstrument `send_message`
102
+ from a2a.client import A2AClient
103
+
104
+ A2AClient.send_message = A2AClient.send_message.__wrapped__
105
+
106
+ # Uninstrument `execute`
107
+ from a2a.server.request_handlers import DefaultRequestHandler
108
+
109
+ DefaultRequestHandler.on_message_send = (
110
+ DefaultRequestHandler.on_message_send.__wrapped__
111
+ )
@@ -14,7 +14,7 @@ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapProp
14
14
 
15
15
  from ioa_observe.sdk import TracerWrapper
16
16
  from ioa_observe.sdk.client import kv_store
17
- from ioa_observe.sdk.tracing import set_execution_id, get_current_traceparent
17
+ from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
18
18
 
19
19
  _instruments = ("slim-bindings >= 0.2",)
20
20
  _global_tracer = None
@@ -50,21 +50,21 @@ class SLIMInstrumentor(BaseInstrumentor):
50
50
  traceparent = get_current_traceparent()
51
51
 
52
52
  # Thread-safe access to kv_store
53
- execution_id = None
53
+ session_id = None
54
54
  if traceparent:
55
55
  with _kv_lock:
56
- execution_id = kv_store.get(f"execution.{traceparent}")
57
- if execution_id:
58
- kv_store.set(f"execution.{traceparent}", execution_id)
56
+ session_id = kv_store.get(f"execution.{traceparent}")
57
+ if session_id:
58
+ kv_store.set(f"execution.{traceparent}", session_id)
59
59
  # Add tracing context to the message headers
60
60
  headers = {
61
- "execution_id": execution_id if execution_id else None,
61
+ "session_id": session_id if session_id else None,
62
62
  "traceparent": traceparent,
63
63
  }
64
64
 
65
65
  # Set baggage context
66
- if traceparent and execution_id:
67
- baggage.set_baggage(f"execution.{traceparent}", execution_id)
66
+ if traceparent and session_id:
67
+ baggage.set_baggage(f"execution.{traceparent}", session_id)
68
68
 
69
69
  # Process message payload
70
70
  if isinstance(message, bytes):
@@ -120,7 +120,7 @@ class SLIMInstrumentor(BaseInstrumentor):
120
120
 
121
121
  # Extract traceparent from headers
122
122
  traceparent = headers.get("traceparent")
123
- execution_id = headers.get("execution_id")
123
+ session_id = headers.get("session_id")
124
124
 
125
125
  # First, extract and restore the trace context from headers
126
126
  carrier = {}
@@ -130,27 +130,27 @@ class SLIMInstrumentor(BaseInstrumentor):
130
130
  if k.lower() == key.lower():
131
131
  carrier[key.lower()] = headers[k]
132
132
 
133
- # Restore the trace context BEFORE calling set_execution_id
133
+ # Restore the trace context BEFORE calling set_session_id
134
134
  if carrier and traceparent:
135
135
  ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
136
136
  ctx = W3CBaggagePropagator().extract(carrier=carrier, context=ctx)
137
137
 
138
138
  # Now set execution ID with the restored context
139
- if execution_id and execution_id != "None":
139
+ if session_id and session_id != "None":
140
140
  # Pass the traceparent explicitly to prevent new context creation
141
- set_execution_id(execution_id, traceparent=traceparent)
141
+ set_session_id(session_id, traceparent=traceparent)
142
142
 
143
143
  # Store in kv_store with thread safety
144
144
  with _kv_lock:
145
- kv_store.set(f"execution.{traceparent}", execution_id)
145
+ kv_store.set(f"execution.{traceparent}", session_id)
146
146
 
147
147
  # Fallback: check stored execution ID if not found in headers
148
- if traceparent and (not execution_id or execution_id == "None"):
148
+ if traceparent and (not session_id or session_id == "None"):
149
149
  with _kv_lock:
150
- stored_execution_id = kv_store.get(f"execution.{traceparent}")
151
- if stored_execution_id:
152
- execution_id = stored_execution_id
153
- set_execution_id(execution_id, traceparent=traceparent)
150
+ stored_session_id = kv_store.get(f"execution.{traceparent}")
151
+ if stored_session_id:
152
+ session_id = stored_session_id
153
+ set_session_id(session_id, traceparent=traceparent)
154
154
 
155
155
  # Process payload
156
156
  payload = message_dict.get("payload", raw_message)
@@ -4,7 +4,7 @@
4
4
  from ioa_observe.sdk.tracing.context_manager import get_tracer
5
5
  from ioa_observe.sdk.tracing.tracing import (
6
6
  set_workflow_name,
7
- set_execution_id,
7
+ set_session_id,
8
8
  get_current_traceparent,
9
9
  session_start,
10
10
  )
@@ -12,7 +12,7 @@ from ioa_observe.sdk.tracing.tracing import (
12
12
  __all__ = [
13
13
  "get_tracer",
14
14
  "set_workflow_name",
15
- "set_execution_id",
15
+ "set_session_id",
16
16
  "get_current_traceparent",
17
17
  "session_start",
18
18
  ]
@@ -4,7 +4,7 @@ from opentelemetry import baggage
4
4
 
5
5
  from ioa_observe.sdk import TracerWrapper
6
6
  from ioa_observe.sdk.client import kv_store
7
- from ioa_observe.sdk.tracing import set_execution_id, get_current_traceparent
7
+ from ioa_observe.sdk.tracing import set_session_id, get_current_traceparent
8
8
  from opentelemetry import context as otel_context
9
9
  from opentelemetry.context import attach
10
10
 
@@ -21,7 +21,7 @@ After receiving a message, extract headers and call set_context_from_headers(hea
21
21
 
22
22
  def get_current_context_headers():
23
23
  """
24
- Extracts the current trace context, baggage, and execution_id into headers.
24
+ Extracts the current trace context, baggage, and session_id into headers.
25
25
  """
26
26
  _global_tracer = TracerWrapper().get_tracer()
27
27
  with _global_tracer.start_as_current_span("get_current_context_headers"):
@@ -31,32 +31,32 @@ def get_current_context_headers():
31
31
  TraceContextTextMapPropagator().inject(carrier, context=current_ctx)
32
32
  W3CBaggagePropagator().inject(carrier, context=current_ctx)
33
33
  traceparent = carrier.get("traceparent")
34
- execution_id = None
34
+ session_id = None
35
35
  if traceparent:
36
- execution_id = kv_store.get(f"execution.{traceparent}")
37
- if execution_id:
38
- carrier["execution_id"] = execution_id
36
+ session_id = kv_store.get(f"execution.{traceparent}")
37
+ if session_id:
38
+ carrier["session_id"] = session_id
39
39
  return carrier
40
40
 
41
41
 
42
42
  def set_context_from_headers(headers):
43
43
  """
44
- Restores the trace context, baggage, and execution_id from headers.
44
+ Restores the trace context, baggage, and session_id from headers.
45
45
  """
46
46
  carrierHeaders = {}
47
47
  if "traceparentID" in headers:
48
48
  carrierHeaders["traceparent"] = headers["traceparentID"]
49
49
  if "executionID" in headers:
50
- carrierHeaders["execution_id"] = headers["executionID"]
50
+ carrierHeaders["session_id"] = headers["executionID"]
51
51
  ctx = TraceContextTextMapPropagator().extract(carrier=carrierHeaders)
52
52
  ctx = W3CBaggagePropagator().extract(carrier=carrierHeaders, context=ctx)
53
53
  attach(ctx)
54
- # Restore execution_id if present
54
+ # Restore session_id if present
55
55
  traceparent = headers.get("traceparentID")
56
- execution_id = headers.get("executionID")
57
- if traceparent and execution_id and execution_id != "None":
58
- set_execution_id(execution_id, traceparent=traceparent)
59
- kv_store.set(f"execution.{traceparent}", execution_id)
56
+ session_id = headers.get("executionID")
57
+ if traceparent and session_id and session_id != "None":
58
+ set_session_id(session_id, traceparent=traceparent)
59
+ kv_store.set(f"execution.{traceparent}", session_id)
60
60
  return ctx
61
61
 
62
62
 
@@ -68,16 +68,16 @@ def get_baggage_item(key):
68
68
  return baggage.get_baggage(key)
69
69
 
70
70
 
71
- def get_current_execution_id():
71
+ def get_current_session_id():
72
72
  traceparent = get_current_traceparent()
73
73
  if traceparent:
74
74
  return kv_store.get(f"execution.{traceparent}")
75
75
  return None
76
76
 
77
77
 
78
- def set_execution_id_from_headers(headers):
78
+ def set_session_id_from_headers(headers):
79
79
  traceparent = headers.get("traceparent")
80
- execution_id = headers.get("execution_id")
81
- if traceparent and execution_id:
82
- set_execution_id(execution_id, traceparent=traceparent)
83
- kv_store.set(f"execution.{traceparent}", execution_id)
80
+ session_id = headers.get("session_id")
81
+ if traceparent and session_id:
82
+ set_session_id(session_id, traceparent=traceparent)
83
+ kv_store.set(f"execution.{traceparent}", session_id)
@@ -19,6 +19,7 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
19
19
  from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
20
20
  OTLPSpanExporter as GRPCExporter,
21
21
  )
22
+ from opentelemetry.metrics import Observation
22
23
  from opentelemetry.sdk.resources import Resource
23
24
  from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
24
25
  from opentelemetry.propagators.textmap import TextMapPropagator
@@ -42,7 +43,6 @@ from ioa_observe.sdk.client import kv_store
42
43
 
43
44
  from ioa_observe.sdk.utils.const import (
44
45
  OBSERVE_WORKFLOW_NAME,
45
- OBSERVE_ENTITY_PATH,
46
46
  OBSERVE_ASSOCIATION_PROPERTIES,
47
47
  OBSERVE_ENTITY_NAME,
48
48
  OBSERVE_PROMPT_TEMPLATE_VARIABLES,
@@ -129,6 +129,7 @@ class TracerWrapper(object):
129
129
  return obj
130
130
 
131
131
  obj.__image_uploader = image_uploader
132
+ obj._agent_execution_counts = {} # {(agent_name): [success_count, total_count]}
132
133
  TracerWrapper.app_name = TracerWrapper.resource_attributes.get(
133
134
  "service.name", "observe"
134
135
  )
@@ -227,6 +228,17 @@ class TracerWrapper(object):
227
228
  obj.error_counter = meter.create_counter(
228
229
  "slim.errors", description="Number of SLIM message errors or drops"
229
230
  )
231
+ obj.agent_chain_completion_time_histogram = meter.create_histogram(
232
+ name="gen_ai.client.ioa.agent.end_to_end_chain_completion_time",
233
+ description="Records the end-to-end chain completion time for a single agent",
234
+ unit="s",
235
+ )
236
+ obj.agent_execution_success_rate = meter.create_observable_gauge(
237
+ name="gen_ai.client.ioa.agent.execution_success_rate",
238
+ description="Success rate of agent executions",
239
+ unit="1",
240
+ callbacks=[obj._observe_agent_execution_success_rate],
241
+ )
230
242
  if propagator:
231
243
  set_global_textmap(propagator)
232
244
 
@@ -262,13 +274,13 @@ class TracerWrapper(object):
262
274
  if workflow_name is not None:
263
275
  span.set_attribute(OBSERVE_WORKFLOW_NAME, workflow_name)
264
276
 
265
- execution_id = get_value("execution.id")
266
- if execution_id is not None:
267
- span.set_attribute("execution.id", execution_id)
277
+ session_id = get_value("session.id")
278
+ if session_id is not None:
279
+ span.set_attribute("session.id", session_id)
268
280
 
269
- entity_path = get_value("entity_path")
270
- if entity_path is not None:
271
- span.set_attribute(OBSERVE_ENTITY_PATH, entity_path)
281
+ agent_id = get_value("agent_id")
282
+ if agent_id is not None:
283
+ span.set_attribute("agent_id", agent_id)
272
284
 
273
285
  if is_llm_span(span):
274
286
  self.llm_call_counter.add(1, attributes=span.attributes)
@@ -327,8 +339,6 @@ class TracerWrapper(object):
327
339
 
328
340
  def span_processor_on_ending(self, span):
329
341
  determine_reliability_score(span)
330
-
331
- # self.rename_span_attribute(span)
332
342
  start_time = span.attributes.get("ioa_start_time")
333
343
  # publish span to the exporter
334
344
 
@@ -336,14 +346,6 @@ class TracerWrapper(object):
336
346
  latency = (time.time() - start_time) * 1000
337
347
  self.response_latency_histogram.record(latency, attributes=span.attributes)
338
348
 
339
- def rename_span_attribute(self, span):
340
- # for each span attribute, if the span attribute name contains traceloop, replace it with ioa
341
- for key in list(span.attributes.keys()):
342
- if "traceloop" in key:
343
- new_key = key.replace("traceloop", "ioa")
344
- span[new_key] = span.attributes[key]
345
- del span[key]
346
-
347
349
  @staticmethod
348
350
  def set_static_params(
349
351
  resource_attributes: dict,
@@ -384,6 +386,24 @@ class TracerWrapper(object):
384
386
  def get_tracer(self):
385
387
  return self.__tracer_provider.get_tracer(TRACER_NAME)
386
388
 
389
+ def record_agent_execution(self, agent_name: str, success: bool):
390
+ counts = self._agent_execution_counts.setdefault(agent_name, [0, 0])
391
+ if success:
392
+ counts[0] += 1 # success count
393
+ counts[1] += 1 # total count
394
+
395
+ def _observe_agent_execution_success_rate(self, observer):
396
+ measurements = []
397
+ for agent_name, (
398
+ success_count,
399
+ total_count,
400
+ ) in self._agent_execution_counts.items():
401
+ rate = (success_count / total_count) if total_count > 0 else 0.0
402
+ measurements.append(
403
+ Observation(value=rate, attributes={"agent_name": agent_name})
404
+ )
405
+ return measurements
406
+
387
407
 
388
408
  def set_association_properties(properties: dict) -> None:
389
409
  attach(set_value("association_properties", properties))
@@ -409,10 +429,10 @@ def session_start():
409
429
  As a context manager, yields session metadata.
410
430
  As a normal function, just sets up the session.
411
431
  """
412
- execution_id = TracerWrapper.app_name + "_" + str(uuid.uuid4())
413
- set_execution_id(execution_id)
432
+ session_id = TracerWrapper.app_name + "_" + str(uuid.uuid4())
433
+ set_session_id(session_id)
414
434
  metadata = {
415
- "executionID": get_value("execution.id") or execution_id,
435
+ "executionID": get_value("session.id") or session_id,
416
436
  "traceparentID": get_current_traceparent(),
417
437
  }
418
438
  import inspect
@@ -431,7 +451,7 @@ def session_start():
431
451
  return contextlib.nullcontext(metadata)
432
452
 
433
453
 
434
- def set_execution_id(execution_id: str, traceparent: str = None) -> None:
454
+ def set_session_id(session_id: str, traceparent: str = None) -> None:
435
455
  """
436
456
  Sets the execution ID in both the key-value store and OpenTelemetry context.
437
457
 
@@ -439,10 +459,10 @@ def set_execution_id(execution_id: str, traceparent: str = None) -> None:
439
459
  proper trace correlation across distributed systems.
440
460
 
441
461
  Args:
442
- execution_id: The execution ID to set
462
+ session_id: The execution ID to set
443
463
  traceparent: Optional traceparent to use (if None, will extract from context)
444
464
  """
445
- if not execution_id:
465
+ if not session_id:
446
466
  return
447
467
 
448
468
  from opentelemetry import trace
@@ -455,10 +475,10 @@ def set_execution_id(execution_id: str, traceparent: str = None) -> None:
455
475
  # Store execution ID with provided traceparent
456
476
  kv_key = f"execution.{traceparent}"
457
477
  if kv_store.get(kv_key) is None:
458
- kv_store.set(kv_key, execution_id)
478
+ kv_store.set(kv_key, session_id)
459
479
 
460
480
  # Store in OpenTelemetry context
461
- attach(set_value("execution.id", execution_id))
481
+ attach(set_value("session.id", session_id))
462
482
  attach(set_value("current_traceparent", traceparent))
463
483
  return
464
484
 
@@ -473,7 +493,7 @@ def set_execution_id(execution_id: str, traceparent: str = None) -> None:
473
493
  else:
474
494
  # Only create new span if absolutely necessary (no existing context)
475
495
  tracer = trace.get_tracer(__name__)
476
- with tracer.start_as_current_span("set_execution_id"):
496
+ with tracer.start_as_current_span("set_session_id"):
477
497
  carrier = {}
478
498
  TraceContextTextMapPropagator().inject(carrier)
479
499
  extracted_traceparent = carrier.get("traceparent")
@@ -482,10 +502,10 @@ def set_execution_id(execution_id: str, traceparent: str = None) -> None:
482
502
  if extracted_traceparent:
483
503
  kv_key = f"execution.{extracted_traceparent}"
484
504
  if kv_store.get(kv_key) is None:
485
- kv_store.set(kv_key, execution_id)
505
+ kv_store.set(kv_key, session_id)
486
506
 
487
507
  # Also store in OpenTelemetry context
488
- attach(set_value("execution.id", execution_id))
508
+ attach(set_value("session.id", session_id))
489
509
  attach(set_value("current_traceparent", extracted_traceparent))
490
510
 
491
511
 
@@ -1,38 +1,38 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ioa-observe-sdk
3
- Version: 1.0.6
3
+ Version: 1.0.8
4
4
  Summary: IOA Observability SDK
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE.md
8
- Requires-Dist: aiohappyeyeballs==2.4.8
9
- Requires-Dist: aiohttp==3.11.18
10
- Requires-Dist: aiosignal==1.3.2
11
- Requires-Dist: annotated-types==0.7.0
12
- Requires-Dist: anyio==4.8.0
13
- Requires-Dist: async-timeout==4.0.3
14
- Requires-Dist: attrs==25.1.0
15
- Requires-Dist: backoff==2.2.1
16
- Requires-Dist: certifi==2025.1.31
17
- Requires-Dist: charset-normalizer==3.4.1
8
+ Requires-Dist: aiohappyeyeballs>=2.4.8
9
+ Requires-Dist: aiohttp>=3.11.18
10
+ Requires-Dist: aiosignal>=1.3.2
11
+ Requires-Dist: annotated-types>=0.7.0
12
+ Requires-Dist: anyio>=4.8.0
13
+ Requires-Dist: async-timeout>=4.0.3
14
+ Requires-Dist: attrs>=25.1.0
15
+ Requires-Dist: backoff>=2.2.1
16
+ Requires-Dist: certifi>=2025.1.31
17
+ Requires-Dist: charset-normalizer>=3.4.1
18
18
  Requires-Dist: colorama==0.4.6
19
- Requires-Dist: Deprecated==1.2.18
20
- Requires-Dist: distro==1.9.0
21
- Requires-Dist: exceptiongroup==1.2.2
22
- Requires-Dist: frozenlist==1.5.0
23
- Requires-Dist: googleapis-common-protos==1.69.0
24
- Requires-Dist: grpcio==1.70.0
25
- Requires-Dist: h11==0.16.0
26
- Requires-Dist: httpcore==1.0.9
27
- Requires-Dist: httpx==0.28.1
28
- Requires-Dist: idna==3.10
29
- Requires-Dist: importlib_metadata==8.5.0
30
- Requires-Dist: Jinja2==3.1.6
31
- Requires-Dist: jiter==0.8.2
32
- Requires-Dist: MarkupSafe==3.0.2
33
- Requires-Dist: monotonic==1.6
34
- Requires-Dist: multidict==6.1.0
35
- Requires-Dist: openai==1.75.0
19
+ Requires-Dist: Deprecated>=1.2.18
20
+ Requires-Dist: distro>=1.9.0
21
+ Requires-Dist: exceptiongroup>=1.2.2
22
+ Requires-Dist: frozenlist>=1.5.0
23
+ Requires-Dist: googleapis-common-protos>=1.69.0
24
+ Requires-Dist: grpcio>=1.70.0
25
+ Requires-Dist: h11>=0.16.0
26
+ Requires-Dist: httpcore>=1.0.9
27
+ Requires-Dist: httpx>=0.28.1
28
+ Requires-Dist: idna>=3.10
29
+ Requires-Dist: importlib_metadata>=8.5.0
30
+ Requires-Dist: Jinja2>=3.1.6
31
+ Requires-Dist: jiter>=0.8.2
32
+ Requires-Dist: MarkupSafe>=3.0.2
33
+ Requires-Dist: monotonic>=1.6
34
+ Requires-Dist: multidict>=6.1.0
35
+ Requires-Dist: openai>=1.75.0
36
36
  Requires-Dist: opentelemetry-api==1.33.1
37
37
  Requires-Dist: opentelemetry-distro
38
38
  Requires-Dist: opentelemetry-exporter-otlp==1.33.1
@@ -52,32 +52,32 @@ Requires-Dist: opentelemetry-sdk==1.33.1
52
52
  Requires-Dist: opentelemetry-semantic-conventions==0.54b1
53
53
  Requires-Dist: opentelemetry-semantic-conventions-ai==0.4.9
54
54
  Requires-Dist: opentelemetry-util-http==0.54b1
55
- Requires-Dist: packaging==24.2
56
- Requires-Dist: propcache==0.3.0
57
- Requires-Dist: protobuf==5.29.3
58
- Requires-Dist: pydantic==2.10.6
59
- Requires-Dist: pydantic_core==2.27.2
60
- Requires-Dist: python-dateutil==2.9.0.post0
55
+ Requires-Dist: packaging>=24.2
56
+ Requires-Dist: propcache>=0.3.0
57
+ Requires-Dist: protobuf>=5.29.3
58
+ Requires-Dist: pydantic>=2.10.6
59
+ Requires-Dist: pydantic_core>=2.27.2
60
+ Requires-Dist: python-dateutil>=2.9.0.post0
61
61
  Requires-Dist: regex==2024.11.6
62
- Requires-Dist: requests==2.32.3
63
- Requires-Dist: six==1.17.0
64
- Requires-Dist: sniffio==1.3.1
65
- Requires-Dist: tenacity==9.0.0
66
- Requires-Dist: tiktoken==0.9.0
67
- Requires-Dist: tqdm==4.67.1
68
- Requires-Dist: typing_extensions==4.12.2
69
- Requires-Dist: urllib3==2.3.0
70
- Requires-Dist: wrapt==1.17.2
71
- Requires-Dist: yarl==1.18.3
72
- Requires-Dist: zipp==3.21.0
62
+ Requires-Dist: requests>=2.32.3
63
+ Requires-Dist: six>=1.17.0
64
+ Requires-Dist: sniffio>=1.3.1
65
+ Requires-Dist: tenacity>=9.0.0
66
+ Requires-Dist: tiktoken>=0.9.0
67
+ Requires-Dist: tqdm>=4.67.1
68
+ Requires-Dist: typing_extensions>=4.12.2
69
+ Requires-Dist: urllib3>=2.3.0
70
+ Requires-Dist: wrapt>=1.17.2
71
+ Requires-Dist: yarl>=1.18.3
72
+ Requires-Dist: zipp>=3.21.0
73
73
  Requires-Dist: langgraph>=0.3.2
74
74
  Requires-Dist: langchain>=0.3.19
75
75
  Requires-Dist: langchain-openai>=0.3.8
76
76
  Requires-Dist: langchain-community>=0.3.25
77
77
  Requires-Dist: llama-index>=0.12.34
78
78
  Requires-Dist: opentelemetry-instrumentation-requests
79
- Requires-Dist: opentelemetry-instrumentation-transformers>=0.40.2
80
- Requires-Dist: opentelemetry-instrumentation-crewai>=0.40.2
79
+ Requires-Dist: opentelemetry-instrumentation-transformers>=0.40.8
80
+ Requires-Dist: opentelemetry-instrumentation-crewai>=0.40.8
81
81
  Requires-Dist: llama-index-utils-workflow>=0.3.1
82
82
  Requires-Dist: pytest
83
83
  Requires-Dist: pytest-vcr
@@ -85,6 +85,8 @@ Dynamic: license-file
85
85
 
86
86
  # Observe-SDK
87
87
 
88
+ [![PyPI version](https://img.shields.io/pypi/v/ioa-observe-sdk.svg)](https://pypi.org/project/ioa-observe-sdk/)
89
+
88
90
  IOA observability SDK for your multi-agentic application.
89
91
 
90
92
  ## Table of Contents
@@ -119,6 +121,20 @@ Link: [AGNTCY Observability Schema](https://github.com/agntcy/observe/blob/main/
119
121
 
120
122
  ## Dev
121
123
 
124
+ Any Opentelemetry compatible backend can be used, but for this guide, we will use ClickhouseDB as the backend database.
125
+
126
+ ### Opentelemetry collector
127
+
128
+ The OpenTelemetry Collector offers a vendor-agnostic implementation of how to receive, process and export telemetry data. It removes the need to run, operate, and maintain multiple agents/collectors.
129
+
130
+ ### Clickhouse DB
131
+
132
+ ClickhouseDB is used as a backend database to store and query the collected telemetry data efficiently, enabling you to analyze and visualize observability information for your multi-agentic applications.
133
+
134
+ ### Grafana (optional)
135
+
136
+ Grafana can be used to visualize the telemetry data collected by the OpenTelemetry Collector and stored in ClickhouseDB.
137
+
122
138
  To get started with development, start a Clickhouse DB and an OTel collector container locally using docker-compose like so:
123
139
 
124
140
  ```
@@ -126,6 +142,8 @@ cd deploy/
126
142
  docker compose up -d
127
143
  ```
128
144
 
145
+ Running both locally allows you to test, monitor, and debug your observability setup in a development environment before deploying to production.
146
+
129
147
  Ensure the contents of `otel-collector.yaml` is correct.
130
148
 
131
149
  Check the logs of the collector to ensure it is running correctly:
@@ -134,6 +152,18 @@ Check the logs of the collector to ensure it is running correctly:
134
152
  docker logs -f otel-collector
135
153
  ```
136
154
 
155
+ Viewing data in Clickhouse DB can be done using the Clickhouse client. The collector is configured to export telemetry data to Clickhouse.
156
+
157
+ The clickhouse exporter creates various tables in the Clickhouse DB, including `otel_traces`, which is used to store trace data.
158
+
159
+ For more info, refer to the [OpenTelemetry Clickhouse Exporter documentation](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/clickhouseexporter/README.md)
160
+
161
+ ```bash
162
+ docker exec -it clickhouse-server clickhouse-client
163
+
164
+ select * from otel_traces LIMIT 10;
165
+ ```
166
+
137
167
  Create a `.env` file with the following content:
138
168
 
139
169
  ```bash
@@ -165,6 +195,28 @@ OPENAI_API_KEY=<KEY> make test
165
195
  For getting started with the SDK, please refer to the [Getting Started](https://github.com/agntcy/observe/blob/main/GETTING-STARTED.md)
166
196
  file. It contains detailed instructions on how to set up and use the SDK effectively.
167
197
 
198
+ ### Grafana
199
+
200
+ To configure Grafana to visualize the telemetry data, follow these steps:
201
+
202
+ 1. Spin up Grafana locally using Docker:
203
+
204
+ ```bash
205
+ docker run -d -p 3000:3000 --name=grafana grafana/grafana
206
+ ```
207
+ 2. Access Grafana by navigating to `http://localhost:3000` in your web browser.
208
+ - Default username: `admin`
209
+ - Default password: `admin`
210
+
211
+ 3. Add a new data source:
212
+ - Choose "ClickHouse" as the data source type.
213
+ - Set the URL to `http://0.0.0.0:8123`.
214
+ - Configure the authentication settings if necessary.
215
+ - Save and test the connection to ensure it works correctly.
216
+
217
+ Refer to the [Grafana ClickHouse plugin documentation](https://grafana.com/grafana/plugins/grafana-clickhouse-datasource/) for more details on configuring ClickHouse as a data source.
218
+
219
+
168
220
  ## Contributing
169
221
 
170
222
  Contributions are welcome! Please follow these steps to contribute:
@@ -9,11 +9,12 @@ ioa_observe/sdk/client/http.py,sha256=LdLYSQPFIhKN5BTB-N78jLO7ITl7jGjA0-qpewEIvO
9
9
  ioa_observe/sdk/config/__init__.py,sha256=8aVNaw0yRNLFPxlf97iOZLlJVcV81ivSDnudH2m1OIo,572
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
- ioa_observe/sdk/decorators/__init__.py,sha256=4Jf207nnJR1B371aAqYh17JWp2lsbG1IdX4-w1JlVwo,2380
13
- ioa_observe/sdk/decorators/base.py,sha256=5NAfap6adbemrSAPCSo-rOWsBoh0-T4xY0eiA4OcOzg,24261
14
- ioa_observe/sdk/decorators/util.py,sha256=QZH5k2vIvf7UVjV5Exw3I4S5NrlWVQdkOyd4bpAsjCw,25097
12
+ ioa_observe/sdk/decorators/__init__.py,sha256=sNc_CRL2jeAS1o9vxxj-kY6PhS8wB4NLYQUdUBHbjs4,2685
13
+ ioa_observe/sdk/decorators/base.py,sha256=4ytlog5Ub02BqmhWO5csXvplxp5k1AzheSDwRb7P2SA,26821
14
+ ioa_observe/sdk/decorators/util.py,sha256=WMkzmwD7Js0g1BbId9_qR4pwhnaIJdW588zVc5dpqdQ,25399
15
15
  ioa_observe/sdk/instrumentations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- ioa_observe/sdk/instrumentations/slim.py,sha256=odOnULHV5WqlyiMQ1W8RmoyWJ1jeU66opiuvl6r2U4s,7708
16
+ ioa_observe/sdk/instrumentations/a2a.py,sha256=ov_9ckkymf_qFXG0iXVWfxlW-3kFcP-knrM_t-Cf72w,4414
17
+ ioa_observe/sdk/instrumentations/slim.py,sha256=w2-1JRB-I05bqH_Y4-98XMOh0x7xgAJWUYUBpe2alXQ,7656
17
18
  ioa_observe/sdk/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
19
  ioa_observe/sdk/logging/logging.py,sha256=HZxW9s8Due7jgiNkdI38cIjv5rC9D-Flta3RQMOnpow,2891
19
20
  ioa_observe/sdk/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -26,19 +27,19 @@ ioa_observe/sdk/metrics/agents/heuristics.py,sha256=lcos_mzxXrTMpNWShW9Biy9pXoh8
26
27
  ioa_observe/sdk/metrics/agents/recovery_tracker.py,sha256=I3bWmA11Ue2Cfxnn7C9r2zvh7Hqa8z6EV8wqQhDLALI,4811
27
28
  ioa_observe/sdk/metrics/agents/tool_call_tracker.py,sha256=qmjaaR7rIaQsqCLlDaiYGUeEO3QijhMwyrcpNjvItzI,3169
28
29
  ioa_observe/sdk/metrics/agents/tracker.py,sha256=KN3VFPXrgB1f5VF87ppqS4zG2vxn-9oFCG5K1W2IscE,1361
29
- ioa_observe/sdk/tracing/__init__.py,sha256=RBc_lmUEdCDy1ICW6TtRor_0Tew1SyzYFjT4Sn1pkdo,437
30
+ ioa_observe/sdk/tracing/__init__.py,sha256=jb3_vIAfrChfeaTNnpUKojnn8uUk_ukdFZ1lLru0kRM,433
30
31
  ioa_observe/sdk/tracing/content_allow_list.py,sha256=1fAkpIwUQ7vDwCTkIVrqeltWQtrIbYvj8gz6_7P6NrE,945
31
32
  ioa_observe/sdk/tracing/context_manager.py,sha256=O0JEXYa9h8anhW78R8KKBuqS0j4by1E1KXxNIMPnLr8,400
32
- ioa_observe/sdk/tracing/context_utils.py,sha256=vlLJEq0cqLCScqg1IGaKWQV19tKkHdHQzAt4-pOBbCg,2981
33
+ ioa_observe/sdk/tracing/context_utils.py,sha256=-sYS9vPLI87davV9ubneq5xqbV583CC_c0SmOQS1TAs,2933
33
34
  ioa_observe/sdk/tracing/manual.py,sha256=KS6WN-zw9vAACzXYmnMoJm9d1fenYMfvzeK1GrGDPDE,1937
34
- ioa_observe/sdk/tracing/tracing.py,sha256=ZvWAsKEG-iGuef5EeLiHtSgLoYUCrQPA5RdaQXF1DC0,37927
35
+ ioa_observe/sdk/tracing/tracing.py,sha256=jU-qeIccoPUFms2uzYUHoBoACzkCqcdEW6PNzi8uz3M,38883
35
36
  ioa_observe/sdk/utils/__init__.py,sha256=UPn182U-UblF_XwXaFpx8F-TmQTbm1LYf9y89uSp5Hw,704
36
37
  ioa_observe/sdk/utils/const.py,sha256=GwbHakKPjBL4wLqAVkDrSoKB-8p18EUrbaqPuRuV_xg,1099
37
38
  ioa_observe/sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
38
39
  ioa_observe/sdk/utils/json_encoder.py,sha256=g4NQ0tTqgWssY6I1D7r4zo0G6PiUo61jhofTAw5-jno,639
39
40
  ioa_observe/sdk/utils/package_check.py,sha256=1d1MjxhwoEZIx9dumirT2pRsEWgn-m-SI4npDeEalew,576
40
- ioa_observe_sdk-1.0.6.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
41
- ioa_observe_sdk-1.0.6.dist-info/METADATA,sha256=uRkW5gpM6gtLbDCCvs5---NfvLYUYbC32IR4Dv6a33o,5428
42
- ioa_observe_sdk-1.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
- ioa_observe_sdk-1.0.6.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
44
- ioa_observe_sdk-1.0.6.dist-info/RECORD,,
41
+ ioa_observe_sdk-1.0.8.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
42
+ ioa_observe_sdk-1.0.8.dist-info/METADATA,sha256=2hYQs5RbHURU_6Eu91BEbM7WTWeJ2I_aFCzLr3JUrIM,7746
43
+ ioa_observe_sdk-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
+ ioa_observe_sdk-1.0.8.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
45
+ ioa_observe_sdk-1.0.8.dist-info/RECORD,,