Skip to content

Commit ba762da

Browse files
PR Suggestion from bots
1 parent 5388c19 commit ba762da

File tree

4 files changed

+412
-35
lines changed

4 files changed

+412
-35
lines changed

examples/meta_tools_example.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def example_openai_meta_tools() -> None:
5353

5454
if openai_key:
5555
client = OpenAI()
56-
model = "gpt-5.1"
56+
model = "gpt-4o"
5757
provider = "OpenAI"
5858
elif google_key:
5959
client = OpenAI(
@@ -85,8 +85,10 @@ def example_openai_meta_tools() -> None:
8585
"content": (
8686
"You are a helpful scheduling assistant. "
8787
"Use tool_search to find relevant tools, then tool_execute to run them. "
88-
"If a tool execution fails, try different parameters or a different tool. "
89-
"Do not repeat the same failed call."
88+
"Always read the parameter schemas from tool_search results carefully. "
89+
"If a tool needs a user URI, first search for and call a "
90+
"'get current user' tool to obtain it. "
91+
"If a tool execution fails, fix the parameters and retry."
9092
),
9193
},
9294
{

stackone_ai/meta_tools.py

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
import json
66
from 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

1010
from 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:
5453
class 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:
118113
class 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
)

stackone_ai/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ def to_langchain(self) -> BaseTool:
422422
python_type = int
423423
elif type_str == "boolean":
424424
python_type = bool
425+
elif type_str == "object":
426+
python_type = dict
427+
elif type_str == "array":
428+
python_type = list
425429

426430
field = Field(description=details.get("description", ""))
427431
else:

0 commit comments

Comments
 (0)