Skip to content

Commit 9643678

Browse files
committed
Test for remote tools in langchain
1 parent 514b057 commit 9643678

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

tests/mlmodel_langchain/_test_tools.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ async def add_exclamation_async(message: str) -> str:
3232
return f"{message}!"
3333

3434

35+
@tool("add_exclamation")
36+
def remote_add_exclamation_sync(message: str) -> str:
37+
"""Adds an exclamation mark to the input message."""
38+
return f"{message}!"
39+
40+
41+
@tool("add_exclamation")
42+
async def remote_add_exclamation_async(message: str) -> str:
43+
"""Adds an exclamation mark to the input message."""
44+
return f"{message}!"
45+
46+
47+
# Simulate a remote tool by setting the __module__ to a known remote tool module
48+
remote_add_exclamation_sync.func.__module__ = "langchain_mcp_adapters.tools"
49+
remote_add_exclamation_async.coroutine.__module__ = "langchain_mcp_adapters.tools"
50+
51+
3552
@pytest.fixture(params=["sync_tool", "async_tool"])
3653
def tool_type(request):
3754
return request.param
@@ -52,3 +69,15 @@ def add_exclamation(tool_type, exercise_agent):
5269
return add_exclamation_async
5370
else:
5471
raise NotImplementedError
72+
73+
74+
@pytest.fixture
75+
def remote_add_exclamation(tool_type, exercise_agent):
76+
if tool_type == "sync_tool":
77+
return remote_add_exclamation_sync
78+
elif tool_type == "async_tool":
79+
if exercise_agent._called_method in ("invoke", "stream"):
80+
pytest.skip("Async tools cannot be invoked synchronously.")
81+
return remote_add_exclamation_async
82+
else:
83+
raise NotImplementedError

tests/mlmodel_langchain/test_tools.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@
3232
from newrelic.common.object_names import callable_name
3333
from newrelic.common.object_wrapper import transient_function_wrapper
3434

35-
from ._test_tools import add_exclamation, tool_method_name, tool_type
35+
from ._test_tools import (
36+
add_exclamation,
37+
add_exclamation_async,
38+
add_exclamation_sync,
39+
remote_add_exclamation,
40+
remote_add_exclamation_async,
41+
remote_add_exclamation_sync,
42+
tool_method_name,
43+
tool_type,
44+
)
3645

3746
PROMPT = {
3847
"messages": [
@@ -155,6 +164,38 @@ def _test():
155164
_test()
156165

157166

167+
@dt_enabled
168+
@reset_core_stats_engine()
169+
def test_tool_remote_no_subcomponent(
170+
exercise_agent, set_trace_info, create_agent_runnable, remote_add_exclamation, tool_method_name
171+
):
172+
@validate_custom_events(tool_recorded_event)
173+
@validate_custom_event_count(count=exercise_agent._expected_event_count)
174+
@validate_transaction_metrics(
175+
"test_tool_remote_no_subcomponent",
176+
scoped_metrics=[(f"Llm/tool/LangChain/{tool_method_name}/add_exclamation", 1)],
177+
rollup_metrics=[(f"Llm/tool/LangChain/{tool_method_name}/add_exclamation", 1)],
178+
background_task=True,
179+
)
180+
# The agent span still has a subcomponent attribute
181+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_AGENT", "name": "my_agent"}'})
182+
# Tool span must NOT have a subcomponent attribute
183+
@validate_span_events(
184+
count=1,
185+
exact_intrinsics={"name": f"Llm/tool/LangChain/{tool_method_name}/add_exclamation"},
186+
unexpected_agents=["subcomponent"],
187+
)
188+
@background_task(name="test_tool_remote_no_subcomponent")
189+
def _test():
190+
set_trace_info()
191+
my_agent = create_agent_runnable(
192+
tools=[remote_add_exclamation], system_prompt="You are a text manipulation algorithm."
193+
)
194+
exercise_agent(my_agent, PROMPT)
195+
196+
_test()
197+
198+
158199
@dt_enabled
159200
@reset_core_stats_engine()
160201
def test_tool_execution_error(exercise_agent, set_trace_info, create_agent_runnable, add_exclamation, tool_method_name):
@@ -219,3 +260,23 @@ def _test():
219260
exercise_agent(my_agent, PROMPT)
220261

221262
_test()
263+
264+
265+
def test_is_local_tool_helper():
266+
from newrelic.hooks.mlmodel_langchain import _is_local_tool
267+
268+
# Local tool functions
269+
assert _is_local_tool(add_exclamation_sync) is True
270+
assert _is_local_tool(add_exclamation_async) is True
271+
272+
# Tools whose __module__ reads as a collection of remote tools
273+
assert _is_local_tool(remote_add_exclamation_sync) is False
274+
assert _is_local_tool(remote_add_exclamation_async) is False
275+
276+
# A tool instance that raises an exception when trying to access its underlying callable
277+
class BrokenTool:
278+
@property
279+
def func(self):
280+
raise RuntimeError("Oops")
281+
282+
assert _is_local_tool(BrokenTool()) is True

0 commit comments

Comments
 (0)