-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtoolset.py
More file actions
178 lines (139 loc) · 6.21 KB
/
toolset.py
File metadata and controls
178 lines (139 loc) · 6.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# TODO: Remove when Python 3.9 support is dropped
from __future__ import annotations
import fnmatch
import os
import warnings
from typing import Any
from stackone_ai.constants import OAS_DIR
from stackone_ai.models import (
StackOneTool,
Tools,
)
from stackone_ai.specs.parser import OpenAPIParser
class ToolsetError(Exception):
"""Base exception for toolset errors"""
pass
class ToolsetConfigError(ToolsetError):
"""Raised when there is an error in the toolset configuration"""
pass
class ToolsetLoadError(ToolsetError):
"""Raised when there is an error loading tools"""
pass
class StackOneToolSet:
"""Main class for accessing StackOne tools"""
def __init__(
self,
api_key: str | None = None,
account_id: str | None = None,
base_url: str | None = None,
) -> None:
"""Initialize StackOne tools with authentication
Args:
api_key: Optional API key. If not provided, will try to get from STACKONE_API_KEY env var
account_id: Optional account ID
base_url: Optional base URL override for API requests. If not provided, uses the URL from the OAS
Raises:
ToolsetConfigError: If no API key is provided or found in environment
"""
api_key_value = api_key or os.getenv("STACKONE_API_KEY")
if not api_key_value:
raise ToolsetConfigError(
"API key must be provided either through api_key parameter or "
"STACKONE_API_KEY environment variable"
)
self.api_key: str = api_key_value
self.account_id = account_id
self.base_url = base_url
def _parse_parameters(self, parameters: list[dict[str, Any]]) -> dict[str, dict[str, str]]:
"""Parse OpenAPI parameters into tool properties
Args:
parameters: List of OpenAPI parameter objects
Returns:
Dict of parameter properties with name as key and schema details as value
"""
properties: dict[str, dict[str, str]] = {}
for param in parameters:
if param["in"] == "path":
# Ensure we only include string values in the nested dict
param_schema = param["schema"]
properties[param["name"]] = {
"type": str(param_schema["type"]),
"description": str(param.get("description", "")),
}
return properties
def _matches_filter(self, tool_name: str, filter_pattern: str | list[str]) -> bool:
"""Check if a tool name matches the filter pattern
Args:
tool_name: Name of the tool to check
filter_pattern: String or list of glob patterns to match against.
Patterns starting with ! are treated as negative matches.
Returns:
True if the tool name matches any positive pattern and no negative patterns,
False otherwise
"""
patterns = [filter_pattern] if isinstance(filter_pattern, str) else filter_pattern
# Split into positive and negative patterns
positive_patterns = [p for p in patterns if not p.startswith("!")]
negative_patterns = [p[1:] for p in patterns if p.startswith("!")]
# If no positive patterns, treat as match all
matches_positive = (
any(fnmatch.fnmatch(tool_name, p) for p in positive_patterns) if positive_patterns else True
)
# If any negative pattern matches, exclude the tool
matches_negative = any(fnmatch.fnmatch(tool_name, p) for p in negative_patterns)
return matches_positive and not matches_negative
def get_tool(self, name: str, *, account_id: str | None = None) -> StackOneTool | None:
"""Get a specific tool by name
Args:
name: Name of the tool to retrieve
account_id: Optional account ID override. If not provided, uses the one from initialization
Returns:
The tool if found, None otherwise
Raises:
ToolsetLoadError: If there is an error loading the tools
"""
tools = self.get_tools(name, account_id=account_id)
return tools.get_tool(name)
def get_tools(
self, filter_pattern: str | list[str] | None = None, *, account_id: str | None = None
) -> Tools:
"""Get tools matching the specified filter pattern
Args:
filter_pattern: Optional glob pattern or list of patterns to filter tools
(e.g. "hris_*", ["crm_*", "ats_*"])
account_id: Optional account ID override. If not provided, uses the one from initialization
Returns:
Collection of tools matching the filter pattern
Raises:
ToolsetLoadError: If there is an error loading the tools
"""
if filter_pattern is None:
warnings.warn(
"No filter pattern provided. Loading all tools may exceed context windows in "
"AI applications.",
UserWarning,
stacklevel=2,
)
try:
all_tools: list[StackOneTool] = []
effective_account_id = account_id or self.account_id
# Load all available specs
for spec_file in OAS_DIR.glob("*.json"):
parser = OpenAPIParser(spec_file, base_url=self.base_url)
tool_definitions = parser.parse_tools()
# Create tools and filter if pattern is provided
for _, tool_def in tool_definitions.items():
if filter_pattern is None or self._matches_filter(tool_def.execute.name, filter_pattern):
tool = StackOneTool(
description=tool_def.description,
parameters=tool_def.parameters,
_execute_config=tool_def.execute,
_api_key=self.api_key,
_account_id=effective_account_id,
)
all_tools.append(tool)
return Tools(all_tools)
except Exception as e:
if isinstance(e, ToolsetError):
raise
raise ToolsetLoadError(f"Error loading tools: {e}") from e