glaip-sdk 0.5.5__py3-none-any.whl → 0.6.1__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 +4 -1
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +996 -0
- glaip_sdk/cli/commands/common_config.py +36 -0
- glaip_sdk/cli/commands/tools.py +2 -5
- glaip_sdk/cli/config.py +13 -2
- glaip_sdk/cli/main.py +20 -0
- glaip_sdk/cli/slash/accounts_controller.py +217 -0
- glaip_sdk/cli/slash/accounts_shared.py +19 -0
- glaip_sdk/cli/slash/session.py +57 -7
- glaip_sdk/cli/slash/tui/accounts.tcss +54 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +379 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +9 -12
- glaip_sdk/client/_agent_payloads.py +10 -9
- glaip_sdk/client/agents.py +70 -8
- glaip_sdk/client/base.py +1 -0
- glaip_sdk/client/main.py +12 -4
- glaip_sdk/client/mcps.py +112 -10
- glaip_sdk/client/tools.py +151 -7
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +65 -31
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +0 -1
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +251 -0
- glaip_sdk/registry/tool.py +238 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +50 -9
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +26 -7
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/sync.py +142 -0
- {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.1.dist-info}/METADATA +5 -3
- {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.1.dist-info}/RECORD +48 -22
- glaip_sdk/models.py +0 -241
- {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.1.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.1.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/agents.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import asyncio
|
|
@@ -15,7 +16,7 @@ from pathlib import Path
|
|
|
15
16
|
from typing import Any, BinaryIO
|
|
16
17
|
|
|
17
18
|
import httpx
|
|
18
|
-
|
|
19
|
+
from glaip_sdk.agents import Agent
|
|
19
20
|
from glaip_sdk.client._agent_payloads import (
|
|
20
21
|
AgentCreateRequest,
|
|
21
22
|
AgentListParams,
|
|
@@ -40,7 +41,7 @@ from glaip_sdk.config.constants import (
|
|
|
40
41
|
DEFAULT_MODEL,
|
|
41
42
|
)
|
|
42
43
|
from glaip_sdk.exceptions import NotFoundError, ValidationError
|
|
43
|
-
from glaip_sdk.models import
|
|
44
|
+
from glaip_sdk.models import AgentResponse
|
|
44
45
|
from glaip_sdk.payload_schemas.agent import list_server_only_fields
|
|
45
46
|
from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
|
|
46
47
|
from glaip_sdk.utils.client_utils import (
|
|
@@ -73,7 +74,9 @@ _DEFAULT_METADATA_TYPE = "custom"
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
@asynccontextmanager
|
|
76
|
-
async def _async_timeout_guard(
|
|
77
|
+
async def _async_timeout_guard(
|
|
78
|
+
timeout_seconds: float | None,
|
|
79
|
+
) -> AsyncGenerator[None, None]:
|
|
77
80
|
"""Apply an asyncio timeout when a custom timeout is provided."""
|
|
78
81
|
if timeout_seconds is None:
|
|
79
82
|
yield
|
|
@@ -260,7 +263,7 @@ class AgentClient(BaseClient):
|
|
|
260
263
|
self._renderer_manager = AgentRunRenderingManager(logger)
|
|
261
264
|
self._tool_client: ToolClient | None = None
|
|
262
265
|
self._mcp_client: MCPClient | None = None
|
|
263
|
-
self._runs_client:
|
|
266
|
+
self._runs_client: AgentRunsClient | None = None
|
|
264
267
|
|
|
265
268
|
def list_agents(
|
|
266
269
|
self,
|
|
@@ -359,7 +362,8 @@ class AgentClient(BaseClient):
|
|
|
359
362
|
status_code=404,
|
|
360
363
|
)
|
|
361
364
|
|
|
362
|
-
|
|
365
|
+
response = AgentResponse(**data)
|
|
366
|
+
return Agent.from_response(response, client=self)
|
|
363
367
|
|
|
364
368
|
def find_agents(self, name: str | None = None) -> list[Agent]:
|
|
365
369
|
"""Find agents by name."""
|
|
@@ -826,7 +830,8 @@ class AgentClient(BaseClient):
|
|
|
826
830
|
get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
|
|
827
831
|
json=payload_dict,
|
|
828
832
|
)
|
|
829
|
-
|
|
833
|
+
response = AgentResponse(**full_agent_data)
|
|
834
|
+
return Agent.from_response(response, client=self)
|
|
830
835
|
|
|
831
836
|
def create_agent(
|
|
832
837
|
self,
|
|
@@ -921,8 +926,9 @@ class AgentClient(BaseClient):
|
|
|
921
926
|
|
|
922
927
|
payload_dict = request.to_payload(current_agent)
|
|
923
928
|
|
|
924
|
-
|
|
925
|
-
|
|
929
|
+
api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
|
|
930
|
+
response = AgentResponse(**api_response)
|
|
931
|
+
return Agent.from_response(response, client=self)
|
|
926
932
|
|
|
927
933
|
def update_agent(
|
|
928
934
|
self,
|
|
@@ -969,6 +975,62 @@ class AgentClient(BaseClient):
|
|
|
969
975
|
"""Delete an agent."""
|
|
970
976
|
self._request("DELETE", f"/agents/{agent_id}")
|
|
971
977
|
|
|
978
|
+
def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
|
|
979
|
+
"""Create or update an agent by instance, ID, or name.
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
identifier: Agent instance, ID (UUID string), or name
|
|
983
|
+
**kwargs: Agent configuration (instruction, description, tools, etc.)
|
|
984
|
+
|
|
985
|
+
Returns:
|
|
986
|
+
The created or updated agent.
|
|
987
|
+
|
|
988
|
+
Example:
|
|
989
|
+
>>> # By name (creates if not exists)
|
|
990
|
+
>>> agent = client.agents.upsert_agent(
|
|
991
|
+
... "hello_agent",
|
|
992
|
+
... instruction="You are a helpful assistant.",
|
|
993
|
+
... description="A friendly agent",
|
|
994
|
+
... )
|
|
995
|
+
>>> # By instance
|
|
996
|
+
>>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
|
|
997
|
+
>>> # By ID
|
|
998
|
+
>>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
|
|
999
|
+
"""
|
|
1000
|
+
# Handle Agent instance
|
|
1001
|
+
if isinstance(identifier, Agent):
|
|
1002
|
+
if identifier.id:
|
|
1003
|
+
logger.info("Updating agent by instance: %s", identifier.name)
|
|
1004
|
+
return self.update_agent(identifier.id, name=identifier.name, **kwargs)
|
|
1005
|
+
identifier = identifier.name
|
|
1006
|
+
|
|
1007
|
+
# Handle string (ID or name)
|
|
1008
|
+
if isinstance(identifier, str):
|
|
1009
|
+
# Check if it's a UUID
|
|
1010
|
+
if is_uuid(identifier):
|
|
1011
|
+
logger.info("Updating agent by ID: %s", identifier)
|
|
1012
|
+
return self.update_agent(identifier, **kwargs)
|
|
1013
|
+
|
|
1014
|
+
# It's a name - find or create
|
|
1015
|
+
return self._upsert_agent_by_name(identifier, **kwargs)
|
|
1016
|
+
|
|
1017
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
1018
|
+
|
|
1019
|
+
def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
|
|
1020
|
+
"""Find agent by name and update, or create if not found."""
|
|
1021
|
+
existing = self.find_agents(name)
|
|
1022
|
+
|
|
1023
|
+
if len(existing) == 1:
|
|
1024
|
+
logger.info("Updating existing agent: %s", name)
|
|
1025
|
+
return self.update_agent(existing[0].id, name=name, **kwargs)
|
|
1026
|
+
|
|
1027
|
+
if len(existing) > 1:
|
|
1028
|
+
raise ValueError(f"Multiple agents found with name '{name}'")
|
|
1029
|
+
|
|
1030
|
+
# Create new agent
|
|
1031
|
+
logger.info("Creating new agent: %s", name)
|
|
1032
|
+
return self.create_agent(name=name, **kwargs)
|
|
1033
|
+
|
|
972
1034
|
def _prepare_sync_request_data(
|
|
973
1035
|
self,
|
|
974
1036
|
message: str,
|
glaip_sdk/client/base.py
CHANGED
glaip_sdk/client/main.py
CHANGED
|
@@ -3,16 +3,24 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
9
12
|
|
|
10
13
|
from glaip_sdk.client.agents import AgentClient
|
|
11
14
|
from glaip_sdk.client.base import BaseClient
|
|
12
15
|
from glaip_sdk.client.mcps import MCPClient
|
|
13
16
|
from glaip_sdk.client.shared import build_shared_config
|
|
14
17
|
from glaip_sdk.client.tools import ToolClient
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
20
|
+
from glaip_sdk.agents import Agent
|
|
21
|
+
from glaip_sdk.client._agent_payloads import AgentListResult
|
|
22
|
+
from glaip_sdk.mcps import MCP
|
|
23
|
+
from glaip_sdk.tools import Tool
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
class Client(BaseClient):
|
|
@@ -49,7 +57,7 @@ class Client(BaseClient):
|
|
|
49
57
|
name: str | None = None,
|
|
50
58
|
version: str | None = None,
|
|
51
59
|
sync_langflow_agents: bool = False,
|
|
52
|
-
) ->
|
|
60
|
+
) -> AgentListResult:
|
|
53
61
|
"""List agents with optional filtering.
|
|
54
62
|
|
|
55
63
|
Args:
|
|
@@ -60,7 +68,7 @@ class Client(BaseClient):
|
|
|
60
68
|
sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
|
|
61
69
|
|
|
62
70
|
Returns:
|
|
63
|
-
|
|
71
|
+
AgentListResult with agents and pagination metadata. Supports iteration and indexing.
|
|
64
72
|
"""
|
|
65
73
|
return self.agents.list_agents(
|
|
66
74
|
agent_type=agent_type,
|
glaip_sdk/client/mcps.py
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from glaip_sdk.client.base import BaseClient
|
|
12
|
-
from glaip_sdk.config.constants import
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
from glaip_sdk.config.constants import DEFAULT_MCP_TRANSPORT, DEFAULT_MCP_TYPE
|
|
14
|
+
from glaip_sdk.mcps import MCP
|
|
15
|
+
from glaip_sdk.models import MCPResponse
|
|
16
|
+
from glaip_sdk.utils.client_utils import (
|
|
17
|
+
add_kwargs_to_payload,
|
|
18
|
+
create_model_instances,
|
|
19
|
+
find_by_name,
|
|
15
20
|
)
|
|
16
|
-
from glaip_sdk.
|
|
17
|
-
from glaip_sdk.utils.client_utils import add_kwargs_to_payload, create_model_instances, find_by_name
|
|
21
|
+
from glaip_sdk.utils.resource_refs import is_uuid
|
|
18
22
|
|
|
19
23
|
# API endpoints
|
|
20
24
|
MCPS_ENDPOINT = "/mcps/"
|
|
@@ -45,7 +49,8 @@ class MCPClient(BaseClient):
|
|
|
45
49
|
def get_mcp_by_id(self, mcp_id: str) -> MCP:
|
|
46
50
|
"""Get MCP by ID."""
|
|
47
51
|
data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}")
|
|
48
|
-
|
|
52
|
+
response = MCPResponse(**data)
|
|
53
|
+
return MCP.from_response(response, client=self)
|
|
49
54
|
|
|
50
55
|
def find_mcps(self, name: str | None = None) -> list[MCP]:
|
|
51
56
|
"""Find MCPs by name."""
|
|
@@ -77,7 +82,8 @@ class MCPClient(BaseClient):
|
|
|
77
82
|
get_endpoint_fmt=f"{MCPS_ENDPOINT}{{id}}",
|
|
78
83
|
json=payload,
|
|
79
84
|
)
|
|
80
|
-
|
|
85
|
+
response = MCPResponse(**full_mcp_data)
|
|
86
|
+
return MCP.from_response(response, client=self)
|
|
81
87
|
|
|
82
88
|
def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
|
|
83
89
|
"""Update an existing MCP.
|
|
@@ -99,12 +105,102 @@ class MCPClient(BaseClient):
|
|
|
99
105
|
method = "PATCH"
|
|
100
106
|
|
|
101
107
|
data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id}", json=kwargs)
|
|
102
|
-
|
|
108
|
+
response = MCPResponse(**data)
|
|
109
|
+
return MCP.from_response(response, client=self)
|
|
103
110
|
|
|
104
111
|
def delete_mcp(self, mcp_id: str) -> None:
|
|
105
112
|
"""Delete an MCP."""
|
|
106
113
|
self._request("DELETE", f"{MCPS_ENDPOINT}{mcp_id}")
|
|
107
114
|
|
|
115
|
+
def upsert_mcp(
|
|
116
|
+
self,
|
|
117
|
+
identifier: str | MCP,
|
|
118
|
+
description: str | None = None,
|
|
119
|
+
config: dict[str, Any] | None = None,
|
|
120
|
+
**kwargs,
|
|
121
|
+
) -> MCP:
|
|
122
|
+
"""Create or update an MCP by instance, ID, or name.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
identifier: MCP instance, ID (UUID string), or name
|
|
126
|
+
description: MCP description
|
|
127
|
+
config: MCP configuration dictionary
|
|
128
|
+
**kwargs: Additional parameters (transport, metadata, etc.)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The created or updated MCP.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
>>> # By name (creates if not exists)
|
|
135
|
+
>>> mcp = client.mcps.upsert_mcp(
|
|
136
|
+
... "deepwiki",
|
|
137
|
+
... transport="sse",
|
|
138
|
+
... config={"url": "https://mcp.deepwiki.com/sse"},
|
|
139
|
+
... )
|
|
140
|
+
>>> # By instance
|
|
141
|
+
>>> mcp = client.mcps.upsert_mcp(existing_mcp, description="Updated")
|
|
142
|
+
>>> # By ID
|
|
143
|
+
>>> mcp = client.mcps.upsert_mcp("uuid-here", description="Updated")
|
|
144
|
+
"""
|
|
145
|
+
# Handle MCP instance
|
|
146
|
+
if isinstance(identifier, MCP):
|
|
147
|
+
if identifier.id:
|
|
148
|
+
logger.info("Updating MCP by instance: %s", identifier.name)
|
|
149
|
+
return self._do_upsert_update(identifier.id, identifier.name, description, config, **kwargs)
|
|
150
|
+
# MCP without ID - treat name as identifier
|
|
151
|
+
identifier = identifier.name
|
|
152
|
+
|
|
153
|
+
# Handle string (ID or name)
|
|
154
|
+
if isinstance(identifier, str):
|
|
155
|
+
if is_uuid(identifier):
|
|
156
|
+
logger.info("Updating MCP by ID: %s", identifier)
|
|
157
|
+
existing = self.get_mcp_by_id(identifier)
|
|
158
|
+
return self._do_upsert_update(identifier, existing.name, description, config, **kwargs)
|
|
159
|
+
|
|
160
|
+
# It's a name - find or create
|
|
161
|
+
return self._upsert_by_name(identifier, description, config, **kwargs)
|
|
162
|
+
|
|
163
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
164
|
+
|
|
165
|
+
def _do_upsert_update(
|
|
166
|
+
self,
|
|
167
|
+
mcp_id: str,
|
|
168
|
+
name: str | None,
|
|
169
|
+
description: str | None,
|
|
170
|
+
config: dict[str, Any] | None,
|
|
171
|
+
**kwargs,
|
|
172
|
+
) -> MCP:
|
|
173
|
+
"""Perform the update part of upsert."""
|
|
174
|
+
update_kwargs = {**kwargs}
|
|
175
|
+
if name is not None:
|
|
176
|
+
update_kwargs["name"] = name
|
|
177
|
+
if description is not None:
|
|
178
|
+
update_kwargs["description"] = description
|
|
179
|
+
if config is not None:
|
|
180
|
+
update_kwargs["config"] = config
|
|
181
|
+
return self.update_mcp(mcp_id, **update_kwargs)
|
|
182
|
+
|
|
183
|
+
def _upsert_by_name(
|
|
184
|
+
self,
|
|
185
|
+
name: str,
|
|
186
|
+
description: str | None,
|
|
187
|
+
config: dict[str, Any] | None,
|
|
188
|
+
**kwargs,
|
|
189
|
+
) -> MCP:
|
|
190
|
+
"""Find by name and update, or create if not found."""
|
|
191
|
+
existing = self.find_mcps(name)
|
|
192
|
+
|
|
193
|
+
if len(existing) == 1:
|
|
194
|
+
logger.info("Updating existing MCP: %s", name)
|
|
195
|
+
return self._do_upsert_update(existing[0].id, name, description, config, **kwargs)
|
|
196
|
+
|
|
197
|
+
if len(existing) > 1:
|
|
198
|
+
raise ValueError(f"Multiple MCPs found with name '{name}'")
|
|
199
|
+
|
|
200
|
+
# Create new MCP
|
|
201
|
+
logger.info("Creating new MCP: %s", name)
|
|
202
|
+
return self.create_mcp(name=name, description=description, config=config, **kwargs)
|
|
203
|
+
|
|
108
204
|
def _build_create_payload(
|
|
109
205
|
self,
|
|
110
206
|
name: str,
|
|
@@ -211,9 +307,15 @@ class MCPClient(BaseClient):
|
|
|
211
307
|
if isinstance(data, dict):
|
|
212
308
|
if "tools" in data:
|
|
213
309
|
return data.get("tools", []) or []
|
|
214
|
-
logger.warning(
|
|
310
|
+
logger.warning(
|
|
311
|
+
"Unexpected MCP tools response keys %s; returning empty list",
|
|
312
|
+
list(data.keys()),
|
|
313
|
+
)
|
|
215
314
|
return []
|
|
216
|
-
logger.warning(
|
|
315
|
+
logger.warning(
|
|
316
|
+
"Unexpected MCP tools response type %s; returning empty list",
|
|
317
|
+
type(data).__name__,
|
|
318
|
+
)
|
|
217
319
|
return []
|
|
218
320
|
|
|
219
321
|
def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
|
glaip_sdk/client/tools.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
@@ -16,12 +17,14 @@ from glaip_sdk.config.constants import (
|
|
|
16
17
|
DEFAULT_TOOL_TYPE,
|
|
17
18
|
DEFAULT_TOOL_VERSION,
|
|
18
19
|
)
|
|
19
|
-
from glaip_sdk.models import
|
|
20
|
+
from glaip_sdk.models import ToolResponse
|
|
21
|
+
from glaip_sdk.tools import Tool
|
|
20
22
|
from glaip_sdk.utils.client_utils import (
|
|
21
23
|
add_kwargs_to_payload,
|
|
22
24
|
create_model_instances,
|
|
23
25
|
find_by_name,
|
|
24
26
|
)
|
|
27
|
+
from glaip_sdk.utils.resource_refs import is_uuid
|
|
25
28
|
|
|
26
29
|
# API endpoints
|
|
27
30
|
TOOLS_ENDPOINT = "/tools/"
|
|
@@ -59,11 +62,11 @@ class ToolClient(BaseClient):
|
|
|
59
62
|
def get_tool_by_id(self, tool_id: str) -> Tool:
|
|
60
63
|
"""Get tool by ID."""
|
|
61
64
|
data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
|
|
62
|
-
|
|
65
|
+
response = ToolResponse(**data)
|
|
66
|
+
return Tool.from_response(response, client=self)
|
|
63
67
|
|
|
64
68
|
def find_tools(self, name: str | None = None) -> list[Tool]:
|
|
65
69
|
"""Find tools by name."""
|
|
66
|
-
# Backend doesn't support name query parameter, so we fetch all and filter client-side
|
|
67
70
|
data = self._request("GET", TOOLS_ENDPOINT)
|
|
68
71
|
tools = create_model_instances(data, Tool, self)
|
|
69
72
|
return find_by_name(tools, name, case_sensitive=False)
|
|
@@ -112,6 +115,7 @@ class ToolClient(BaseClient):
|
|
|
112
115
|
data = {
|
|
113
116
|
"name": name,
|
|
114
117
|
"framework": framework,
|
|
118
|
+
"type": kwargs.pop("tool_type", DEFAULT_TOOL_TYPE), # Default to custom
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
if description:
|
|
@@ -153,7 +157,8 @@ class ToolClient(BaseClient):
|
|
|
153
157
|
data=upload_data,
|
|
154
158
|
)
|
|
155
159
|
|
|
156
|
-
|
|
160
|
+
tool_response = ToolResponse(**response)
|
|
161
|
+
return Tool.from_response(tool_response, client=self)
|
|
157
162
|
|
|
158
163
|
def _build_create_payload(
|
|
159
164
|
self,
|
|
@@ -275,6 +280,9 @@ class ToolClient(BaseClient):
|
|
|
275
280
|
or getattr(current_tool, "type", None)
|
|
276
281
|
or DEFAULT_TOOL_TYPE
|
|
277
282
|
)
|
|
283
|
+
# Convert enum to string value for API payload
|
|
284
|
+
if hasattr(current_type, "value"):
|
|
285
|
+
current_type = current_type.value
|
|
278
286
|
|
|
279
287
|
update_data = {
|
|
280
288
|
"name": name if name is not None else current_tool.name,
|
|
@@ -434,12 +442,147 @@ class ToolClient(BaseClient):
|
|
|
434
442
|
def update_tool(self, tool_id: str, **kwargs) -> Tool:
|
|
435
443
|
"""Update an existing tool."""
|
|
436
444
|
data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
|
|
437
|
-
|
|
445
|
+
response = ToolResponse(**data)
|
|
446
|
+
return Tool.from_response(response, client=self)
|
|
438
447
|
|
|
439
448
|
def delete_tool(self, tool_id: str) -> None:
|
|
440
449
|
"""Delete a tool."""
|
|
441
450
|
self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
|
|
442
451
|
|
|
452
|
+
def upsert_tool(
|
|
453
|
+
self,
|
|
454
|
+
identifier: str | Tool,
|
|
455
|
+
code: str | None = None,
|
|
456
|
+
description: str | None = None,
|
|
457
|
+
framework: str = "langchain",
|
|
458
|
+
**kwargs,
|
|
459
|
+
) -> Tool:
|
|
460
|
+
"""Create or update a tool by instance, ID, or name.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
identifier: Tool instance, ID (UUID string), or name
|
|
464
|
+
code: Python code containing the tool plugin (required for create)
|
|
465
|
+
description: Tool description
|
|
466
|
+
framework: Tool framework (defaults to "langchain")
|
|
467
|
+
**kwargs: Additional parameters (tags, version, etc.)
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
The created or updated tool.
|
|
471
|
+
|
|
472
|
+
Example:
|
|
473
|
+
>>> # By name with code (creates if not exists)
|
|
474
|
+
>>> tool = client.tools.upsert_tool(
|
|
475
|
+
... "greeting",
|
|
476
|
+
... code=bundled_source,
|
|
477
|
+
... description="A greeting tool",
|
|
478
|
+
... )
|
|
479
|
+
>>> # By instance
|
|
480
|
+
>>> tool = client.tools.upsert_tool(existing_tool, code=new_code)
|
|
481
|
+
>>> # By ID
|
|
482
|
+
>>> tool = client.tools.upsert_tool("uuid-here", code=new_code)
|
|
483
|
+
"""
|
|
484
|
+
# Handle Tool instance
|
|
485
|
+
if isinstance(identifier, Tool):
|
|
486
|
+
if identifier.id:
|
|
487
|
+
logger.info("Updating tool by instance: %s", identifier.name)
|
|
488
|
+
return self._do_tool_upsert_update(
|
|
489
|
+
identifier.id,
|
|
490
|
+
identifier.name,
|
|
491
|
+
code,
|
|
492
|
+
description,
|
|
493
|
+
framework,
|
|
494
|
+
**kwargs,
|
|
495
|
+
)
|
|
496
|
+
identifier = identifier.name
|
|
497
|
+
|
|
498
|
+
# Handle string (ID or name)
|
|
499
|
+
if isinstance(identifier, str):
|
|
500
|
+
if is_uuid(identifier):
|
|
501
|
+
logger.info("Updating tool by ID: %s", identifier)
|
|
502
|
+
existing = self.get_tool_by_id(identifier)
|
|
503
|
+
return self._do_tool_upsert_update(identifier, existing.name, code, description, framework, **kwargs)
|
|
504
|
+
|
|
505
|
+
# It's a name - find or create
|
|
506
|
+
return self._upsert_tool_by_name(identifier, code, description, framework, **kwargs)
|
|
507
|
+
|
|
508
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
509
|
+
|
|
510
|
+
def _do_tool_upsert_update(
|
|
511
|
+
self,
|
|
512
|
+
tool_id: str,
|
|
513
|
+
name: str | None,
|
|
514
|
+
code: str | None,
|
|
515
|
+
description: str | None,
|
|
516
|
+
framework: str,
|
|
517
|
+
**kwargs,
|
|
518
|
+
) -> Tool:
|
|
519
|
+
"""Perform the update part of tool upsert."""
|
|
520
|
+
if code:
|
|
521
|
+
# Update via file upload
|
|
522
|
+
with tempfile.NamedTemporaryFile(
|
|
523
|
+
mode="w",
|
|
524
|
+
suffix=".py",
|
|
525
|
+
prefix=f"{name or 'tool'}_",
|
|
526
|
+
delete=False,
|
|
527
|
+
encoding="utf-8",
|
|
528
|
+
) as temp_file:
|
|
529
|
+
temp_file.write(code)
|
|
530
|
+
temp_file_path = temp_file.name
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
return self.update_tool_via_file(
|
|
534
|
+
tool_id,
|
|
535
|
+
temp_file_path,
|
|
536
|
+
name=name,
|
|
537
|
+
description=description,
|
|
538
|
+
framework=framework,
|
|
539
|
+
**kwargs,
|
|
540
|
+
)
|
|
541
|
+
finally:
|
|
542
|
+
try:
|
|
543
|
+
os.unlink(temp_file_path)
|
|
544
|
+
except OSError:
|
|
545
|
+
pass
|
|
546
|
+
else:
|
|
547
|
+
# Metadata-only update
|
|
548
|
+
update_kwargs = {"framework": framework, **kwargs}
|
|
549
|
+
if name:
|
|
550
|
+
update_kwargs["name"] = name
|
|
551
|
+
if description:
|
|
552
|
+
update_kwargs["description"] = description
|
|
553
|
+
return self.update_tool(tool_id, **update_kwargs)
|
|
554
|
+
|
|
555
|
+
def _upsert_tool_by_name(
|
|
556
|
+
self,
|
|
557
|
+
name: str,
|
|
558
|
+
code: str | None,
|
|
559
|
+
description: str | None,
|
|
560
|
+
framework: str,
|
|
561
|
+
**kwargs,
|
|
562
|
+
) -> Tool:
|
|
563
|
+
"""Find tool by name and update, or create if not found."""
|
|
564
|
+
existing = self.find_tools(name)
|
|
565
|
+
|
|
566
|
+
if len(existing) == 1:
|
|
567
|
+
logger.info("Updating existing tool: %s", name)
|
|
568
|
+
return self._do_tool_upsert_update(existing[0].id, name, code, description, framework, **kwargs)
|
|
569
|
+
|
|
570
|
+
if len(existing) > 1:
|
|
571
|
+
raise ValueError(f"Multiple tools found with name '{name}'")
|
|
572
|
+
|
|
573
|
+
# Create new tool - code is required
|
|
574
|
+
if not code:
|
|
575
|
+
raise ValueError(f"Tool '{name}' not found and no code provided for creation")
|
|
576
|
+
|
|
577
|
+
logger.info("Creating new tool: %s", name)
|
|
578
|
+
return self.create_tool_from_code(
|
|
579
|
+
name=name,
|
|
580
|
+
code=code,
|
|
581
|
+
framework=framework,
|
|
582
|
+
description=description,
|
|
583
|
+
**kwargs,
|
|
584
|
+
)
|
|
585
|
+
|
|
443
586
|
def get_tool_script(self, tool_id: str) -> str:
|
|
444
587
|
"""Get the tool script content.
|
|
445
588
|
|
|
@@ -508,8 +651,9 @@ class ToolClient(BaseClient):
|
|
|
508
651
|
data=update_payload,
|
|
509
652
|
)
|
|
510
653
|
|
|
511
|
-
|
|
654
|
+
tool_response = ToolResponse(**response)
|
|
655
|
+
return Tool.from_response(tool_response, client=self)
|
|
512
656
|
|
|
513
657
|
except Exception as e:
|
|
514
|
-
logger.error(
|
|
658
|
+
logger.error("Failed to update tool %s via file: %s", tool_id, e)
|
|
515
659
|
raise
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) package for GL AIP platform.
|
|
2
|
+
|
|
3
|
+
This package provides the MCP class and MCPRegistry for managing
|
|
4
|
+
Model Context Protocol configurations on the GL AIP platform.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from glaip_sdk.mcps import MCP, get_mcp_registry
|
|
8
|
+
>>> mcp = MCP.from_native("arxiv-search")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from glaip_sdk.mcps.base import MCP, MCPConfigValue
|
|
14
|
+
from glaip_sdk.registry.mcp import MCPRegistry, get_mcp_registry
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"MCP",
|
|
18
|
+
"MCPConfigValue",
|
|
19
|
+
"MCPRegistry",
|
|
20
|
+
"get_mcp_registry",
|
|
21
|
+
]
|