monocle-apptrace 0.4.2__py3-none-any.whl → 0.5.0__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 (88) hide show
  1. monocle_apptrace/__main__.py +1 -1
  2. monocle_apptrace/exporters/file_exporter.py +125 -37
  3. monocle_apptrace/instrumentation/common/__init__.py +16 -1
  4. monocle_apptrace/instrumentation/common/constants.py +14 -1
  5. monocle_apptrace/instrumentation/common/instrumentor.py +19 -152
  6. monocle_apptrace/instrumentation/common/method_wrappers.py +376 -0
  7. monocle_apptrace/instrumentation/common/span_handler.py +58 -32
  8. monocle_apptrace/instrumentation/common/utils.py +52 -15
  9. monocle_apptrace/instrumentation/common/wrapper.py +124 -18
  10. monocle_apptrace/instrumentation/common/wrapper_method.py +47 -1
  11. monocle_apptrace/instrumentation/metamodel/a2a/__init__.py +0 -0
  12. monocle_apptrace/instrumentation/metamodel/a2a/_helper.py +37 -0
  13. monocle_apptrace/instrumentation/metamodel/a2a/entities/__init__.py +0 -0
  14. monocle_apptrace/instrumentation/metamodel/a2a/entities/inference.py +112 -0
  15. monocle_apptrace/instrumentation/metamodel/a2a/methods.py +22 -0
  16. monocle_apptrace/instrumentation/metamodel/adk/__init__.py +0 -0
  17. monocle_apptrace/instrumentation/metamodel/adk/_helper.py +182 -0
  18. monocle_apptrace/instrumentation/metamodel/adk/entities/agent.py +50 -0
  19. monocle_apptrace/instrumentation/metamodel/adk/entities/tool.py +57 -0
  20. monocle_apptrace/instrumentation/metamodel/adk/methods.py +24 -0
  21. monocle_apptrace/instrumentation/metamodel/agents/__init__.py +0 -0
  22. monocle_apptrace/instrumentation/metamodel/agents/_helper.py +220 -0
  23. monocle_apptrace/instrumentation/metamodel/agents/agents_processor.py +152 -0
  24. monocle_apptrace/instrumentation/metamodel/agents/entities/__init__.py +0 -0
  25. monocle_apptrace/instrumentation/metamodel/agents/entities/inference.py +191 -0
  26. monocle_apptrace/instrumentation/metamodel/agents/methods.py +56 -0
  27. monocle_apptrace/instrumentation/metamodel/aiohttp/_helper.py +6 -11
  28. monocle_apptrace/instrumentation/metamodel/anthropic/_helper.py +112 -18
  29. monocle_apptrace/instrumentation/metamodel/anthropic/entities/inference.py +18 -10
  30. monocle_apptrace/instrumentation/metamodel/azfunc/_helper.py +13 -11
  31. monocle_apptrace/instrumentation/metamodel/azfunc/entities/http.py +5 -0
  32. monocle_apptrace/instrumentation/metamodel/azureaiinference/_helper.py +88 -8
  33. monocle_apptrace/instrumentation/metamodel/azureaiinference/entities/inference.py +22 -8
  34. monocle_apptrace/instrumentation/metamodel/botocore/_helper.py +92 -16
  35. monocle_apptrace/instrumentation/metamodel/botocore/entities/inference.py +13 -8
  36. monocle_apptrace/instrumentation/metamodel/botocore/handlers/botocore_span_handler.py +1 -1
  37. monocle_apptrace/instrumentation/metamodel/fastapi/__init__.py +0 -0
  38. monocle_apptrace/instrumentation/metamodel/fastapi/_helper.py +82 -0
  39. monocle_apptrace/instrumentation/metamodel/fastapi/entities/__init__.py +0 -0
  40. monocle_apptrace/instrumentation/metamodel/fastapi/entities/http.py +44 -0
  41. monocle_apptrace/instrumentation/metamodel/fastapi/methods.py +23 -0
  42. monocle_apptrace/instrumentation/metamodel/finish_types.py +463 -0
  43. monocle_apptrace/instrumentation/metamodel/flask/_helper.py +6 -11
  44. monocle_apptrace/instrumentation/metamodel/gemini/_helper.py +51 -7
  45. monocle_apptrace/instrumentation/metamodel/gemini/entities/inference.py +22 -11
  46. monocle_apptrace/instrumentation/metamodel/gemini/entities/retrieval.py +43 -0
  47. monocle_apptrace/instrumentation/metamodel/gemini/methods.py +18 -1
  48. monocle_apptrace/instrumentation/metamodel/haystack/_helper.py +79 -8
  49. monocle_apptrace/instrumentation/metamodel/haystack/entities/inference.py +15 -10
  50. monocle_apptrace/instrumentation/metamodel/haystack/methods.py +7 -0
  51. monocle_apptrace/instrumentation/metamodel/lambdafunc/_helper.py +78 -0
  52. monocle_apptrace/instrumentation/metamodel/lambdafunc/entities/http.py +51 -0
  53. monocle_apptrace/instrumentation/metamodel/lambdafunc/methods.py +23 -0
  54. monocle_apptrace/instrumentation/metamodel/lambdafunc/wrapper.py +23 -0
  55. monocle_apptrace/instrumentation/metamodel/langchain/_helper.py +145 -19
  56. monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +19 -10
  57. monocle_apptrace/instrumentation/metamodel/langgraph/_helper.py +67 -10
  58. monocle_apptrace/instrumentation/metamodel/langgraph/entities/inference.py +127 -20
  59. monocle_apptrace/instrumentation/metamodel/langgraph/langgraph_processor.py +46 -0
  60. monocle_apptrace/instrumentation/metamodel/langgraph/methods.py +35 -9
  61. monocle_apptrace/instrumentation/metamodel/litellm/__init__.py +0 -0
  62. monocle_apptrace/instrumentation/metamodel/litellm/_helper.py +89 -0
  63. monocle_apptrace/instrumentation/metamodel/litellm/entities/__init__.py +0 -0
  64. monocle_apptrace/instrumentation/metamodel/litellm/entities/inference.py +108 -0
  65. monocle_apptrace/instrumentation/metamodel/litellm/methods.py +19 -0
  66. monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +227 -16
  67. monocle_apptrace/instrumentation/metamodel/llamaindex/entities/agent.py +127 -10
  68. monocle_apptrace/instrumentation/metamodel/llamaindex/entities/inference.py +13 -8
  69. monocle_apptrace/instrumentation/metamodel/llamaindex/llamaindex_processor.py +62 -0
  70. monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +68 -1
  71. monocle_apptrace/instrumentation/metamodel/mcp/__init__.py +0 -0
  72. monocle_apptrace/instrumentation/metamodel/mcp/_helper.py +118 -0
  73. monocle_apptrace/instrumentation/metamodel/mcp/entities/__init__.py +0 -0
  74. monocle_apptrace/instrumentation/metamodel/mcp/entities/inference.py +48 -0
  75. monocle_apptrace/instrumentation/metamodel/mcp/mcp_processor.py +8 -0
  76. monocle_apptrace/instrumentation/metamodel/mcp/methods.py +21 -0
  77. monocle_apptrace/instrumentation/metamodel/openai/_helper.py +188 -16
  78. monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +148 -92
  79. monocle_apptrace/instrumentation/metamodel/openai/entities/retrieval.py +1 -1
  80. monocle_apptrace/instrumentation/metamodel/teamsai/_helper.py +53 -23
  81. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/actionplanner_output_processor.py +1 -1
  82. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py +15 -9
  83. monocle_apptrace/instrumentation/metamodel/teamsai/sample.json +0 -4
  84. {monocle_apptrace-0.4.2.dist-info → monocle_apptrace-0.5.0.dist-info}/METADATA +27 -11
  85. {monocle_apptrace-0.4.2.dist-info → monocle_apptrace-0.5.0.dist-info}/RECORD +88 -47
  86. {monocle_apptrace-0.4.2.dist-info → monocle_apptrace-0.5.0.dist-info}/WHEEL +0 -0
  87. {monocle_apptrace-0.4.2.dist-info → monocle_apptrace-0.5.0.dist-info}/licenses/LICENSE +0 -0
  88. {monocle_apptrace-0.4.2.dist-info → monocle_apptrace-0.5.0.dist-info}/licenses/NOTICE +0 -0
@@ -2,7 +2,7 @@ from monocle_apptrace.instrumentation.common.wrapper import atask_wrapper, task_
2
2
  from monocle_apptrace.instrumentation.metamodel.llamaindex.entities.inference import (
3
3
  INFERENCE,
4
4
  )
5
- from monocle_apptrace.instrumentation.metamodel.llamaindex.entities.agent import AGENT
5
+ from monocle_apptrace.instrumentation.metamodel.llamaindex.entities.agent import AGENT, TOOLS, AGENT_REQUEST
6
6
  from monocle_apptrace.instrumentation.metamodel.llamaindex.entities.retrieval import (
7
7
  RETRIEVAL,
8
8
  )
@@ -78,6 +78,14 @@ LLAMAINDEX_METHODS = [
78
78
  "wrapper_method": atask_wrapper,
79
79
  "output_processor": INFERENCE
80
80
  },
81
+ {
82
+ "package": "llama_index.core.agent.workflow.multi_agent_workflow",
83
+ "object": "AgentWorkflow",
84
+ "method": "run",
85
+ "span_handler": "llamaindex_agent_handler",
86
+ "wrapper_method": task_wrapper,
87
+ "output_processor": AGENT_REQUEST
88
+ },
81
89
  {
82
90
  "package": "llama_index.core.agent",
83
91
  "object": "ReActAgent",
@@ -85,6 +93,51 @@ LLAMAINDEX_METHODS = [
85
93
  "wrapper_method": task_wrapper,
86
94
  "output_processor": AGENT
87
95
  },
96
+ {
97
+ "package": "llama_index.core.agent",
98
+ "object": "ReActAgent",
99
+ "method": "achat",
100
+ "wrapper_method": atask_wrapper,
101
+ "output_processor": AGENT
102
+ },
103
+ {
104
+ "package": "llama_index.core.agent.workflow.function_agent",
105
+ "object": "FunctionAgent",
106
+ "method": "finalize",
107
+ "wrapper_method": atask_wrapper,
108
+ "output_processor": AGENT
109
+ },
110
+ {
111
+ "package": "llama_index.core.agent.workflow.function_agent",
112
+ "object": "FunctionAgent",
113
+ "method": "take_step",
114
+ "span_handler": "llamaindex_agent_handler",
115
+ "wrapper_method": atask_wrapper
116
+ },
117
+ {
118
+ "package": "llama_index.core.tools.function_tool",
119
+ "object": "FunctionTool",
120
+ "method": "call",
121
+ "span_handler": "llamaindex_single_agent_tool_handler",
122
+ "wrapper_method": task_wrapper,
123
+ "output_processor": TOOLS
124
+ },
125
+ {
126
+ "package": "llama_index.core.tools.function_tool",
127
+ "object": "FunctionTool",
128
+ "method": "acall",
129
+ "span_handler": "llamaindex_single_agent_tool_handler",
130
+ "wrapper_method": atask_wrapper,
131
+ "output_processor": TOOLS
132
+ },
133
+ {
134
+ "package": "llama_index.core.agent.workflow.multi_agent_workflow",
135
+ "object": "AgentWorkflow",
136
+ "method": "_call_tool",
137
+ "span_handler": "llamaindex_tool_handler",
138
+ "wrapper_method": atask_wrapper,
139
+ "output_processor": TOOLS
140
+ },
88
141
  {
89
142
  "package": "llama_index.llms.anthropic",
90
143
  "object": "Anthropic",
@@ -98,5 +151,19 @@ LLAMAINDEX_METHODS = [
98
151
  "method": "achat",
99
152
  "wrapper_method": atask_wrapper,
100
153
  "output_processor": INFERENCE
154
+ },
155
+ {
156
+ "package": "llama_index.llms.gemini",
157
+ "object": "Gemini",
158
+ "method": "chat",
159
+ "wrapper_method": task_wrapper,
160
+ "output_processor": INFERENCE
161
+ },
162
+ {
163
+ "package": "llama_index.llms.gemini",
164
+ "object": "Gemini",
165
+ "method": "achat",
166
+ "wrapper_method": atask_wrapper,
167
+ "output_processor": INFERENCE
101
168
  }
102
169
  ]
@@ -0,0 +1,118 @@
1
+ from monocle_apptrace.instrumentation.common.utils import with_tracer_wrapper
2
+ from opentelemetry.context import attach, set_value, get_value, detach
3
+ from monocle_apptrace.instrumentation.common.utils import resolve_from_alias
4
+ import logging
5
+ import json
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def log(arguments):
11
+ print(f"Arguments: {arguments}")
12
+
13
+
14
+ def get_output_text(arguments):
15
+ # arguments["result"].content[0].text
16
+ if (
17
+ "result" in arguments
18
+ and hasattr(arguments["result"], "tools")
19
+ and isinstance(arguments["result"].tools, list)
20
+ ):
21
+ tools = []
22
+ for tool in arguments["result"].tools:
23
+ if hasattr(tool, "name"):
24
+ tools.append(tool.name)
25
+ return tools
26
+ if (
27
+ "result" in arguments
28
+ and hasattr(arguments["result"], "content")
29
+ and isinstance(arguments["result"].content, list)
30
+ ):
31
+ ret_val = []
32
+ for content in arguments["result"].content:
33
+ if hasattr(content, "text"):
34
+ ret_val.append(content.text)
35
+ return ret_val
36
+
37
+
38
+ def get_name(arguments):
39
+ """Get the name of the tool from the instance."""
40
+
41
+ args = arguments["args"]
42
+ if (
43
+ args
44
+ and hasattr(args[0], "root")
45
+ and hasattr(args[0].root, "params")
46
+ and hasattr(args[0].root.params, "name")
47
+ ):
48
+ # If the first argument has a root with params and name, return that name
49
+ return args[0].root.params.name
50
+
51
+
52
+ def get_type(arguments):
53
+ """Get the type of the tool from the instance."""
54
+ args = arguments["args"]
55
+ if args and hasattr(args[0], "root") and hasattr(args[0].root, "method"):
56
+ # If the first argument has a root with a method, return that method's name
57
+ return args[0].root.method
58
+
59
+
60
+ def get_params_arguments(arguments):
61
+ """Get the params of the tool from the instance."""
62
+
63
+ args = arguments["args"]
64
+ if (
65
+ args
66
+ and hasattr(args[0], "root")
67
+ and hasattr(args[0].root, "params")
68
+ and hasattr(args[0].root.params, "arguments")
69
+ ):
70
+ # If the first argument has a root with params and arguments, return those arguments
71
+ try:
72
+ return json.dumps(args[0].root.params.arguments)
73
+ except (TypeError, ValueError) as e:
74
+ logger.error(f"Error serializing arguments: {e}")
75
+ return str(args[0].root.params.arguments)
76
+
77
+
78
+ def get_url(arguments):
79
+ """Get the URL of the tool from the instance."""
80
+ url = get_value("mcp.url", None)
81
+
82
+ return url
83
+
84
+ # this extracts the url from the langchain mcp adapter tools and attaches it to the context.
85
+ @with_tracer_wrapper
86
+ def langchain_mcp_wrapper(
87
+ tracer: any, handler: any, to_wrap, wrapped, instance, source_path, args, kwargs
88
+ ):
89
+ return_value = None
90
+ try:
91
+ return_value = wrapped(*args, **kwargs)
92
+ return return_value
93
+ finally:
94
+ if (
95
+ return_value
96
+ and hasattr(return_value, "coroutine")
97
+ and kwargs.get("connection", None)
98
+ ):
99
+ try:
100
+ # extract the URL from the connection and attach it to the context
101
+ url = kwargs.get("connection").get("url", None)
102
+ if url:
103
+ # wrap coroutine methods and attach the URL to the context
104
+
105
+ original_coroutine = return_value.coroutine
106
+
107
+ async def wrapped_coroutine(*args1, **kwargs1):
108
+ token = None
109
+ try:
110
+ token = attach(set_value("mcp.url", url))
111
+ return await original_coroutine(*args1, **kwargs1)
112
+ finally:
113
+ detach(token)
114
+
115
+ return_value.coroutine = wrapped_coroutine
116
+
117
+ except Exception as e:
118
+ pass
@@ -0,0 +1,48 @@
1
+ from monocle_apptrace.instrumentation.metamodel.mcp import _helper
2
+
3
+ TOOLS = {
4
+ "type": "agentic.mcp.invocation",
5
+ "attributes": [
6
+ [
7
+ {
8
+ "_comment": "name of the tool",
9
+ "attribute": "name",
10
+ "accessor": lambda arguments: _helper.get_name(arguments),
11
+ },
12
+ {
13
+ "_comment": "tool type",
14
+ "attribute": "type",
15
+ "accessor": lambda arguments: "mcp.server",
16
+ },
17
+ {
18
+ "_comment": "tool url",
19
+ "attribute": "url",
20
+ "accessor": lambda arguments: _helper.get_url(arguments),
21
+ },
22
+ ]
23
+ ],
24
+ "events": [
25
+ {
26
+ "name": "data.input",
27
+ "attributes": [
28
+ {
29
+ "_comment": "this is Tool input",
30
+ "attribute": "input",
31
+ "accessor": lambda arguments: _helper.get_params_arguments(
32
+ arguments
33
+ ),
34
+ },
35
+ ],
36
+ },
37
+ {
38
+ "name": "data.output",
39
+ "attributes": [
40
+ {
41
+ "_comment": "this is Tool output",
42
+ "attribute": "output",
43
+ "accessor": lambda arguments: _helper.get_output_text(arguments)
44
+ },
45
+ ],
46
+ },
47
+ ],
48
+ }
@@ -0,0 +1,8 @@
1
+ from monocle_apptrace.instrumentation.common.span_handler import SpanHandler
2
+ from monocle_apptrace.instrumentation.metamodel.mcp._helper import (
3
+ get_name
4
+ )
5
+
6
+ class MCPAgentHandler(SpanHandler):
7
+ def skip_span(self, to_wrap, wrapped, instance, args, kwargs) -> bool:
8
+ return get_name({"args": args, "kwargs": kwargs}) is None or args[0].root.method == "tools/list"
@@ -0,0 +1,21 @@
1
+ from monocle_apptrace.instrumentation.common.wrapper import atask_wrapper
2
+ from monocle_apptrace.instrumentation.metamodel.mcp import _helper
3
+ from monocle_apptrace.instrumentation.metamodel.mcp.entities.inference import TOOLS
4
+
5
+
6
+ MCP_METHODS = [
7
+ {
8
+ "package": "mcp.shared.session",
9
+ "object": "BaseSession",
10
+ "method": "send_request",
11
+ "wrapper_method": atask_wrapper,
12
+ "span_handler": "mcp_agent_handler",
13
+ "output_processor": TOOLS,
14
+ },
15
+ {
16
+ "package": "langchain_mcp_adapters.tools",
17
+ "object": "",
18
+ "method": "convert_mcp_tool_to_langchain_tool",
19
+ "wrapper_method": _helper.langchain_mcp_wrapper,
20
+ },
21
+ ]
@@ -3,33 +3,123 @@ This module provides utility functions for extracting system, user,
3
3
  and assistant messages from various input formats.
4
4
  """
5
5
 
6
+ import json
6
7
  import logging
8
+ from opentelemetry.context import get_value
7
9
  from monocle_apptrace.instrumentation.common.utils import (
8
10
  Option,
11
+ get_json_dumps,
9
12
  try_option,
10
13
  get_exception_message,
11
14
  get_parent_span,
12
15
  get_status_code,
13
16
  )
14
17
  from monocle_apptrace.instrumentation.common.span_handler import NonFrameworkSpanHandler, WORKFLOW_TYPE_MAP
18
+ from monocle_apptrace.instrumentation.metamodel.finish_types import (
19
+ map_openai_finish_reason_to_finish_type,
20
+ OPENAI_FINISH_REASON_MAPPING
21
+ )
22
+ from monocle_apptrace.instrumentation.common.constants import AGENT_PREFIX_KEY, CHILD_ERROR_CODE, INFERENCE_AGENT_DELEGATION, INFERENCE_COMMUNICATION, INFERENCE_TOOL_CALL
15
23
 
16
24
  logger = logging.getLogger(__name__)
17
25
 
18
-
19
26
  def extract_messages(kwargs):
20
27
  """Extract system and user messages"""
21
28
  try:
22
29
  messages = []
23
30
  if 'instructions' in kwargs:
24
- messages.append({'instructions': kwargs.get('instructions', {})})
31
+ messages.append({'system': kwargs.get('instructions', {})})
25
32
  if 'input' in kwargs:
26
- messages.append({'input': kwargs.get('input', {})})
33
+ if isinstance(kwargs['input'], str):
34
+ messages.append({'user': kwargs.get('input', "")})
35
+ # [
36
+ # {
37
+ # "role": "developer",
38
+ # "content": "Talk like a pirate."
39
+ # },
40
+ # {
41
+ # "role": "user",
42
+ # "content": "Are semicolons optional in JavaScript?"
43
+ # }
44
+ # ]
45
+ if isinstance(kwargs['input'], list):
46
+ # kwargs['input']
47
+ # [
48
+ # {
49
+ # "content": "I need to book a flight from NYC to LAX and also book the Hilton hotel in Los Angeles. Also check the weather in Los Angeles.",
50
+ # "role": "user"
51
+ # },
52
+ # {
53
+ # "arguments": "{}",
54
+ # "call_id": "call_dSljcToR2LWwqWibPt0qjeHD",
55
+ # "name": "transfer_to_flight_agent",
56
+ # "type": "function_call",
57
+ # "id": "fc_689c30f96f708191aabb0ffd8098cdbd016ef325124ac05f",
58
+ # "status": "completed"
59
+ # },
60
+ # {
61
+ # "arguments": "{}",
62
+ # "call_id": "call_z0MTZroziWDUd0fxVemGM5Pg",
63
+ # "name": "transfer_to_hotel_agent",
64
+ # "type": "function_call",
65
+ # "id": "fc_689c30f99b808191a8743ff407fa8ee2016ef325124ac05f",
66
+ # "status": "completed"
67
+ # },
68
+ # {
69
+ # "arguments": "{\"city\":\"Los Angeles\"}",
70
+ # "call_id": "call_rrdRSPv5vcB4pgl6P4W8U2bX",
71
+ # "name": "get_weather_tool",
72
+ # "type": "function_call",
73
+ # "id": "fc_689c30f9b824819196d4ad9379d570f7016ef325124ac05f",
74
+ # "status": "completed"
75
+ # },
76
+ # {
77
+ # "call_id": "call_rrdRSPv5vcB4pgl6P4W8U2bX",
78
+ # "output": "The weather in Los Angeles is sunny and 75.",
79
+ # "type": "function_call_output"
80
+ # },
81
+ # {
82
+ # "call_id": "call_z0MTZroziWDUd0fxVemGM5Pg",
83
+ # "output": "Multiple handoffs detected, ignoring this one.",
84
+ # "type": "function_call_output"
85
+ # },
86
+ # {
87
+ # "call_id": "call_dSljcToR2LWwqWibPt0qjeHD",
88
+ # "output": "{\"assistant\": \"Flight Agent\"}",
89
+ # "type": "function_call_output"
90
+ # }
91
+ # ]
92
+ for item in kwargs['input']:
93
+ if isinstance(item, dict) and 'role' in item and 'content' in item:
94
+ messages.append({item['role']: item['content']})
95
+ elif isinstance(item, dict) and 'type' in item and item['type'] == 'function_call':
96
+ messages.append({
97
+ "tool_function": item.get("name", ""),
98
+ "tool_arguments": item.get("arguments", ""),
99
+ "call_id": item.get("call_id", "")
100
+ })
101
+ elif isinstance(item, dict) and 'type' in item and item['type'] == 'function_call_output':
102
+ messages.append({
103
+ "call_id": item.get("call_id", ""),
104
+ "output": item.get("output", "")
105
+ })
27
106
  if 'messages' in kwargs and len(kwargs['messages']) >0:
28
107
  for msg in kwargs['messages']:
29
108
  if msg.get('content') and msg.get('role'):
30
109
  messages.append({msg['role']: msg['content']})
110
+ elif msg.get('tool_calls') and msg.get('role'):
111
+ try:
112
+ tool_call_messages = []
113
+ for tool_call in msg['tool_calls']:
114
+ tool_call_messages.append(get_json_dumps({
115
+ "tool_function": tool_call.function.name,
116
+ "tool_arguments": tool_call.function.arguments,
117
+ }))
118
+ messages.append({msg['role']: tool_call_messages})
119
+ except Exception as e:
120
+ logger.warning("Warning: Error occurred while processing tool calls: %s", str(e))
31
121
 
32
- return [str(message) for message in messages]
122
+ return [get_json_dumps(message) for message in messages]
33
123
  except Exception as e:
34
124
  logger.warning("Warning: Error occurred in extract_messages: %s", str(e))
35
125
  return []
@@ -37,25 +127,62 @@ def extract_messages(kwargs):
37
127
 
38
128
  def extract_assistant_message(arguments):
39
129
  try:
130
+ messages = []
40
131
  status = get_status_code(arguments)
41
- response: str = ""
42
- if status == 'success':
132
+ if status == 'success' or status == 'completed':
43
133
  response = arguments["result"]
44
- if hasattr(response,"output_text") and len(response.output_text):
45
- return response.output_text
46
- if response is not None and hasattr(response,"choices") and len(response.choices) >0:
47
- if hasattr(response.choices[0],"message"):
48
- return response.choices[0].message.content
134
+ if hasattr(response, "tools") and isinstance(response.tools, list) and len(response.tools) > 0 and isinstance(response.tools[0], dict):
135
+ tools = []
136
+ for tool in response.tools:
137
+ tools.append({
138
+ "tool_id": tool.get("id", ""),
139
+ "tool_name": tool.get("name", ""),
140
+ "tool_arguments": tool.get("arguments", "")
141
+ })
142
+ messages.append({"tools": tools})
143
+ if hasattr(response, "output") and isinstance(response.output, list) and len(response.output) > 0:
144
+ response_messages = []
145
+ role = "assistant"
146
+ for response_message in response.output:
147
+ if(response_message.type == "function_call"):
148
+ role = "tools"
149
+ response_messages.append({
150
+ "tool_id": response_message.call_id,
151
+ "tool_name": response_message.name,
152
+ "tool_arguments": response_message.arguments
153
+ })
154
+ if len(response_messages) > 0:
155
+ messages.append({role: response_messages})
156
+
157
+ if hasattr(response, "output_text") and len(response.output_text):
158
+ role = response.role if hasattr(response, "role") else "assistant"
159
+ messages.append({role: response.output_text})
160
+ if (
161
+ response is not None
162
+ and hasattr(response, "choices")
163
+ and len(response.choices) > 0
164
+ ):
165
+ if hasattr(response.choices[0], "message"):
166
+ role = (
167
+ response.choices[0].message.role
168
+ if hasattr(response.choices[0].message, "role")
169
+ else "assistant"
170
+ )
171
+ messages.append({role: response.choices[0].message.content})
172
+ return get_json_dumps(messages[0]) if messages else ""
49
173
  else:
50
174
  if arguments["exception"] is not None:
51
- response = get_exception_message(arguments)
175
+ return get_exception_message(arguments)
52
176
  elif hasattr(arguments["result"], "error"):
53
- response = arguments["result"].error
54
- return response
177
+ return arguments["result"].error
178
+
55
179
  except (IndexError, AttributeError) as e:
56
- logger.warning("Warning: Error occurred in extract_assistant_message: %s", str(e))
180
+ logger.warning(
181
+ "Warning: Error occurred in extract_assistant_message: %s", str(e)
182
+ )
57
183
  return None
58
184
 
185
+
59
186
  def extract_provider_name(instance):
60
187
  provider_url: Option[str] = try_option(getattr, instance._client.base_url, 'host')
61
188
  return provider_url.unwrap_or(None)
@@ -129,7 +256,7 @@ def get_inference_type(instance):
129
256
 
130
257
  class OpenAISpanHandler(NonFrameworkSpanHandler):
131
258
  def is_teams_span_in_progress(self) -> bool:
132
- return self.is_framework_span_in_progess() and self.get_workflow_name_in_progress() == WORKFLOW_TYPE_MAP["teams.ai"]
259
+ return self.is_framework_span_in_progress() and self.get_workflow_name_in_progress() == WORKFLOW_TYPE_MAP["teams.ai"]
133
260
 
134
261
  # If openAI is being called by Teams AI SDK, then retain the metadata part of the span events
135
262
  def skip_processor(self, to_wrap, wrapped, instance, span, args, kwargs) -> list[str]:
@@ -144,3 +271,48 @@ class OpenAISpanHandler(NonFrameworkSpanHandler):
144
271
  return super().hydrate_events(to_wrap, wrapped, instance, args, kwargs, ret_result, span=parent_span, parent_span=None, ex=ex)
145
272
 
146
273
  return super().hydrate_events(to_wrap, wrapped, instance, args, kwargs, ret_result, span, parent_span=parent_span, ex=ex)
274
+
275
+ def post_task_processing(self, to_wrap, wrapped, instance, args, kwargs, result, ex, span, parent_span):
276
+ # TeamsAI doesn't capture the status and other metadata from underlying OpenAI SDK.
277
+ # Thus we save the OpenAI status code in the parent span and retrieve it here to preserve meaningful error codes.
278
+ if self.is_teams_span_in_progress() and ex is not None:
279
+ if len(span.events) > 1 and span.events[1].name == "data.output" and span.events[1].attributes.get("error_code") is not None:
280
+ parent_span.set_attribute(CHILD_ERROR_CODE, span.events[1].attributes.get("error_code"))
281
+ super().post_task_processing(to_wrap, wrapped, instance, args, kwargs, result, ex, span, parent_span)
282
+
283
+ def extract_finish_reason(arguments):
284
+ """Extract finish_reason from OpenAI response"""
285
+ try:
286
+ if arguments["exception"] is not None:
287
+ if hasattr(arguments["exception"], "code") and arguments["exception"].code in OPENAI_FINISH_REASON_MAPPING.keys():
288
+ return arguments["exception"].code
289
+ response = arguments["result"]
290
+
291
+ # Handle streaming responses
292
+ if hasattr(response, "finish_reason") and response.finish_reason:
293
+ return response.finish_reason
294
+
295
+ # Handle non-streaming responses
296
+ if response is not None and hasattr(response, "choices") and len(response.choices) > 0:
297
+ if hasattr(response.choices[0], "finish_reason"):
298
+ return response.choices[0].finish_reason
299
+ except (IndexError, AttributeError) as e:
300
+ logger.warning("Warning: Error occurred in extract_finish_reason: %s", str(e))
301
+ return None
302
+ return None
303
+
304
+ def map_finish_reason_to_finish_type(finish_reason):
305
+ """Map OpenAI finish_reason to finish_type based on the possible errors mapping"""
306
+ return map_openai_finish_reason_to_finish_type(finish_reason)
307
+
308
+ def agent_inference_type(arguments):
309
+ """Extract agent inference type from OpenAI response"""
310
+ message = json.loads(extract_assistant_message(arguments))
311
+ # message["tools"][0]["tool_name"]
312
+ if message and message.get("tools") and isinstance(message["tools"], list) and len(message["tools"]) > 0:
313
+ agent_prefix = get_value(AGENT_PREFIX_KEY)
314
+ tool_name = message["tools"][0].get("tool_name", "")
315
+ if tool_name and agent_prefix and tool_name.startswith(agent_prefix):
316
+ return INFERENCE_AGENT_DELEGATION
317
+ return INFERENCE_TOOL_CALL
318
+ return INFERENCE_COMMUNICATION