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.
- monocle_apptrace/__main__.py +1 -1
- monocle_apptrace/exporters/file_exporter.py +125 -37
- monocle_apptrace/instrumentation/common/__init__.py +16 -1
- monocle_apptrace/instrumentation/common/constants.py +14 -1
- monocle_apptrace/instrumentation/common/instrumentor.py +19 -152
- monocle_apptrace/instrumentation/common/method_wrappers.py +376 -0
- monocle_apptrace/instrumentation/common/span_handler.py +58 -32
- monocle_apptrace/instrumentation/common/utils.py +52 -15
- monocle_apptrace/instrumentation/common/wrapper.py +124 -18
- monocle_apptrace/instrumentation/common/wrapper_method.py +48 -1
- monocle_apptrace/instrumentation/metamodel/a2a/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/a2a/_helper.py +37 -0
- monocle_apptrace/instrumentation/metamodel/a2a/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/a2a/entities/inference.py +112 -0
- monocle_apptrace/instrumentation/metamodel/a2a/methods.py +22 -0
- monocle_apptrace/instrumentation/metamodel/adk/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/adk/_helper.py +182 -0
- monocle_apptrace/instrumentation/metamodel/adk/entities/agent.py +50 -0
- monocle_apptrace/instrumentation/metamodel/adk/entities/tool.py +57 -0
- monocle_apptrace/instrumentation/metamodel/adk/methods.py +24 -0
- monocle_apptrace/instrumentation/metamodel/agents/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/agents/_helper.py +220 -0
- monocle_apptrace/instrumentation/metamodel/agents/agents_processor.py +152 -0
- monocle_apptrace/instrumentation/metamodel/agents/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/agents/entities/inference.py +191 -0
- monocle_apptrace/instrumentation/metamodel/agents/methods.py +56 -0
- monocle_apptrace/instrumentation/metamodel/aiohttp/_helper.py +6 -11
- monocle_apptrace/instrumentation/metamodel/anthropic/_helper.py +112 -18
- monocle_apptrace/instrumentation/metamodel/anthropic/entities/inference.py +18 -10
- monocle_apptrace/instrumentation/metamodel/azfunc/_helper.py +13 -11
- monocle_apptrace/instrumentation/metamodel/azfunc/entities/http.py +5 -0
- monocle_apptrace/instrumentation/metamodel/azureaiinference/_helper.py +88 -8
- monocle_apptrace/instrumentation/metamodel/azureaiinference/entities/inference.py +22 -8
- monocle_apptrace/instrumentation/metamodel/botocore/_helper.py +92 -16
- monocle_apptrace/instrumentation/metamodel/botocore/entities/inference.py +13 -8
- monocle_apptrace/instrumentation/metamodel/botocore/handlers/botocore_span_handler.py +1 -1
- monocle_apptrace/instrumentation/metamodel/fastapi/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/fastapi/_helper.py +82 -0
- monocle_apptrace/instrumentation/metamodel/fastapi/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/fastapi/entities/http.py +44 -0
- monocle_apptrace/instrumentation/metamodel/fastapi/methods.py +23 -0
- monocle_apptrace/instrumentation/metamodel/finish_types.py +463 -0
- monocle_apptrace/instrumentation/metamodel/flask/_helper.py +6 -11
- monocle_apptrace/instrumentation/metamodel/gemini/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/gemini/_helper.py +120 -0
- monocle_apptrace/instrumentation/metamodel/gemini/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/gemini/entities/inference.py +86 -0
- monocle_apptrace/instrumentation/metamodel/gemini/entities/retrieval.py +43 -0
- monocle_apptrace/instrumentation/metamodel/gemini/methods.py +31 -0
- monocle_apptrace/instrumentation/metamodel/haystack/_helper.py +79 -8
- monocle_apptrace/instrumentation/metamodel/haystack/entities/inference.py +15 -10
- monocle_apptrace/instrumentation/metamodel/haystack/methods.py +7 -0
- monocle_apptrace/instrumentation/metamodel/lambdafunc/_helper.py +78 -0
- monocle_apptrace/instrumentation/metamodel/lambdafunc/entities/http.py +51 -0
- monocle_apptrace/instrumentation/metamodel/lambdafunc/methods.py +23 -0
- monocle_apptrace/instrumentation/metamodel/lambdafunc/wrapper.py +23 -0
- monocle_apptrace/instrumentation/metamodel/langchain/_helper.py +145 -19
- monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +19 -10
- monocle_apptrace/instrumentation/metamodel/langgraph/_helper.py +67 -10
- monocle_apptrace/instrumentation/metamodel/langgraph/entities/inference.py +127 -20
- monocle_apptrace/instrumentation/metamodel/langgraph/langgraph_processor.py +46 -0
- monocle_apptrace/instrumentation/metamodel/langgraph/methods.py +35 -9
- monocle_apptrace/instrumentation/metamodel/litellm/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/litellm/_helper.py +89 -0
- monocle_apptrace/instrumentation/metamodel/litellm/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/litellm/entities/inference.py +108 -0
- monocle_apptrace/instrumentation/metamodel/litellm/methods.py +19 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +227 -16
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/agent.py +127 -10
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/inference.py +13 -8
- monocle_apptrace/instrumentation/metamodel/llamaindex/llamaindex_processor.py +62 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +68 -1
- monocle_apptrace/instrumentation/metamodel/mcp/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/mcp/_helper.py +118 -0
- monocle_apptrace/instrumentation/metamodel/mcp/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/mcp/entities/inference.py +48 -0
- monocle_apptrace/instrumentation/metamodel/mcp/mcp_processor.py +8 -0
- monocle_apptrace/instrumentation/metamodel/mcp/methods.py +21 -0
- monocle_apptrace/instrumentation/metamodel/openai/_helper.py +188 -16
- monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +148 -92
- monocle_apptrace/instrumentation/metamodel/openai/entities/retrieval.py +1 -1
- monocle_apptrace/instrumentation/metamodel/teamsai/_helper.py +53 -23
- monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/actionplanner_output_processor.py +1 -1
- monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py +15 -9
- monocle_apptrace/instrumentation/metamodel/teamsai/sample.json +0 -4
- {monocle_apptrace-0.4.1.dist-info → monocle_apptrace-0.5.0.dist-info}/METADATA +27 -11
- monocle_apptrace-0.5.0.dist-info/RECORD +142 -0
- monocle_apptrace-0.4.1.dist-info/RECORD +0 -96
- {monocle_apptrace-0.4.1.dist-info → monocle_apptrace-0.5.0.dist-info}/WHEEL +0 -0
- {monocle_apptrace-0.4.1.dist-info → monocle_apptrace-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {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({'
|
|
31
|
+
messages.append({'system': kwargs.get('instructions', {})})
|
|
25
32
|
if 'input' in kwargs:
|
|
26
|
-
|
|
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 [
|
|
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
|
-
|
|
42
|
-
if status == 'success':
|
|
132
|
+
if status == 'success' or status == 'completed':
|
|
43
133
|
response = arguments["result"]
|
|
44
|
-
if hasattr(response,"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
175
|
+
return get_exception_message(arguments)
|
|
52
176
|
elif hasattr(arguments["result"], "error"):
|
|
53
|
-
|
|
54
|
-
|
|
177
|
+
return arguments["result"].error
|
|
178
|
+
|
|
55
179
|
except (IndexError, AttributeError) as e:
|
|
56
|
-
logger.warning(
|
|
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.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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 =
|
|
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
|
-
|
|
84
|
-
|
|
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 =
|
|
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
|
},
|