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.
Files changed (39) hide show
  1. glaip_sdk/__init__.py +4 -1
  2. glaip_sdk/agents/__init__.py +27 -0
  3. glaip_sdk/agents/base.py +989 -0
  4. glaip_sdk/cli/commands/accounts.py +210 -23
  5. glaip_sdk/cli/commands/tools.py +2 -5
  6. glaip_sdk/client/_agent_payloads.py +10 -9
  7. glaip_sdk/client/agents.py +70 -8
  8. glaip_sdk/client/base.py +1 -0
  9. glaip_sdk/client/main.py +12 -4
  10. glaip_sdk/client/mcps.py +112 -10
  11. glaip_sdk/client/tools.py +151 -7
  12. glaip_sdk/mcps/__init__.py +21 -0
  13. glaip_sdk/mcps/base.py +345 -0
  14. glaip_sdk/models/__init__.py +65 -31
  15. glaip_sdk/models/agent.py +47 -0
  16. glaip_sdk/models/agent_runs.py +0 -1
  17. glaip_sdk/models/common.py +42 -0
  18. glaip_sdk/models/mcp.py +33 -0
  19. glaip_sdk/models/tool.py +33 -0
  20. glaip_sdk/registry/__init__.py +55 -0
  21. glaip_sdk/registry/agent.py +164 -0
  22. glaip_sdk/registry/base.py +139 -0
  23. glaip_sdk/registry/mcp.py +251 -0
  24. glaip_sdk/registry/tool.py +238 -0
  25. glaip_sdk/tools/__init__.py +22 -0
  26. glaip_sdk/tools/base.py +435 -0
  27. glaip_sdk/utils/__init__.py +50 -9
  28. glaip_sdk/utils/bundler.py +267 -0
  29. glaip_sdk/utils/client.py +111 -0
  30. glaip_sdk/utils/client_utils.py +26 -7
  31. glaip_sdk/utils/discovery.py +78 -0
  32. glaip_sdk/utils/import_resolver.py +500 -0
  33. glaip_sdk/utils/instructions.py +101 -0
  34. glaip_sdk/utils/sync.py +142 -0
  35. {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/METADATA +5 -3
  36. {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/RECORD +38 -18
  37. glaip_sdk/models.py +0 -241
  38. {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/WHEEL +0 -0
  39. {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
- DEFAULT_MCP_TRANSPORT,
14
- DEFAULT_MCP_TYPE,
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.models import MCP
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
- return MCP(**data)._set_client(self)
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
- return MCP(**full_mcp_data)._set_client(self)
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
- return MCP(**data)._set_client(self)
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("Unexpected MCP tools response keys %s; returning empty list", list(data.keys()))
310
+ logger.warning(
311
+ "Unexpected MCP tools response keys %s; returning empty list",
312
+ list(data.keys()),
313
+ )
215
314
  return []
216
- logger.warning("Unexpected MCP tools response type %s; returning empty list", type(data).__name__)
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 Tool
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
- return Tool(**data)._set_client(self)
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
- return Tool(**response)._set_client(self)
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
- return Tool(**data)._set_client(self)
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
- return Tool(**response)._set_client(self)
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(f"Failed to update tool {tool_id} via file: {e}")
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
+ ]