Skip to content

Commit 8904b1d

Browse files
committed
feat: bring Python SDK to feature parity with Node SDK
- Implement tool calling ability with call() and acall() methods - Add glob pattern filtering support (already existed, confirmed working) - Implement meta tools for dynamic tool discovery and execution - Add BM25-based search for tool discovery - Include comprehensive tests for all new features - Add example demonstrating meta tools usage - Update README with new feature documentation
1 parent dfde2a6 commit 8904b1d

File tree

9 files changed

+1152
-3
lines changed

9 files changed

+1152
-3
lines changed

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,44 @@ from stackone_ai import StackOneToolSet
1717
toolset = StackOneToolSet() # Uses STACKONE_API_KEY env var
1818
# Or explicitly: toolset = StackOneToolSet(api_key="your-api-key")
1919

20-
# Get HRIS-related tools
20+
# Get HRIS-related tools with glob patterns
2121
tools = toolset.get_tools("hris_*", account_id="your-account-id")
22+
# Exclude certain tools with negative patterns
23+
tools = toolset.get_tools(["hris_*", "!hris_delete_*"])
2224

23-
# Use a specific tool
25+
# Use a specific tool with the new call method
2426
employee_tool = tools.get_tool("hris_get_employee")
27+
# Call with keyword arguments
28+
employee = employee_tool.call(id="employee-id")
29+
# Or with traditional execute method
2530
employee = employee_tool.execute({"id": "employee-id"})
2631
```
2732

33+
## Meta Tools (Beta)
34+
35+
Meta tools enable dynamic tool discovery and execution without hardcoding tool names:
36+
37+
```python
38+
# Get meta tools for dynamic discovery
39+
tools = toolset.get_tools("hris_*")
40+
meta_tools = tools.meta_tools()
41+
42+
# Search for relevant tools using natural language
43+
filter_tool = meta_tools.get_tool("meta_filter_relevant_tools")
44+
results = filter_tool.call(query="manage employees", limit=5)
45+
46+
# Execute discovered tools dynamically
47+
execute_tool = meta_tools.get_tool("meta_execute_tool")
48+
result = execute_tool.call(toolName="hris_list_employees", params={"limit": 10})
49+
```
50+
2851
## Features
2952

3053
- Unified interface for multiple SaaS tools
3154
- AI-friendly tool descriptions and parameters
55+
- **Tool Calling**: Direct method calling with `tool.call()` for intuitive usage
56+
- **Glob Pattern Filtering**: Advanced tool filtering with patterns like `"hris_*"` and exclusions `"!hris_delete_*"`
57+
- **Meta Tools** (Beta): Dynamic tool discovery and execution based on natural language queries
3258
- Integration with popular AI frameworks:
3359
- OpenAI Functions
3460
- LangChain Tools

examples/meta_tools_example.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
#!/usr/bin/env python
2+
"""
3+
Example demonstrating meta tools for dynamic tool discovery and execution.
4+
5+
Meta tools allow AI agents to search for relevant tools based on natural language queries
6+
and execute them dynamically without hardcoding tool names.
7+
"""
8+
9+
import os
10+
11+
from dotenv import load_dotenv
12+
13+
from stackone_ai import StackOneToolSet
14+
15+
# Load environment variables
16+
load_dotenv()
17+
18+
19+
def example_meta_tools_basic():
20+
"""Basic example of using meta tools for tool discovery"""
21+
print("🔍 Example 1: Dynamic tool discovery\n")
22+
23+
# Initialize StackOne toolset
24+
toolset = StackOneToolSet()
25+
26+
# Get all available tools (you can also use pattern like "hris_*")
27+
all_tools = toolset.get_tools("hris_*")
28+
print(f"Total HRIS tools available: {len(all_tools)}")
29+
30+
# Get meta tools for dynamic discovery
31+
meta_tools = all_tools.meta_tools()
32+
33+
# Get the filter tool to search for relevant tools
34+
filter_tool = meta_tools.get_tool("meta_filter_relevant_tools")
35+
if filter_tool:
36+
# Search for employee management tools
37+
result = filter_tool.call(query="manage employees create update list", limit=5, minScore=0.0)
38+
39+
print("Found relevant tools:")
40+
for tool in result.get("tools", []):
41+
print(f" - {tool['name']} (score: {tool['score']:.2f}): {tool['description']}")
42+
43+
print()
44+
45+
46+
def example_meta_tools_with_execution():
47+
"""Example of discovering and executing tools dynamically"""
48+
print("🚀 Example 2: Dynamic tool execution\n")
49+
50+
# Initialize toolset
51+
toolset = StackOneToolSet()
52+
53+
# Get all tools
54+
all_tools = toolset.get_tools()
55+
meta_tools = all_tools.meta_tools()
56+
57+
# Step 1: Search for relevant tools
58+
filter_tool = meta_tools.get_tool("meta_filter_relevant_tools")
59+
execute_tool = meta_tools.get_tool("meta_execute_tool")
60+
61+
if filter_tool and execute_tool:
62+
# Find tools for listing employees
63+
search_result = filter_tool.call(query="list all employees", limit=1)
64+
65+
tools_found = search_result.get("tools", [])
66+
if tools_found:
67+
best_tool = tools_found[0]
68+
print(f"Best matching tool: {best_tool['name']}")
69+
print(f"Description: {best_tool['description']}")
70+
print(f"Relevance score: {best_tool['score']:.2f}")
71+
72+
# Step 2: Execute the found tool
73+
try:
74+
print(f"\nExecuting {best_tool['name']}...")
75+
result = execute_tool.call(toolName=best_tool["name"], params={"limit": 5})
76+
print(f"Execution result: {result}")
77+
except Exception as e:
78+
print(f"Execution failed (expected in example): {e}")
79+
80+
print()
81+
82+
83+
def example_tool_calling():
84+
"""Example of the new tool calling functionality"""
85+
print("📞 Example 3: Tool calling functionality\n")
86+
87+
# Initialize toolset
88+
toolset = StackOneToolSet()
89+
90+
# Get a specific tool
91+
tool = toolset.get_tool("hris_list_employees")
92+
93+
if tool:
94+
print(f"Tool: {tool.name}")
95+
print(f"Description: {tool.description}")
96+
97+
# New calling methods
98+
try:
99+
# Method 1: Call with keyword arguments
100+
result = tool.call(limit=10, page=1)
101+
print(f"Called with kwargs: {result}")
102+
except Exception as e:
103+
print(f"Call with kwargs (expected to fail in example): {e}")
104+
105+
try:
106+
# Method 2: Call with dictionary
107+
result = tool.call({"limit": 10, "page": 1})
108+
print(f"Called with dict: {result}")
109+
except Exception as e:
110+
print(f"Call with dict (expected to fail in example): {e}")
111+
112+
print()
113+
114+
115+
def example_with_openai():
116+
"""Example of using meta tools with OpenAI"""
117+
print("🤖 Example 4: Using meta tools with OpenAI\n")
118+
119+
try:
120+
from openai import OpenAI
121+
122+
# Initialize OpenAI client
123+
client = OpenAI()
124+
125+
# Initialize StackOne toolset
126+
toolset = StackOneToolSet()
127+
128+
# Get HRIS tools and their meta tools
129+
hris_tools = toolset.get_tools("hris_*")
130+
meta_tools = hris_tools.meta_tools()
131+
132+
# Convert to OpenAI format
133+
openai_tools = meta_tools.to_openai()
134+
135+
# Create a chat completion with meta tools
136+
response = client.chat.completions.create(
137+
model="gpt-4",
138+
messages=[
139+
{
140+
"role": "system",
141+
"content": "You are an HR assistant. Use meta_filter_relevant_tools to find appropriate tools, then meta_execute_tool to execute them.",
142+
},
143+
{"role": "user", "content": "Can you help me find tools for managing employee records?"},
144+
],
145+
tools=openai_tools,
146+
tool_choice="auto",
147+
)
148+
149+
print("OpenAI Response:", response.choices[0].message.content)
150+
151+
if response.choices[0].message.tool_calls:
152+
print("\nTool calls made:")
153+
for tool_call in response.choices[0].message.tool_calls:
154+
print(f" - {tool_call.function.name}")
155+
156+
except ImportError:
157+
print("OpenAI library not installed. Install with: pip install openai")
158+
except Exception as e:
159+
print(f"OpenAI example failed: {e}")
160+
161+
print()
162+
163+
164+
def example_with_langchain():
165+
"""Example of using tools with LangChain"""
166+
print("🔗 Example 5: Using tools with LangChain\n")
167+
168+
try:
169+
from langchain.agents import AgentExecutor, create_tool_calling_agent
170+
from langchain_core.prompts import ChatPromptTemplate
171+
from langchain_openai import ChatOpenAI
172+
173+
# Initialize StackOne toolset
174+
toolset = StackOneToolSet()
175+
176+
# Get tools and convert to LangChain format
177+
tools = toolset.get_tools("hris_list_*")
178+
langchain_tools = tools.to_langchain()
179+
180+
# Get meta tools as well
181+
meta_tools = tools.meta_tools()
182+
langchain_meta_tools = meta_tools.to_langchain()
183+
184+
# Combine all tools
185+
all_langchain_tools = list(langchain_tools) + list(langchain_meta_tools)
186+
187+
print(f"Available tools for LangChain: {len(all_langchain_tools)}")
188+
for tool in all_langchain_tools:
189+
print(f" - {tool.name}: {tool.description}")
190+
191+
# Create LangChain agent
192+
llm = ChatOpenAI(model="gpt-4", temperature=0)
193+
194+
prompt = ChatPromptTemplate.from_messages(
195+
[
196+
(
197+
"system",
198+
"You are an HR assistant. Use the meta tools to discover and execute relevant tools.",
199+
),
200+
("human", "{input}"),
201+
("placeholder", "{agent_scratchpad}"),
202+
]
203+
)
204+
205+
agent = create_tool_calling_agent(llm, all_langchain_tools, prompt)
206+
agent_executor = AgentExecutor(agent=agent, tools=all_langchain_tools, verbose=True)
207+
208+
# Run the agent
209+
result = agent_executor.invoke({"input": "Find tools that can list employee data"})
210+
211+
print(f"\nAgent result: {result['output']}")
212+
213+
except ImportError as e:
214+
print(f"LangChain dependencies not installed: {e}")
215+
print("Install with: pip install langchain-openai")
216+
except Exception as e:
217+
print(f"LangChain example failed: {e}")
218+
219+
print()
220+
221+
222+
def main():
223+
"""Run all examples"""
224+
print("=" * 60)
225+
print("StackOne AI SDK - Meta Tools & Tool Calling Examples")
226+
print("=" * 60)
227+
print()
228+
229+
# Basic examples that work without external APIs
230+
example_meta_tools_basic()
231+
example_meta_tools_with_execution()
232+
example_tool_calling()
233+
234+
# Examples that require OpenAI API
235+
if os.getenv("OPENAI_API_KEY"):
236+
example_with_openai()
237+
example_with_langchain()
238+
else:
239+
print("ℹ️ Set OPENAI_API_KEY to run OpenAI and LangChain examples\n")
240+
241+
print("=" * 60)
242+
print("Examples completed!")
243+
print("=" * 60)
244+
245+
246+
if __name__ == "__main__":
247+
main()

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ dependencies = [
2020
"requests>=2.32.3",
2121
"langchain-core>=0.1.0",
2222
"mcp[cli]>=1.3.0",
23+
"bm25s>=0.2.2",
24+
"numpy>=1.24.0",
2325
]
2426

2527
[project.scripts]

stackone_ai/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""StackOne AI SDK"""
22

3+
from .models import StackOneTool, Tools
34
from .toolset import StackOneToolSet
45

5-
__all__ = ["StackOneToolSet"]
6+
__all__ = ["StackOneToolSet", "StackOneTool", "Tools"]
67
__version__ = "0.0.4"

0 commit comments

Comments
 (0)