glaip-sdk 0.0.1b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/__init__.py +12 -0
- glaip_sdk/cli/__init__.py +9 -0
- glaip_sdk/cli/commands/__init__.py +5 -0
- glaip_sdk/cli/commands/agents.py +415 -0
- glaip_sdk/cli/commands/configure.py +316 -0
- glaip_sdk/cli/commands/init.py +168 -0
- glaip_sdk/cli/commands/mcps.py +473 -0
- glaip_sdk/cli/commands/models.py +52 -0
- glaip_sdk/cli/commands/tools.py +309 -0
- glaip_sdk/cli/config.py +592 -0
- glaip_sdk/cli/main.py +298 -0
- glaip_sdk/cli/utils.py +733 -0
- glaip_sdk/client/__init__.py +179 -0
- glaip_sdk/client/agents.py +441 -0
- glaip_sdk/client/base.py +223 -0
- glaip_sdk/client/mcps.py +94 -0
- glaip_sdk/client/tools.py +193 -0
- glaip_sdk/client/validators.py +166 -0
- glaip_sdk/config/constants.py +28 -0
- glaip_sdk/exceptions.py +93 -0
- glaip_sdk/models.py +190 -0
- glaip_sdk/utils/__init__.py +95 -0
- glaip_sdk/utils/run_renderer.py +1009 -0
- glaip_sdk/utils.py +167 -0
- glaip_sdk-0.0.1b5.dist-info/METADATA +633 -0
- glaip_sdk-0.0.1b5.dist-info/RECORD +28 -0
- glaip_sdk-0.0.1b5.dist-info/WHEEL +4 -0
- glaip_sdk-0.0.1b5.dist-info/entry_points.txt +2 -0
glaip_sdk/client/base.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Base client for AIP SDK.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Union
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
|
|
15
|
+
from glaip_sdk.exceptions import (
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
ConflictError,
|
|
18
|
+
ForbiddenError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ServerError,
|
|
22
|
+
TimeoutError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Set up logging without basicConfig (library best practice)
|
|
27
|
+
logger = logging.getLogger("glaip_sdk")
|
|
28
|
+
logger.addHandler(logging.NullHandler())
|
|
29
|
+
|
|
30
|
+
client_log = logging.getLogger("glaip_sdk.client")
|
|
31
|
+
client_log.addHandler(logging.NullHandler())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseClient:
|
|
35
|
+
"""Base client with HTTP operations and authentication."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
api_url: str | None = None,
|
|
40
|
+
api_key: str | None = None,
|
|
41
|
+
timeout: float = 30.0,
|
|
42
|
+
*,
|
|
43
|
+
parent_client: Union["BaseClient", None] = None,
|
|
44
|
+
load_env: bool = True,
|
|
45
|
+
):
|
|
46
|
+
"""Initialize the base client.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
api_url: API base URL
|
|
50
|
+
api_key: API authentication key
|
|
51
|
+
timeout: Request timeout in seconds
|
|
52
|
+
parent_client: Parent client to adopt session/config from
|
|
53
|
+
load_env: Whether to load environment variables
|
|
54
|
+
"""
|
|
55
|
+
self._parent_client = parent_client
|
|
56
|
+
|
|
57
|
+
if parent_client is not None:
|
|
58
|
+
# Adopt parent's session/config; DO NOT call super().__init__
|
|
59
|
+
client_log.debug("Adopting parent client configuration")
|
|
60
|
+
self.api_url = parent_client.api_url
|
|
61
|
+
self.api_key = parent_client.api_key
|
|
62
|
+
self._timeout = parent_client._timeout
|
|
63
|
+
self.http_client = parent_client.http_client
|
|
64
|
+
else:
|
|
65
|
+
# Initialize as standalone client
|
|
66
|
+
if load_env:
|
|
67
|
+
load_dotenv()
|
|
68
|
+
|
|
69
|
+
self.api_url = api_url or os.getenv("AIP_API_URL")
|
|
70
|
+
self.api_key = api_key or os.getenv("AIP_API_KEY")
|
|
71
|
+
self._timeout = timeout
|
|
72
|
+
|
|
73
|
+
if not self.api_url:
|
|
74
|
+
client_log.error("AIP_API_URL not found in environment or parameters")
|
|
75
|
+
raise ValueError("AIP_API_URL not found")
|
|
76
|
+
if not self.api_key:
|
|
77
|
+
client_log.error("AIP_API_KEY not found in environment or parameters")
|
|
78
|
+
raise ValueError("AIP_API_KEY not found")
|
|
79
|
+
|
|
80
|
+
client_log.info(f"Initializing client with API URL: {self.api_url}")
|
|
81
|
+
self.http_client = self._build_client(timeout)
|
|
82
|
+
|
|
83
|
+
def _build_client(self, timeout: float) -> httpx.Client:
|
|
84
|
+
"""Build HTTP client with configuration."""
|
|
85
|
+
from glaip_sdk.config.constants import SDK_NAME, SDK_VERSION
|
|
86
|
+
|
|
87
|
+
return httpx.Client(
|
|
88
|
+
base_url=self.api_url,
|
|
89
|
+
headers={
|
|
90
|
+
"X-API-Key": self.api_key,
|
|
91
|
+
"User-Agent": f"{SDK_NAME}/{SDK_VERSION}",
|
|
92
|
+
},
|
|
93
|
+
timeout=httpx.Timeout(timeout),
|
|
94
|
+
follow_redirects=True,
|
|
95
|
+
http2=False,
|
|
96
|
+
limits=httpx.Limits(max_keepalive_connections=10, max_connections=100),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def timeout(self) -> float:
|
|
101
|
+
"""Get current timeout value."""
|
|
102
|
+
return self._timeout
|
|
103
|
+
|
|
104
|
+
@timeout.setter
|
|
105
|
+
def timeout(self, value: float):
|
|
106
|
+
"""Set timeout and rebuild client."""
|
|
107
|
+
self._timeout = value
|
|
108
|
+
if (
|
|
109
|
+
hasattr(self, "http_client")
|
|
110
|
+
and self.http_client
|
|
111
|
+
and not self._parent_client
|
|
112
|
+
):
|
|
113
|
+
self.http_client.close()
|
|
114
|
+
self.http_client = self._build_client(value)
|
|
115
|
+
|
|
116
|
+
def _request(self, method: str, endpoint: str, **kwargs) -> Any:
|
|
117
|
+
"""Make HTTP request with error handling."""
|
|
118
|
+
client_log.debug(f"Making {method} request to {endpoint}")
|
|
119
|
+
try:
|
|
120
|
+
response = self.http_client.request(method, endpoint, **kwargs)
|
|
121
|
+
client_log.debug(f"Response status: {response.status_code}")
|
|
122
|
+
return self._handle_response(response)
|
|
123
|
+
except httpx.ConnectError as e:
|
|
124
|
+
client_log.warning(
|
|
125
|
+
f"Connection error on {method} {endpoint}, retrying once: {e}"
|
|
126
|
+
)
|
|
127
|
+
try:
|
|
128
|
+
response = self.http_client.request(method, endpoint, **kwargs)
|
|
129
|
+
client_log.debug(
|
|
130
|
+
f"Retry successful, response status: {response.status_code}"
|
|
131
|
+
)
|
|
132
|
+
return self._handle_response(response)
|
|
133
|
+
except httpx.ConnectError:
|
|
134
|
+
client_log.error(f"Retry failed for {method} {endpoint}: {e}")
|
|
135
|
+
raise e
|
|
136
|
+
|
|
137
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
138
|
+
"""Handle HTTP response with proper error handling."""
|
|
139
|
+
if response.status_code == 204:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
parsed = None
|
|
143
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
144
|
+
if "json" in content_type:
|
|
145
|
+
try:
|
|
146
|
+
parsed = response.json()
|
|
147
|
+
except ValueError:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
if parsed is None:
|
|
151
|
+
if 200 <= response.status_code < 300:
|
|
152
|
+
return response.text
|
|
153
|
+
else:
|
|
154
|
+
self._raise_api_error(response.status_code, response.text)
|
|
155
|
+
|
|
156
|
+
if isinstance(parsed, dict) and "success" in parsed:
|
|
157
|
+
if parsed.get("success"):
|
|
158
|
+
return parsed.get("data", parsed)
|
|
159
|
+
else:
|
|
160
|
+
error_type = parsed.get("error", "UnknownError")
|
|
161
|
+
message = parsed.get("message", "Unknown error")
|
|
162
|
+
self._raise_api_error(
|
|
163
|
+
response.status_code, message, error_type, payload=parsed
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if 200 <= response.status_code < 300:
|
|
167
|
+
return parsed
|
|
168
|
+
|
|
169
|
+
message = parsed.get("message") if isinstance(parsed, dict) else str(parsed)
|
|
170
|
+
self._raise_api_error(response.status_code, message, payload=parsed)
|
|
171
|
+
|
|
172
|
+
def _raise_api_error(
|
|
173
|
+
self, status: int, message: str, error_type: str | None = None, *, payload=None
|
|
174
|
+
):
|
|
175
|
+
"""Raise appropriate exception with rich context."""
|
|
176
|
+
request_id = None
|
|
177
|
+
try:
|
|
178
|
+
request_id = self.http_client.headers.get("X-Request-Id")
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
mapping = {
|
|
183
|
+
400: ValidationError,
|
|
184
|
+
401: AuthenticationError,
|
|
185
|
+
403: ForbiddenError,
|
|
186
|
+
404: NotFoundError,
|
|
187
|
+
408: TimeoutError,
|
|
188
|
+
409: ConflictError,
|
|
189
|
+
429: RateLimitError,
|
|
190
|
+
500: ServerError,
|
|
191
|
+
503: ServerError,
|
|
192
|
+
504: TimeoutError,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
exception_class = mapping.get(status, ValidationError)
|
|
196
|
+
error_msg = f"HTTP {status}: {message}"
|
|
197
|
+
if request_id:
|
|
198
|
+
error_msg += f" (Request ID: {request_id})"
|
|
199
|
+
|
|
200
|
+
raise exception_class(
|
|
201
|
+
error_msg,
|
|
202
|
+
status_code=status,
|
|
203
|
+
error_type=error_type,
|
|
204
|
+
payload=payload,
|
|
205
|
+
request_id=request_id,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def close(self):
|
|
209
|
+
"""Close the HTTP client."""
|
|
210
|
+
if (
|
|
211
|
+
hasattr(self, "http_client")
|
|
212
|
+
and self.http_client
|
|
213
|
+
and not self._parent_client
|
|
214
|
+
):
|
|
215
|
+
self.http_client.close()
|
|
216
|
+
|
|
217
|
+
def __enter__(self):
|
|
218
|
+
"""Context manager entry."""
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
222
|
+
"""Context manager exit."""
|
|
223
|
+
self.close()
|
glaip_sdk/client/mcps.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""MCP client for AIP SDK.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from glaip_sdk.client.base import BaseClient
|
|
12
|
+
from glaip_sdk.models import MCP
|
|
13
|
+
|
|
14
|
+
# Set up module-level logger
|
|
15
|
+
logger = logging.getLogger("glaip_sdk.mcps")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MCPClient(BaseClient):
|
|
19
|
+
"""Client for MCP operations."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, parent_client: BaseClient | None = None, **kwargs):
|
|
22
|
+
"""Initialize the MCP client.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
parent_client: Parent client to adopt session/config from
|
|
26
|
+
**kwargs: Additional arguments for standalone initialization
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(parent_client=parent_client, **kwargs)
|
|
29
|
+
|
|
30
|
+
def list_mcps(self) -> list[MCP]:
|
|
31
|
+
"""List all MCPs."""
|
|
32
|
+
data = self._request("GET", "/mcps/")
|
|
33
|
+
return [MCP(**mcp_data)._set_client(self) for mcp_data in (data or [])]
|
|
34
|
+
|
|
35
|
+
def get_mcp_by_id(self, mcp_id: str) -> MCP:
|
|
36
|
+
"""Get MCP by ID."""
|
|
37
|
+
data = self._request("GET", f"/mcps/{mcp_id}")
|
|
38
|
+
return MCP(**data)._set_client(self)
|
|
39
|
+
|
|
40
|
+
def find_mcps(self, name: str | None = None) -> list[MCP]:
|
|
41
|
+
"""Find MCPs by name."""
|
|
42
|
+
# Backend doesn't support name query parameter, so we fetch all and filter client-side
|
|
43
|
+
data = self._request("GET", "/mcps/")
|
|
44
|
+
mcps = [MCP(**mcp_data)._set_client(self) for mcp_data in (data or [])]
|
|
45
|
+
|
|
46
|
+
if name:
|
|
47
|
+
# Client-side filtering by name (case-insensitive)
|
|
48
|
+
mcps = [mcp for mcp in mcps if name.lower() in mcp.name.lower()]
|
|
49
|
+
|
|
50
|
+
return mcps
|
|
51
|
+
|
|
52
|
+
def create_mcp(
|
|
53
|
+
self,
|
|
54
|
+
name: str,
|
|
55
|
+
description: str,
|
|
56
|
+
config: dict[str, Any] | None = None,
|
|
57
|
+
**kwargs,
|
|
58
|
+
) -> MCP:
|
|
59
|
+
"""Create a new MCP."""
|
|
60
|
+
payload = {
|
|
61
|
+
"name": name,
|
|
62
|
+
"description": description,
|
|
63
|
+
**kwargs,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if config:
|
|
67
|
+
payload["config"] = config
|
|
68
|
+
|
|
69
|
+
# Create the MCP (backend returns only the ID)
|
|
70
|
+
response_data = self._request("POST", "/mcps/", json=payload)
|
|
71
|
+
|
|
72
|
+
# Extract the ID from the response
|
|
73
|
+
if isinstance(response_data, dict) and "id" in response_data:
|
|
74
|
+
mcp_id = response_data["id"]
|
|
75
|
+
else:
|
|
76
|
+
# Fallback: assume response_data is the ID directly
|
|
77
|
+
mcp_id = str(response_data)
|
|
78
|
+
|
|
79
|
+
# Fetch the full MCP details
|
|
80
|
+
return self.get_mcp_by_id(mcp_id)
|
|
81
|
+
|
|
82
|
+
def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
|
|
83
|
+
"""Update an existing MCP."""
|
|
84
|
+
data = self._request("PUT", f"/mcps/{mcp_id}", json=kwargs)
|
|
85
|
+
return MCP(**data)._set_client(self)
|
|
86
|
+
|
|
87
|
+
def delete_mcp(self, mcp_id: str) -> None:
|
|
88
|
+
"""Delete an MCP."""
|
|
89
|
+
self._request("DELETE", f"/mcps/{mcp_id}")
|
|
90
|
+
|
|
91
|
+
def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
|
|
92
|
+
"""Get tools available from an MCP."""
|
|
93
|
+
data = self._request("GET", f"/mcps/{mcp_id}/tools")
|
|
94
|
+
return data or []
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tool client for AIP SDK.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.client.base import BaseClient
|
|
11
|
+
from glaip_sdk.models import Tool
|
|
12
|
+
|
|
13
|
+
# Set up module-level logger
|
|
14
|
+
logger = logging.getLogger("glaip_sdk.tools")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolClient(BaseClient):
|
|
18
|
+
"""Client for tool operations."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, *, parent_client: BaseClient | None = None, **kwargs):
|
|
21
|
+
"""Initialize the tool client.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
parent_client: Parent client to adopt session/config from
|
|
25
|
+
**kwargs: Additional arguments for standalone initialization
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(parent_client=parent_client, **kwargs)
|
|
28
|
+
|
|
29
|
+
def list_tools(self) -> list[Tool]:
|
|
30
|
+
"""List all tools."""
|
|
31
|
+
data = self._request("GET", "/tools/")
|
|
32
|
+
return [Tool(**tool_data)._set_client(self) for tool_data in (data or [])]
|
|
33
|
+
|
|
34
|
+
def get_tool_by_id(self, tool_id: str) -> Tool:
|
|
35
|
+
"""Get tool by ID."""
|
|
36
|
+
data = self._request("GET", f"/tools/{tool_id}")
|
|
37
|
+
return Tool(**data)._set_client(self)
|
|
38
|
+
|
|
39
|
+
def find_tools(self, name: str | None = None) -> list[Tool]:
|
|
40
|
+
"""Find tools by name."""
|
|
41
|
+
# Backend doesn't support name query parameter, so we fetch all and filter client-side
|
|
42
|
+
data = self._request("GET", "/tools/")
|
|
43
|
+
tools = [Tool(**tool_data)._set_client(self) for tool_data in (data or [])]
|
|
44
|
+
|
|
45
|
+
if name:
|
|
46
|
+
# Client-side filtering by name (case-insensitive)
|
|
47
|
+
tools = [tool for tool in tools if name.lower() in tool.name.lower()]
|
|
48
|
+
|
|
49
|
+
return tools
|
|
50
|
+
|
|
51
|
+
def create_tool(
|
|
52
|
+
self,
|
|
53
|
+
name: str | None = None,
|
|
54
|
+
tool_type: str = "custom",
|
|
55
|
+
description: str | None = None,
|
|
56
|
+
tool_script: str | None = None,
|
|
57
|
+
tool_file: str | None = None,
|
|
58
|
+
file_path: str | None = None,
|
|
59
|
+
code: str | None = None,
|
|
60
|
+
framework: str = "langchain",
|
|
61
|
+
**kwargs,
|
|
62
|
+
) -> Tool:
|
|
63
|
+
"""Create a new tool.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
name: Tool name (required if not provided via file)
|
|
67
|
+
tool_type: Tool type (defaults to "custom")
|
|
68
|
+
description: Tool description (optional)
|
|
69
|
+
tool_script: Tool script content (optional)
|
|
70
|
+
tool_file: Tool file path (optional)
|
|
71
|
+
file_path: Alternative to tool_file (for compatibility)
|
|
72
|
+
code: Alternative to tool_script (for compatibility)
|
|
73
|
+
framework: Tool framework (defaults to "langchain")
|
|
74
|
+
**kwargs: Additional tool parameters
|
|
75
|
+
"""
|
|
76
|
+
# Handle compatibility parameters
|
|
77
|
+
if file_path and not tool_file:
|
|
78
|
+
tool_file = file_path
|
|
79
|
+
if code and not tool_script:
|
|
80
|
+
tool_script = code
|
|
81
|
+
|
|
82
|
+
# Auto-detect name from file if not provided
|
|
83
|
+
if not name and tool_file:
|
|
84
|
+
import os
|
|
85
|
+
|
|
86
|
+
name = os.path.splitext(os.path.basename(tool_file))[0]
|
|
87
|
+
|
|
88
|
+
if not name:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"Tool name is required (either explicitly or via file path)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Auto-detect description if not provided
|
|
94
|
+
if not description:
|
|
95
|
+
description = f"A {tool_type} tool"
|
|
96
|
+
|
|
97
|
+
payload = {
|
|
98
|
+
"name": name,
|
|
99
|
+
"tool_type": tool_type,
|
|
100
|
+
"description": description,
|
|
101
|
+
"framework": framework,
|
|
102
|
+
**kwargs,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if tool_script:
|
|
106
|
+
payload["tool_script"] = tool_script
|
|
107
|
+
if tool_file:
|
|
108
|
+
payload["tool_file"] = tool_file
|
|
109
|
+
|
|
110
|
+
data = self._request("POST", "/tools/", json=payload)
|
|
111
|
+
return Tool(**data)._set_client(self)
|
|
112
|
+
|
|
113
|
+
def create_tool_from_code(
|
|
114
|
+
self,
|
|
115
|
+
name: str,
|
|
116
|
+
code: str,
|
|
117
|
+
framework: str = "langchain",
|
|
118
|
+
) -> Tool:
|
|
119
|
+
"""Create a new tool plugin from code string.
|
|
120
|
+
|
|
121
|
+
This method uses the /tools/upload endpoint which properly processes
|
|
122
|
+
and registers tool plugins, unlike the regular create_tool method
|
|
123
|
+
which only creates metadata.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
name: Name for the tool (used for temporary file naming)
|
|
127
|
+
code: Python code containing the tool plugin
|
|
128
|
+
framework: Tool framework (defaults to "langchain")
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Tool: The created tool object
|
|
132
|
+
"""
|
|
133
|
+
import os
|
|
134
|
+
import tempfile
|
|
135
|
+
|
|
136
|
+
# Create a temporary file with the tool code
|
|
137
|
+
with tempfile.NamedTemporaryFile(
|
|
138
|
+
mode="w", suffix=".py", prefix=f"{name}_", delete=False, encoding="utf-8"
|
|
139
|
+
) as temp_file:
|
|
140
|
+
temp_file.write(code)
|
|
141
|
+
temp_file_path = temp_file.name
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Read the file content and upload it
|
|
145
|
+
with open(temp_file_path, encoding="utf-8") as f:
|
|
146
|
+
script_content = f.read()
|
|
147
|
+
|
|
148
|
+
# Create a file-like object for upload
|
|
149
|
+
import io
|
|
150
|
+
|
|
151
|
+
file_obj = io.BytesIO(script_content.encode("utf-8"))
|
|
152
|
+
file_obj.name = os.path.basename(temp_file_path)
|
|
153
|
+
|
|
154
|
+
# Use multipart form data for file upload
|
|
155
|
+
files = {"file": (file_obj.name, file_obj, "text/plain")}
|
|
156
|
+
data = {"framework": framework}
|
|
157
|
+
|
|
158
|
+
# Make the upload request
|
|
159
|
+
response = self._request("POST", "/tools/upload", files=files, data=data)
|
|
160
|
+
return Tool(**response)._set_client(self)
|
|
161
|
+
finally:
|
|
162
|
+
# Clean up the temporary file
|
|
163
|
+
try:
|
|
164
|
+
os.unlink(temp_file_path)
|
|
165
|
+
except OSError:
|
|
166
|
+
pass # Ignore cleanup errors
|
|
167
|
+
|
|
168
|
+
def update_tool(self, tool_id: str, **kwargs) -> Tool:
|
|
169
|
+
"""Update an existing tool."""
|
|
170
|
+
data = self._request("PUT", f"/tools/{tool_id}", json=kwargs)
|
|
171
|
+
return Tool(**data)._set_client(self)
|
|
172
|
+
|
|
173
|
+
def delete_tool(self, tool_id: str) -> None:
|
|
174
|
+
"""Delete a tool."""
|
|
175
|
+
self._request("DELETE", f"/tools/{tool_id}")
|
|
176
|
+
|
|
177
|
+
def install_tool(self, tool_id: str) -> bool:
|
|
178
|
+
"""Install a tool."""
|
|
179
|
+
try:
|
|
180
|
+
self._request("POST", f"/tools/{tool_id}/install")
|
|
181
|
+
return True
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Failed to install tool {tool_id}: {e}")
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def uninstall_tool(self, tool_id: str) -> bool:
|
|
187
|
+
"""Uninstall a tool."""
|
|
188
|
+
try:
|
|
189
|
+
self._request("POST", f"/tools/{tool_id}/uninstall")
|
|
190
|
+
return True
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error(f"Failed to uninstall tool {tool_id}: {e}")
|
|
193
|
+
return False
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Validation utilities for AIP SDK.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.exceptions import AmbiguousResourceError, NotFoundError, ValidationError
|
|
11
|
+
from glaip_sdk.models import Tool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResourceValidator:
|
|
15
|
+
"""Validates and resolves resource references."""
|
|
16
|
+
|
|
17
|
+
RESERVED_NAMES = {
|
|
18
|
+
"research-agent",
|
|
19
|
+
"github-agent",
|
|
20
|
+
"aws-pricing-filter-generator-agent",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def is_reserved_name(cls, name: str) -> bool:
|
|
25
|
+
"""Check if a name is reserved."""
|
|
26
|
+
return name in cls.RESERVED_NAMES
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def extract_tool_ids(cls, tools: list[str | Tool], client) -> list[str]:
|
|
30
|
+
"""Extract tool IDs from a list of tool names, IDs, or Tool objects.
|
|
31
|
+
|
|
32
|
+
For agent creation, the backend expects tool IDs (UUIDs).
|
|
33
|
+
This method handles:
|
|
34
|
+
- Tool objects (extracts their ID)
|
|
35
|
+
- UUID strings (passes through)
|
|
36
|
+
- Tool names (finds tool and extracts ID)
|
|
37
|
+
"""
|
|
38
|
+
tool_ids = []
|
|
39
|
+
for tool in tools:
|
|
40
|
+
if isinstance(tool, str):
|
|
41
|
+
# Check if it's already a UUID
|
|
42
|
+
try:
|
|
43
|
+
UUID(tool)
|
|
44
|
+
tool_ids.append(tool) # Already a UUID string
|
|
45
|
+
except ValueError:
|
|
46
|
+
# It's a name, try to find the tool and get its ID
|
|
47
|
+
try:
|
|
48
|
+
found_tools = client.find_tools(name=tool)
|
|
49
|
+
if len(found_tools) == 1:
|
|
50
|
+
tool_ids.append(str(found_tools[0].id))
|
|
51
|
+
elif len(found_tools) > 1:
|
|
52
|
+
raise AmbiguousResourceError(
|
|
53
|
+
f"Multiple tools found with name '{tool}': {[t.id for t in found_tools]}"
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
raise NotFoundError(f"Tool not found: {tool}")
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise ValidationError(
|
|
59
|
+
f"Failed to resolve tool name '{tool}' to ID: {e}"
|
|
60
|
+
)
|
|
61
|
+
elif hasattr(tool, "id") and tool.id is not None: # Tool object with ID
|
|
62
|
+
tool_ids.append(str(tool.id))
|
|
63
|
+
elif isinstance(tool, UUID): # UUID object
|
|
64
|
+
tool_ids.append(str(tool))
|
|
65
|
+
elif (
|
|
66
|
+
hasattr(tool, "name") and tool.name is not None
|
|
67
|
+
): # Tool object with name but no ID
|
|
68
|
+
# Try to find the tool by name and get its ID
|
|
69
|
+
try:
|
|
70
|
+
found_tools = client.find_tools(name=tool.name)
|
|
71
|
+
if len(found_tools) == 1:
|
|
72
|
+
tool_ids.append(str(found_tools[0].id))
|
|
73
|
+
elif len(found_tools) > 1:
|
|
74
|
+
raise AmbiguousResourceError(
|
|
75
|
+
f"Multiple tools found with name '{tool.name}': {[t.id for t in found_tools]}"
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
raise NotFoundError(f"Tool not found: {tool.name}")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise ValidationError(
|
|
81
|
+
f"Failed to resolve tool name '{tool.name}' to ID: {e}"
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
raise ValidationError(
|
|
85
|
+
f"Invalid tool reference: {tool} - must have 'id' or 'name' attribute"
|
|
86
|
+
)
|
|
87
|
+
return tool_ids
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def extract_agent_ids(cls, agents: list[str | Any], client) -> list[str]:
|
|
91
|
+
"""Extract agent IDs from a list of agent names, IDs, or agent objects.
|
|
92
|
+
|
|
93
|
+
For agent creation, the backend expects agent IDs (UUIDs).
|
|
94
|
+
This method handles:
|
|
95
|
+
- Agent objects (extracts their ID)
|
|
96
|
+
- UUID strings (passes through)
|
|
97
|
+
- Agent names (finds agent and extracts ID)
|
|
98
|
+
"""
|
|
99
|
+
agent_ids = []
|
|
100
|
+
for agent in agents:
|
|
101
|
+
if isinstance(agent, str):
|
|
102
|
+
# Check if it's already a UUID
|
|
103
|
+
try:
|
|
104
|
+
UUID(agent)
|
|
105
|
+
agent_ids.append(agent) # Already a UUID string
|
|
106
|
+
except ValueError:
|
|
107
|
+
# It's a name, try to find the agent and get its ID
|
|
108
|
+
try:
|
|
109
|
+
found_agents = client.find_agents(name=agent)
|
|
110
|
+
if len(found_agents) == 1:
|
|
111
|
+
agent_ids.append(str(found_agents[0].id))
|
|
112
|
+
elif len(found_agents) > 1:
|
|
113
|
+
raise AmbiguousResourceError(
|
|
114
|
+
f"Multiple agents found with name '{agent}': {[a.id for a in found_agents]}"
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
raise NotFoundError(f"Agent not found: {agent}")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
raise ValidationError(
|
|
120
|
+
f"Failed to resolve agent name '{agent}' to ID: {e}"
|
|
121
|
+
)
|
|
122
|
+
elif hasattr(agent, "id") and agent.id is not None: # Agent object with ID
|
|
123
|
+
agent_ids.append(str(agent.id))
|
|
124
|
+
elif isinstance(agent, UUID): # UUID object
|
|
125
|
+
agent_ids.append(str(agent))
|
|
126
|
+
elif (
|
|
127
|
+
hasattr(agent, "name") and agent.name is not None
|
|
128
|
+
): # Agent object with name but no ID
|
|
129
|
+
# Try to find the agent by name and get its ID
|
|
130
|
+
try:
|
|
131
|
+
found_agents = client.find_agents(name=agent.name)
|
|
132
|
+
if len(found_agents) == 1:
|
|
133
|
+
agent_ids.append(str(found_agents[0].id))
|
|
134
|
+
elif len(found_agents) > 1:
|
|
135
|
+
raise AmbiguousResourceError(
|
|
136
|
+
f"Multiple agents found with name '{agent.name}': {[a.id for a in found_agents]}"
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
raise NotFoundError(f"Agent not found: {agent.name}")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise ValidationError(
|
|
142
|
+
f"Failed to resolve agent name '{agent.name}' to ID: {e}"
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
raise ValidationError(
|
|
146
|
+
f"Invalid agent reference: {agent} - must have 'id' or 'name' attribute"
|
|
147
|
+
)
|
|
148
|
+
return agent_ids
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def validate_tools_exist(cls, tool_ids: list[str], client) -> None:
|
|
152
|
+
"""Validate that all tool IDs exist."""
|
|
153
|
+
for tool_id in tool_ids:
|
|
154
|
+
try:
|
|
155
|
+
client.get_tool_by_id(tool_id)
|
|
156
|
+
except NotFoundError:
|
|
157
|
+
raise ValidationError(f"Tool not found: {tool_id}")
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def validate_agents_exist(cls, agent_ids: list[str], client) -> None:
|
|
161
|
+
"""Validate that all agent IDs exist."""
|
|
162
|
+
for agent_id in agent_ids:
|
|
163
|
+
try:
|
|
164
|
+
client.get_agent_by_id(agent_id)
|
|
165
|
+
except NotFoundError:
|
|
166
|
+
raise ValidationError(f"Agent not found: {agent_id}")
|