Skip to content

Commit 5244f05

Browse files
committed
feat: add provider and action filtering to fetch_tools()
This commit introduces comprehensive filtering capabilities to the fetch_tools() method in StackOneToolSet, matching the functionality available in the Node.js SDK (PR #124). Changes: 1. Core Implementation (stackone_ai/toolset.py): - Add 'providers' option to fetch_tools() * Filters tools by provider names (e.g., ['hibob', 'bamboohr']) * Case-insensitive matching for robustness - Add 'actions' option to fetch_tools() * Supports exact action name matching * Supports glob patterns (e.g., '*_list_employees') - Add set_accounts() method for account ID filtering * Returns self for method chaining * Account IDs can be set via options or set_accounts() - Implement private _filter_by_provider() and _filter_by_action() methods - Filters can be combined for precise tool selection 2. Enhanced Models (stackone_ai/models.py): - Add to_list() method to Tools class - Add __iter__() method to make Tools iterable - Both methods support better integration with filtering logic 3. Comprehensive Test Coverage (tests/test_toolset.py): - Add 8 new test cases covering: * set_accounts() method * Provider filtering (single and multiple providers) * Action filtering (exact match and glob patterns) * Combined filters (providers + actions) * Account ID integration - All tests pass (11/11 tests passing) 4. Documentation Updates (README.md): - Add comprehensive "Tool Filtering" section - Document all filtering options with code examples: * get_tools() with glob patterns * fetch_tools() with provider filtering * fetch_tools() with action filtering * Combined filters * set_accounts() for chaining - Include use cases for each filtering pattern - Update Features section to highlight advanced filtering Technical Details: - Provider extraction uses tool name convention (provider_action format) - Glob matching uses fnmatch for flexible patterns - Filters are applied sequentially and can be combined - All filtering is case-insensitive for providers - Maintains full backward compatibility with existing code Testing: - All 11 tests pass successfully - Linting and type checking pass (ruff, mypy) - No breaking changes to existing API Reference: StackOneHQ/stackone-ai-node#124
1 parent a684c24 commit 5244f05

File tree

4 files changed

+362
-1
lines changed

4 files changed

+362
-1
lines changed

README.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ StackOne AI provides a unified interface for accessing various SaaS tools throug
1010
- Unified interface for multiple SaaS tools
1111
- AI-friendly tool descriptions and parameters
1212
- **Tool Calling**: Direct method calling with `tool.call()` for intuitive usage
13-
- **Glob Pattern Filtering**: Advanced tool filtering with patterns like `"hris_*"` and exclusions `"!hris_delete_*"`
13+
- **Advanced Tool Filtering**:
14+
- Glob pattern filtering with patterns like `"hris_*"` and exclusions `"!hris_delete_*"`
15+
- Provider and action filtering with `fetch_tools()`
16+
- Multi-account support
1417
- **Meta Tools** (Beta): Dynamic tool discovery and execution based on natural language queries
1518
- Integration with popular AI frameworks:
1619
- OpenAI Functions
@@ -75,6 +78,68 @@ employee = employee_tool.call(id="employee-id")
7578
employee = employee_tool.execute({"id": "employee-id"})
7679
```
7780

81+
## Tool Filtering
82+
83+
StackOne AI SDK provides powerful filtering capabilities to help you select the exact tools you need.
84+
85+
### Filtering with `get_tools()`
86+
87+
Use glob patterns to filter tools by name:
88+
89+
```python
90+
from stackone_ai import StackOneToolSet
91+
92+
toolset = StackOneToolSet()
93+
94+
# Get all HRIS tools
95+
tools = toolset.get_tools("hris_*", account_id="your-account-id")
96+
97+
# Get multiple categories
98+
tools = toolset.get_tools(["hris_*", "ats_*"])
99+
100+
# Exclude specific tools with negative patterns
101+
tools = toolset.get_tools(["hris_*", "!hris_delete_*"])
102+
```
103+
104+
### Advanced Filtering with `fetch_tools()`
105+
106+
The `fetch_tools()` method provides advanced filtering by providers, actions, and account IDs:
107+
108+
```python
109+
from stackone_ai import StackOneToolSet
110+
111+
toolset = StackOneToolSet()
112+
113+
# Filter by account IDs
114+
tools = toolset.fetch_tools(account_ids=["acc-123", "acc-456"])
115+
116+
# Filter by providers (case-insensitive)
117+
tools = toolset.fetch_tools(providers=["hibob", "bamboohr"])
118+
119+
# Filter by action patterns with glob support
120+
tools = toolset.fetch_tools(actions=["*_list_employees"])
121+
122+
# Combine multiple filters
123+
tools = toolset.fetch_tools(
124+
account_ids=["acc-123"],
125+
providers=["hibob"],
126+
actions=["*_list_*"]
127+
)
128+
129+
# Use set_accounts() for chaining
130+
toolset.set_accounts(["acc-123", "acc-456"])
131+
tools = toolset.fetch_tools(providers=["hibob"])
132+
```
133+
134+
**Filtering Options:**
135+
136+
- **`account_ids`**: Filter tools by account IDs. Tools will be loaded for each specified account.
137+
- **`providers`**: Filter by provider names (e.g., `["hibob", "bamboohr"]`). Case-insensitive matching.
138+
- **`actions`**: Filter by action patterns with glob support:
139+
- Exact match: `["hris_list_employees"]`
140+
- Glob pattern: `["*_list_employees"]` matches all tools ending with `_list_employees`
141+
- Provider prefix: `["hris_*"]` matches all HRIS tools
142+
78143
## Implicit Feedback (Beta)
79144

80145
The Python SDK can emit implicit behavioural feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.

stackone_ai/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,18 @@ def __getitem__(self, index: int) -> StackOneTool:
472472
def __len__(self) -> int:
473473
return len(self.tools)
474474

475+
def __iter__(self) -> Any:
476+
"""Make Tools iterable"""
477+
return iter(self.tools)
478+
479+
def to_list(self) -> list[StackOneTool]:
480+
"""Convert to list of tools
481+
482+
Returns:
483+
List of StackOneTool instances
484+
"""
485+
return list(self.tools)
486+
475487
def get_tool(self, name: str) -> StackOneTool | None:
476488
"""Get a tool by its name
477489

stackone_ai/toolset.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def __init__(
6060
self.api_key: str = api_key_value
6161
self.account_id = account_id
6262
self.base_url = base_url
63+
self._account_ids: list[str] = []
6364

6465
def _parse_parameters(self, parameters: list[dict[str, Any]]) -> dict[str, dict[str, str]]:
6566
"""Parse OpenAPI parameters into tool properties
@@ -109,6 +110,119 @@ def _matches_filter(self, tool_name: str, filter_pattern: str | list[str]) -> bo
109110

110111
return matches_positive and not matches_negative
111112

113+
def set_accounts(self, account_ids: list[str]) -> StackOneToolSet:
114+
"""Set account IDs for filtering tools
115+
116+
Args:
117+
account_ids: List of account IDs to filter tools by
118+
119+
Returns:
120+
This toolset instance for chaining
121+
"""
122+
self._account_ids = account_ids
123+
return self
124+
125+
def _filter_by_provider(self, tool_name: str, providers: list[str]) -> bool:
126+
"""Check if a tool name matches any of the provider filters
127+
128+
Args:
129+
tool_name: Name of the tool to check
130+
providers: List of provider names (case-insensitive)
131+
132+
Returns:
133+
True if the tool matches any provider, False otherwise
134+
"""
135+
# Extract provider from tool name (assuming format: provider_action)
136+
provider = tool_name.split("_")[0].lower()
137+
provider_set = {p.lower() for p in providers}
138+
return provider in provider_set
139+
140+
def _filter_by_action(self, tool_name: str, actions: list[str]) -> bool:
141+
"""Check if a tool name matches any of the action patterns
142+
143+
Args:
144+
tool_name: Name of the tool to check
145+
actions: List of action patterns (supports glob patterns)
146+
147+
Returns:
148+
True if the tool matches any action pattern, False otherwise
149+
"""
150+
return any(fnmatch.fnmatch(tool_name, pattern) for pattern in actions)
151+
152+
def fetch_tools(
153+
self,
154+
*,
155+
account_ids: list[str] | None = None,
156+
providers: list[str] | None = None,
157+
actions: list[str] | None = None,
158+
) -> Tools:
159+
"""Fetch tools with optional filtering by account IDs, providers, and actions
160+
161+
Args:
162+
account_ids: Optional list of account IDs to filter by.
163+
If not provided, uses accounts set via set_accounts()
164+
providers: Optional list of provider names (e.g., ['hibob', 'bamboohr']).
165+
Case-insensitive matching.
166+
actions: Optional list of action patterns with glob support
167+
(e.g., ['*_list_employees', 'hibob_create_employees'])
168+
169+
Returns:
170+
Collection of tools matching the filter criteria
171+
172+
Raises:
173+
ToolsetLoadError: If there is an error loading the tools
174+
175+
Examples:
176+
# Filter by account IDs
177+
tools = toolset.fetch_tools(account_ids=['123', '456'])
178+
179+
# Filter by providers
180+
tools = toolset.fetch_tools(providers=['hibob', 'bamboohr'])
181+
182+
# Filter by actions with glob patterns
183+
tools = toolset.fetch_tools(actions=['*_list_employees'])
184+
185+
# Combine filters
186+
tools = toolset.fetch_tools(
187+
account_ids=['123'],
188+
providers=['hibob'],
189+
actions=['*_list_*']
190+
)
191+
192+
# Use set_accounts() for account filtering
193+
toolset.set_accounts(['123', '456'])
194+
tools = toolset.fetch_tools()
195+
"""
196+
try:
197+
# Use account IDs from options, or fall back to instance state
198+
effective_account_ids = account_ids or self._account_ids
199+
200+
all_tools: list[StackOneTool] = []
201+
202+
# Load tools for each account ID or once if no account filtering
203+
if effective_account_ids:
204+
for acc_id in effective_account_ids:
205+
tools = self.get_tools(account_id=acc_id)
206+
all_tools.extend(tools.to_list())
207+
else:
208+
tools = self.get_tools()
209+
all_tools.extend(tools.to_list())
210+
211+
# Apply provider filtering
212+
if providers:
213+
all_tools = [t for t in all_tools if self._filter_by_provider(t.name, providers)]
214+
215+
# Apply action filtering
216+
if actions:
217+
all_tools = [t for t in all_tools if self._filter_by_action(t.name, actions)]
218+
219+
return Tools(all_tools)
220+
221+
except Exception as e:
222+
if isinstance(e, ToolsetError):
223+
raise
224+
raise ToolsetLoadError(f"Error fetching tools: {e}") from e
225+
112226
def get_tool(self, name: str, *, account_id: str | None = None) -> StackOneTool | None:
113227
"""Get a specific tool by name
114228

0 commit comments

Comments
 (0)