monocle-apptrace 0.5.0b1__py3-none-any.whl → 0.5.1b1__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/exporters/file_exporter.py +2 -1
- monocle_apptrace/instrumentation/common/__init__.py +7 -5
- monocle_apptrace/instrumentation/common/constants.py +103 -12
- monocle_apptrace/instrumentation/common/instrumentor.py +1 -6
- monocle_apptrace/instrumentation/common/method_wrappers.py +10 -125
- monocle_apptrace/instrumentation/common/scope_wrapper.py +126 -0
- monocle_apptrace/instrumentation/common/span_handler.py +32 -8
- monocle_apptrace/instrumentation/common/utils.py +34 -3
- monocle_apptrace/instrumentation/common/wrapper.py +208 -41
- monocle_apptrace/instrumentation/common/wrapper_method.py +9 -1
- monocle_apptrace/instrumentation/metamodel/a2a/entities/inference.py +3 -1
- monocle_apptrace/instrumentation/metamodel/adk/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/adk/_helper.py +206 -0
- monocle_apptrace/instrumentation/metamodel/adk/entities/agent.py +111 -0
- monocle_apptrace/instrumentation/metamodel/adk/entities/tool.py +59 -0
- monocle_apptrace/instrumentation/metamodel/adk/methods.py +31 -0
- monocle_apptrace/instrumentation/metamodel/agents/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/agents/_helper.py +225 -0
- monocle_apptrace/instrumentation/metamodel/agents/agents_processor.py +174 -0
- monocle_apptrace/instrumentation/metamodel/agents/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/agents/entities/inference.py +196 -0
- monocle_apptrace/instrumentation/metamodel/agents/methods.py +55 -0
- monocle_apptrace/instrumentation/metamodel/aiohttp/entities/http.py +2 -1
- monocle_apptrace/instrumentation/metamodel/anthropic/_helper.py +82 -5
- monocle_apptrace/instrumentation/metamodel/anthropic/entities/inference.py +6 -1
- monocle_apptrace/instrumentation/metamodel/azfunc/entities/http.py +2 -1
- monocle_apptrace/instrumentation/metamodel/azureaiinference/entities/inference.py +2 -1
- monocle_apptrace/instrumentation/metamodel/botocore/entities/inference.py +2 -1
- monocle_apptrace/instrumentation/metamodel/fastapi/entities/http.py +2 -1
- monocle_apptrace/instrumentation/metamodel/fastapi/methods.py +18 -18
- monocle_apptrace/instrumentation/metamodel/finish_types.py +79 -1
- monocle_apptrace/instrumentation/metamodel/flask/entities/http.py +2 -1
- monocle_apptrace/instrumentation/metamodel/gemini/entities/inference.py +7 -3
- monocle_apptrace/instrumentation/metamodel/gemini/entities/retrieval.py +2 -1
- monocle_apptrace/instrumentation/metamodel/gemini/methods.py +8 -1
- monocle_apptrace/instrumentation/metamodel/haystack/_helper.py +64 -0
- monocle_apptrace/instrumentation/metamodel/haystack/entities/inference.py +12 -1
- monocle_apptrace/instrumentation/metamodel/haystack/entities/retrieval.py +2 -1
- monocle_apptrace/instrumentation/metamodel/lambdafunc/entities/http.py +2 -1
- monocle_apptrace/instrumentation/metamodel/langchain/_helper.py +18 -0
- monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +6 -1
- monocle_apptrace/instrumentation/metamodel/langchain/entities/retrieval.py +2 -1
- monocle_apptrace/instrumentation/metamodel/langgraph/_helper.py +6 -0
- monocle_apptrace/instrumentation/metamodel/langgraph/entities/inference.py +10 -5
- monocle_apptrace/instrumentation/metamodel/langgraph/langgraph_processor.py +11 -4
- monocle_apptrace/instrumentation/metamodel/langgraph/methods.py +27 -23
- 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 +109 -0
- monocle_apptrace/instrumentation/metamodel/litellm/methods.py +19 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/agent.py +9 -4
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/inference.py +2 -1
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/retrieval.py +2 -1
- monocle_apptrace/instrumentation/metamodel/llamaindex/llamaindex_processor.py +14 -3
- monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +1 -1
- monocle_apptrace/instrumentation/metamodel/mcp/_helper.py +2 -1
- monocle_apptrace/instrumentation/metamodel/mcp/entities/inference.py +3 -1
- monocle_apptrace/instrumentation/metamodel/mcp/mcp_processor.py +0 -5
- monocle_apptrace/instrumentation/metamodel/mcp/methods.py +1 -1
- monocle_apptrace/instrumentation/metamodel/openai/_helper.py +110 -5
- monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +59 -13
- monocle_apptrace/instrumentation/metamodel/requests/entities/http.py +2 -1
- monocle_apptrace/instrumentation/metamodel/teamsai/_helper.py +12 -1
- monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py +12 -1
- monocle_apptrace/mcp_server.py +94 -0
- {monocle_apptrace-0.5.0b1.dist-info → monocle_apptrace-0.5.1b1.dist-info}/METADATA +41 -11
- {monocle_apptrace-0.5.0b1.dist-info → monocle_apptrace-0.5.1b1.dist-info}/RECORD +72 -53
- monocle_apptrace-0.5.1b1.dist-info/entry_points.txt +2 -0
- {monocle_apptrace-0.5.0b1.dist-info → monocle_apptrace-0.5.1b1.dist-info}/WHEEL +0 -0
- {monocle_apptrace-0.5.0b1.dist-info → monocle_apptrace-0.5.1b1.dist-info}/licenses/LICENSE +0 -0
- {monocle_apptrace-0.5.0b1.dist-info → monocle_apptrace-0.5.1b1.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from opentelemetry.context import attach, detach, get_current, get_value, set_value, Context
|
|
2
|
+
from monocle_apptrace.instrumentation.common.constants import AGENT_PREFIX_KEY
|
|
2
3
|
from monocle_apptrace.instrumentation.common.span_handler import SpanHandler
|
|
3
4
|
from monocle_apptrace.instrumentation.metamodel.llamaindex._helper import (
|
|
4
5
|
is_delegation_tool, LLAMAINDEX_AGENT_NAME_KEY, get_agent_name
|
|
@@ -21,14 +22,15 @@ class DelegationHandler(SpanHandler):
|
|
|
21
22
|
|
|
22
23
|
return super().hydrate_span(agent_request_wrapper, wrapped, instance, args, kwargs, result, span, parent_span, ex)
|
|
23
24
|
|
|
24
|
-
|
|
25
25
|
# There are two different APIs for tool calling FunctionTool.call() and AgentWorkflow.tool_call(). In case of single agent calling tool, only the FunctionTool.call() is used. In case of multi agent case,
|
|
26
26
|
# the AgentWorkflow.tool_call() is used which inturn calls FunctionTool.call(). We can't entirely rely on the FunctionTool.call() to extract tool span details, especially the agent delegation details are not available there.
|
|
27
27
|
# Hence we want to distinguish between single agent tool call and multi agent tool call. In case of multi agent tool call, we suppress the FunctionTool.call() span and use AgentWorkflow.tool_call() span to capture the tool call details.
|
|
28
28
|
class LlamaIndexToolHandler(DelegationHandler):
|
|
29
29
|
def pre_tracing(self, to_wrap, wrapped, instance, args, kwargs):
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
cur_context = get_current()
|
|
31
|
+
cur_context = set_value(TOOL_INVOCATION_STARTED, True, cur_context)
|
|
32
|
+
return attach(cur_context)
|
|
33
|
+
|
|
32
34
|
def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value, token=None):
|
|
33
35
|
if token:
|
|
34
36
|
detach(token)
|
|
@@ -40,6 +42,15 @@ class LlamaIndexSingleAgenttToolHandlerWrapper(DelegationHandler):
|
|
|
40
42
|
return super().skip_span(to_wrap, wrapped, instance, args, kwargs)
|
|
41
43
|
|
|
42
44
|
class LlamaIndexAgentHandler(SpanHandler):
|
|
45
|
+
def pre_tracing(self, to_wrap, wrapped, instance, args, kwargs):
|
|
46
|
+
cur_context = get_current()
|
|
47
|
+
cur_context = set_value(AGENT_PREFIX_KEY, "handoff", cur_context)
|
|
48
|
+
return attach(cur_context)
|
|
49
|
+
|
|
50
|
+
def post_tracing(self, to_wrap, wrapped, instance, args, kwargs, return_value, token=None):
|
|
51
|
+
if token:
|
|
52
|
+
detach(token)
|
|
53
|
+
|
|
43
54
|
# LlamaIndex uses direct OpenAI call for agent inferences. Given that the workflow type is set to llamaindex, the openAI inference does not record the input/output events.
|
|
44
55
|
# To avoid this, we set the workflow type to generic for agent inference spans so we can capture the prompts and responses.
|
|
45
56
|
def hydrate_span(self, to_wrap, wrapped, instance, args, kwargs, result, span, parent_span = None, ex:Exception = None) -> bool:
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
from monocle_apptrace.instrumentation.common.constants import SPAN_SUBTYPES, SPAN_TYPES
|
|
1
2
|
from monocle_apptrace.instrumentation.metamodel.mcp import _helper
|
|
2
3
|
|
|
3
4
|
TOOLS = {
|
|
4
|
-
"type":
|
|
5
|
+
"type": SPAN_TYPES.AGENTIC_MCP_INVOCATION,
|
|
6
|
+
"subtype": SPAN_SUBTYPES.ROUTING,
|
|
5
7
|
"attributes": [
|
|
6
8
|
[
|
|
7
9
|
{
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
from opentelemetry.context import set_value, attach, detach
|
|
2
1
|
from monocle_apptrace.instrumentation.common.span_handler import SpanHandler
|
|
3
2
|
from monocle_apptrace.instrumentation.metamodel.mcp._helper import (
|
|
4
3
|
get_name
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
from monocle_apptrace.instrumentation.metamodel.langgraph.entities.inference import (
|
|
8
|
-
AGENT_DELEGATION, AGENT_REQUEST
|
|
9
4
|
)
|
|
10
5
|
|
|
11
6
|
class MCPAgentHandler(SpanHandler):
|
|
@@ -3,7 +3,9 @@ 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,
|
|
9
11
|
get_json_dumps,
|
|
@@ -17,7 +19,7 @@ from monocle_apptrace.instrumentation.metamodel.finish_types import (
|
|
|
17
19
|
map_openai_finish_reason_to_finish_type,
|
|
18
20
|
OPENAI_FINISH_REASON_MAPPING
|
|
19
21
|
)
|
|
20
|
-
from monocle_apptrace.instrumentation.common.constants import CHILD_ERROR_CODE
|
|
22
|
+
from monocle_apptrace.instrumentation.common.constants import AGENT_PREFIX_KEY, CHILD_ERROR_CODE, INFERENCE_AGENT_DELEGATION, INFERENCE_TURN_END, INFERENCE_TOOL_CALL
|
|
21
23
|
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
23
25
|
|
|
@@ -28,7 +30,7 @@ def extract_messages(kwargs):
|
|
|
28
30
|
if 'instructions' in kwargs:
|
|
29
31
|
messages.append({'system': kwargs.get('instructions', {})})
|
|
30
32
|
if 'input' in kwargs:
|
|
31
|
-
if isinstance(kwargs['input'], str):
|
|
33
|
+
if isinstance(kwargs['input'], str):
|
|
32
34
|
messages.append({'user': kwargs.get('input', "")})
|
|
33
35
|
# [
|
|
34
36
|
# {
|
|
@@ -41,13 +43,81 @@ def extract_messages(kwargs):
|
|
|
41
43
|
# }
|
|
42
44
|
# ]
|
|
43
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
|
+
# ]
|
|
44
92
|
for item in kwargs['input']:
|
|
45
93
|
if isinstance(item, dict) and 'role' in item and 'content' in item:
|
|
46
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
|
+
})
|
|
47
106
|
if 'messages' in kwargs and len(kwargs['messages']) >0:
|
|
48
107
|
for msg in kwargs['messages']:
|
|
49
108
|
if msg.get('content') and msg.get('role'):
|
|
50
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))
|
|
51
121
|
|
|
52
122
|
return [get_json_dumps(message) for message in messages]
|
|
53
123
|
except Exception as e:
|
|
@@ -61,6 +131,29 @@ def extract_assistant_message(arguments):
|
|
|
61
131
|
status = get_status_code(arguments)
|
|
62
132
|
if status == 'success' or status == 'completed':
|
|
63
133
|
response = arguments["result"]
|
|
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
|
+
|
|
64
157
|
if hasattr(response, "output_text") and len(response.output_text):
|
|
65
158
|
role = response.role if hasattr(response, "role") else "assistant"
|
|
66
159
|
messages.append({role: response.output_text})
|
|
@@ -82,7 +175,7 @@ def extract_assistant_message(arguments):
|
|
|
82
175
|
return get_exception_message(arguments)
|
|
83
176
|
elif hasattr(arguments["result"], "error"):
|
|
84
177
|
return arguments["result"].error
|
|
85
|
-
|
|
178
|
+
|
|
86
179
|
except (IndexError, AttributeError) as e:
|
|
87
180
|
logger.warning(
|
|
88
181
|
"Warning: Error occurred in extract_assistant_message: %s", str(e)
|
|
@@ -194,11 +287,11 @@ def extract_finish_reason(arguments):
|
|
|
194
287
|
if hasattr(arguments["exception"], "code") and arguments["exception"].code in OPENAI_FINISH_REASON_MAPPING.keys():
|
|
195
288
|
return arguments["exception"].code
|
|
196
289
|
response = arguments["result"]
|
|
197
|
-
|
|
290
|
+
|
|
198
291
|
# Handle streaming responses
|
|
199
292
|
if hasattr(response, "finish_reason") and response.finish_reason:
|
|
200
293
|
return response.finish_reason
|
|
201
|
-
|
|
294
|
+
|
|
202
295
|
# Handle non-streaming responses
|
|
203
296
|
if response is not None and hasattr(response, "choices") and len(response.choices) > 0:
|
|
204
297
|
if hasattr(response.choices[0], "finish_reason"):
|
|
@@ -211,3 +304,15 @@ def extract_finish_reason(arguments):
|
|
|
211
304
|
def map_finish_reason_to_finish_type(finish_reason):
|
|
212
305
|
"""Map OpenAI finish_reason to finish_type based on the possible errors mapping"""
|
|
213
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_TURN_END
|
|
@@ -2,13 +2,14 @@ import logging
|
|
|
2
2
|
import random
|
|
3
3
|
import time
|
|
4
4
|
from types import SimpleNamespace
|
|
5
|
+
from monocle_apptrace.instrumentation.common.constants import SPAN_TYPES
|
|
5
6
|
from monocle_apptrace.instrumentation.metamodel.openai import (
|
|
6
7
|
_helper,
|
|
7
8
|
)
|
|
8
9
|
from monocle_apptrace.instrumentation.common.utils import (
|
|
9
10
|
get_error_message,
|
|
10
11
|
patch_instance_method,
|
|
11
|
-
resolve_from_alias
|
|
12
|
+
resolve_from_alias,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
@@ -17,7 +18,11 @@ logger = logging.getLogger(__name__)
|
|
|
17
18
|
def _process_stream_item(item, state):
|
|
18
19
|
"""Process a single stream item and update state."""
|
|
19
20
|
try:
|
|
20
|
-
if
|
|
21
|
+
if (
|
|
22
|
+
hasattr(item, "type")
|
|
23
|
+
and isinstance(item.type, str)
|
|
24
|
+
and item.type.startswith("response.")
|
|
25
|
+
):
|
|
21
26
|
if state["waiting_for_first_token"]:
|
|
22
27
|
state["waiting_for_first_token"] = False
|
|
23
28
|
state["first_token_time"] = time.time_ns()
|
|
@@ -40,7 +45,11 @@ def _process_stream_item(item, state):
|
|
|
40
45
|
state["first_token_time"] = time.time_ns()
|
|
41
46
|
|
|
42
47
|
state["accumulated_response"] += item.choices[0].delta.content
|
|
43
|
-
elif
|
|
48
|
+
elif (
|
|
49
|
+
hasattr(item, "object")
|
|
50
|
+
and item.object == "chat.completion.chunk"
|
|
51
|
+
and item.usage
|
|
52
|
+
):
|
|
44
53
|
# Handle the case where the response is a chunk
|
|
45
54
|
state["token_usage"] = item.usage
|
|
46
55
|
state["stream_closed_time"] = time.time_ns()
|
|
@@ -49,7 +58,7 @@ def _process_stream_item(item, state):
|
|
|
49
58
|
hasattr(item, "choices")
|
|
50
59
|
and item.choices
|
|
51
60
|
and len(item.choices) > 0
|
|
52
|
-
and hasattr(item.choices[0],
|
|
61
|
+
and hasattr(item.choices[0], "finish_reason")
|
|
53
62
|
and item.choices[0].finish_reason
|
|
54
63
|
):
|
|
55
64
|
finish_reason = item.choices[0].finish_reason
|
|
@@ -65,6 +74,37 @@ def _process_stream_item(item, state):
|
|
|
65
74
|
|
|
66
75
|
|
|
67
76
|
def _create_span_result(state, stream_start_time):
|
|
77
|
+
# extract tool calls from the accumulated_temp_list
|
|
78
|
+
# this can only be done when all the streaming is complete.
|
|
79
|
+
for item in state["accumulated_temp_list"]:
|
|
80
|
+
try:
|
|
81
|
+
if (
|
|
82
|
+
item.choices
|
|
83
|
+
and isinstance(item.choices, list)
|
|
84
|
+
and hasattr(item.choices[0], "delta")
|
|
85
|
+
and hasattr(item.choices[0].delta, "tool_calls")
|
|
86
|
+
and item.choices[0].delta.tool_calls
|
|
87
|
+
and item.choices[0].delta.tool_calls[0].id
|
|
88
|
+
and item.choices[0].delta.tool_calls[0].function
|
|
89
|
+
):
|
|
90
|
+
state["tools"] = state.get("tools", [])
|
|
91
|
+
state["tools"].append(
|
|
92
|
+
{
|
|
93
|
+
"id": item.choices[0].delta.tool_calls[0].id,
|
|
94
|
+
"name": item.choices[0].delta.tool_calls[0].function.name,
|
|
95
|
+
"arguments": item.choices[0]
|
|
96
|
+
.delta.tool_calls[0]
|
|
97
|
+
.function.arguments,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
if (item.choices and item.choices[0].finish_reason):
|
|
101
|
+
state["finish_reason"] = item.choices[0].finish_reason
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.warning(
|
|
104
|
+
"Warning: Error occurred while processing tool calls: %s",
|
|
105
|
+
str(e),
|
|
106
|
+
)
|
|
107
|
+
|
|
68
108
|
"""Create the span result object."""
|
|
69
109
|
return SimpleNamespace(
|
|
70
110
|
type="stream",
|
|
@@ -75,14 +115,15 @@ def _create_span_result(state, stream_start_time):
|
|
|
75
115
|
"metadata": int(state["stream_closed_time"] or time.time_ns()),
|
|
76
116
|
},
|
|
77
117
|
output_text=state["accumulated_response"],
|
|
118
|
+
tools=state["tools"] if "tools" in state else None,
|
|
78
119
|
usage=state["token_usage"],
|
|
79
|
-
finish_reason=state["finish_reason"]
|
|
120
|
+
finish_reason=state["finish_reason"],
|
|
80
121
|
)
|
|
81
122
|
|
|
82
123
|
|
|
83
124
|
def process_stream(to_wrap, response, span_processor):
|
|
84
125
|
stream_start_time = time.time_ns()
|
|
85
|
-
|
|
126
|
+
|
|
86
127
|
# Shared state for both sync and async processing
|
|
87
128
|
state = {
|
|
88
129
|
"waiting_for_first_token": True,
|
|
@@ -108,7 +149,7 @@ def process_stream(to_wrap, response, span_processor):
|
|
|
108
149
|
span_processor(ret_val)
|
|
109
150
|
|
|
110
151
|
patch_instance_method(response, "__iter__", new_iter)
|
|
111
|
-
|
|
152
|
+
|
|
112
153
|
if to_wrap and hasattr(response, "__aiter__"):
|
|
113
154
|
original_iter = response.__aiter__
|
|
114
155
|
|
|
@@ -125,7 +166,7 @@ def process_stream(to_wrap, response, span_processor):
|
|
|
125
166
|
|
|
126
167
|
|
|
127
168
|
INFERENCE = {
|
|
128
|
-
"type":
|
|
169
|
+
"type": SPAN_TYPES.INFERENCE,
|
|
129
170
|
"is_auto_close": lambda kwargs: kwargs.get("stream", False) is False,
|
|
130
171
|
"response_processor": process_stream,
|
|
131
172
|
"attributes": [
|
|
@@ -200,10 +241,9 @@ INFERENCE = {
|
|
|
200
241
|
{
|
|
201
242
|
"name": "data.output",
|
|
202
243
|
"attributes": [
|
|
203
|
-
|
|
204
244
|
{
|
|
205
245
|
"attribute": "error_code",
|
|
206
|
-
"accessor": lambda arguments: get_error_message(arguments)
|
|
246
|
+
"accessor": lambda arguments: get_error_message(arguments),
|
|
207
247
|
},
|
|
208
248
|
{
|
|
209
249
|
"_comment": "this is result from LLM",
|
|
@@ -211,7 +251,7 @@ INFERENCE = {
|
|
|
211
251
|
"accessor": lambda arguments: _helper.extract_assistant_message(
|
|
212
252
|
arguments,
|
|
213
253
|
),
|
|
214
|
-
}
|
|
254
|
+
},
|
|
215
255
|
],
|
|
216
256
|
},
|
|
217
257
|
{
|
|
@@ -226,14 +266,20 @@ INFERENCE = {
|
|
|
226
266
|
{
|
|
227
267
|
"_comment": "finish reason from OpenAI response",
|
|
228
268
|
"attribute": "finish_reason",
|
|
229
|
-
"accessor": lambda arguments: _helper.extract_finish_reason(
|
|
269
|
+
"accessor": lambda arguments: _helper.extract_finish_reason(
|
|
270
|
+
arguments
|
|
271
|
+
),
|
|
230
272
|
},
|
|
231
273
|
{
|
|
232
274
|
"_comment": "finish type mapped from finish reason",
|
|
233
275
|
"attribute": "finish_type",
|
|
234
276
|
"accessor": lambda arguments: _helper.map_finish_reason_to_finish_type(
|
|
235
277
|
_helper.extract_finish_reason(arguments)
|
|
236
|
-
)
|
|
278
|
+
),
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"attribute": "inference_sub_type",
|
|
282
|
+
"accessor": lambda arguments: _helper.agent_inference_type(arguments)
|
|
237
283
|
}
|
|
238
284
|
],
|
|
239
285
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
from monocle_apptrace.instrumentation.common.utils import (
|
|
3
4
|
Option,
|
|
@@ -170,4 +171,14 @@ def extract_inference_endpoint(instance):
|
|
|
170
171
|
if inference_endpoint.is_none() and "meta" in instance.client.__dict__:
|
|
171
172
|
inference_endpoint = try_option(getattr, instance.client.meta, 'endpoint_url').map(str)
|
|
172
173
|
|
|
173
|
-
return inference_endpoint.unwrap_or(extract_provider_name(instance))
|
|
174
|
+
return inference_endpoint.unwrap_or(extract_provider_name(instance))
|
|
175
|
+
|
|
176
|
+
def agent_inference_type(arguments):
|
|
177
|
+
"""
|
|
178
|
+
Extracts the agent inference type from the arguments.
|
|
179
|
+
"""
|
|
180
|
+
output = extract_assistant_message(arguments)
|
|
181
|
+
command = json.loads(json.loads(output).get("assistant", "")).get("action", "").get("name")
|
|
182
|
+
if command == "SAY":
|
|
183
|
+
return "turn"
|
|
184
|
+
return "tool_call"
|
monocle_apptrace/instrumentation/metamodel/teamsai/entities/inference/teamsai_output_processor.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
from monocle_apptrace.instrumentation.common.constants import SPAN_TYPES
|
|
1
2
|
from monocle_apptrace.instrumentation.metamodel.teamsai import (
|
|
2
3
|
_helper,
|
|
3
4
|
)
|
|
4
5
|
from monocle_apptrace.instrumentation.common.utils import get_error_message, get_llm_type
|
|
5
6
|
TEAMAI_OUTPUT_PROCESSOR = {
|
|
6
|
-
"type":
|
|
7
|
+
"type": SPAN_TYPES.INFERENCE_FRAMEWORK,
|
|
7
8
|
"attributes": [
|
|
8
9
|
[
|
|
9
10
|
{
|
|
@@ -66,5 +67,15 @@ TEAMAI_OUTPUT_PROCESSOR = {
|
|
|
66
67
|
}
|
|
67
68
|
]
|
|
68
69
|
},
|
|
70
|
+
{
|
|
71
|
+
"name": "metadata",
|
|
72
|
+
"_comment": "metadata for Teams AI",
|
|
73
|
+
"attributes": [
|
|
74
|
+
{
|
|
75
|
+
"attribute": "inference_sub_type",
|
|
76
|
+
"accessor": lambda arguments: _helper.agent_inference_type(arguments)
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
69
80
|
]
|
|
70
81
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import click
|
|
3
|
+
from mcp.server.fastmcp import FastMCP
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
app = FastMCP(
|
|
8
|
+
"monocle-mcp",
|
|
9
|
+
)
|
|
10
|
+
code_snippets = {
|
|
11
|
+
"python": "from monocle_apptrace import setup_monocle_telemetry"
|
|
12
|
+
"setup_monocle_telemetry(workflow_name='my_workflow')\n",
|
|
13
|
+
"typescript": "import { trace } from 'monocle';\n\ntrace();\n",
|
|
14
|
+
}
|
|
15
|
+
setup_instructions = {
|
|
16
|
+
"python": "pip install monocle_apptrace package and add monocle_apptrace at the end of the requirement.txt for future. This code snippet provided below should be just below other imports in your application code.",
|
|
17
|
+
"typescript": "Install monocle package and add the trace function call at the top of your application. This code snippet provided below should be just below other imports in your application code.",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
description = {
|
|
21
|
+
"python": "This will automatically trace your Python application, capturing telemetry data for your genAI operations.",
|
|
22
|
+
"typescript": "This will automatically trace your TypeScript application, capturing telemetry data for your genAI operations.",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Use the existing dictionaries to get language-specific information
|
|
27
|
+
# code_snippet = code_snippets.get(app_language, code_snippets['python'])
|
|
28
|
+
# instructions = setup_instructions.get(app_language, setup_instructions['python'])
|
|
29
|
+
# desc = description.get(app_language, description['python'])
|
|
30
|
+
enable_monocle_prompt = """Do following changes to the project:
|
|
31
|
+
1. run command 'pip install monocle_apptrace'
|
|
32
|
+
2. Add following line to requirements.txt in current directory 'monocle_apptrace' if it's not there already
|
|
33
|
+
3. Add following lines just below other imports in the file if it's not there already:
|
|
34
|
+
from monocle_apptrace import setup_monocle_telemetry
|
|
35
|
+
setup_monocle_telemetry(workflow_name=<module-name>, monocle_exporters_list='file'))
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
monocle_trace_prompt: str = """
|
|
39
|
+
You are a helpful assistant to analyze given Monocle trace data. Review the trace data and provide a summary of whether the execution was successful or if there were any errors. Your job is to analyze the trace data and provide a summary. Be very concise in reporting. Check for errors or problem situations. First provide a headline that indicates status of the trace and if there are problems. If there are problems, then provide a brief summary of the problems. If there are problems, then provide a brief summary of the root causes.
|
|
40
|
+
Use the trace details provided below to understand the data.
|
|
41
|
+
|
|
42
|
+
Monocle trace has recorded the traced the execution of a genAI application. Each step of the genAI operation is captured as span. The trace file is a JSON structure that is an array of span JSON elements. There's a root span that represents the overall execution context.
|
|
43
|
+
The important span JSON elements are as follows:
|
|
44
|
+
- context.trace_id is the unique id common for alls spans.
|
|
45
|
+
- context.span_id is the unique id for each span.
|
|
46
|
+
- parent_id indicates the parent span of that span. The span that has no parent is the root span.
|
|
47
|
+
- status.status_code indicate the status of span. It can be OK or ERROR or UNSET. If the span status is ERROR, then it generally propogated to it's parent and all the way to the root span. This means the entire trace is in error. However, sometimes the parent span will handle the error (eg. if parent name indicagtes 'retry') and the trace will be successful, even though an individual span has error.
|
|
48
|
+
- attributes captures various attributes involved in that span's operation
|
|
49
|
+
- attribute."span.type" indicates the type of operation
|
|
50
|
+
- When the span.type is "workflow", that span captures the summary of the entire application
|
|
51
|
+
- entity.[NUMBER] are the different entties involved in the operation, for example large language model, agent, tool etc
|
|
52
|
+
- entity.type indicates the type of that entity
|
|
53
|
+
- events is an array that captures the details of the operation such as input, output and metadata
|
|
54
|
+
- events array element with name=data.input captures the input provided to the operation
|
|
55
|
+
- events array element with name=data.output captures the output generated by the operation
|
|
56
|
+
- events array element with name=metaata captures the metadata of the operation
|
|
57
|
+
- total_tokens is the count of tokens
|
|
58
|
+
- completion_tokens is the count of ouput tokens
|
|
59
|
+
- prompt_tokens is the count of input tokens
|
|
60
|
+
- finish_type indicates how the operation finished, for example stop sequence or max tokens etc. If the finish_type is not success and indicates problems such as max tokens, then the operation is not successful. Check the status of it's parent span and check if it's due to the finish type of this child span. In that case callout the root cause as the child span error for the parent span error.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@app.prompt(name="enable_tracing")
|
|
64
|
+
def enable_monocle_tracing_prompt(app_language: str = "python") -> str:
|
|
65
|
+
"""Trace agentic code"""
|
|
66
|
+
return enable_monocle_prompt.format(app_language=app_language)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.prompt(name="analyze")
|
|
70
|
+
def analyze_monocle_tracing_prompt() -> str:
|
|
71
|
+
"""Identify root cause from trace"""
|
|
72
|
+
return monocle_trace_prompt
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@click.command()
|
|
76
|
+
@click.option(
|
|
77
|
+
"--log-level",
|
|
78
|
+
default="info",
|
|
79
|
+
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
|
|
80
|
+
)
|
|
81
|
+
def main(log_level: str) -> int:
|
|
82
|
+
# Configure logging
|
|
83
|
+
logging.basicConfig(
|
|
84
|
+
level=getattr(logging, log_level.upper()),
|
|
85
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Run the FastMCP server using stdio transport
|
|
89
|
+
app.run(transport="stdio")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
main() # type: ignore
|