Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ StackOne AI provides a unified interface for accessing various SaaS tools throug
- Unified interface for multiple SaaS tools
- AI-friendly tool descriptions and parameters
- **Tool Calling**: Direct method calling with `tool.call()` for intuitive usage
- **Glob Pattern Filtering**: Advanced tool filtering with patterns like `"hris_*"` and exclusions `"!hris_delete_*"`
- **Advanced Tool Filtering**:
- Glob pattern filtering with patterns like `"hris_*"` and exclusions `"!hris_delete_*"`
- Provider and action filtering with `fetch_tools()`
- Multi-account support
- **Meta Tools** (Beta): Dynamic tool discovery and execution based on natural language queries
- Integration with popular AI frameworks:
- OpenAI Functions
Expand Down Expand Up @@ -75,6 +78,68 @@ employee = employee_tool.call(id="employee-id")
employee = employee_tool.execute({"id": "employee-id"})
```

## Tool Filtering

StackOne AI SDK provides powerful filtering capabilities to help you select the exact tools you need.

### Filtering with `get_tools()`

Use glob patterns to filter tools by name:

```python
from stackone_ai import StackOneToolSet

toolset = StackOneToolSet()

# Get all HRIS tools
tools = toolset.get_tools("hris_*", account_id="your-account-id")

# Get multiple categories
tools = toolset.get_tools(["hris_*", "ats_*"])

# Exclude specific tools with negative patterns
tools = toolset.get_tools(["hris_*", "!hris_delete_*"])
```

### Advanced Filtering with `fetch_tools()`

The `fetch_tools()` method provides advanced filtering by providers, actions, and account IDs:

```python
from stackone_ai import StackOneToolSet

toolset = StackOneToolSet()

# Filter by account IDs
tools = toolset.fetch_tools(account_ids=["acc-123", "acc-456"])

# Filter by providers (case-insensitive)
tools = toolset.fetch_tools(providers=["hibob", "bamboohr"])

# Filter by action patterns with glob support
tools = toolset.fetch_tools(actions=["*_list_employees"])

# Combine multiple filters
tools = toolset.fetch_tools(
account_ids=["acc-123"],
providers=["hibob"],
actions=["*_list_*"]
)

# Use set_accounts() for chaining
toolset.set_accounts(["acc-123", "acc-456"])
tools = toolset.fetch_tools(providers=["hibob"])
```

**Filtering Options:**

- **`account_ids`**: Filter tools by account IDs. Tools will be loaded for each specified account.
- **`providers`**: Filter by provider names (e.g., `["hibob", "bamboohr"]`). Case-insensitive matching.
- **`actions`**: Filter by action patterns with glob support:
- Exact match: `["hris_list_employees"]`
- Glob pattern: `["*_list_employees"]` matches all tools ending with `_list_employees`
- Provider prefix: `["hris_*"]` matches all HRIS tools

## Implicit Feedback (Beta)

The Python SDK can emit implicit behavioural feedback to LangSmith so you can triage low-quality tool results without manually tagging runs.
Expand Down
12 changes: 12 additions & 0 deletions stackone_ai/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,18 @@ def __getitem__(self, index: int) -> StackOneTool:
def __len__(self) -> int:
return len(self.tools)

def __iter__(self) -> Any:
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type annotation Any is too broad for the __iter__ method. It should return Iterator[StackOneTool] for better type safety and IDE support. Add from collections.abc import Iterator to the imports and update the return type.

Copilot uses AI. Check for mistakes.
"""Make Tools iterable"""
return iter(self.tools)

def to_list(self) -> list[StackOneTool]:
"""Convert to list of tools

Returns:
List of StackOneTool instances
"""
return list(self.tools)

def get_tool(self, name: str) -> StackOneTool | None:
"""Get a tool by its name

Expand Down
114 changes: 114 additions & 0 deletions stackone_ai/toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(
self.api_key: str = api_key_value
self.account_id = account_id
self.base_url = base_url
self._account_ids: list[str] = []

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

return matches_positive and not matches_negative

def set_accounts(self, account_ids: list[str]) -> StackOneToolSet:
"""Set account IDs for filtering tools

Args:
account_ids: List of account IDs to filter tools by

Returns:
This toolset instance for chaining
"""
self._account_ids = account_ids
return self

def _filter_by_provider(self, tool_name: str, providers: list[str]) -> bool:
"""Check if a tool name matches any of the provider filters

Args:
tool_name: Name of the tool to check
providers: List of provider names (case-insensitive)

Returns:
True if the tool matches any provider, False otherwise
"""
# Extract provider from tool name (assuming format: provider_action)
provider = tool_name.split("_")[0].lower()
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _filter_by_provider() method will raise an IndexError if tool_name is an empty string or doesn't contain an underscore. Consider adding a guard to handle edge cases, such as checking if the split result is non-empty before accessing index 0.

Copilot uses AI. Check for mistakes.
provider_set = {p.lower() for p in providers}
return provider in provider_set

def _filter_by_action(self, tool_name: str, actions: list[str]) -> bool:
"""Check if a tool name matches any of the action patterns

Args:
tool_name: Name of the tool to check
actions: List of action patterns (supports glob patterns)

Returns:
True if the tool matches any action pattern, False otherwise
"""
return any(fnmatch.fnmatch(tool_name, pattern) for pattern in actions)

def fetch_tools(
self,
*,
account_ids: list[str] | None = None,
providers: list[str] | None = None,
actions: list[str] | None = None,
) -> Tools:
"""Fetch tools with optional filtering by account IDs, providers, and actions

Args:
account_ids: Optional list of account IDs to filter by.
If not provided, uses accounts set via set_accounts()
providers: Optional list of provider names (e.g., ['hibob', 'bamboohr']).
Case-insensitive matching.
actions: Optional list of action patterns with glob support
(e.g., ['*_list_employees', 'hibob_create_employees'])

Returns:
Collection of tools matching the filter criteria

Raises:
ToolsetLoadError: If there is an error loading the tools

Examples:
# Filter by account IDs
tools = toolset.fetch_tools(account_ids=['123', '456'])

# Filter by providers
tools = toolset.fetch_tools(providers=['hibob', 'bamboohr'])

# Filter by actions with glob patterns
tools = toolset.fetch_tools(actions=['*_list_employees'])

# Combine filters
tools = toolset.fetch_tools(
account_ids=['123'],
providers=['hibob'],
actions=['*_list_*']
)

# Use set_accounts() for account filtering
toolset.set_accounts(['123', '456'])
tools = toolset.fetch_tools()
"""
try:
# Use account IDs from options, or fall back to instance state
effective_account_ids = account_ids or self._account_ids
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using or for fallback will cause issues if account_ids is provided as an empty list [], as it will fallback to self._account_ids instead of treating empty list as "no filtering". Use explicit None check: effective_account_ids = account_ids if account_ids is not None else self._account_ids.

Suggested change
effective_account_ids = account_ids or self._account_ids
effective_account_ids = account_ids if account_ids is not None else self._account_ids

Copilot uses AI. Check for mistakes.

all_tools: list[StackOneTool] = []

# Load tools for each account ID or once if no account filtering
if effective_account_ids:
for acc_id in effective_account_ids:
tools = self.get_tools(account_id=acc_id)
all_tools.extend(tools.to_list())
else:
tools = self.get_tools()
all_tools.extend(tools.to_list())

# Apply provider filtering
if providers:
all_tools = [t for t in all_tools if self._filter_by_provider(t.name, providers)]

# Apply action filtering
if actions:
all_tools = [t for t in all_tools if self._filter_by_action(t.name, actions)]

return Tools(all_tools)

except Exception as e:
if isinstance(e, ToolsetError):
raise
raise ToolsetLoadError(f"Error fetching tools: {e}") from e

def get_tool(self, name: str, *, account_id: str | None = None) -> StackOneTool | None:
"""Get a specific tool by name

Expand Down
Loading
Loading