monocle-apptrace 0.4.1__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 (91) 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 +48 -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/__init__.py +0 -0
  45. monocle_apptrace/instrumentation/metamodel/gemini/_helper.py +120 -0
  46. monocle_apptrace/instrumentation/metamodel/gemini/entities/__init__.py +0 -0
  47. monocle_apptrace/instrumentation/metamodel/gemini/entities/inference.py +86 -0
  48. monocle_apptrace/instrumentation/metamodel/gemini/entities/retrieval.py +43 -0
  49. monocle_apptrace/instrumentation/metamodel/gemini/methods.py +31 -0
  50. monocle_apptrace/instrumentation/metamodel/haystack/_helper.py +79 -8
  51. monocle_apptrace/instrumentation/metamodel/haystack/entities/inference.py +15 -10
  52. monocle_apptrace/instrumentation/metamodel/haystack/methods.py +7 -0
  53. monocle_apptrace/instrumentation/metamodel/lambdafunc/_helper.py +78 -0
  54. monocle_apptrace/instrumentation/metamodel/lambdafunc/entities/http.py +51 -0
  55. monocle_apptrace/instrumentation/metamodel/lambdafunc/methods.py +23 -0
  56. monocle_apptrace/instrumentation/metamodel/lambdafunc/wrapper.py +23 -0
  57. monocle_apptrace/instrumentation/metamodel/langchain/_helper.py +145 -19
  58. monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +19 -10
  59. monocle_apptrace/instrumentation/metamodel/langgraph/_helper.py +67 -10
  60. monocle_apptrace/instrumentation/metamodel/langgraph/entities/inference.py +127 -20
  61. monocle_apptrace/instrumentation/metamodel/langgraph/langgraph_processor.py +46 -0
  62. monocle_apptrace/instrumentation/metamodel/langgraph/methods.py +35 -9
  63. monocle_apptrace/instrumentation/metamodel/litellm/__init__.py +0 -0
  64. monocle_apptrace/instrumentation/metamodel/litellm/_helper.py +89 -0
  65. monocle_apptrace/instrumentation/metamodel/litellm/entities/__init__.py +0 -0
  66. monocle_apptrace/instrumentation/metamodel/litellm/entities/inference.py +108 -0
  67. monocle_apptrace/instrumentation/metamodel/litellm/methods.py +19 -0
  68. monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +227 -16
  69. monocle_apptrace/instrumentation/metamodel/llamaindex/entities/agent.py +127 -10
  70. monocle_apptrace/instrumentation/metamodel/llamaindex/entities/inference.py +13 -8
  71. monocle_apptrace/instrumentation/metamodel/llamaindex/llamaindex_processor.py +62 -0
  72. monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +68 -1
  73. monocle_apptrace/instrumentation/metamodel/mcp/__init__.py +0 -0
  74. monocle_apptrace/instrumentation/metamodel/mcp/_helper.py +118 -0
  75. monocle_apptrace/instrumentation/metamodel/mcp/entities/__init__.py +0 -0
  76. monocle_apptrace/instrumentation/metamodel/mcp/entities/inference.py +48 -0
  77. monocle_apptrace/instrumentation/metamodel/mcp/mcp_processor.py +8 -0
  78. monocle_apptrace/instrumentation/metamodel/mcp/methods.py +21 -0
  79. monocle_apptrace/instrumentation/metamodel/openai/_helper.py +188 -16
  80. monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +148 -92
  81. monocle_apptrace/instrumentation/metamodel/openai/entities/retrieval.py +1 -1
  82. monocle_apptrace/instrumentation/metamodel/teamsai/_helper.py +53 -23
  83. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/actionplanner_output_processor.py +1 -1
  84. monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py +15 -9
  85. monocle_apptrace/instrumentation/metamodel/teamsai/sample.json +0 -4
  86. {monocle_apptrace-0.4.1.dist-info → monocle_apptrace-0.5.0.dist-info}/METADATA +27 -11
  87. monocle_apptrace-0.5.0.dist-info/RECORD +142 -0
  88. monocle_apptrace-0.4.1.dist-info/RECORD +0 -96
  89. {monocle_apptrace-0.4.1.dist-info → monocle_apptrace-0.5.0.dist-info}/WHEEL +0 -0
  90. {monocle_apptrace-0.4.1.dist-info → monocle_apptrace-0.5.0.dist-info}/licenses/LICENSE +0 -0
  91. {monocle_apptrace-0.4.1.dist-info → monocle_apptrace-0.5.0.dist-info}/licenses/NOTICE +0 -0
@@ -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
@@ -6,117 +6,159 @@ from monocle_apptrace.instrumentation.metamodel.openai import (
6
6
  _helper,
7
7
  )
8
8
  from monocle_apptrace.instrumentation.common.utils import (
9
+ get_error_message,
9
10
  patch_instance_method,
10
11
  resolve_from_alias,
11
- get_status,
12
- get_exception_status_code,
13
- get_status_code,
14
12
  )
15
13
 
16
14
  logger = logging.getLogger(__name__)
17
15
 
18
16
 
17
+ def _process_stream_item(item, state):
18
+ """Process a single stream item and update state."""
19
+ try:
20
+ if (
21
+ hasattr(item, "type")
22
+ and isinstance(item.type, str)
23
+ and item.type.startswith("response.")
24
+ ):
25
+ if state["waiting_for_first_token"]:
26
+ state["waiting_for_first_token"] = False
27
+ state["first_token_time"] = time.time_ns()
28
+ if item.type == "response.output_text.delta":
29
+ state["accumulated_response"] += item.delta
30
+ if item.type == "response.completed":
31
+ state["stream_closed_time"] = time.time_ns()
32
+ if hasattr(item, "response") and hasattr(item.response, "usage"):
33
+ state["token_usage"] = item.response.usage
34
+ elif (
35
+ hasattr(item, "choices")
36
+ and item.choices
37
+ and item.choices[0].delta
38
+ and item.choices[0].delta.content
39
+ ):
40
+ if hasattr(item.choices[0].delta, "role") and item.choices[0].delta.role:
41
+ state["role"] = item.choices[0].delta.role
42
+ if state["waiting_for_first_token"]:
43
+ state["waiting_for_first_token"] = False
44
+ state["first_token_time"] = time.time_ns()
45
+
46
+ state["accumulated_response"] += item.choices[0].delta.content
47
+ elif (
48
+ hasattr(item, "object")
49
+ and item.object == "chat.completion.chunk"
50
+ and item.usage
51
+ ):
52
+ # Handle the case where the response is a chunk
53
+ state["token_usage"] = item.usage
54
+ state["stream_closed_time"] = time.time_ns()
55
+ # Capture finish_reason from the chunk
56
+ if (
57
+ hasattr(item, "choices")
58
+ and item.choices
59
+ and len(item.choices) > 0
60
+ and hasattr(item.choices[0], "finish_reason")
61
+ and item.choices[0].finish_reason
62
+ ):
63
+ finish_reason = item.choices[0].finish_reason
64
+ state["finish_reason"] = finish_reason
65
+
66
+ except Exception as e:
67
+ logger.warning(
68
+ "Warning: Error occurred while processing stream item: %s",
69
+ str(e),
70
+ )
71
+ finally:
72
+ state["accumulated_temp_list"].append(item)
73
+
74
+
75
+ def _create_span_result(state, stream_start_time):
76
+ # extract tool calls from the accumulated_temp_list
77
+ # this can only be done when all the streaming is complete.
78
+ for item in state["accumulated_temp_list"]:
79
+ try:
80
+ if (
81
+ item.choices
82
+ and isinstance(item.choices, list)
83
+ and hasattr(item.choices[0], "delta")
84
+ and hasattr(item.choices[0].delta, "tool_calls")
85
+ and item.choices[0].delta.tool_calls
86
+ and item.choices[0].delta.tool_calls[0].id
87
+ and item.choices[0].delta.tool_calls[0].function
88
+ ):
89
+ state["tools"] = state.get("tools", [])
90
+ state["tools"].append(
91
+ {
92
+ "id": item.choices[0].delta.tool_calls[0].id,
93
+ "name": item.choices[0].delta.tool_calls[0].function.name,
94
+ "arguments": item.choices[0]
95
+ .delta.tool_calls[0]
96
+ .function.arguments,
97
+ }
98
+ )
99
+ if (item.choices and item.choices[0].finish_reason):
100
+ state["finish_reason"] = item.choices[0].finish_reason
101
+ except Exception as e:
102
+ logger.warning(
103
+ "Warning: Error occurred while processing tool calls: %s",
104
+ str(e),
105
+ )
106
+
107
+ """Create the span result object."""
108
+ return SimpleNamespace(
109
+ type="stream",
110
+ timestamps={
111
+ "role": state["role"],
112
+ "data.input": int(stream_start_time),
113
+ "data.output": int(state["first_token_time"]),
114
+ "metadata": int(state["stream_closed_time"] or time.time_ns()),
115
+ },
116
+ output_text=state["accumulated_response"],
117
+ tools=state["tools"] if "tools" in state else None,
118
+ usage=state["token_usage"],
119
+ finish_reason=state["finish_reason"],
120
+ )
121
+
122
+
19
123
  def process_stream(to_wrap, response, span_processor):
20
- waiting_for_first_token = True
21
124
  stream_start_time = time.time_ns()
22
- first_token_time = stream_start_time
23
- stream_closed_time = None
24
- accumulated_response = ""
25
- token_usage = None
26
- accumulated_temp_list = []
125
+
126
+ # Shared state for both sync and async processing
127
+ state = {
128
+ "waiting_for_first_token": True,
129
+ "first_token_time": stream_start_time,
130
+ "stream_closed_time": None,
131
+ "accumulated_response": "",
132
+ "token_usage": None,
133
+ "accumulated_temp_list": [],
134
+ "finish_reason": None,
135
+ "role": "assistant",
136
+ }
27
137
 
28
138
  if to_wrap and hasattr(response, "__iter__"):
29
139
  original_iter = response.__iter__
30
140
 
31
141
  def new_iter(self):
32
- nonlocal waiting_for_first_token, first_token_time, stream_closed_time, accumulated_response, token_usage
33
-
34
142
  for item in original_iter():
35
- try:
36
- if (
37
- item.choices
38
- and item.choices[0].delta
39
- and item.choices[0].delta.content
40
- ):
41
- if waiting_for_first_token:
42
- waiting_for_first_token = False
43
- first_token_time = time.time_ns()
44
-
45
- accumulated_response += item.choices[0].delta.content
46
- # token_usage = item.usage
47
- elif item.object == "chat.completion.chunk" and item.usage:
48
- # Handle the case where the response is a chunk
49
- token_usage = item.usage
50
- stream_closed_time = time.time_ns()
51
-
52
- except Exception as e:
53
- logger.warning(
54
- "Warning: Error occurred while processing item in new_iter: %s",
55
- str(e),
56
- )
57
- finally:
58
- accumulated_temp_list.append(item)
59
- yield item
143
+ _process_stream_item(item, state)
144
+ yield item
60
145
 
61
146
  if span_processor:
62
- ret_val = SimpleNamespace(
63
- type="stream",
64
- timestamps={
65
- "data.input": int(stream_start_time),
66
- "data.output": int(first_token_time),
67
- "metadata": int(stream_closed_time or time.time_ns()),
68
- },
69
- output_text=accumulated_response,
70
- usage=token_usage,
71
- )
147
+ ret_val = _create_span_result(state, stream_start_time)
72
148
  span_processor(ret_val)
73
149
 
74
150
  patch_instance_method(response, "__iter__", new_iter)
75
-
151
+
76
152
  if to_wrap and hasattr(response, "__aiter__"):
77
153
  original_iter = response.__aiter__
78
154
 
79
155
  async def new_aiter(self):
80
- nonlocal waiting_for_first_token, first_token_time, stream_closed_time, accumulated_response, token_usage
81
-
82
156
  async for item in original_iter():
83
- try:
84
- if (
85
- item.choices
86
- and item.choices[0].delta
87
- and item.choices[0].delta.content
88
- ):
89
- if waiting_for_first_token:
90
- waiting_for_first_token = False
91
- first_token_time = time.time_ns()
92
-
93
- accumulated_response += item.choices[0].delta.content
94
- # token_usage = item.usage
95
- elif item.object == "chat.completion.chunk" and item.usage:
96
- # Handle the case where the response is a chunk
97
- token_usage = item.usage
98
- stream_closed_time = time.time_ns()
99
-
100
- except Exception as e:
101
- logger.warning(
102
- "Warning: Error occurred while processing item in new_aiter: %s",
103
- str(e),
104
- )
105
- finally:
106
- accumulated_temp_list.append(item)
107
- yield item
157
+ _process_stream_item(item, state)
158
+ yield item
108
159
 
109
160
  if span_processor:
110
- ret_val = SimpleNamespace(
111
- type="stream",
112
- timestamps={
113
- "data.input": int(stream_start_time),
114
- "data.output": int(first_token_time),
115
- "metadata": int(stream_closed_time or time.time_ns()),
116
- },
117
- output_text=accumulated_response,
118
- usage=token_usage,
119
- )
161
+ ret_val = _create_span_result(state, stream_start_time)
120
162
  span_processor(ret_val)
121
163
 
122
164
  patch_instance_method(response, "__aiter__", new_aiter)
@@ -198,6 +240,10 @@ INFERENCE = {
198
240
  {
199
241
  "name": "data.output",
200
242
  "attributes": [
243
+ {
244
+ "attribute": "error_code",
245
+ "accessor": lambda arguments: get_error_message(arguments),
246
+ },
201
247
  {
202
248
  "_comment": "this is result from LLM",
203
249
  "attribute": "response",
@@ -205,14 +251,6 @@ INFERENCE = {
205
251
  arguments,
206
252
  ),
207
253
  },
208
- {
209
- "attribute": "status",
210
- "accessor": lambda arguments: get_status(arguments)
211
- },
212
- {
213
- "attribute": "status_code",
214
- "accessor": lambda arguments: get_status_code(arguments)
215
- }
216
254
  ],
217
255
  },
218
256
  {
@@ -223,6 +261,24 @@ INFERENCE = {
223
261
  "accessor": lambda arguments: _helper.update_span_from_llm_response(
224
262
  arguments["result"]
225
263
  ),
264
+ },
265
+ {
266
+ "_comment": "finish reason from OpenAI response",
267
+ "attribute": "finish_reason",
268
+ "accessor": lambda arguments: _helper.extract_finish_reason(
269
+ arguments
270
+ ),
271
+ },
272
+ {
273
+ "_comment": "finish type mapped from finish reason",
274
+ "attribute": "finish_type",
275
+ "accessor": lambda arguments: _helper.map_finish_reason_to_finish_type(
276
+ _helper.extract_finish_reason(arguments)
277
+ ),
278
+ },
279
+ {
280
+ "attribute": "inference_sub_type",
281
+ "accessor": lambda arguments: _helper.agent_inference_type(arguments)
226
282
  }
227
283
  ],
228
284
  },
@@ -4,7 +4,7 @@ from monocle_apptrace.instrumentation.metamodel.openai import (
4
4
  from monocle_apptrace.instrumentation.common.utils import resolve_from_alias
5
5
 
6
6
  RETRIEVAL = {
7
- "type": "retrieval",
7
+ "type": "embedding",
8
8
  "attributes": [
9
9
  [
10
10
  {