-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmeta_tools.py
More file actions
282 lines (225 loc) · 8.28 KB
/
meta_tools.py
File metadata and controls
282 lines (225 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
"""Meta tools for dynamic tool discovery and execution"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
import bm25s
import numpy as np
from pydantic import BaseModel
from stackone_ai.models import ExecuteConfig, JsonDict, StackOneTool, ToolParameters
if TYPE_CHECKING:
from stackone_ai.models import Tools
class MetaToolSearchResult(BaseModel):
"""Result from meta_search_tools"""
name: str
description: str
score: float
class ToolIndex:
"""BM25-based tool search index"""
def __init__(self, tools: list[StackOneTool]) -> None:
self.tools = tools
self.tool_map = {tool.name: tool for tool in tools}
# Prepare corpus for BM25
corpus = []
self.tool_names = []
for tool in tools:
# Extract category and action from tool name
parts = tool.name.split("_")
category = parts[0] if parts else ""
# Extract action types
action_types = ["create", "update", "delete", "get", "list", "search"]
actions = [p for p in parts if p in action_types]
# Combine name, description, category and tags for indexing
doc_text = " ".join(
[
tool.name,
tool.description,
category,
" ".join(parts),
" ".join(actions),
]
)
corpus.append(doc_text)
self.tool_names.append(tool.name)
# Create BM25 index
self.retriever = bm25s.BM25()
# Tokenize without stemming for simplicity
corpus_tokens = bm25s.tokenize(corpus, stemmer=None, show_progress=False)
self.retriever.index(corpus_tokens)
def search(self, query: str, limit: int = 5, min_score: float = 0.0) -> list[MetaToolSearchResult]:
"""Search for relevant tools using BM25
Args:
query: Natural language query
limit: Maximum number of results
min_score: Minimum relevance score (0-1)
Returns:
List of search results sorted by relevance
"""
# Tokenize query
query_tokens = bm25s.tokenize([query], stemmer=None, show_progress=False)
# Search with BM25
results, scores = self.retriever.retrieve(query_tokens, k=min(limit * 2, len(self.tools)))
# Process results
search_results = []
# TODO: Add strict=False when Python 3.9 support is dropped
for idx, score in zip(results[0], scores[0]):
if score < min_score:
continue
tool_name = self.tool_names[idx]
tool = self.tool_map[tool_name]
# Normalize score to 0-1 range
normalized_score = float(1 / (1 + np.exp(-score / 10)))
search_results.append(
MetaToolSearchResult(
name=tool.name,
description=tool.description,
score=normalized_score,
)
)
if len(search_results) >= limit:
break
return search_results
def create_meta_search_tools(index: ToolIndex) -> StackOneTool:
"""Create the meta_search_tools tool
Args:
index: Tool search index
Returns:
Meta tool for searching relevant tools
"""
name = "meta_search_tools"
description = (
"Searches for relevant tools based on a natural language query. "
"This tool should be called first to discover available tools before executing them."
)
parameters = ToolParameters(
type="object",
properties={
"query": {
"type": "string",
"description": (
"Natural language query describing what tools you need "
'(e.g., "tools for managing employees", "create time off request")'
),
},
"limit": {
"type": "number",
"description": "Maximum number of tools to return (default: 5)",
"default": 5,
},
"minScore": {
"type": "number",
"description": "Minimum relevance score (0-1) to filter results (default: 0.0)",
"default": 0.0,
},
},
)
def execute_filter(arguments: str | JsonDict | None = None) -> JsonDict:
"""Execute the filter tool"""
# Parse arguments
if isinstance(arguments, str):
kwargs = json.loads(arguments)
else:
kwargs = arguments or {}
query = kwargs.get("query", "")
limit = int(kwargs.get("limit", 5))
min_score = float(kwargs.get("minScore", 0.0))
# Search for tools
results = index.search(query, limit, min_score)
# Format results
tools_data = [
{
"name": r.name,
"description": r.description,
"score": r.score,
}
for r in results
]
return {"tools": tools_data}
# Create execute config for the meta tool
execute_config = ExecuteConfig(
name=name,
method="POST",
url="", # Meta tools don't make HTTP requests
headers={},
)
# Create a wrapper class that delegates execute to our custom function
class MetaSearchTool(StackOneTool):
"""Meta tool for searching relevant tools"""
def __init__(self) -> None:
super().__init__(
description=description,
parameters=parameters,
_execute_config=execute_config,
_api_key="", # Meta tools don't need API key
_account_id=None,
)
def execute(
self, arguments: str | JsonDict | None = None, *, options: JsonDict | None = None
) -> JsonDict:
return execute_filter(arguments)
return MetaSearchTool()
def create_meta_execute_tool(tools_collection: Tools) -> StackOneTool:
"""Create the meta_execute_tool
Args:
tools_collection: Collection of tools to execute from
Returns:
Meta tool for executing discovered tools
"""
name = "meta_execute_tool"
description = (
"Executes a tool by name with the provided parameters. "
"Use this after discovering tools with meta_search_tools."
)
parameters = ToolParameters(
type="object",
properties={
"toolName": {
"type": "string",
"description": "Name of the tool to execute",
},
"params": {
"type": "object",
"description": "Parameters to pass to the tool",
"additionalProperties": True,
},
},
)
def execute_tool(arguments: str | JsonDict | None = None) -> JsonDict:
"""Execute the meta execute tool"""
# Parse arguments
if isinstance(arguments, str):
kwargs = json.loads(arguments)
else:
kwargs = arguments or {}
tool_name = kwargs.get("toolName")
params = kwargs.get("params", {})
if not tool_name:
raise ValueError("toolName is required")
# Get the tool
tool = tools_collection.get_tool(tool_name)
if not tool:
raise ValueError(f"Tool '{tool_name}' not found")
# Execute the tool
return tool.execute(params)
# Create execute config for the meta tool
execute_config = ExecuteConfig(
name=name,
method="POST",
url="", # Meta tools don't make HTTP requests
headers={},
)
# Create a wrapper class that delegates execute to our custom function
class MetaExecuteTool(StackOneTool):
"""Meta tool for executing discovered tools"""
def __init__(self) -> None:
super().__init__(
description=description,
parameters=parameters,
_execute_config=execute_config,
_api_key="", # Meta tools don't need API key
_account_id=None,
)
def execute(
self, arguments: str | JsonDict | None = None, *, options: JsonDict | None = None
) -> JsonDict:
return execute_tool(arguments)
return MetaExecuteTool()