Skip to content

Commit 88bb4db

Browse files
committed
Fix tool filtering for MCP tools
1 parent 1718ad4 commit 88bb4db

4 files changed

Lines changed: 127 additions & 116 deletions

File tree

lightspeed_stack_providers/providers/inline/agents/lightspeed_inline_agent/agents.py

Lines changed: 54 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -97,34 +97,13 @@ async def create_openai_response(
9797

9898
# Apply tool filtering if enabled and tools are provided
9999
filtered_tools = tools
100-
if (
101-
tools
102-
and self.config.tools_filter.enabled
103-
and len(tools) > self.config.tools_filter.min_tools
104-
):
105-
logger.info(
106-
"Tool filtering enabled - filtering %d tools (threshold: %d)",
107-
len(tools),
108-
self.config.tools_filter.min_tools,
109-
)
100+
if tools and self.config.tools_filter.enabled:
110101
filtered_tools = await self._filter_tools_for_response(
111102
input=input,
112103
tools=tools,
113104
model=model,
114105
conversation=conversation,
115106
)
116-
logger.info(
117-
"Tool filtering complete - reduced from %d to %d tools",
118-
len(tools),
119-
len(filtered_tools) if filtered_tools else 0,
120-
)
121-
else:
122-
logger.info(
123-
"Skipping tool filtering - %d tools (threshold: %d, enabled: %s)",
124-
len(tools) if tools else 0,
125-
self.config.tools_filter.min_tools,
126-
self.config.tools_filter.enabled,
127-
)
128107

129108
# Call parent with filtered tools and temperature
130109
return await super().create_openai_response(
@@ -189,6 +168,20 @@ async def _filter_tools_for_response(
189168
logger.warning("No tool definitions found for filtering")
190169
return tools
191170

171+
if len(tools_for_filtering) <= self.config.tools_filter.min_tools:
172+
logger.info(
173+
"Skipping tool filtering - %d tools (threshold: %d)",
174+
len(tools_for_filtering),
175+
self.config.tools_filter.min_tools,
176+
)
177+
return tools
178+
179+
logger.info(
180+
"Tool filtering enabled - filtering %d tools (threshold: %d)",
181+
len(tools_for_filtering),
182+
self.config.tools_filter.min_tools,
183+
)
184+
192185
# Extract user prompt text from input
193186
if isinstance(input, str):
194187
user_prompt = input
@@ -246,28 +239,28 @@ async def _filter_tools_for_response(
246239
logger.error("Failed to parse LLM response as JSON: %s", exp)
247240
filtered_tool_names = []
248241

249-
# Filter the original tools list
250-
if filtered_tool_names or always_included_tools:
251-
# Create a mapping from tool names to tool configs
252-
tool_name_to_config = {}
253-
for i, tool in enumerate(tools):
242+
# Merge always-included tools into filtered list
243+
filtered_tool_names = list(set(filtered_tool_names) | always_included_tools)
244+
245+
# Filter using expanded tool definitions
246+
if filtered_tool_names:
247+
result = []
248+
for tool in tools:
254249
tool_dict = tool if isinstance(tool, dict) else tool.model_dump()
255-
tool_name = self._get_tool_name_from_config(tool_dict, i)
256-
tool_name_to_config[tool_name] = tool
250+
tool_type = tool_dict.get("type")
257251

258-
# Filter based on LLM response and always included tools
259-
filtered_tools = [
260-
tool_name_to_config[name]
261-
for name in tool_name_to_config
262-
if name in filtered_tool_names or name in always_included_tools
263-
]
252+
if tool_type == "mcp" and len(filtered_tool_names) > 0:
253+
tool.allowed_tools = filtered_tool_names
254+
result.append(tool)
255+
else:
256+
result.append(tool)
264257

265258
logger.info(
266-
"Filtered tools count: %d removed, %d remaining",
267-
len(tools) - len(filtered_tools),
268-
len(filtered_tools),
259+
"Filtered tools: %d removed, %d remaining",
260+
len(tools_for_filtering) - len(filtered_tool_names),
261+
len(filtered_tool_names),
269262
)
270-
return filtered_tools
263+
return result
271264
else:
272265
logger.warning("No tools matched filtering criteria, returning empty list")
273266
return []
@@ -295,6 +288,9 @@ async def _get_previously_called_tools(self, conversation_id: str) -> set[str]:
295288
if item_type == "function_call":
296289
if hasattr(item, "name") and item.name:
297290
tool_names.add(item.name)
291+
elif item_type in ("mcp_call", "mcp_approval_request"):
292+
if hasattr(item, "name") and item.name:
293+
tool_names.add(item.name)
298294
# Also check for nested tool_calls (legacy format)
299295
elif hasattr(item, "tool_calls") and item.tool_calls:
300296
for tool_call in item.tool_calls:
@@ -321,34 +317,23 @@ async def _extract_tool_definitions(
321317
List of dicts with tool_name and description
322318
"""
323319
tool_defs = []
320+
seen_tool_names: set[str] = set()
324321

325-
for i, tool in enumerate(tools):
322+
for tool in tools:
326323
tool_dict = tool if isinstance(tool, dict) else tool.model_dump()
327324
tool_type = tool_dict.get("type")
328325

329326
if tool_type == "mcp":
330327
mcp_tools = await self._get_mcp_tool_definitions(tool_dict)
331-
tool_defs.extend(mcp_tools)
332-
elif tool_type == "file_search":
333-
tool_defs.append(
334-
{
335-
"tool_name": "file_search",
336-
"description": "Search through uploaded files and knowledge base",
337-
}
338-
)
339-
elif tool_type == "function":
340-
name = tool_dict.get("name", f"function_{i}")
341-
description = tool_dict.get("description", "")
342-
tool_defs.append({"tool_name": name, "description": description})
343-
else:
344-
logger.warning("Unknown tool type: %s", tool_type)
345-
tool_defs.append(
346-
{
347-
"tool_name": f"{tool_type}_{i}",
348-
"description": f"Tool of type {tool_type}",
349-
}
350-
)
351-
328+
for mcp_tool in mcp_tools:
329+
if mcp_tool["tool_name"] not in seen_tool_names:
330+
seen_tool_names.add(mcp_tool["tool_name"])
331+
tool_defs.append(mcp_tool)
332+
333+
logger.info(
334+
"Extracted %d unique tool definitions from %d tool configs",
335+
len(tool_defs), len(tools),
336+
)
352337
return tool_defs
353338

354339
async def _get_mcp_tool_definitions(
@@ -376,6 +361,9 @@ async def _get_mcp_tool_definitions(
376361
from llama_stack_api.common.content_types import URL
377362

378363
mcp_endpoint = URL(uri=server_url)
364+
# Note: llama-stack 0.4.x ignores the mcp_endpoint parameter and
365+
# returns tools from ALL registered MCP servers. Deduplication is
366+
# handled in _extract_tool_definitions.
379367
tools_response = await self.tool_runtime_api.list_runtime_tools(
380368
mcp_endpoint=mcp_endpoint
381369
)
@@ -385,6 +373,11 @@ async def _get_mcp_tool_definitions(
385373
{
386374
"tool_name": tool_def.name,
387375
"description": tool_def.description or "",
376+
"parameters": (
377+
tool_def.parameters
378+
if hasattr(tool_def, "parameters")
379+
else {}
380+
),
388381
}
389382
)
390383

resources/external_providers/inline/agents/lightspeed_inline_agent.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ adapter_type: lightspeed_inline_agent
22
pip_packages: []
33
config_class: lightspeed_stack_providers.providers.inline.agents.lightspeed_inline_agent.config.LightspeedAgentsImplConfig
44
module: lightspeed_stack_providers.providers.inline.agents.lightspeed_inline_agent
5-
api_dependencies: [ inference, safety, vector_io, vector_stores, tool_runtime, tool_groups ]
6-
optional_api_dependencies: []
5+
api_dependencies: [ inference, vector_io, tool_runtime, tool_groups, conversations, prompts, files ]
6+
optional_api_dependencies: [ safety ]

run.yaml

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ apis:
66
- vector_io
77
- agents
88
- tool_runtime
9+
- vector_io
10+
- files
911

1012
external_providers_dir: ./resources/external_providers
1113

@@ -32,6 +34,16 @@ storage:
3234
backend: kv_default
3335

3436
providers:
37+
38+
files:
39+
- provider_id: meta-reference-files
40+
provider_type: inline::localfs
41+
config:
42+
storage_dir: ${env.FILES_STORAGE_DIR:=/tmp/llama_files}
43+
metadata_store:
44+
table_name: files_metadata
45+
backend: sql_default
46+
3547
inference:
3648
- provider_id: openai
3749
provider_type: remote::openai
@@ -42,7 +54,7 @@ providers:
4254
- provider_id: lightspeed_question_validity
4355
provider_type: inline::lightspeed_question_validity
4456
config:
45-
model_id: openai/gpt-4o
57+
model_id: openai/gpt-4o-mini
4658
temperature: 0.0
4759
invalid_question_response: |
4860
Hi, I'm the OpenShift Lightspeed assistant, I can help you with questions about OpenShift,
@@ -88,6 +100,11 @@ vector_stores:
88100
- pattern: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"
89101
replacement: "[REDACTED_EMAIL]"
90102

103+
tool_runtime:
104+
- provider_id: model-context-protocol
105+
provider_type: remote::model-context-protocol
106+
config: {}
107+
91108
agents:
92109
- provider_id: lightspeed_inline_agent
93110
provider_type: inline::lightspeed_inline_agent
@@ -108,11 +125,6 @@ vector_stores:
108125
chatbot_temperature_override: 1.0
109126

110127
registered_resources:
111-
models:
112-
- model_id: gpt-4o
113-
provider_id: openai
114-
model_type: llm
115-
provider_model_id: gpt-4o
116128

117129
shields:
118130
- shield_id: lightspeed_question_validity-shield
@@ -121,7 +133,11 @@ registered_resources:
121133
provider_id: lightspeed_redaction
122134
provider_shield_id: lightspeed-redaction-shield
123135

124-
tool_groups: []
136+
tool_groups:
137+
- toolgroup_id: openshift-tools
138+
provider_id: model-context-protocol
139+
mcp_endpoint:
140+
uri: http://localhost:8401/sse
125141

126142
server:
127143
host: 0.0.0.0

0 commit comments

Comments
 (0)