55import json
66from typing import TYPE_CHECKING , Any
77
8- from pydantic import BaseModel , Field , field_validator
8+ from pydantic import BaseModel , Field , PrivateAttr , ValidationError , field_validator
99
1010from stackone_ai .models import (
1111 ExecuteConfig ,
1212 JsonDict ,
1313 ParameterLocation ,
1414 StackOneAPIError ,
15- StackOneError ,
1615 StackOneTool ,
1716 ToolParameters ,
1817 Tools ,
@@ -54,8 +53,8 @@ def validate_query(cls, v: str) -> str:
5453class SearchMetaTool (StackOneTool ):
5554 """LLM-callable tool that searches for available StackOne tools."""
5655
57- _toolset : Any = None
58- _options : MetaToolsOptions = None # type: ignore[assignment]
56+ _toolset : Any = PrivateAttr ( default = None )
57+ _options : MetaToolsOptions = PrivateAttr ( default = None ) # type: ignore[assignment]
5958
6059 def execute (
6160 self , arguments : str | JsonDict | None = None , * , options : JsonDict | None = None
@@ -89,12 +88,8 @@ def execute(
8988 "total" : len (results ),
9089 "query" : parsed .query ,
9190 }
92- except json .JSONDecodeError as exc :
93- raise StackOneError (f"Invalid JSON in arguments: { exc } " ) from exc
94- except Exception as error :
95- if isinstance (error , StackOneError ):
96- raise
97- raise StackOneError (f"Error searching tools: { error } " ) from error
91+ except (json .JSONDecodeError , ValidationError ) as exc :
92+ return {"error" : f"Invalid input: { exc } " , "query" : raw_params if "raw_params" in dir () else None }
9893
9994
10095# --- tool_execute ---
@@ -118,42 +113,45 @@ def validate_tool_name(cls, v: str) -> str:
118113class ExecuteMetaTool (StackOneTool ):
119114 """LLM-callable tool that executes a StackOne tool by name."""
120115
121- _toolset : Any = None
122- _options : MetaToolsOptions = None # type: ignore[assignment]
116+ _toolset : Any = PrivateAttr (default = None )
117+ _options : MetaToolsOptions = PrivateAttr (default = None ) # type: ignore[assignment]
118+ _cached_tools : Any = PrivateAttr (default = None )
123119
124120 def execute (
125121 self , arguments : str | JsonDict | None = None , * , options : JsonDict | None = None
126122 ) -> JsonDict :
123+ tool_name = "unknown"
127124 try :
128125 if isinstance (arguments , str ):
129126 raw_params = json .loads (arguments )
130127 else :
131128 raw_params = arguments or {}
132129
133130 parsed = ExecuteInput (** raw_params )
131+ tool_name = parsed .tool_name
134132
135- all_tools = self ._toolset .fetch_tools (account_ids = self ._options .account_ids )
136- target = all_tools .get_tool (parsed .tool_name )
133+ if self ._cached_tools is None :
134+ self ._cached_tools = self ._toolset .fetch_tools (account_ids = self ._options .account_ids )
135+
136+ target = self ._cached_tools .get_tool (parsed .tool_name )
137137
138138 if target is None :
139139 return {
140- "error" : f'Tool "{ parsed .tool_name } " not found. Use tool_search to find available tools.' ,
140+ "error" : (
141+ f'Tool "{ parsed .tool_name } " not found. Use tool_search to find available tools.'
142+ ),
141143 }
142144
143145 return target .execute (parsed .parameters , options = options )
144146 except StackOneAPIError as exc :
145- # Return API errors to the LLM so it can adjust parameters and retry
146147 return {
147148 "error" : str (exc ),
148149 "status_code" : exc .status_code ,
149- "tool_name" : parsed .tool_name if "parsed" in dir () else "unknown" ,
150+ "response_body" : exc .response_body ,
151+ "tool_name" : tool_name ,
150152 }
151- except json .JSONDecodeError as exc :
152- raise StackOneError (f"Invalid JSON in arguments: { exc } " ) from exc
153- except Exception as error :
154- if isinstance (error , StackOneError ):
155- raise
156- raise StackOneError (f"Error executing tool: { error } " ) from error
153+ except (json .JSONDecodeError , ValidationError ) as exc :
154+ return {"error" : f"Invalid input: { exc } " , "tool_name" : tool_name }
157155
158156
159157# --- Factory ---
@@ -176,24 +174,25 @@ def create_meta_tools(
176174 api_key = toolset .api_key
177175
178176 # tool_search
179- search_tool = _create_search_tool (api_key , opts )
177+ search_tool = _create_search_tool (api_key )
180178 search_tool ._toolset = toolset
181179 search_tool ._options = opts
182180
183181 # tool_execute
184- execute_tool = _create_execute_tool (api_key , opts )
182+ execute_tool = _create_execute_tool (api_key )
185183 execute_tool ._toolset = toolset
186184 execute_tool ._options = opts
187185
188186 return Tools ([search_tool , execute_tool ])
189187
190188
191- def _create_search_tool (api_key : str , opts : MetaToolsOptions ) -> SearchMetaTool :
189+ def _create_search_tool (api_key : str ) -> SearchMetaTool :
192190 name = "tool_search"
193191 description = (
194192 "Search for available tools by describing what you need. "
195193 "Returns matching tool names, descriptions, and parameter schemas. "
196- "Use the returned parameter schemas to know exactly what to pass when calling tool_execute."
194+ "Use the returned parameter schemas to know exactly what to pass "
195+ "when calling tool_execute."
197196 )
198197 parameters = ToolParameters (
199198 type = "object" ,
@@ -207,13 +206,15 @@ def _create_search_tool(api_key: str, opts: MetaToolsOptions) -> SearchMetaTool:
207206 },
208207 "connector" : {
209208 "type" : "string" ,
210- "description" : 'Optional connector filter (e.g. "bamboohr", "hibob")' ,
209+ "description" : 'Optional connector filter (e.g. "bamboohr")' ,
210+ "nullable" : True ,
211211 },
212212 "top_k" : {
213213 "type" : "integer" ,
214214 "description" : "Max results to return (1-50, default 5)" ,
215215 "minimum" : 1 ,
216216 "maximum" : 50 ,
217+ "nullable" : True ,
217218 },
218219 },
219220 )
@@ -239,13 +240,14 @@ def _create_search_tool(api_key: str, opts: MetaToolsOptions) -> SearchMetaTool:
239240 return tool
240241
241242
242- def _create_execute_tool (api_key : str , opts : MetaToolsOptions ) -> ExecuteMetaTool :
243+ def _create_execute_tool (api_key : str ) -> ExecuteMetaTool :
243244 name = "tool_execute"
244245 description = (
245246 "Execute a tool by name with the given parameters. "
246247 "Use tool_search first to find available tools. "
247- "The parameters field must match the parameter schema returned by tool_search. "
248- "Pass parameters as a nested object matching the schema structure."
248+ "The parameters field must match the parameter schema returned "
249+ "by tool_search. Pass parameters as a nested object matching "
250+ "the schema structure."
249251 )
250252 parameters = ToolParameters (
251253 type = "object" ,
@@ -256,7 +258,8 @@ def _create_execute_tool(api_key: str, opts: MetaToolsOptions) -> ExecuteMetaToo
256258 },
257259 "parameters" : {
258260 "type" : "object" ,
259- "description" : "Parameters for the tool. Pass {} if none needed." ,
261+ "description" : "Parameters for the tool, matching the schema from tool_search." ,
262+ "nullable" : True ,
260263 },
261264 },
262265 )
0 commit comments