Skip to content

Commit f0d7430

Browse files
authored
Handle collisions between tools and subagents names better (#44)
This change: - Introduces a reserved "__" namespace for internally generated names - Update subagent prefix to "__agent-" - If a tool name begins with "__", prepend "__tool-" to ensure unprefixed tools never conflict with reserved/internal identifiers.
1 parent 42bec3a commit f0d7430

3 files changed

Lines changed: 114 additions & 46 deletions

File tree

splunklib/ai/core/backend.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ class InvalidModelError(Exception):
2323
"""Raised when an invalid model is specified for a backend."""
2424

2525

26-
class InvalidToolNameError(Exception):
27-
"""Raised when a tool name contains invalid prefix."""
28-
29-
3026
class InvalidMessageTypeError(Exception):
3127
"""Raised when a message type is not supported by the backend."""
3228

splunklib/ai/engines/langchain.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
Backend,
4848
InvalidMessageTypeError,
4949
InvalidModelError,
50-
InvalidToolNameError,
5150
)
5251
from splunklib.ai.messages import (
5352
AgentCall,
@@ -70,7 +69,21 @@
7069
)
7170
from splunklib.ai.tools import Tool, ToolException
7271

73-
AGENT_PREFIX = "agent-"
72+
# RESERVED_LC_TOOL_PREFIX represents a prefix that is reserved for internal use
73+
# and no user-visible tool or subagent name can contain it (as a prefix).
74+
RESERVED_LC_TOOL_PREFIX = "__"
75+
76+
# AGENT_PREFIX is a prefix prepended to a name of an agent,
77+
# during the conversion of a subagent to a tool.
78+
# All subagents as tools have this prefix.
79+
AGENT_PREFIX = f"{RESERVED_LC_TOOL_PREFIX}agent-"
80+
81+
# CONFLICTING_TOOL_PREFIX is a prefix that is prepended to a tool name
82+
# in case the tool name already starts with RESERVED_LC_TOOL_PREFIX.
83+
# This prevents the user-provided tools to start with AGENT_PREFIX and also
84+
# serves as a backward compatibility mechanism for us i.e. we are free to use
85+
# any tool name that starts with RESERVED_LC_TOOL_PREFIX for other uses.
86+
CONFLICTING_TOOL_PREFIX = f"{RESERVED_LC_TOOL_PREFIX}tool-"
7487

7588
AGENT_AS_TOOLS_PROMPT = f"""
7689
You are provided with Agents.
@@ -202,7 +215,7 @@ async def _tool_call(
202215
return result.content, result.structured_content
203216

204217
return StructuredTool(
205-
name=tool.name,
218+
name=_normalize_tool_name(tool.name),
206219
description=tool.description,
207220
args_schema=tool.input_schema,
208221
coroutine=_tool_call,
@@ -217,17 +230,23 @@ def langchain_backend_factory() -> LangChainBackend:
217230

218231

219232
def _normalize_agent_name(name: str) -> str:
220-
# TODO: should we check for collisions here?
221-
# TODO: we shouldn't change the name here - only add a prefix.
222-
# We should validate the name when the Agent is created
223-
name = "-".join(name.strip().split())
224233
return f"{AGENT_PREFIX}{name}"
225234

226235

227236
def _denormalize_agent_name(name: str) -> str:
228237
return name.removeprefix(AGENT_PREFIX)
229238

230239

240+
def _normalize_tool_name(name: str) -> str:
241+
if name.startswith(RESERVED_LC_TOOL_PREFIX):
242+
return f"{CONFLICTING_TOOL_PREFIX}{name}"
243+
return name
244+
245+
246+
def _denormalize_tool_name(name: str) -> str:
247+
return name.removeprefix(CONFLICTING_TOOL_PREFIX)
248+
249+
231250
def _agent_as_tool(agent: BaseAgent[OutputT]):
232251
if not agent.name:
233252
raise AssertionError("Agent must have a name to be used by other Agents")
@@ -274,21 +293,18 @@ def _map_tool_call_from_langchain(tool_call: LC_ToolCall) -> ToolCall | AgentCal
274293
)
275294

276295
return ToolCall(
277-
name=tool_call["name"],
296+
name=_denormalize_tool_name(tool_call["name"]),
278297
args=tool_call["args"],
279298
id=tool_call["id"],
280299
)
281300

282301

283302
def _map_tool_call_to_langchain(call: ToolCall | AgentCall) -> LC_ToolCall:
284-
if AGENT_PREFIX in call.name:
285-
raise InvalidToolNameError(
286-
f"ToolCall name cannot contain agent prefix: {call.name}"
287-
)
288-
289-
name = call.name
290-
if isinstance(call, AgentCall):
291-
name = _normalize_agent_name(call.name)
303+
match call:
304+
case AgentCall():
305+
name = _normalize_agent_name(call.name)
306+
case ToolCall():
307+
name = _normalize_tool_name(call.name)
292308

293309
return LC_ToolCall(
294310
name=name,
@@ -320,7 +336,7 @@ def _map_message_from_langchain(message: LC_BaseMessage) -> BaseMessage:
320336
"langchain responded with a tool call that does not have a name"
321337
)
322338
return ToolMessage(
323-
name=message.name,
339+
name=_denormalize_tool_name(message.name),
324340
content=str(message.content),
325341
call_id=message.tool_call_id,
326342
status=message.status,
@@ -351,9 +367,9 @@ def _map_message_to_langchain(message: BaseMessage) -> LC_BaseMessage:
351367
)
352368
case ToolMessage():
353369
return LC_ToolMessage(
370+
name=_normalize_tool_name(message.name),
354371
content=message.content,
355372
tool_call_id=message.call_id,
356-
name=message.name,
357373
status=message.status,
358374
)
359375
case SystemMessage():

tests/unit/ai/engine/test_langchain_backend.py

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
from splunklib.ai.core.backend import (
2929
InvalidMessageTypeError,
3030
InvalidModelError,
31-
InvalidToolNameError,
3231
)
3332
from splunklib.ai.engines import langchain as lc
3433
from splunklib.ai.messages import (
@@ -159,38 +158,95 @@ def test_map_message_to_langchain_ai_with_agent_call(self) -> None:
159158
)
160159
]
161160

162-
def test_map_message_to_langchain_tool_call_with_agent_prefix_raises(
161+
def test_map_message_to_langchain_human(self) -> None:
162+
message = HumanMessage(content="hello")
163+
mapped = lc._map_message_to_langchain(message)
164+
165+
assert isinstance(mapped, LC_HumanMessage)
166+
assert mapped.content == "hello"
167+
168+
def test_map_message_to_langchain_tool_call_with_reserved_prefix(
163169
self,
164170
) -> None:
165-
message = AIMessage(
166-
content="hi",
167-
calls=[ToolCall(name=f"{lc.AGENT_PREFIX}bad-tool", args={}, id="tc-3")],
171+
message = lc._map_message_to_langchain(
172+
AIMessage(
173+
content="hi",
174+
calls=[ToolCall(name=f"{lc.AGENT_PREFIX}bad-tool", args={}, id="tc-1")],
175+
)
176+
)
177+
assert isinstance(message, LC_AIMessage)
178+
assert message.tool_calls == [
179+
LC_ToolCall(name="__tool-__agent-bad-tool", args={}, id="tc-1")
180+
]
181+
182+
message = lc._map_message_to_langchain(
183+
AIMessage(
184+
content="hi",
185+
calls=[ToolCall(name="__bad-tool", args={}, id="tc-2")],
186+
)
168187
)
188+
assert isinstance(message, LC_AIMessage)
189+
assert message.tool_calls == [
190+
LC_ToolCall(name="__tool-__bad-tool", args={}, id="tc-2")
191+
]
169192

170-
with pytest.raises(InvalidToolNameError):
171-
lc._map_message_to_langchain(message)
193+
message = lc._map_message_to_langchain(
194+
ToolMessage(content="hi", name="__bad-tool")
195+
)
196+
assert isinstance(message, LC_ToolMessage)
197+
assert message.name == "__tool-__bad-tool"
172198

173-
def test_map_message_to_langchain_agent_call_with_agent_prefix_raises(
199+
def test_map_message_from_langchain_tool_call_with_reserved_prefix(
174200
self,
175201
) -> None:
176-
message = AIMessage(
177-
content="hi",
178-
calls=[
179-
AgentCall(
180-
name=f"{lc.AGENT_PREFIX}bad-agent", args={"q": "test"}, id="tc-4"
181-
)
182-
],
202+
message = lc._map_message_from_langchain(
203+
LC_AIMessage(
204+
content="hi",
205+
tool_calls=[
206+
LC_ToolCall(
207+
name="__tool-__bad-tool",
208+
args={},
209+
id="tc-1",
210+
)
211+
],
212+
)
183213
)
214+
assert isinstance(message, AIMessage)
215+
assert len(message.calls) > 0
216+
assert message.calls[0].name == "__bad-tool"
217+
218+
message = lc._map_message_from_langchain(
219+
message=LC_ToolMessage(
220+
name="__tool-__bad-tool",
221+
content="result",
222+
tool_call_id="call-1",
223+
status="success",
224+
)
225+
)
226+
assert isinstance(message, ToolMessage)
227+
assert message.name == "__bad-tool"
184228

185-
with pytest.raises(InvalidToolNameError):
186-
lc._map_message_to_langchain(message)
187-
188-
def test_map_message_to_langchain_human(self) -> None:
189-
message = HumanMessage(content="hello")
190-
mapped = lc._map_message_to_langchain(message)
229+
def test_map_message_to_langchain_agent_call_with_agent_prefix_raises(
230+
self,
231+
) -> None:
232+
message = lc._map_message_to_langchain(
233+
AIMessage(
234+
content="hi",
235+
calls=[
236+
AgentCall(
237+
name=f"{lc.AGENT_PREFIX}bad-agent",
238+
args={},
239+
id="tc-1",
240+
)
241+
],
242+
)
243+
)
191244

192-
assert isinstance(mapped, LC_HumanMessage)
193-
assert mapped.content == "hello"
245+
# Fine, but in practice a unnecessary prefix.
246+
assert isinstance(message, LC_AIMessage)
247+
assert message.tool_calls == [
248+
LC_ToolCall(name="__agent-__agent-bad-agent", args={}, id="tc-1")
249+
]
194250

195251
def test_map_message_to_langchain_system(self) -> None:
196252
message = SystemMessage(content="be helpful")
@@ -219,7 +275,7 @@ def test_map_message_to_langchain_subagent(self) -> None:
219275

220276
assert isinstance(mapped, LC_ToolMessage)
221277
assert mapped.content == "ping"
222-
assert mapped.name == f"{lc.AGENT_PREFIX}My-Agent"
278+
assert mapped.name == f"{lc.AGENT_PREFIX}My Agent"
223279
assert mapped.tool_call_id == "call-2"
224280
assert mapped.status == "error"
225281

0 commit comments

Comments
 (0)