monocle-apptrace 0.6.0__py3-none-any.whl → 0.6.6__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.

Potentially problematic release.


This version of monocle-apptrace might be problematic. Click here for more details.

Files changed (45) hide show
  1. monocle_apptrace/instrumentation/common/constants.py +8 -0
  2. monocle_apptrace/instrumentation/common/span_handler.py +73 -23
  3. monocle_apptrace/instrumentation/common/utils.py +63 -6
  4. monocle_apptrace/instrumentation/common/wrapper.py +111 -42
  5. monocle_apptrace/instrumentation/common/wrapper_method.py +4 -2
  6. monocle_apptrace/instrumentation/metamodel/a2a/methods.py +1 -1
  7. monocle_apptrace/instrumentation/metamodel/adk/_helper.py +2 -1
  8. monocle_apptrace/instrumentation/metamodel/agents/_helper.py +3 -3
  9. monocle_apptrace/instrumentation/metamodel/agents/entities/inference.py +2 -0
  10. monocle_apptrace/instrumentation/metamodel/aiohttp/_helper.py +1 -1
  11. monocle_apptrace/instrumentation/metamodel/anthropic/entities/inference.py +1 -4
  12. monocle_apptrace/instrumentation/metamodel/azfunc/_helper.py +1 -1
  13. monocle_apptrace/instrumentation/metamodel/botocore/_helper.py +5 -0
  14. monocle_apptrace/instrumentation/metamodel/botocore/entities/inference.py +4 -0
  15. monocle_apptrace/instrumentation/metamodel/fastapi/_helper.py +4 -4
  16. monocle_apptrace/instrumentation/metamodel/fastapi/methods.py +4 -4
  17. monocle_apptrace/instrumentation/metamodel/flask/_helper.py +3 -3
  18. monocle_apptrace/instrumentation/metamodel/hugging_face/_helper.py +1 -1
  19. monocle_apptrace/instrumentation/metamodel/hugging_face/entities/inference.py +1 -4
  20. monocle_apptrace/instrumentation/metamodel/lambdafunc/_helper.py +1 -1
  21. monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +1 -4
  22. monocle_apptrace/instrumentation/metamodel/langgraph/_helper.py +30 -6
  23. monocle_apptrace/instrumentation/metamodel/langgraph/entities/inference.py +1 -1
  24. monocle_apptrace/instrumentation/metamodel/langgraph/langgraph_processor.py +88 -19
  25. monocle_apptrace/instrumentation/metamodel/langgraph/methods.py +22 -6
  26. monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +30 -10
  27. monocle_apptrace/instrumentation/metamodel/llamaindex/entities/agent.py +4 -3
  28. monocle_apptrace/instrumentation/metamodel/llamaindex/llamaindex_processor.py +15 -7
  29. monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +1 -8
  30. monocle_apptrace/instrumentation/metamodel/mcp/_helper.py +1 -1
  31. monocle_apptrace/instrumentation/metamodel/mistral/_helper.py +1 -1
  32. monocle_apptrace/instrumentation/metamodel/mistral/entities/inference.py +1 -4
  33. monocle_apptrace/instrumentation/metamodel/mistral/methods.py +0 -8
  34. monocle_apptrace/instrumentation/metamodel/openai/_helper.py +47 -7
  35. monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +20 -4
  36. monocle_apptrace/instrumentation/metamodel/openai/methods.py +1 -1
  37. monocle_apptrace/instrumentation/metamodel/strands/_helper.py +44 -0
  38. monocle_apptrace/instrumentation/metamodel/strands/entities/agent.py +179 -0
  39. monocle_apptrace/instrumentation/metamodel/strands/entities/tool.py +62 -0
  40. monocle_apptrace/instrumentation/metamodel/strands/methods.py +20 -0
  41. {monocle_apptrace-0.6.0.dist-info → monocle_apptrace-0.6.6.dist-info}/METADATA +15 -4
  42. {monocle_apptrace-0.6.0.dist-info → monocle_apptrace-0.6.6.dist-info}/RECORD +45 -41
  43. {monocle_apptrace-0.6.0.dist-info → monocle_apptrace-0.6.6.dist-info}/WHEEL +0 -0
  44. {monocle_apptrace-0.6.0.dist-info → monocle_apptrace-0.6.6.dist-info}/entry_points.txt +0 -0
  45. {monocle_apptrace-0.6.0.dist-info → monocle_apptrace-0.6.6.dist-info}/licenses/LICENSE +0 -0
@@ -4,6 +4,7 @@ This module provides utility functions to extract various attributes from agent
4
4
  """
5
5
 
6
6
  from ast import arguments
7
+ import json
7
8
  from typing import Any, Dict, Optional
8
9
  from monocle_apptrace.instrumentation.metamodel.finish_types import map_adk_finish_reason_to_finish_type
9
10
 
@@ -181,7 +182,7 @@ def extract_tool_input(arguments: Dict[str, Any]) -> Any:
181
182
  Returns:
182
183
  Any: The extracted input data
183
184
  """
184
- return [str(arguments['kwargs'].get('args'))]
185
+ return json.dumps(arguments['kwargs'].get('args', {}))
185
186
 
186
187
  def extract_tool_response(result: Any) -> Any:
187
188
  """
@@ -134,12 +134,12 @@ def extract_tool_input(arguments):
134
134
  1
135
135
  ] # Second argument is usually the JSON string with params
136
136
  if isinstance(tool_input, str):
137
- return [tool_input]
137
+ return tool_input
138
138
  elif isinstance(tool_input, dict):
139
- return [get_json_dumps(tool_input)]
139
+ return get_json_dumps(tool_input)
140
140
 
141
141
  # Fallback to all args
142
- return [str(arg) for arg in arguments["args"]]
142
+ return str(arguments["args"])
143
143
  except Exception as e:
144
144
  logger.warning("Warning: Error occurred in extract_tool_input: %s", str(e))
145
145
  return []
@@ -128,6 +128,7 @@ TOOLS = {
128
128
  {
129
129
  "_comment": "tool type",
130
130
  "attribute": "type",
131
+ "phase": "post_execution",
131
132
  "accessor": lambda arguments: _helper.get_tool_type(arguments["span"]),
132
133
  },
133
134
  {
@@ -206,6 +207,7 @@ AGENT_DELEGATION = {
206
207
  {
207
208
  "_comment": "name of the target agent",
208
209
  "attribute": "to_agent",
210
+ "phase": "post_execution",
209
211
  "accessor": lambda arguments: _helper.extract_handoff_target(arguments),
210
212
  },
211
213
  ]
@@ -67,7 +67,7 @@ def get_function_name(args) -> str:
67
67
  class aiohttpSpanHandler(SpanHandler):
68
68
 
69
69
  def pre_tracing(self, to_wrap, wrapped, instance, args, kwargs):
70
- return aiohttp_pre_tracing(args)
70
+ return aiohttp_pre_tracing(args), None
71
71
 
72
72
  def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value, token):
73
73
  aiohttp_post_tracing(token)
@@ -6,6 +6,7 @@ from monocle_apptrace.instrumentation.common.utils import (get_error_message, re
6
6
 
7
7
  INFERENCE = {
8
8
  "type": SPAN_TYPES.INFERENCE,
9
+ "subtype": lambda arguments: _helper.agent_inference_type(arguments),
9
10
  "attributes": [
10
11
  [
11
12
  {
@@ -80,10 +81,6 @@ INFERENCE = {
80
81
  "_comment": "finish type mapped from finish reason",
81
82
  "attribute": "finish_type",
82
83
  "accessor": lambda arguments: _helper.map_finish_reason_to_finish_type(_helper.extract_finish_reason(arguments))
83
- },
84
- {
85
- "attribute": "inference_sub_type",
86
- "accessor": lambda arguments: _helper.agent_inference_type(arguments)
87
84
  }
88
85
  ]
89
86
  }
@@ -84,7 +84,7 @@ def azure_func_post_tracing(token):
84
84
  class azureSpanHandler(SpanHandler):
85
85
 
86
86
  def pre_tracing(self, to_wrap, wrapped, instance, args, kwargs):
87
- return azure_func_pre_tracing(kwargs)
87
+ return azure_func_pre_tracing(kwargs), None
88
88
 
89
89
  def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value, token):
90
90
  azure_func_post_tracing(token)
@@ -7,6 +7,8 @@ import logging
7
7
  import json
8
8
  from io import BytesIO
9
9
  from functools import wraps
10
+
11
+ from rfc3986 import urlparse
10
12
  from monocle_apptrace.instrumentation.common.span_handler import SpanHandler
11
13
  from monocle_apptrace.instrumentation.common.utils import ( get_exception_message, get_json_dumps, get_status_code,)
12
14
  from monocle_apptrace.instrumentation.metamodel.finish_types import map_bedrock_finish_reason_to_finish_type
@@ -194,3 +196,6 @@ def extract_finish_reason(arguments):
194
196
  def map_finish_reason_to_finish_type(finish_reason):
195
197
  """Map Bedrock finish_reason/stopReason to finish_type."""
196
198
  return map_bedrock_finish_reason_to_finish_type(finish_reason)
199
+
200
+ def extract_provider_name(instance):
201
+ return urlparse(instance.meta.endpoint_url).hostname
@@ -15,6 +15,10 @@ INFERENCE = {
15
15
  {
16
16
  "attribute": "inference_endpoint",
17
17
  "accessor": lambda arguments: arguments['instance'].meta.endpoint_url
18
+ },
19
+ {
20
+ "attribute": "provider_name",
21
+ "accessor": lambda arguments: _helper.extract_provider_name(arguments['instance'])
18
22
  }
19
23
  ],
20
24
  [
@@ -77,12 +77,12 @@ class FastAPISpanHandler(SpanHandler):
77
77
  fastapi_pre_tracing(scope)
78
78
  return super().pre_tracing(to_wrap, wrapped, instance, args, kwargs)
79
79
 
80
- def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value):
80
+ def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value, token):
81
81
  fastapi_post_tracing()
82
- return super().post_tracing(to_wrap, wrapped, instance, args, kwargs, return_value)
82
+ return super().post_tracing(to_wrap, wrapped, instance, args, kwargs, return_value, token)
83
83
 
84
84
  class FastAPIResponseSpanHandler(SpanHandler):
85
- def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value):
85
+ def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value, token):
86
86
  try:
87
87
  ctx = get_current()
88
88
  if ctx is not None:
@@ -92,4 +92,4 @@ class FastAPIResponseSpanHandler(SpanHandler):
92
92
  return_value, parent_span=parent_span)
93
93
  except Exception as e:
94
94
  logger.info(f"Failed to propagate fastapi response: {e}")
95
- super().post_tracing(to_wrap, wrapped, instance, args, kwargs, return_value)
95
+ super().post_tracing(to_wrap, wrapped, instance, args, kwargs, return_value, token)
@@ -3,10 +3,10 @@ from monocle_apptrace.instrumentation.metamodel.fastapi.entities.http import FAS
3
3
 
4
4
  FASTAPI_METHODS = [
5
5
  {
6
- "package": "fastapi",
7
- "object": "FastAPI",
8
- "method": "__call__",
9
- "wrapper_method": task_wrapper,
6
+ "package": "fastapi.routing",
7
+ "object": "APIRoute",
8
+ "method": "handle",
9
+ "wrapper_method": atask_wrapper,
10
10
  "span_name": "fastapi.request",
11
11
  "span_handler": "fastapi_handler",
12
12
  "output_processor": FASTAPI_HTTP_PROCESSOR,
@@ -67,13 +67,13 @@ def flask_post_tracing(token):
67
67
  class FlaskSpanHandler(SpanHandler):
68
68
 
69
69
  def pre_tracing(self, to_wrap, wrapped, instance, args, kwargs):
70
- return flask_pre_tracing(args)
70
+ return flask_pre_tracing(args), None
71
71
 
72
72
  def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value, token):
73
73
  flask_post_tracing(token)
74
74
 
75
75
  class FlaskResponseSpanHandler(SpanHandler):
76
- def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value):
76
+ def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value, token):
77
77
  try:
78
78
  _parent_span_context = get_current()
79
79
  if _parent_span_context is not None:
@@ -82,4 +82,4 @@ class FlaskResponseSpanHandler(SpanHandler):
82
82
  self.hydrate_events(to_wrap, wrapped, instance, args, kwargs, return_value, parent_span=parent_span)
83
83
  except Exception as e:
84
84
  logger.info(f"Failed to propogate flask response: {e}")
85
- super().post_tracing(to_wrap, wrapped, instance, args, kwargs, return_value)
85
+ super().post_tracing(to_wrap, wrapped, instance, args, kwargs, return_value, token)
@@ -95,7 +95,7 @@ def update_span_from_llm_response(result, include_token_counts=False):
95
95
  "total_tokens": getattr(result.usage, "total_tokens", 0),
96
96
  } if include_token_counts else {}
97
97
  # Add other metadata fields like finish_reason, etc.
98
- return {**tokens, "inference_sub_type": "turn_end"}
98
+ return {**tokens}
99
99
 
100
100
 
101
101
  def get_exception_status_code(exc):
@@ -5,6 +5,7 @@ from monocle_apptrace.instrumentation.metamodel.hugging_face import _helper
5
5
 
6
6
  INFERENCE = {
7
7
  "type": SPAN_TYPES.INFERENCE,
8
+ "subtype": lambda arguments: _helper.agent_inference_type(arguments),
8
9
  "attributes": [
9
10
  [
10
11
  {
@@ -86,10 +87,6 @@ INFERENCE = {
86
87
  "_comment": "finish type mapped from finish reason",
87
88
  "attribute": "finish_type",
88
89
  "accessor": lambda arguments: _helper.map_finish_reason_to_finish_type(_helper.extract_finish_reason(arguments))
89
- },
90
- {
91
- "attribute": "inference_sub_type",
92
- "accessor": lambda arguments: _helper.agent_inference_type(arguments)
93
90
  }
94
91
  ]
95
92
  }
@@ -83,7 +83,7 @@ def lambda_func_post_tracing(token):
83
83
 
84
84
  class lambdaSpanHandler(SpanHandler):
85
85
  def pre_tracing(self, to_wrap, wrapped, instance, args, kwargs):
86
- return lambda_func_pre_tracing(kwargs)
86
+ return lambda_func_pre_tracing(kwargs), None
87
87
 
88
88
  def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value,token):
89
89
  lambda_func_post_tracing(token)
@@ -6,6 +6,7 @@ from monocle_apptrace.instrumentation.common.utils import get_error_message, res
6
6
 
7
7
  INFERENCE = {
8
8
  "type": SPAN_TYPES.INFERENCE_FRAMEWORK,
9
+ "subtype": lambda arguments: _helper.agent_inference_type(arguments),
9
10
  "attributes": [
10
11
  [
11
12
  {
@@ -79,10 +80,6 @@ INFERENCE = {
79
80
  "accessor": lambda arguments: _helper.map_finish_reason_to_finish_type(
80
81
  _helper.extract_finish_reason(arguments)
81
82
  )
82
- },
83
- {
84
- "attribute": "inference_sub_type",
85
- "accessor": lambda arguments: _helper.agent_inference_type(arguments)
86
83
  }
87
84
  ]
88
85
  }
@@ -22,9 +22,19 @@ def agent_instructions(arguments):
22
22
  else:
23
23
  return arguments['kwargs']['agent'].instructions
24
24
 
25
+ def extract_request_agent_input(arguments):
26
+ if arguments['kwargs'] is not None and 'input' in arguments['kwargs']:
27
+ history = arguments['kwargs']['input']['messages']
28
+ messages = []
29
+ for message in history:
30
+ if 'content' in message and 'role' in message and message['role'] == "user": # Check if the message is a UserMessage
31
+ messages.append(message['content'])
32
+ return messages
33
+ return []
34
+
25
35
  def extract_agent_input(arguments):
26
- if arguments['result'] is not None and 'messages' in arguments['result']:
27
- history = arguments['result']['messages']
36
+ if arguments['args'] is not None and len(arguments['args']) > 0 and 'messages' in arguments['args'][0]:
37
+ history = arguments['args'][0]['messages']
28
38
  messages = []
29
39
  for message in history:
30
40
  if hasattr(message, 'content') and hasattr(message, 'type') and message.type == "human": # Check if the message is a HumanMessage
@@ -58,6 +68,10 @@ def update_span_from_llm_response(response):
58
68
  def extract_tool_response(result):
59
69
  if result is not None and hasattr(result, 'content'):
60
70
  return result.content
71
+ if isinstance(result, str):
72
+ return result
73
+ if isinstance(result[0], str):
74
+ return result[0]
61
75
  return None
62
76
 
63
77
  def get_status(result):
@@ -66,11 +80,21 @@ def get_status(result):
66
80
  return None
67
81
 
68
82
  def extract_tool_input(arguments):
69
- tool_input = arguments['args'][0]
70
- if isinstance(tool_input, str):
71
- return [tool_input]
83
+ if arguments['args'] and len(arguments['args']) > 0:
84
+ tool_input = arguments['args'][0]
72
85
  else:
73
- return list(tool_input.values())
86
+ tool_input:dict = arguments['kwargs'].copy()
87
+ tool_input.pop('run_manager', None) # remove run_manager if exists
88
+ tool_input.pop('config', None) # remove config if exists
89
+ return str(tool_input)
90
+
91
+ # if isinstance(tool_input, str):
92
+ # return [tool_input]
93
+ # elif isinstance(tool_input, dict):
94
+ # # return array of key value pairs
95
+ # return [f"'{k}': '{str(v)}'" for k, v in tool_input.items()]
96
+ # else:
97
+ # return [str(tool_input)]
74
98
 
75
99
  def get_name(instance):
76
100
  return instance.name if hasattr(instance, 'name') else ""
@@ -73,7 +73,7 @@ AGENT_REQUEST = {
73
73
  {
74
74
  "_comment": "this is Agent input",
75
75
  "attribute": "input",
76
- "accessor": lambda arguments: _helper.extract_agent_input(arguments)
76
+ "accessor": lambda arguments: _helper.extract_request_agent_input(arguments)
77
77
  }
78
78
  ]
79
79
  },
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from opentelemetry.context import set_value, attach, detach, get_value
2
3
  from monocle_apptrace.instrumentation.common.constants import AGENT_PREFIX_KEY, SCOPE_NAME
3
4
  from monocle_apptrace.instrumentation.common.span_handler import SpanHandler
@@ -5,46 +6,114 @@ from monocle_apptrace.instrumentation.metamodel.langgraph._helper import (
5
6
  DELEGATION_NAME_PREFIX, get_name, is_root_agent_name, is_delegation_tool, LANGGRAPTH_AGENT_NAME_KEY
6
7
  )
7
8
  from monocle_apptrace.instrumentation.metamodel.langgraph.entities.inference import (
8
- AGENT_DELEGATION, AGENT_REQUEST
9
+ AGENT_DELEGATION, AGENT_REQUEST, AGENT
9
10
  )
10
11
  from monocle_apptrace.instrumentation.common.scope_wrapper import start_scope, stop_scope
12
+ from monocle_apptrace.instrumentation.common.utils import is_scope_set
13
+ try:
14
+ from langgraph.errors import ParentCommand
15
+ except ImportError:
16
+ ParentCommand = None
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class ParentCommandFilterSpan:
21
+ """A wrapper for spans that filters out ParentCommand exceptions from being recorded."""
22
+
23
+ def __init__(self, span):
24
+ self.span = span
25
+ self.original_record_exception = span.record_exception
26
+
27
+ def record_exception(self, exception, attributes=None, timestamp=None, escaped=False):
28
+ """Filter out ParentCommand exceptions before recording them."""
29
+ try:
30
+ # Check if this is a ParentCommand exception
31
+ if ParentCommand is not None and isinstance(exception, ParentCommand):
32
+ logger.debug("Filtering out ParentCommand exception from span recording")
33
+ return # Don't record ParentCommand exceptions
34
+
35
+ # For all other exceptions, use the original record_exception method
36
+ return self.original_record_exception(exception, attributes, timestamp, escaped)
37
+ except Exception as e:
38
+ logger.debug(f"Error in ParentCommand filtering: {e}")
39
+ # If filtering fails, fall back to original behavior
40
+ return self.original_record_exception(exception, attributes, timestamp, escaped)
11
41
 
12
42
  class LanggraphAgentHandler(SpanHandler):
13
43
  def pre_tracing(self, to_wrap, wrapped, instance, args, kwargs):
14
44
  context = set_value(LANGGRAPTH_AGENT_NAME_KEY, get_name(instance))
15
45
  context = set_value(AGENT_PREFIX_KEY, DELEGATION_NAME_PREFIX, context)
16
46
  scope_name = AGENT_REQUEST.get("type")
17
- if scope_name is not None and is_root_agent_name(instance) and get_value(scope_name, context) is None:
18
- return start_scope(scope_name, scope_value=None, context=context)
47
+ if not is_scope_set(scope_name):
48
+ agent_request_wrapper = to_wrap.copy()
49
+ agent_request_wrapper["output_processor"] = AGENT_REQUEST
50
+ # return start_scope(scope_name, scope_value=None, context=context)
51
+ return attach(context), agent_request_wrapper
19
52
  else:
20
- return attach(context)
53
+ return attach(context), None
21
54
 
22
55
  def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, result, token):
23
56
  if token is not None:
24
57
  detach(token)
25
58
 
59
+ def post_task_processing(self, to_wrap, wrapped, instance, args, kwargs, result, ex, span, parent_span):
60
+ """Apply ParentCommand filtering to the span before task execution."""
61
+ # Apply ParentCommand filtering to this span
62
+ self._apply_parent_command_filtering(span)
63
+ super().post_task_processing(to_wrap, wrapped, instance, args, kwargs, result, ex, span, parent_span)
64
+
65
+ def _apply_parent_command_filtering(self, span):
66
+ """Apply ParentCommand exception filtering to a span."""
67
+ try:
68
+ if hasattr(span, 'record_exception'):
69
+ # Create a filtered wrapper and replace the record_exception method
70
+ filter_wrapper = ParentCommandFilterSpan(span)
71
+ span.record_exception = filter_wrapper.record_exception
72
+ logger.debug("Applied ParentCommand filtering to LangGraph agent span")
73
+ except Exception as e:
74
+ logger.debug(f"Failed to apply ParentCommand filtering: {e}")
75
+
26
76
  # In multi agent scenarios, the root agent is the one that orchestrates the other agents. LangGraph generates an extra root level invoke()
27
77
  # call on top of the supervisor agent invoke().
28
78
  # This span handler resets the parent invoke call as generic type to avoid duplicate attributes/events in supervisor span and this root span.
29
- def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span, parent_span = None, ex:Exception = None) -> bool:
30
- if is_root_agent_name(instance) and "parent.agent.span" in span.attributes:
31
- agent_request_wrapper = to_wrap.copy()
32
- agent_request_wrapper["output_processor"] = AGENT_REQUEST
33
- else:
34
- agent_request_wrapper = to_wrap
35
- if hasattr(instance, 'name') and parent_span is not None and not SpanHandler.is_root_span(parent_span):
36
- parent_span.set_attribute("parent.agent.span", True)
37
- return super().hydrate_span(agent_request_wrapper, wrapped, instance, args, kwargs, result, span, parent_span, ex)
79
+
80
+ def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span, parent_span = None, ex:Exception = None, is_post_exec:bool= False) -> bool:
81
+ # Filter out ParentCommand exceptions as they are LangGraph control flow mechanisms, not actual errors
82
+ if ParentCommand is not None and isinstance(ex, ParentCommand):
83
+ ex = None # Suppress the ParentCommand exception from being recorded
84
+
85
+ return super().hydrate_span(to_wrap, wrapped, instance, args, kwargs, result, span, parent_span, ex, is_post_exec)
38
86
 
39
87
  class LanggraphToolHandler(SpanHandler):
40
- # LangGraph uses an internal tool to initate delegation to other agents. The method is tool invoke() with tool name as `transfer_to_<agent_name>`.
41
- # Hence we usea different output processor for tool invoke() to format the span as agentic.delegation.
42
- def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span, parent_span = None, ex:Exception = None) -> bool:
88
+ def pre_tracing(self, to_wrap, wrapped, instance, args, kwargs):
43
89
  if is_delegation_tool(instance):
44
90
  agent_request_wrapper = to_wrap.copy()
45
91
  agent_request_wrapper["output_processor"] = AGENT_DELEGATION
46
92
  else:
47
- agent_request_wrapper = to_wrap
93
+ agent_request_wrapper = None
94
+ return None, agent_request_wrapper
95
+
96
+ # LangGraph uses an internal tool to initate delegation to other agents. The method is tool invoke() with tool name as `transfer_to_<agent_name>`.
97
+ # Hence we usea different output processor for tool invoke() to format the span as agentic.delegation.
98
+ def post_task_processing(self, to_wrap, wrapped, instance, args, kwargs, result, ex, span, parent_span):
99
+ """Apply ParentCommand filtering to the span before task execution."""
100
+ # Apply ParentCommand filtering to this span
101
+ self._apply_parent_command_filtering(span)
102
+ super().post_task_processing(to_wrap, wrapped, instance, args, kwargs, result, ex, span, parent_span)
103
+
104
+ def _apply_parent_command_filtering(self, span):
105
+ """Apply ParentCommand exception filtering to a span."""
106
+ try:
107
+ if hasattr(span, 'record_exception'):
108
+ # Create a filtered wrapper and replace the record_exception method
109
+ filter_wrapper = ParentCommandFilterSpan(span)
110
+ span.record_exception = filter_wrapper.record_exception
111
+ logger.debug("Applied ParentCommand filtering to LangGraph tool span")
112
+ except Exception as e:
113
+ logger.debug(f"Failed to apply ParentCommand filtering: {e}")
48
114
 
49
- return super().hydrate_span(agent_request_wrapper, wrapped, instance, args, kwargs, result, span, parent_span, ex)
50
-
115
+ def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span, parent_span = None, ex:Exception = None, is_post_exec:bool= False) -> bool:
116
+ # Filter out ParentCommand exceptions as they are LangGraph control flow mechanisms, not actual errors
117
+ if ParentCommand is not None and isinstance(ex, ParentCommand):
118
+ ex = None # Suppress the ParentCommand exception from being recorded
119
+ return super().hydrate_span(to_wrap, wrapped, instance, args, kwargs, result, span, parent_span, ex, is_post_exec)
@@ -24,17 +24,33 @@ LANGGRAPH_METHODS = [
24
24
  "output_processor": AGENT,
25
25
  },
26
26
  {
27
- "package": "langchain_core.tools.base",
28
- "object": "BaseTool",
29
- "method": "run",
27
+ "package": "langchain_core.tools.simple",
28
+ "object": "Tool",
29
+ "method": "_run",
30
30
  "wrapper_method": task_wrapper,
31
31
  "span_handler": "langgraph_tool_handler",
32
32
  "output_processor": TOOLS,
33
33
  },
34
34
  {
35
- "package": "langchain_core.tools.base",
36
- "object": "BaseTool",
37
- "method": "arun",
35
+ "package": "langchain_core.tools.structured",
36
+ "object": "StructuredTool",
37
+ "method": "_run",
38
+ "wrapper_method": task_wrapper,
39
+ "span_handler": "langgraph_tool_handler",
40
+ "output_processor": TOOLS,
41
+ },
42
+ {
43
+ "package": "langchain_core.tools.simple",
44
+ "object": "Tool",
45
+ "method": "_arun",
46
+ "wrapper_method": atask_wrapper,
47
+ "span_handler": "langgraph_tool_handler",
48
+ "output_processor": TOOLS,
49
+ },
50
+ {
51
+ "package": "langchain_core.tools.structured",
52
+ "object": "StructuredTool",
53
+ "method": "_arun",
38
54
  "wrapper_method": atask_wrapper,
39
55
  "span_handler": "langgraph_tool_handler",
40
56
  "output_processor": TOOLS,
@@ -20,6 +20,11 @@ from monocle_apptrace.instrumentation.common.utils import (
20
20
  from monocle_apptrace.instrumentation.metamodel.finish_types import map_llamaindex_finish_reason_to_finish_type
21
21
 
22
22
  LLAMAINDEX_AGENT_NAME_KEY = "_active_agent_name"
23
+
24
+ # Thread-local storage for current agent context
25
+ import threading
26
+ _thread_local = threading.local()
27
+
23
28
  logger = logging.getLogger(__name__)
24
29
 
25
30
  def get_status(result):
@@ -64,18 +69,18 @@ def get_tool_description(arguments):
64
69
  return ""
65
70
 
66
71
  def extract_tool_args(arguments):
67
- tool_args = []
72
+ tool_args = {}
68
73
  if len(arguments['args']) > 1:
69
74
  for key, value in arguments['args'][2].items():
70
75
  # check if value is builtin type or a string
71
76
  if value is not None and isinstance(value, (str, int, float, bool)):
72
- tool_args.append({key, value})
77
+ tool_args[key] = value
73
78
  else:
74
79
  for key, value in arguments['kwargs'].items():
75
80
  # check if value is builtin type or a string
76
81
  if value is not None and isinstance(value, (str, int, float, bool)):
77
- tool_args.append({key, value})
78
- return [get_json_dumps(tool_arg) for tool_arg in tool_args]
82
+ tool_args[key] = value
83
+ return get_json_dumps(tool_args)
79
84
 
80
85
  def extract_tool_response(response):
81
86
  if hasattr(response, 'raw_output'):
@@ -96,12 +101,27 @@ def get_agent_description(instance) -> str:
96
101
  return instance.description
97
102
  return ""
98
103
 
99
- def get_source_agent(parent_span:Span) -> str:
100
- source_agent_name = parent_span.attributes.get(LLAMAINDEX_AGENT_NAME_KEY, "")
101
- if source_agent_name == "" and parent_span.name.startswith("llama_index.core.agent.ReActAgent."):
102
- # Fallback to the agent name from the parent span if not set
103
- source_agent_name = "ReactAgent"
104
- return source_agent_name
104
+ def get_name(instance):
105
+ return instance.name if hasattr(instance, 'name') else ""
106
+
107
+ def set_current_agent(agent_name: str):
108
+ """Set the current agent name in thread-local storage."""
109
+ _thread_local.current_agent = agent_name
110
+
111
+ def get_current_agent() -> str:
112
+ """Get the current agent name from thread-local storage."""
113
+ return getattr(_thread_local, 'current_agent', '')
114
+
115
+ def get_source_agent() -> str:
116
+ """Get the name of the agent that initiated the request."""
117
+ source_agent = get_value(LLAMAINDEX_AGENT_NAME_KEY)
118
+ if source_agent is not None and isinstance(source_agent,str) and source_agent != "":
119
+ return source_agent
120
+
121
+ source_agent = get_current_agent()
122
+ if source_agent:
123
+ return source_agent
124
+ return ""
105
125
 
106
126
  def get_target_agent(results) -> str:
107
127
  if hasattr(results, 'raw_input'):
@@ -111,7 +111,7 @@ TOOLS = {
111
111
  {
112
112
  "_comment": "name of the agent",
113
113
  "attribute": "name",
114
- "accessor": lambda arguments: _helper.get_source_agent(arguments['parent_span'])
114
+ "accessor": lambda arguments: _helper.get_source_agent()
115
115
  },
116
116
  {
117
117
  "_comment": "agent type",
@@ -157,12 +157,13 @@ AGENT_DELEGATION = {
157
157
  {
158
158
  "_comment": "name of the agent",
159
159
  "attribute": "from_agent",
160
- "accessor": lambda arguments: _helper.get_source_agent(arguments['parent_span'])
160
+ "accessor": lambda arguments: _helper.get_source_agent()
161
161
  },
162
162
  {
163
163
  "_comment": "name of the agent called",
164
164
  "attribute": "to_agent",
165
- "accessor": lambda arguments: _helper.get_target_agent(arguments['result'])
165
+ "accessor": lambda arguments: _helper.get_target_agent(arguments['result']),
166
+ "phase": "post_execution"
166
167
  }
167
168
  ]
168
169
  ]