glaip-sdk 0.0.4__py3-none-any.whl → 0.0.5b1__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 (48) hide show
  1. glaip_sdk/__init__.py +5 -5
  2. glaip_sdk/branding.py +18 -17
  3. glaip_sdk/cli/__init__.py +1 -1
  4. glaip_sdk/cli/agent_config.py +82 -0
  5. glaip_sdk/cli/commands/__init__.py +3 -3
  6. glaip_sdk/cli/commands/agents.py +570 -673
  7. glaip_sdk/cli/commands/configure.py +2 -2
  8. glaip_sdk/cli/commands/mcps.py +148 -143
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +250 -179
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +14 -18
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +305 -264
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +631 -191
  19. glaip_sdk/client/base.py +66 -4
  20. glaip_sdk/client/main.py +226 -0
  21. glaip_sdk/client/mcps.py +143 -18
  22. glaip_sdk/client/tools.py +146 -11
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +42 -2
  25. glaip_sdk/rich_components.py +29 -0
  26. glaip_sdk/utils/__init__.py +18 -171
  27. glaip_sdk/utils/agent_config.py +181 -0
  28. glaip_sdk/utils/client_utils.py +159 -79
  29. glaip_sdk/utils/display.py +100 -0
  30. glaip_sdk/utils/general.py +94 -0
  31. glaip_sdk/utils/import_export.py +140 -0
  32. glaip_sdk/utils/rendering/formatting.py +6 -1
  33. glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
  34. glaip_sdk/utils/rendering/renderer/base.py +340 -247
  35. glaip_sdk/utils/rendering/renderer/debug.py +3 -2
  36. glaip_sdk/utils/rendering/renderer/panels.py +11 -10
  37. glaip_sdk/utils/rendering/steps.py +1 -1
  38. glaip_sdk/utils/resource_refs.py +192 -0
  39. glaip_sdk/utils/rich_utils.py +29 -0
  40. glaip_sdk/utils/serialization.py +285 -0
  41. glaip_sdk/utils/validation.py +273 -0
  42. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5b1.dist-info}/METADATA +22 -21
  43. glaip_sdk-0.0.5b1.dist-info/RECORD +55 -0
  44. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5b1.dist-info}/WHEEL +1 -1
  45. glaip_sdk-0.0.5b1.dist-info/entry_points.txt +3 -0
  46. glaip_sdk/cli/commands/init.py +0 -93
  47. glaip_sdk-0.0.4.dist-info/RECORD +0 -41
  48. glaip_sdk-0.0.4.dist-info/entry_points.txt +0 -2
glaip_sdk/client/tools.py CHANGED
@@ -8,14 +8,25 @@ Authors:
8
8
  import logging
9
9
  import os
10
10
  import tempfile
11
+ from typing import Any
11
12
 
12
13
  from glaip_sdk.client.base import BaseClient
14
+ from glaip_sdk.config.constants import (
15
+ DEFAULT_TOOL_FRAMEWORK,
16
+ DEFAULT_TOOL_TYPE,
17
+ DEFAULT_TOOL_VERSION,
18
+ )
13
19
  from glaip_sdk.models import Tool
14
20
  from glaip_sdk.utils.client_utils import (
15
21
  create_model_instances,
16
22
  find_by_name,
17
23
  )
18
24
 
25
+ # API endpoints
26
+ TOOLS_ENDPOINT = "/tools/"
27
+ TOOLS_UPLOAD_ENDPOINT = "/tools/upload"
28
+ TOOLS_UPLOAD_BY_ID_ENDPOINT_FMT = "/tools/{tool_id}/upload"
29
+
19
30
  # Set up module-level logger
20
31
  logger = logging.getLogger("glaip_sdk.tools")
21
32
 
@@ -32,20 +43,27 @@ class ToolClient(BaseClient):
32
43
  """
33
44
  super().__init__(parent_client=parent_client, **kwargs)
34
45
 
35
- def list_tools(self) -> list[Tool]:
36
- """List all tools."""
37
- data = self._request("GET", "/tools/")
46
+ def list_tools(self, tool_type: str | None = None) -> list[Tool]:
47
+ """List all tools, optionally filtered by type.
48
+
49
+ Args:
50
+ tool_type: Filter tools by type (e.g., "custom", "native")
51
+ """
52
+ endpoint = TOOLS_ENDPOINT
53
+ if tool_type:
54
+ endpoint += f"?type={tool_type}"
55
+ data = self._request("GET", endpoint)
38
56
  return create_model_instances(data, Tool, self)
39
57
 
40
58
  def get_tool_by_id(self, tool_id: str) -> Tool:
41
59
  """Get tool by ID."""
42
- data = self._request("GET", f"/tools/{tool_id}")
60
+ data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
43
61
  return Tool(**data)._set_client(self)
44
62
 
45
63
  def find_tools(self, name: str | None = None) -> list[Tool]:
46
64
  """Find tools by name."""
47
65
  # Backend doesn't support name query parameter, so we fetch all and filter client-side
48
- data = self._request("GET", "/tools/")
66
+ data = self._request("GET", TOOLS_ENDPOINT)
49
67
  tools = create_model_instances(data, Tool, self)
50
68
  return find_by_name(tools, name, case_sensitive=False)
51
69
 
@@ -101,7 +119,7 @@ class ToolClient(BaseClient):
101
119
  data["description"] = description
102
120
 
103
121
  # Handle tags if provided in kwargs
104
- if "tags" in kwargs and kwargs["tags"]:
122
+ if kwargs.get("tags"):
105
123
  if isinstance(kwargs["tags"], list):
106
124
  data["tags"] = ",".join(kwargs["tags"])
107
125
  else:
@@ -131,13 +149,130 @@ class ToolClient(BaseClient):
131
149
 
132
150
  response = self._request(
133
151
  "POST",
134
- "/tools/upload",
152
+ TOOLS_UPLOAD_ENDPOINT,
135
153
  files=files,
136
154
  data=upload_data,
137
155
  )
138
156
 
139
157
  return Tool(**response)._set_client(self)
140
158
 
159
+ def _build_create_payload(
160
+ self,
161
+ name: str,
162
+ description: str | None = None,
163
+ framework: str = DEFAULT_TOOL_FRAMEWORK,
164
+ tool_type: str = DEFAULT_TOOL_TYPE,
165
+ **kwargs,
166
+ ) -> dict[str, Any]:
167
+ """Build payload for tool creation with proper metadata handling.
168
+
169
+ CENTRALIZED PAYLOAD BUILDING LOGIC:
170
+ - Handles file vs metadata-only tool creation
171
+ - Sets proper defaults and required fields
172
+ - Processes tags and other metadata consistently
173
+
174
+ Args:
175
+ name: Tool name
176
+ description: Tool description
177
+ framework: Tool framework (defaults to langchain)
178
+ tool_type: Tool type (defaults to custom)
179
+ **kwargs: Additional parameters (tags, version, etc.)
180
+
181
+ Returns:
182
+ Complete payload dictionary for tool creation
183
+ """
184
+ # Prepare the creation payload with required fields
185
+ payload: dict[str, any] = {
186
+ "name": name.strip(),
187
+ "type": tool_type,
188
+ "framework": framework,
189
+ "version": kwargs.get("version", DEFAULT_TOOL_VERSION),
190
+ }
191
+
192
+ # Add description if provided
193
+ if description:
194
+ payload["description"] = description.strip()
195
+
196
+ # Handle tags - convert list to comma-separated string for API
197
+ if kwargs.get("tags"):
198
+ if isinstance(kwargs["tags"], list):
199
+ payload["tags"] = ",".join(str(tag).strip() for tag in kwargs["tags"])
200
+ else:
201
+ payload["tags"] = str(kwargs["tags"])
202
+
203
+ # Add any other kwargs (excluding already handled ones)
204
+ excluded_keys = {"tags", "version"}
205
+ for key, value in kwargs.items():
206
+ if key not in excluded_keys:
207
+ payload[key] = value
208
+
209
+ return payload
210
+
211
+ def _build_update_payload(
212
+ self,
213
+ current_tool: Tool,
214
+ name: str | None = None,
215
+ description: str | None = None,
216
+ **kwargs,
217
+ ) -> dict[str, Any]:
218
+ """Build payload for tool update with proper current state preservation.
219
+
220
+ Args:
221
+ current_tool: Current tool object to update
222
+ name: New tool name (None to keep current)
223
+ description: New description (None to keep current)
224
+ **kwargs: Additional parameters (tags, framework, etc.)
225
+
226
+ Returns:
227
+ Complete payload dictionary for tool update
228
+
229
+ Notes:
230
+ - Preserves current values as defaults when new values not provided
231
+ - Handles metadata updates properly
232
+ """
233
+ # Prepare the update payload with current values as defaults
234
+ update_data = {
235
+ "name": name if name is not None else current_tool.name,
236
+ "type": DEFAULT_TOOL_TYPE, # Required by backend
237
+ "framework": kwargs.get(
238
+ "framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)
239
+ ),
240
+ "version": kwargs.get(
241
+ "version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)
242
+ ),
243
+ }
244
+
245
+ # Handle description with proper None handling
246
+ if description is not None:
247
+ update_data["description"] = description.strip()
248
+ elif hasattr(current_tool, "description") and current_tool.description:
249
+ update_data["description"] = current_tool.description
250
+
251
+ # Handle tags - convert list to comma-separated string for API
252
+ if kwargs.get("tags"):
253
+ if isinstance(kwargs["tags"], list):
254
+ update_data["tags"] = ",".join(
255
+ str(tag).strip() for tag in kwargs["tags"]
256
+ )
257
+ else:
258
+ update_data["tags"] = str(kwargs["tags"])
259
+ elif hasattr(current_tool, "tags") and current_tool.tags:
260
+ # Preserve existing tags if present
261
+ if isinstance(current_tool.tags, list):
262
+ update_data["tags"] = ",".join(
263
+ str(tag).strip() for tag in current_tool.tags
264
+ )
265
+ else:
266
+ update_data["tags"] = str(current_tool.tags)
267
+
268
+ # Add any other kwargs (excluding already handled ones)
269
+ excluded_keys = {"tags", "framework", "version"}
270
+ for key, value in kwargs.items():
271
+ if key not in excluded_keys:
272
+ update_data[key] = value
273
+
274
+ return update_data
275
+
141
276
  def _create_tool_from_file(
142
277
  self,
143
278
  file_path: str,
@@ -279,12 +414,12 @@ class ToolClient(BaseClient):
279
414
 
280
415
  def update_tool(self, tool_id: str, **kwargs) -> Tool:
281
416
  """Update an existing tool."""
282
- data = self._request("PUT", f"/tools/{tool_id}", json=kwargs)
417
+ data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
283
418
  return Tool(**data)._set_client(self)
284
419
 
285
420
  def delete_tool(self, tool_id: str) -> None:
286
421
  """Delete a tool."""
287
- self._request("DELETE", f"/tools/{tool_id}")
422
+ self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
288
423
 
289
424
  def get_tool_script(self, tool_id: str) -> str:
290
425
  """Get the tool script content.
@@ -299,7 +434,7 @@ class ToolClient(BaseClient):
299
434
  Exception: If the tool script cannot be retrieved
300
435
  """
301
436
  try:
302
- response = self._request("GET", f"/tools/{tool_id}/script")
437
+ response = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}/script")
303
438
  return response.get("script", "") or response.get("content", "")
304
439
  except Exception as e:
305
440
  logger.error(f"Failed to get tool script for {tool_id}: {e}")
@@ -336,7 +471,7 @@ class ToolClient(BaseClient):
336
471
 
337
472
  response = self._request(
338
473
  "PUT",
339
- f"/tools/{tool_id}/upload",
474
+ TOOLS_UPLOAD_BY_ID_ENDPOINT_FMT.format(tool_id=tool_id),
340
475
  files=files,
341
476
  data=kwargs, # Pass kwargs directly as data
342
477
  )
@@ -10,7 +10,7 @@ DEFAULT_MODEL_PROVIDER = "openai"
10
10
 
11
11
  # Default timeout values
12
12
  DEFAULT_TIMEOUT = 30.0
13
- DEFAULT_AGENT_RUN_TIMEOUT = 300.0
13
+ DEFAULT_AGENT_RUN_TIMEOUT = 300
14
14
 
15
15
  # User agent and version
16
16
 
@@ -33,3 +33,12 @@ DEFAULT_AGENT_TYPE = "config"
33
33
  DEFAULT_AGENT_FRAMEWORK = "langchain"
34
34
  DEFAULT_AGENT_VERSION = "1.0"
35
35
  DEFAULT_AGENT_PROVIDER = "openai"
36
+
37
+ # Tool creation/update constants
38
+ DEFAULT_TOOL_TYPE = "custom"
39
+ DEFAULT_TOOL_FRAMEWORK = "langchain"
40
+ DEFAULT_TOOL_VERSION = "1.0"
41
+
42
+ # MCP creation/update constants
43
+ DEFAULT_MCP_TYPE = "server"
44
+ DEFAULT_MCP_TRANSPORT = "stdio"
glaip_sdk/models.py CHANGED
@@ -5,6 +5,7 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
+ from collections.abc import AsyncGenerator
8
9
  from datetime import datetime
9
10
  from typing import Any
10
11
 
@@ -60,6 +61,31 @@ class Agent(BaseModel):
60
61
  # Pass verbose flag through to enable event JSON output
61
62
  return self._client.run_agent(self.id, message, verbose=verbose, **kwargs)
62
63
 
64
+ async def arun(self, message: str, **kwargs) -> AsyncGenerator[dict, None]:
65
+ """Async run the agent with a message, yielding streaming JSON chunks.
66
+
67
+ Args:
68
+ message: The message to send to the agent
69
+ **kwargs: Additional arguments passed to arun_agent
70
+
71
+ Yields:
72
+ Dictionary containing parsed JSON chunks from the streaming response
73
+
74
+ Raises:
75
+ RuntimeError: When no client is available
76
+ AgentTimeoutError: When agent execution times out
77
+ Exception: For other unexpected errors
78
+ """
79
+ if not self._client:
80
+ raise RuntimeError(
81
+ "No client available. Use client.get_agent_by_id() to get a client-connected agent."
82
+ )
83
+ # Automatically pass the agent name for better context
84
+ kwargs.setdefault("agent_name", self.name)
85
+
86
+ async for chunk in self._client.arun_agent(self.id, message, **kwargs):
87
+ yield chunk
88
+
63
89
  def update(self, **kwargs) -> "Agent":
64
90
  """Update agent attributes."""
65
91
  if not self._client:
@@ -110,12 +136,26 @@ class Tool(BaseModel):
110
136
  return "No script content available"
111
137
 
112
138
  def update(self, **kwargs) -> "Tool":
113
- """Update tool attributes."""
139
+ """Update tool attributes.
140
+
141
+ Supports both metadata updates and file uploads.
142
+ Pass 'file' parameter to update tool code via file upload.
143
+ """
114
144
  if not self._client:
115
145
  raise RuntimeError(
116
146
  "No client available. Use client.get_tool_by_id() to get a client-connected tool."
117
147
  )
118
- updated_tool = self._client.tools.update_tool(self.id, **kwargs)
148
+
149
+ # Check if file upload is requested
150
+ if "file" in kwargs:
151
+ file_path = kwargs.pop("file") # Remove file from kwargs for metadata
152
+ updated_tool = self._client.tools.update_tool_via_file(
153
+ self.id, file_path, **kwargs
154
+ )
155
+ else:
156
+ # Regular metadata update
157
+ updated_tool = self._client.tools.update_tool(self.id, **kwargs)
158
+
119
159
  # Update current instance with new data
120
160
  for key, value in updated_tool.model_dump().items():
121
161
  if hasattr(self, key):
@@ -0,0 +1,29 @@
1
+ """Custom Rich components with copy-friendly defaults."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich import box
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+
9
+
10
+ class AIPPanel(Panel):
11
+ """Rich Panel configured without vertical borders by default."""
12
+
13
+ def __init__(self, *args, **kwargs):
14
+ kwargs.setdefault("box", box.HORIZONTALS)
15
+ kwargs.setdefault("padding", (0, 1))
16
+ super().__init__(*args, **kwargs)
17
+
18
+
19
+ class AIPTable(Table):
20
+ """Rich Table configured without vertical borders by default."""
21
+
22
+ def __init__(self, *args, **kwargs):
23
+ kwargs.setdefault("box", box.HORIZONTALS)
24
+ kwargs.setdefault("show_edge", False)
25
+ kwargs.setdefault("pad_edge", False)
26
+ super().__init__(*args, **kwargs)
27
+
28
+
29
+ __all__ = ["AIPPanel", "AIPTable"]
@@ -4,177 +4,23 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
- import re
8
- from uuid import UUID
9
-
10
- import click
11
-
12
- from .rendering.models import RunStats, Step
13
- from .rendering.steps import StepManager
14
- from .run_renderer import RichStreamRenderer
15
-
16
- # Rich imports with availability check
17
- try:
18
- from rich import box
19
- from rich.console import Console
20
- from rich.panel import Panel
21
- from rich.text import Text
22
-
23
- RICH_AVAILABLE = True
24
- except ImportError:
25
- RICH_AVAILABLE = False
26
-
27
- # Functions are defined below in this file
28
-
29
-
30
- def is_uuid(value: str) -> bool:
31
- """Check if a string is a valid UUID.
32
-
33
- Args:
34
- value: String to check
35
-
36
- Returns:
37
- True if value is a valid UUID, False otherwise
38
- """
39
- try:
40
- UUID(value)
41
- return True
42
- except (ValueError, TypeError):
43
- return False
44
-
45
-
46
- def sanitize_name(name: str) -> str:
47
- """Sanitize a name for resource creation.
48
-
49
- Args:
50
- name: Raw name input
51
-
52
- Returns:
53
- Sanitized name suitable for resource creation
54
- """
55
- # Remove special characters and normalize
56
- sanitized = re.sub(r"[^a-zA-Z0-9\-_]", "-", name.strip())
57
- sanitized = re.sub(r"-+", "-", sanitized) # Collapse multiple dashes
58
- return sanitized.lower().strip("-")
59
-
60
-
61
- def format_file_size(size_bytes: int) -> str:
62
- """Format file size in human readable format.
63
-
64
- Args:
65
- size_bytes: Size in bytes
66
-
67
- Returns:
68
- Human readable size string (e.g., "1.5 MB")
69
- """
70
- for unit in ["B", "KB", "MB", "GB"]:
71
- if size_bytes < 1024.0:
72
- return f"{size_bytes:.1f} {unit}"
73
- size_bytes /= 1024.0
74
- return f"{size_bytes:.1f} TB"
75
-
76
-
77
- def progress_bar(iterable, description: str = "Processing"):
78
- """Simple progress bar using click.
79
-
80
- Args:
81
- iterable: Iterable to process
82
- description: Progress description
83
-
84
- Yields:
85
- Items from iterable with progress display
86
- """
87
- try:
88
- with click.progressbar(iterable, label=description) as bar:
89
- for item in bar:
90
- yield item
91
- except ImportError:
92
- # Fallback if click not available
93
- for item in iterable:
94
- yield item
95
-
96
-
97
- # Rich display utilities for enhanced output
98
- # RICH_AVAILABLE is determined by the try/except block above
99
-
100
-
101
- def print_agent_output(output: str, title: str = "Agent Output") -> None:
102
- """Print agent output with rich formatting.
103
-
104
- Args:
105
- output: The agent's response text
106
- title: Title for the output panel
107
- """
108
- if RICH_AVAILABLE:
109
- console = Console()
110
- panel = Panel(
111
- Text(output, style="green"),
112
- title=title,
113
- border_style="green",
114
- box=box.ROUNDED,
115
- )
116
- console.print(panel)
117
- else:
118
- print(f"\n=== {title} ===")
119
- print(output)
120
- print("=" * (len(title) + 8))
121
-
122
-
123
- def print_agent_created(agent, title: str = "🤖 Agent Created") -> None:
124
- """Print agent creation success with rich formatting.
125
-
126
- Args:
127
- agent: The created agent object
128
- title: Title for the output panel
129
- """
130
- if RICH_AVAILABLE:
131
- console = Console()
132
- panel = Panel(
133
- f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
134
- f"ID: {agent.id}\n"
135
- f"Model: {getattr(agent, 'model', 'N/A')}\n"
136
- f"Type: {getattr(agent, 'type', 'config')}\n"
137
- f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
138
- f"Version: {getattr(agent, 'version', '1.0')}",
139
- title=title,
140
- border_style="green",
141
- box=box.ROUNDED,
142
- )
143
- console.print(panel)
144
- else:
145
- print(f"✅ Agent '{agent.name}' created successfully!")
146
- print(f"ID: {agent.id}")
147
- print(f"Model: {getattr(agent, 'model', 'N/A')}")
148
- print(f"Type: {getattr(agent, 'type', 'config')}")
149
- print(f"Framework: {getattr(agent, 'framework', 'langchain')}")
150
- print(f"Version: {getattr(agent, 'version', '1.0')}")
151
-
152
-
153
- def print_agent_updated(agent) -> None:
154
- """Print agent update success with rich formatting.
155
-
156
- Args:
157
- agent: The updated agent object
158
- """
159
- if RICH_AVAILABLE:
160
- console = Console()
161
- console.print(f"[green]✅ Agent '{agent.name}' updated successfully[/green]")
162
- else:
163
- print(f"✅ Agent '{agent.name}' updated successfully")
164
-
165
-
166
- def print_agent_deleted(agent_id: str) -> None:
167
- """Print agent deletion success with rich formatting.
168
-
169
- Args:
170
- agent_id: The deleted agent's ID
171
- """
172
- if RICH_AVAILABLE:
173
- console = Console()
174
- console.print(f"[green]✅ Agent deleted successfully (ID: {agent_id})[/green]")
175
- else:
176
- print(f"✅ Agent deleted successfully (ID: {agent_id})")
177
-
7
+ from glaip_sdk.utils.display import (
8
+ print_agent_created,
9
+ print_agent_deleted,
10
+ print_agent_output,
11
+ print_agent_updated,
12
+ )
13
+ from glaip_sdk.utils.general import (
14
+ format_datetime,
15
+ format_file_size,
16
+ is_uuid,
17
+ progress_bar,
18
+ sanitize_name,
19
+ )
20
+ from glaip_sdk.utils.rendering.models import RunStats, Step
21
+ from glaip_sdk.utils.rendering.steps import StepManager
22
+ from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
23
+ from glaip_sdk.utils.run_renderer import RichStreamRenderer
178
24
 
179
25
  __all__ = [
180
26
  "RichStreamRenderer",
@@ -185,6 +31,7 @@ __all__ = [
185
31
  "is_uuid",
186
32
  "sanitize_name",
187
33
  "format_file_size",
34
+ "format_datetime",
188
35
  "progress_bar",
189
36
  "print_agent_output",
190
37
  "print_agent_created",