Skip to content

Commit aa955db

Browse files
remove toolset.get_meta_tools() and update to new API
1 parent 0337e25 commit aa955db

File tree

3 files changed

+88
-65
lines changed

3 files changed

+88
-65
lines changed

examples/meta_tools_example.py

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99
(tool_search + tool_execute). The LLM discovers and runs tools on-demand,
1010
keeping token usage constant regardless of catalog size.
1111
12-
This example demonstrates approach 2 with OpenAI and LangChain clients.
12+
This example demonstrates approach 2 with a Gemini client (OpenAI-compatible).
1313
1414
Prerequisites:
1515
- STACKONE_API_KEY environment variable
1616
- STACKONE_ACCOUNT_ID environment variable
1717
- GOOGLE_API_KEY environment variable (for Gemini)
18-
- OPENAI_API_KEY environment variable (optional, for LangChain example)
1918
2019
Run with:
2120
uv run python examples/meta_tools_example.py
@@ -68,8 +67,7 @@ def example_gemini() -> None:
6867
execute={"account_ids": [account_id]} if account_id else None,
6968
)
7069

71-
# 2. Get meta tools in OpenAI format
72-
meta_tools = toolset.get_meta_tools()
70+
# 2. Get tools in OpenAI format
7371
openai_tools = toolset.openai(mode="search_and_execute")
7472

7573
# 3. Create Gemini client (OpenAI-compatible) and run agent loop
@@ -100,8 +98,7 @@ def example_gemini() -> None:
10098
messages.append(choice.message.model_dump(exclude_none=True))
10199
for tool_call in choice.message.tool_calls:
102100
print(f" -> {tool_call.function.name}({tool_call.function.arguments})")
103-
tool = meta_tools.get_tool(tool_call.function.name)
104-
result = tool.execute(tool_call.function.arguments) if tool else {"error": "Unknown tool"}
101+
result = toolset.execute(tool_call.function.name, tool_call.function.arguments)
105102
messages.append(
106103
{
107104
"role": "tool",
@@ -113,64 +110,6 @@ def example_gemini() -> None:
113110
print()
114111

115112

116-
def example_langchain() -> None:
117-
"""Complete LangChain integration with meta tools.
118-
119-
Shows: init toolset -> bind tools to ChatOpenAI -> agent loop -> final answer.
120-
"""
121-
print("=" * 60)
122-
print("Example 2: LangChain client with meta tools")
123-
print("=" * 60)
124-
print()
125-
126-
try:
127-
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
128-
from langchain_google_genai import ChatGoogleGenerativeAI
129-
except ImportError:
130-
print("Skipped: pip install langchain-google-genai")
131-
print()
132-
return
133-
134-
if not os.getenv("GOOGLE_API_KEY"):
135-
print("Skipped: Set GOOGLE_API_KEY to run this example.")
136-
print()
137-
return
138-
139-
# 1. Init toolset
140-
account_id = os.getenv("STACKONE_ACCOUNT_ID")
141-
toolset = StackOneToolSet(
142-
account_id=account_id,
143-
search={"method": "semantic", "top_k": 3},
144-
execute={"account_ids": [account_id]} if account_id else None,
145-
)
146-
147-
# 2. Get meta tools in LangChain format and bind to model
148-
meta_tools = toolset.get_meta_tools()
149-
langchain_tools = meta_tools.to_langchain()
150-
model = ChatGoogleGenerativeAI(model="gemini-3-pro-preview").bind_tools(langchain_tools)
151-
152-
# 3. Run agent loop
153-
messages = [HumanMessage(content="List my upcoming Calendly events for the next week.")]
154-
155-
for _step in range(10):
156-
response: AIMessage = model.invoke(messages)
157-
158-
# 4. If no tool calls, print final answer and stop
159-
if not response.tool_calls:
160-
print(f"Answer: {response.content}")
161-
break
162-
163-
# 5. Execute tool calls and feed results back
164-
messages.append(response)
165-
for tool_call in response.tool_calls:
166-
print(f" -> {tool_call['name']}({json.dumps(tool_call['args'])})")
167-
tool = meta_tools.get_tool(tool_call["name"])
168-
result = tool.execute(tool_call["args"]) if tool else {"error": "Unknown tool"}
169-
messages.append(ToolMessage(content=json.dumps(result), tool_call_id=tool_call["id"]))
170-
171-
print()
172-
173-
174113
def main() -> None:
175114
"""Run all meta tools examples."""
176115
api_key = os.getenv("STACKONE_API_KEY")
@@ -179,7 +118,6 @@ def main() -> None:
179118
return
180119

181120
example_gemini()
182-
example_langchain()
183121

184122

185123
if __name__ == "__main__":

stackone_ai/toolset.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ def __init__(
367367
self._semantic_client: SemanticSearchClient | None = None
368368
self._search_config: SearchConfig | None = search
369369
self._execute_config: ExecuteToolsConfig | None = execute
370+
self._meta_tools_cache: Tools | None = None
370371

371372
def set_accounts(self, account_ids: list[str]) -> StackOneToolSet:
372373
"""Set account IDs for filtering tools
@@ -502,6 +503,36 @@ def openai(
502503

503504
return self.fetch_tools(account_ids=effective_account_ids).to_openai()
504505

506+
507+
def execute(
508+
self,
509+
tool_name: str,
510+
arguments: str | dict[str, Any] | None = None,
511+
) -> dict[str, Any]:
512+
"""Execute a tool by name.
513+
514+
Use with ``openai(mode="search_and_execute")`` in manual agent loops —
515+
pass the tool name and arguments from the LLM's tool call directly.
516+
517+
Meta tools are cached after the first call.
518+
519+
Args:
520+
tool_name: The tool name from the LLM's tool call
521+
(e.g. ``"tool_search"`` or ``"tool_execute"``).
522+
arguments: The arguments from the LLM's tool call,
523+
as a JSON string or dict.
524+
525+
Returns:
526+
Tool execution result as a dict.
527+
"""
528+
if self._meta_tools_cache is None:
529+
self._meta_tools_cache = self.get_meta_tools()
530+
531+
tool = self._meta_tools_cache.get_tool(tool_name)
532+
if tool is None:
533+
return {"error": f'Tool "{tool_name}" not found.'}
534+
return tool.execute(arguments)
535+
505536
@property
506537
def semantic_client(self) -> SemanticSearchClient:
507538
"""Lazy initialization of semantic search client.

tests/test_meta_tools.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,57 @@ def test_openai_search_and_execute_with_execute_config(self):
431431
toolset.openai(mode="search_and_execute")
432432

433433
mock_get.assert_called_once_with(account_ids=["acc-1"])
434+
435+
436+
class TestToolSetExecuteMethod:
437+
"""Tests for StackOneToolSet.execute() convenience method."""
438+
439+
def test_execute_delegates_to_meta_tool(self):
440+
toolset = StackOneToolSet(api_key="test-key", search={"method": "auto"})
441+
mock_tool = MagicMock()
442+
mock_tool.execute.return_value = {"result": "ok"}
443+
mock_meta = MagicMock()
444+
mock_meta.get_tool.return_value = mock_tool
445+
446+
with patch.object(toolset, "get_meta_tools", return_value=mock_meta):
447+
result = toolset.execute("tool_search", {"query": "employees"})
448+
449+
assert result == {"result": "ok"}
450+
mock_meta.get_tool.assert_called_once_with("tool_search")
451+
mock_tool.execute.assert_called_once_with({"query": "employees"})
452+
453+
def test_execute_caches_meta_tools(self):
454+
toolset = StackOneToolSet(api_key="test-key", search={"method": "auto"})
455+
mock_tool = MagicMock()
456+
mock_tool.execute.return_value = {"ok": True}
457+
mock_meta = MagicMock()
458+
mock_meta.get_tool.return_value = mock_tool
459+
460+
with patch.object(toolset, "get_meta_tools", return_value=mock_meta) as mock_get:
461+
toolset.execute("tool_search", {"query": "a"})
462+
toolset.execute("tool_execute", {"tool_name": "b"})
463+
464+
mock_get.assert_called_once()
465+
466+
def test_execute_returns_error_for_unknown_tool(self):
467+
toolset = StackOneToolSet(api_key="test-key", search={"method": "auto"})
468+
mock_meta = MagicMock()
469+
mock_meta.get_tool.return_value = None
470+
471+
with patch.object(toolset, "get_meta_tools", return_value=mock_meta):
472+
result = toolset.execute("nonexistent", {})
473+
474+
assert "error" in result
475+
476+
def test_execute_accepts_string_arguments(self):
477+
toolset = StackOneToolSet(api_key="test-key", search={"method": "auto"})
478+
mock_tool = MagicMock()
479+
mock_tool.execute.return_value = {"ok": True}
480+
mock_meta = MagicMock()
481+
mock_meta.get_tool.return_value = mock_tool
482+
483+
with patch.object(toolset, "get_meta_tools", return_value=mock_meta):
484+
result = toolset.execute("tool_search", '{"query": "test"}')
485+
486+
assert result == {"ok": True}
487+
mock_tool.execute.assert_called_once_with('{"query": "test"}')

0 commit comments

Comments
 (0)