ioa-observe-sdk 1.0.12__py3-none-any.whl → 1.0.13__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.
@@ -10,13 +10,20 @@ import types
10
10
  from typing import Optional, TypeVar, Callable, Awaitable, Any, cast, Union
11
11
  import inspect
12
12
 
13
+ from ioa_observe.sdk.decorators.helpers import (
14
+ _is_async_method,
15
+ _get_original_function_name,
16
+ _is_async_generator,
17
+ )
18
+
19
+
13
20
  from langgraph.graph.state import CompiledStateGraph
14
21
  from opentelemetry import trace
15
22
  from opentelemetry import context as context_api
16
23
  from pydantic_core import PydanticSerializationError
17
24
  from typing_extensions import ParamSpec
18
25
 
19
- from ioa_observe.sdk.decorators.util import determine_workflow_type
26
+ from ioa_observe.sdk.decorators.util import determine_workflow_type, _serialize_object
20
27
  from ioa_observe.sdk.metrics.agents.availability import agent_availability
21
28
  from ioa_observe.sdk.metrics.agents.recovery_tracker import agent_recovery_tracker
22
29
  from ioa_observe.sdk.metrics.agents.tool_call_tracker import tool_call_tracker
@@ -79,14 +86,6 @@ def _should_send_prompts():
79
86
  ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
80
87
 
81
88
 
82
- # Unified Decorators : handles both sync and async operations
83
-
84
-
85
- def _is_async_method(fn):
86
- # check if co-routine function or async generator( example : using async & yield)
87
- return inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn)
88
-
89
-
90
89
  def _setup_span(
91
90
  entity_name,
92
91
  tlp_span_kind: Optional[ObserveSpanKindValues] = None,
@@ -164,24 +163,24 @@ def _handle_span_input(span, args, kwargs, cls=None):
164
163
  # Safely convert args
165
164
  for arg in args:
166
165
  try:
167
- # Test if the object can be JSON serialized
166
+ # Check if the object can be JSON serialized directly
168
167
  json.dumps(arg)
169
168
  safe_args.append(arg)
170
169
  except (TypeError, ValueError, PydanticSerializationError):
171
- # If it can't be serialized, use string representation
172
- safe_args.append(str(arg))
170
+ # Use intelligent serialization
171
+ safe_args.append(_serialize_object(arg))
173
172
 
174
173
  # Safely convert kwargs
175
174
  for key, value in kwargs.items():
176
175
  try:
177
- # Test if the object can be JSON serialized
176
+ # Test if the object can be JSON serialized directly
178
177
  json.dumps(value)
179
178
  safe_kwargs[key] = value
180
179
  except (TypeError, ValueError, PydanticSerializationError):
181
- # If it can't be serialized, use string representation
182
- safe_kwargs[key] = str(value)
180
+ # Use intelligent serialization
181
+ safe_kwargs[key] = _serialize_object(value)
183
182
 
184
- # Create the JSON without custom encoder to avoid recursion
183
+ # Create the JSON
185
184
  json_input = json.dumps({"args": safe_args, "kwargs": safe_kwargs})
186
185
 
187
186
  if _is_json_size_valid(json_input):
@@ -212,62 +211,55 @@ def _handle_span_output(span, tlp_span_kind, res, cls=None):
212
211
  # end_span.end() # end the span immediately
213
212
  set_agent_id_event("") # reset the agent id event
214
213
  # Add agent interpretation scoring
215
- if (
216
- tlp_span_kind == ObserveSpanKindValues.AGENT
217
- or tlp_span_kind == ObserveSpanKindValues.WORKFLOW
218
- ):
219
- current_agent = span.attributes.get("agent_id", "unknown")
220
-
221
- # Determine next agent from response (if Command object with goto)
222
- next_agent = None
223
- if isinstance(res, dict) and "goto" in res:
224
- next_agent = res["goto"]
225
- # Check if there's an error flag in the response
226
- success = not (
227
- res.get("error", False) or res.get("goto") == "__end__"
214
+ if (
215
+ tlp_span_kind == ObserveSpanKindValues.AGENT
216
+ or tlp_span_kind == ObserveSpanKindValues.WORKFLOW
217
+ ):
218
+ current_agent = span.attributes.get("agent_id", "unknown")
219
+
220
+ # Determine next agent from response (if Command object with goto)
221
+ next_agent = None
222
+ if isinstance(res, dict) and "goto" in res:
223
+ next_agent = res["goto"]
224
+ # Check if there's an error flag in the response
225
+ success = not (res.get("error", False) or res.get("goto") == "__end__")
226
+
227
+ # If we have a chain of communication, compute interpretation score
228
+ if next_agent and next_agent != "__end__":
229
+ score = compute_agent_interpretation_score(
230
+ sender_agent=current_agent,
231
+ receiver_agent=next_agent,
232
+ data=res,
233
+ )
234
+ span.set_attribute("gen_ai.ioa.agent.interpretation_score", score)
235
+ reliability = connection_tracker.record_connection(
236
+ sender=current_agent, receiver=next_agent, success=success
237
+ )
238
+ span.set_attribute(
239
+ "gen_ai.ioa.agent.connection_reliability", reliability
228
240
  )
229
241
 
230
- # If we have a chain of communication, compute interpretation score
231
- if next_agent and next_agent != "__end__":
232
- score = compute_agent_interpretation_score(
233
- sender_agent=current_agent,
234
- receiver_agent=next_agent,
235
- data=res,
236
- )
237
- span.set_attribute(
238
- "gen_ai.ioa.agent.interpretation_score", score
239
- )
240
- reliability = connection_tracker.record_connection(
241
- sender=current_agent, receiver=next_agent, success=success
242
- )
243
- span.set_attribute(
244
- "gen_ai.ioa.agent.connection_reliability", reliability
245
- )
246
-
247
- if _should_send_prompts():
242
+ if _should_send_prompts():
243
+ try:
244
+ # Try direct JSON serialization first
245
+ json_output = json.dumps(res)
246
+ except (TypeError, PydanticSerializationError, ValueError):
247
+ # Use intelligent serialization for complex objects
248
248
  try:
249
- # Try direct JSON serialization first (without custom encoder)
250
- json_output = json.dumps(res)
251
- except (TypeError, PydanticSerializationError, ValueError):
252
- # Fallback for objects that can't be directly serialized
253
- try:
254
- # Try to serialize a string representation
255
- safe_output = str(res)
256
- json_output = json.dumps(
257
- {"__str_representation__": safe_output}
258
- )
259
- except Exception:
260
- # If all serialization fails, skip output attribute
261
- json_output = None
249
+ serialized_res = _serialize_object(res)
250
+ json_output = json.dumps(serialized_res)
251
+ except Exception:
252
+ # If all serialization fails, skip output attribute
253
+ json_output = None
262
254
 
263
- if json_output and _is_json_size_valid(json_output):
264
- span.set_attribute(
265
- OBSERVE_ENTITY_OUTPUT,
266
- json_output,
267
- )
268
- TracerWrapper().span_processor_on_ending(
269
- span
270
- ) # record the response latency
255
+ if json_output and _is_json_size_valid(json_output):
256
+ span.set_attribute(
257
+ OBSERVE_ENTITY_OUTPUT,
258
+ json_output,
259
+ )
260
+ TracerWrapper().span_processor_on_ending(
261
+ span
262
+ ) # record the response latency
271
263
  except Exception as e:
272
264
  print(f"Warning: Failed to serialize output for span: {e}")
273
265
  Telemetry().log_exception(e)
@@ -301,9 +293,9 @@ def entity_method(
301
293
  ) -> Callable[[F], F]:
302
294
  def decorate(fn: F) -> F:
303
295
  is_async = _is_async_method(fn)
304
- entity_name = name or fn.__qualname__
296
+ entity_name = name or _get_original_function_name(fn)
305
297
  if is_async:
306
- if inspect.isasyncgenfunction(fn):
298
+ if _is_async_generator(fn):
307
299
 
308
300
  @wraps(fn)
309
301
  async def async_gen_wrap(*args: Any, **kwargs: Any) -> Any:
@@ -0,0 +1,66 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import inspect
5
+
6
+
7
+ def _is_async_method(fn):
8
+ # check if co-routine function or async generator( example : using async & yield)
9
+ if inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn):
10
+ return True
11
+
12
+ # Check if this is a wrapped function that might hide the original async nature
13
+ # Look for common wrapper attributes that might contain the original function
14
+ for attr_name in ["__wrapped__", "func", "_func", "function"]:
15
+ if hasattr(fn, attr_name):
16
+ wrapped_fn = getattr(fn, attr_name)
17
+ if wrapped_fn and callable(wrapped_fn):
18
+ if inspect.iscoroutinefunction(
19
+ wrapped_fn
20
+ ) or inspect.isasyncgenfunction(wrapped_fn):
21
+ return True
22
+ # Recursively check in case of multiple levels of wrapping
23
+ if _is_async_method(wrapped_fn):
24
+ return True
25
+
26
+ return False
27
+
28
+
29
+ def _is_async_generator(fn):
30
+ """Check if function is an async generator, looking through wrapped functions"""
31
+ if inspect.isasyncgenfunction(fn):
32
+ return True
33
+
34
+ # Check if this is a wrapped function that might hide the original async generator nature
35
+ for attr_name in ["__wrapped__", "func", "_func", "function"]:
36
+ if hasattr(fn, attr_name):
37
+ wrapped_fn = getattr(fn, attr_name)
38
+ if wrapped_fn and callable(wrapped_fn):
39
+ if inspect.isasyncgenfunction(wrapped_fn):
40
+ return True
41
+ # Recursively check in case of multiple levels of wrapping
42
+ if _is_async_generator(wrapped_fn):
43
+ return True
44
+
45
+ return False
46
+
47
+
48
+ def _get_original_function_name(fn):
49
+ """Extract the original function name from potentially wrapped functions"""
50
+ if hasattr(fn, "__qualname__") and fn.__qualname__:
51
+ return fn.__qualname__
52
+
53
+ # Look for the original function in common wrapper attributes
54
+ for attr_name in ["__wrapped__", "func", "_func", "function"]:
55
+ if hasattr(fn, attr_name):
56
+ wrapped_fn = getattr(fn, attr_name)
57
+ if wrapped_fn and callable(wrapped_fn):
58
+ if hasattr(wrapped_fn, "__qualname__") and wrapped_fn.__qualname__:
59
+ return wrapped_fn.__qualname__
60
+ # Recursively check in case of multiple levels of wrapping
61
+ result = _get_original_function_name(wrapped_fn)
62
+ if result:
63
+ return result
64
+
65
+ # Fallback to function name if qualname is not available
66
+ return getattr(fn, "__name__", "unknown_function")
@@ -13,6 +13,129 @@ from llama_index.core.workflow.utils import (
13
13
  )
14
14
 
15
15
 
16
+ def _serialize_object(obj, max_depth=5, current_depth=0):
17
+ """
18
+ Intelligently serialize an object to a more meaningful representation
19
+ """
20
+ if current_depth > max_depth:
21
+ return f"<{type(obj).__name__}:max_depth_reached>"
22
+
23
+ # Handle basic JSON-serializable types
24
+ if obj is None or isinstance(obj, (bool, int, float, str)):
25
+ return obj
26
+
27
+ # Handle lists and tuples
28
+ if isinstance(obj, (list, tuple)):
29
+ try:
30
+ return [
31
+ _serialize_object(item, max_depth, current_depth + 1)
32
+ for item in obj[:10]
33
+ ] # Limit to first 10 items
34
+ except Exception:
35
+ return f"<{type(obj).__name__}:length={len(obj)}>"
36
+
37
+ # Handle dictionaries
38
+ if isinstance(obj, dict):
39
+ try:
40
+ serialized = {}
41
+ for key, value in list(obj.items())[:10]: # Limit to first 10 items
42
+ serialized[str(key)] = _serialize_object(
43
+ value, max_depth, current_depth + 1
44
+ )
45
+ return serialized
46
+ except Exception:
47
+ return f"<dict:keys={len(obj)}>"
48
+
49
+ # Handle common object types with meaningful attributes
50
+ try:
51
+ # Check class attributes first
52
+ class_attrs = {}
53
+ for attr_name in dir(type(obj)):
54
+ if (
55
+ not attr_name.startswith("_")
56
+ and not callable(getattr(type(obj), attr_name, None))
57
+ and hasattr(obj, attr_name)
58
+ ):
59
+ try:
60
+ attr_value = getattr(obj, attr_name)
61
+ if not callable(attr_value):
62
+ class_attrs[attr_name] = _serialize_object(
63
+ attr_value, max_depth, current_depth + 1
64
+ )
65
+ if len(class_attrs) >= 5: # Limit attributes
66
+ break
67
+ except Exception:
68
+ continue
69
+
70
+ # Check if object has a __dict__ with interesting attributes
71
+ instance_attrs = {}
72
+ if hasattr(obj, "__dict__"):
73
+ obj_dict = obj.__dict__
74
+ if obj_dict:
75
+ # Extract meaningful attributes (skip private ones and callables)
76
+ for key, value in obj_dict.items():
77
+ if not key.startswith("_") and not callable(value):
78
+ try:
79
+ instance_attrs[key] = _serialize_object(
80
+ value, max_depth, current_depth + 1
81
+ )
82
+ if len(instance_attrs) >= 5: # Limit attributes
83
+ break
84
+ except Exception:
85
+ continue
86
+
87
+ # Combine class and instance attributes
88
+ all_attrs = {**class_attrs, **instance_attrs}
89
+
90
+ if all_attrs:
91
+ return {
92
+ "__class__": type(obj).__name__,
93
+ "__module__": getattr(type(obj), "__module__", "unknown"),
94
+ "attributes": all_attrs,
95
+ }
96
+
97
+ # Special handling for specific types
98
+ if hasattr(obj, "message") and hasattr(obj.message, "parts"):
99
+ # Handle RequestContext-like objects
100
+ try:
101
+ parts_content = []
102
+ for part in obj.message.parts:
103
+ if hasattr(part, "root") and hasattr(part.root, "text"):
104
+ parts_content.append(part.root.text)
105
+ return {
106
+ "__class__": type(obj).__name__,
107
+ "message_content": parts_content,
108
+ }
109
+ except Exception:
110
+ pass
111
+
112
+ # Check for common readable attributes
113
+ for attr in ["name", "id", "type", "value", "content", "text", "data"]:
114
+ if hasattr(obj, attr):
115
+ try:
116
+ attr_value = getattr(obj, attr)
117
+ if not callable(attr_value):
118
+ return {
119
+ "__class__": type(obj).__name__,
120
+ attr: _serialize_object(
121
+ attr_value, max_depth, current_depth + 1
122
+ ),
123
+ }
124
+ except Exception:
125
+ continue
126
+
127
+ # Fallback to class information
128
+ return {
129
+ "__class__": type(obj).__name__,
130
+ "__module__": getattr(type(obj), "__module__", "unknown"),
131
+ "__repr__": str(obj)[:100] + ("..." if len(str(obj)) > 100 else ""),
132
+ }
133
+
134
+ except Exception:
135
+ # Final fallback
136
+ return f"<{type(obj).__name__}:serialization_failed>"
137
+
138
+
16
139
  def determine_workflow_type(workflow_obj: Any) -> Union[None, dict]:
17
140
  """Determines the workflow type and generates appropriate topology."""
18
141
  # Check if it's a dict mapping agent roles to agent names
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ioa-observe-sdk
3
- Version: 1.0.12
3
+ Version: 1.0.13
4
4
  Summary: IOA Observability SDK
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -10,8 +10,9 @@ 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=Lv5EbouBazvWaYB0N82v26pqKtj2FAqlwfLKEh5e8Q0,3251
13
- ioa_observe/sdk/decorators/base.py,sha256=kR_0KWMMbOdJ_t9m5EZ9ZUj3pmKWEgkj0-E1F3c-nIE,29969
14
- ioa_observe/sdk/decorators/util.py,sha256=WMkzmwD7Js0g1BbId9_qR4pwhnaIJdW588zVc5dpqdQ,25399
13
+ ioa_observe/sdk/decorators/base.py,sha256=wdllPcsb_vtq8Kbu1oF7eg2Z5e-Rgv8XUu1Shsk1Fp4,29395
14
+ ioa_observe/sdk/decorators/helpers.py,sha256=I9HXMBivkZpGDtPe9Ad_UU35p_m_wEPate4r_fU0oOA,2705
15
+ ioa_observe/sdk/decorators/util.py,sha256=v0dBY-ynyiLFHwQSteBfoCZ-rxwPUTgfczokKlevSKM,30068
15
16
  ioa_observe/sdk/instrumentations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  ioa_observe/sdk/instrumentations/a2a.py,sha256=ov_9ckkymf_qFXG0iXVWfxlW-3kFcP-knrM_t-Cf72w,4414
17
18
  ioa_observe/sdk/instrumentations/slim.py,sha256=J5e6XeshH55xXaUiT9_j4R_n6VQELzBjgRAU-AgZGOg,11435
@@ -38,8 +39,8 @@ ioa_observe/sdk/utils/const.py,sha256=GwbHakKPjBL4wLqAVkDrSoKB-8p18EUrbaqPuRuV_x
38
39
  ioa_observe/sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
39
40
  ioa_observe/sdk/utils/json_encoder.py,sha256=g4NQ0tTqgWssY6I1D7r4zo0G6PiUo61jhofTAw5-jno,639
40
41
  ioa_observe/sdk/utils/package_check.py,sha256=1d1MjxhwoEZIx9dumirT2pRsEWgn-m-SI4npDeEalew,576
41
- ioa_observe_sdk-1.0.12.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
42
- ioa_observe_sdk-1.0.12.dist-info/METADATA,sha256=3owAgvI_xgARwMohc3ticBRqKwju2w-poBOu1SILHC4,6109
43
- ioa_observe_sdk-1.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
- ioa_observe_sdk-1.0.12.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
45
- ioa_observe_sdk-1.0.12.dist-info/RECORD,,
42
+ ioa_observe_sdk-1.0.13.dist-info/licenses/LICENSE.md,sha256=55VjUfgjWOS4vv3Cf55gfq-RxjPgRIO2vlgYPUuC5lA,11362
43
+ ioa_observe_sdk-1.0.13.dist-info/METADATA,sha256=h0RjY4epTI6SNW15L-Lxlv0OjN9L0UrfQyQJYGk6lCg,6109
44
+ ioa_observe_sdk-1.0.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
+ ioa_observe_sdk-1.0.13.dist-info/top_level.txt,sha256=Yt-6Y1olZEDqCs2REeqI30WjYx0pLGQSVqzYmDd67N8,12
46
+ ioa_observe_sdk-1.0.13.dist-info/RECORD,,