glaip-sdk 0.5.3__py3-none-any.whl → 0.6.0__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 +989 -0
- glaip_sdk/cli/commands/accounts.py +210 -23
- glaip_sdk/cli/commands/tools.py +2 -5
- 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 +500 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/sync.py +142 -0
- {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/METADATA +5 -3
- {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/RECORD +38 -18
- glaip_sdk/models.py +0 -241
- {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/entry_points.txt +0 -0
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
|
+
]
|