Skip to content

Commit 0b3850f

Browse files
umaannamalaimergify[bot]TimPansino
authored
Add autogen instrumentation and MCP updates (#1520)
* Add instrumentation for autogen MCP tool adapter. (#1409) * Add instrumentation for MCP tool adapter. * [MegaLinter] Apply linters fixes * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Autogen Agent Instrumentation (#1412) * Add autogen agent instrumentation. * Address review feedback. * [MegaLinter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Sync autogen feature branch (#1472) * Add instrumentation for autogen MCP tool adapter. (#1409) * Add instrumentation for MCP tool adapter. * [MegaLinter] Apply linters fixes * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Autogen Agent Instrumentation (#1412) * Add autogen agent instrumentation. * Address review feedback. * [MegaLinter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Add instrumentation for autogen MCP tool adapter. (#1409) * Add instrumentation for MCP tool adapter. * [MegaLinter] Apply linters fixes * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Autogen Agent Instrumentation (#1412) * Add autogen agent instrumentation. * Address review feedback. * [MegaLinter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Save linking metadata on generator proxy. * Add comments to changes in instrumentation. * Add LLMAgent events to autogen instrumentation. (#1455) --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * MCP Streamable HTTP Spans (DO NOT MERGE YET) (#1462) * Add tool manager span. * Add tool manager spans to MCP instrumentation to support streaming. --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Update MCP instrumentation to check if AIM is enabled. (#1456) * Update MCP instrumentation to check if AIM is enabled. * Linting --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Tim Pansino <[email protected]> * Add clause to avoid reporting empty content in openai. (#1517) * Add instrumentation for autogen MCP tool adapter. (#1409) * Add instrumentation for MCP tool adapter. * [MegaLinter] Apply linters fixes * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Autogen Agent Instrumentation (#1412) * Add autogen agent instrumentation. * Address review feedback. * [MegaLinter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Sync autogen feature branch (#1472) * Add instrumentation for autogen MCP tool adapter. (#1409) * Add instrumentation for MCP tool adapter. * [MegaLinter] Apply linters fixes * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Autogen Agent Instrumentation (#1412) * Add autogen agent instrumentation. * Address review feedback. * [MegaLinter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Add instrumentation for autogen MCP tool adapter. (#1409) * Add instrumentation for MCP tool adapter. * [MegaLinter] Apply linters fixes * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Add autogen agent instrumentation. * Revert "Add autogen agent instrumentation." This reverts commit a26766a. * Autogen Agent Instrumentation (#1412) * Add autogen agent instrumentation. * Address review feedback. * [MegaLinter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Save linking metadata on generator proxy. * Add comments to changes in instrumentation. * Add LLMAgent events to autogen instrumentation. (#1455) --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * MCP Streamable HTTP Spans (DO NOT MERGE YET) (#1462) * Add tool manager span. * Add tool manager spans to MCP instrumentation to support streaming. --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Update MCP instrumentation to check if AIM is enabled. (#1456) * Update MCP instrumentation to check if AIM is enabled. * Linting --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Tim Pansino <[email protected]> * Add clause to avoid reporting empty content in openai. (#1517) * Update validator path. * Add MCP adapter test for no transaction. * Linting fixups. * Linting --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Tim Pansino <[email protected]>
1 parent baecae8 commit 0b3850f

File tree

14 files changed

+1437
-20
lines changed

14 files changed

+1437
-20
lines changed

newrelic/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2842,7 +2842,20 @@ def _process_module_builtin_defaults():
28422842
_process_module_definition("loguru", "newrelic.hooks.logger_loguru", "instrument_loguru")
28432843
_process_module_definition("loguru._logger", "newrelic.hooks.logger_loguru", "instrument_loguru_logger")
28442844

2845+
_process_module_definition(
2846+
"autogen_ext.tools.mcp._base", "newrelic.hooks.mlmodel_autogen", "instrument_autogen_ext_tools_mcp__base"
2847+
)
2848+
_process_module_definition(
2849+
"autogen_agentchat.agents._assistant_agent",
2850+
"newrelic.hooks.mlmodel_autogen",
2851+
"instrument_autogen_agentchat_agents__assistant_agent",
2852+
)
28452853
_process_module_definition("mcp.client.session", "newrelic.hooks.adapter_mcp", "instrument_mcp_client_session")
2854+
_process_module_definition(
2855+
"mcp.server.fastmcp.tools.tool_manager",
2856+
"newrelic.hooks.adapter_mcp",
2857+
"instrument_mcp_server_fastmcp_tools_tool_manager",
2858+
)
28462859

28472860
_process_module_definition("structlog._base", "newrelic.hooks.logger_structlog", "instrument_structlog__base")
28482861
_process_module_definition("structlog._frames", "newrelic.hooks.logger_structlog", "instrument_structlog__frames")

newrelic/hooks/adapter_mcp.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from newrelic.common.object_names import callable_name
2020
from newrelic.common.object_wrapper import wrap_function_wrapper
2121
from newrelic.common.signature import bind_args
22+
from newrelic.core.config import global_settings
2223

2324
_logger = logging.getLogger(__name__)
2425

@@ -28,6 +29,10 @@ async def wrap_call_tool(wrapped, instance, args, kwargs):
2829
if not transaction:
2930
return await wrapped(*args, **kwargs)
3031

32+
settings = transaction.settings or global_settings()
33+
if not settings.ai_monitoring.enabled:
34+
return await wrapped(*args, **kwargs)
35+
3136
func_name = callable_name(wrapped)
3237
bound_args = bind_args(wrapped, args, kwargs)
3338
tool_name = bound_args.get("name") or "tool"
@@ -42,6 +47,10 @@ async def wrap_read_resource(wrapped, instance, args, kwargs):
4247
if not transaction:
4348
return await wrapped(*args, **kwargs)
4449

50+
settings = transaction.settings or global_settings()
51+
if not settings.ai_monitoring.enabled:
52+
return await wrapped(*args, **kwargs)
53+
4554
func_name = callable_name(wrapped)
4655
bound_args = bind_args(wrapped, args, kwargs)
4756
# Set a default value in case we can't parse out the URI scheme successfully
@@ -64,6 +73,10 @@ async def wrap_get_prompt(wrapped, instance, args, kwargs):
6473
if not transaction:
6574
return await wrapped(*args, **kwargs)
6675

76+
settings = transaction.settings or global_settings()
77+
if not settings.ai_monitoring.enabled:
78+
return await wrapped(*args, **kwargs)
79+
6780
func_name = callable_name(wrapped)
6881
bound_args = bind_args(wrapped, args, kwargs)
6982
prompt_name = bound_args.get("name") or "prompt"
@@ -81,3 +94,9 @@ def instrument_mcp_client_session(module):
8194
wrap_function_wrapper(module, "ClientSession.read_resource", wrap_read_resource)
8295
if hasattr(module.ClientSession, "get_prompt"):
8396
wrap_function_wrapper(module, "ClientSession.get_prompt", wrap_get_prompt)
97+
98+
99+
def instrument_mcp_server_fastmcp_tools_tool_manager(module):
100+
if hasattr(module, "ToolManager"):
101+
if hasattr(module.ToolManager, "call_tool"):
102+
wrap_function_wrapper(module, "ToolManager.call_tool", wrap_call_tool)

newrelic/hooks/mlmodel_autogen.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import logging
17+
import sys
18+
import uuid
19+
20+
from newrelic.api.function_trace import FunctionTrace
21+
from newrelic.api.time_trace import get_trace_linking_metadata
22+
from newrelic.api.transaction import current_transaction
23+
from newrelic.common.object_names import callable_name
24+
from newrelic.common.object_wrapper import wrap_function_wrapper
25+
from newrelic.common.package_version_utils import get_package_version
26+
from newrelic.common.signature import bind_args
27+
from newrelic.core.config import global_settings
28+
29+
# Check for the presence of the autogen-core, autogen-agentchat, or autogen-ext package as they should all have the
30+
# same version and one or multiple could be installed
31+
AUTOGEN_VERSION = (
32+
get_package_version("autogen-core")
33+
or get_package_version("autogen-agentchat")
34+
or get_package_version("autogen-ext")
35+
)
36+
37+
38+
RECORD_EVENTS_FAILURE_LOG_MESSAGE = "Exception occurred in Autogen instrumentation: Failed to record LLM events. Please report this issue to New Relic Support.\n%s"
39+
40+
41+
_logger = logging.getLogger(__name__)
42+
43+
44+
async def wrap_from_server_params(wrapped, instance, args, kwargs):
45+
transaction = current_transaction()
46+
if not transaction:
47+
return await wrapped(*args, **kwargs)
48+
49+
func_name = callable_name(wrapped)
50+
bound_args = bind_args(wrapped, args, kwargs)
51+
tool_name = bound_args.get("tool_name") or "tool"
52+
function_trace_name = f"{func_name}/{tool_name}"
53+
with FunctionTrace(name=function_trace_name, group="Llm", source=wrapped):
54+
return await wrapped(*args, **kwargs)
55+
56+
57+
def wrap_on_messages_stream(wrapped, instance, args, kwargs):
58+
transaction = current_transaction()
59+
if not transaction:
60+
return wrapped(*args, **kwargs)
61+
62+
settings = transaction.settings or global_settings()
63+
if not settings.ai_monitoring.enabled:
64+
return wrapped(*args, **kwargs)
65+
66+
# Framework metric also used for entity tagging in the UI
67+
transaction.add_ml_model_info("Autogen", AUTOGEN_VERSION)
68+
transaction._add_agent_attribute("llm", True)
69+
70+
agent_name = getattr(instance, "name", "agent")
71+
agent_id = str(uuid.uuid4())
72+
agent_event_dict = _construct_base_agent_event_dict(agent_name, agent_id, transaction)
73+
func_name = callable_name(wrapped)
74+
function_trace_name = f"{func_name}/{agent_name}"
75+
76+
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/Autogen")
77+
ft.__enter__()
78+
79+
try:
80+
return_val = wrapped(*args, **kwargs)
81+
except Exception:
82+
ft.notice_error(attributes={"agent_id": agent_id})
83+
ft.__exit__(*sys.exc_info())
84+
# If we hit an exception, append the error attribute and duration from the exited function trace
85+
agent_event_dict.update({"duration": ft.duration * 1000, "error": True})
86+
transaction.record_custom_event("LlmAgent", agent_event_dict)
87+
raise
88+
89+
ft.__exit__(None, None, None)
90+
agent_event_dict.update({"duration": ft.duration * 1000})
91+
92+
transaction.record_custom_event("LlmAgent", agent_event_dict)
93+
94+
return return_val
95+
96+
97+
def _get_llm_metadata(transaction):
98+
# Grab LLM-related custom attributes off of the transaction to store as metadata on LLM events
99+
custom_attrs_dict = transaction._custom_params
100+
llm_metadata_dict = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}
101+
llm_context_attrs = getattr(transaction, "_llm_context_attrs", None)
102+
if llm_context_attrs:
103+
llm_metadata_dict.update(llm_context_attrs)
104+
105+
return llm_metadata_dict
106+
107+
108+
def _extract_tool_output(return_val, tool_name):
109+
try:
110+
output = getattr(return_val[1], "content", None)
111+
return output
112+
except Exception:
113+
_logger.warning("Unable to parse tool output value from %s. Omitting output from LlmTool event.", tool_name)
114+
return None
115+
116+
117+
def _construct_base_tool_event_dict(bound_args, tool_call_data, tool_id, transaction, settings):
118+
try:
119+
_input = getattr(tool_call_data, "arguments", None)
120+
tool_input = str(_input) if _input else None
121+
run_id = getattr(tool_call_data, "id", None)
122+
tool_name = getattr(tool_call_data, "name", "tool")
123+
agent_name = bound_args.get("agent_name")
124+
linking_metadata = get_trace_linking_metadata()
125+
126+
tool_event_dict = {
127+
"id": tool_id,
128+
"run_id": run_id,
129+
"name": tool_name,
130+
"span_id": linking_metadata.get("span.id"),
131+
"trace_id": linking_metadata.get("trace.id"),
132+
"agent_name": agent_name,
133+
"vendor": "autogen",
134+
"ingest_source": "Python",
135+
}
136+
if settings.ai_monitoring.record_content.enabled:
137+
tool_event_dict.update({"input": tool_input})
138+
tool_event_dict.update(_get_llm_metadata(transaction))
139+
except Exception:
140+
tool_event_dict = {}
141+
_logger.warning(RECORD_EVENTS_FAILURE_LOG_MESSAGE, exc_info=True)
142+
143+
return tool_event_dict
144+
145+
146+
def _construct_base_agent_event_dict(agent_name, agent_id, transaction):
147+
try:
148+
linking_metadata = get_trace_linking_metadata()
149+
150+
agent_event_dict = {
151+
"id": agent_id,
152+
"name": agent_name,
153+
"span_id": linking_metadata.get("span.id"),
154+
"trace_id": linking_metadata.get("trace.id"),
155+
"vendor": "autogen",
156+
"ingest_source": "Python",
157+
}
158+
agent_event_dict.update(_get_llm_metadata(transaction))
159+
except Exception:
160+
agent_event_dict = {}
161+
_logger.warning(RECORD_EVENTS_FAILURE_LOG_MESSAGE, exc_info=True)
162+
163+
return agent_event_dict
164+
165+
166+
async def wrap__execute_tool_call(wrapped, instance, args, kwargs):
167+
transaction = current_transaction()
168+
if not transaction:
169+
return await wrapped(*args, **kwargs)
170+
171+
settings = transaction.settings or global_settings()
172+
if not settings.ai_monitoring.enabled:
173+
return await wrapped(*args, **kwargs)
174+
175+
# Framework metric also used for entity tagging in the UI
176+
transaction.add_ml_model_info("Autogen", AUTOGEN_VERSION)
177+
transaction._add_agent_attribute("llm", True)
178+
179+
tool_id = str(uuid.uuid4())
180+
bound_args = bind_args(wrapped, args, kwargs)
181+
tool_call_data = bound_args.get("tool_call")
182+
tool_event_dict = _construct_base_tool_event_dict(bound_args, tool_call_data, tool_id, transaction, settings)
183+
184+
tool_name = getattr(tool_call_data, "name", "tool")
185+
186+
func_name = callable_name(wrapped)
187+
ft = FunctionTrace(name=f"{func_name}/{tool_name}", group="Llm/tool/Autogen")
188+
ft.__enter__()
189+
190+
try:
191+
return_val = await wrapped(*args, **kwargs)
192+
except Exception:
193+
ft.notice_error(attributes={"tool_id": tool_id})
194+
ft.__exit__(*sys.exc_info())
195+
# If we hit an exception, append the error attribute and duration from the exited function trace
196+
tool_event_dict.update({"duration": ft.duration * 1000, "error": True})
197+
transaction.record_custom_event("LlmTool", tool_event_dict)
198+
raise
199+
200+
ft.__exit__(None, None, None)
201+
tool_event_dict.update({"duration": ft.duration * 1000})
202+
203+
# If the tool was executed successfully, we can grab the tool output from the result
204+
tool_output = _extract_tool_output(return_val, tool_name)
205+
if settings.ai_monitoring.record_content.enabled:
206+
tool_event_dict.update({"output": tool_output})
207+
208+
transaction.record_custom_event("LlmTool", tool_event_dict)
209+
210+
return return_val
211+
212+
213+
def instrument_autogen_agentchat_agents__assistant_agent(module):
214+
if hasattr(module, "AssistantAgent"):
215+
if hasattr(module.AssistantAgent, "on_messages_stream"):
216+
wrap_function_wrapper(module, "AssistantAgent.on_messages_stream", wrap_on_messages_stream)
217+
if hasattr(module.AssistantAgent, "_execute_tool_call"):
218+
wrap_function_wrapper(module, "AssistantAgent._execute_tool_call", wrap__execute_tool_call)
219+
220+
221+
def instrument_autogen_ext_tools_mcp__base(module):
222+
if hasattr(module, "McpToolAdapter"):
223+
if hasattr(module.McpToolAdapter, "from_server_params"):
224+
wrap_function_wrapper(module, "McpToolAdapter.from_server_params", wrap_from_server_params)

newrelic/hooks/mlmodel_openai.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ def _handle_completion_success(transaction, linking_metadata, completion_id, kwa
442442
# The function trace will be exited when in the final iteration of the response
443443
# generator.
444444
return_val._nr_ft = ft
445+
return_val._nr_metadata = linking_metadata
445446
return_val._nr_openai_attrs = getattr(return_val, "_nr_openai_attrs", {})
446447
return_val._nr_openai_attrs["messages"] = kwargs.get("messages", [])
447448
return_val._nr_openai_attrs["temperature"] = kwargs.get("temperature")
@@ -488,14 +489,20 @@ def _record_completion_success(transaction, linking_metadata, completion_id, kwa
488489
choices[0].get("message") or {"content": choices[0].get("text"), "role": "assistant"}
489490
]
490491
finish_reason = choices[0].get("finish_reason")
492+
if "tool_calls" in output_message_list[0] and not output_message_list[0].get("content"):
493+
output_message_list = []
491494
else:
492495
response_model = kwargs.get("response.model")
493496
response_id = kwargs.get("id")
494497
output_message_list = []
495-
finish_reason = None
498+
finish_reason = kwargs.get("finish_reason")
496499
if "content" in kwargs:
497500
output_message_list = [{"content": kwargs.get("content"), "role": kwargs.get("role")}]
498-
finish_reason = kwargs.get("finish_reason")
501+
# When tools are involved, the content key may hold an empty string which we do not want to report
502+
# In this case, the content we are interested in capturing will already be covered in the input_message_list
503+
# We empty out the output_message_list so that we do not report an empty message
504+
if "tool_call" in finish_reason and not kwargs.get("content"):
505+
output_message_list = []
499506
request_model = kwargs.get("model") or kwargs.get("engine")
500507

501508
request_id = response_headers.get("x-request-id")
@@ -765,7 +772,10 @@ def _record_stream_chunk(self, return_val):
765772

766773
def _record_events_on_stop_iteration(self, transaction):
767774
if hasattr(self, "_nr_ft"):
768-
linking_metadata = get_trace_linking_metadata()
775+
# We first check for our saved linking metadata before making a new call to get_trace_linking_metadata
776+
# Directly calling get_trace_linking_metadata() causes the incorrect span ID to be captured and associated with the LLM call
777+
# This leads to incorrect linking of the LLM call in the UI
778+
linking_metadata = self._nr_metadata or get_trace_linking_metadata()
769779
self._nr_ft.__exit__(None, None, None)
770780
try:
771781
openai_attrs = getattr(self, "_nr_openai_attrs", {})
@@ -872,6 +882,8 @@ def set_attrs_on_generator_proxy(proxy, instance):
872882
proxy._nr_response_headers = instance._nr_response_headers
873883
if hasattr(instance, "_nr_openai_attrs"):
874884
proxy._nr_openai_attrs = instance._nr_openai_attrs
885+
if hasattr(instance, "_nr_metadata"):
886+
proxy._nr_metadata = instance._nr_metadata
875887

876888

877889
def wrap_engine_api_resource_create_sync(wrapped, instance, args, kwargs):

tests/adapter_mcp/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"transaction_tracer.stack_trace_threshold": 0.0,
2323
"debug.log_data_collector_payloads": True,
2424
"debug.record_transaction_failure": True,
25+
"ai_monitoring.enabled": True,
2526
}
2627

2728
collector_agent_registration = collector_agent_registration_fixture(

0 commit comments

Comments
 (0)