glaip-sdk 0.0.3__py3-none-any.whl → 0.0.5__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 (47) hide show
  1. glaip_sdk/__init__.py +5 -5
  2. glaip_sdk/branding.py +146 -0
  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 +786 -271
  7. glaip_sdk/cli/commands/configure.py +19 -19
  8. glaip_sdk/cli/commands/mcps.py +151 -141
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +252 -178
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +27 -20
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +372 -213
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +632 -171
  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 +327 -104
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +43 -3
  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.3.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
  43. glaip_sdk-0.0.5.dist-info/RECORD +55 -0
  44. glaip_sdk/cli/commands/init.py +0 -177
  45. glaip_sdk-0.0.3.dist-info/RECORD +0 -40
  46. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
  47. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
@@ -11,7 +11,8 @@ from typing import Any
11
11
 
12
12
  from rich.console import Console
13
13
  from rich.markdown import Markdown
14
- from rich.panel import Panel
14
+
15
+ from glaip_sdk.rich_components import AIPPanel
15
16
 
16
17
 
17
18
  def render_debug_event(
@@ -76,7 +77,7 @@ def render_debug_event(
76
77
 
77
78
  # Render using Markdown with JSON code block (consistent with tool panels)
78
79
  md = Markdown(f"```json\n{event_json}\n```", code_theme="monokai")
79
- console.print(Panel(md, title=title, border_style=border))
80
+ console.print(AIPPanel(md, title=title, border_style=border))
80
81
  except Exception as e:
81
82
  # Debug helpers must not break streaming
82
83
  print(f"Debug error: {e}") # Fallback debug output
@@ -7,11 +7,12 @@ Authors:
7
7
  from __future__ import annotations
8
8
 
9
9
  from rich.markdown import Markdown
10
- from rich.panel import Panel
11
10
  from rich.text import Text
12
11
 
12
+ from glaip_sdk.rich_components import AIPPanel
13
13
 
14
- def create_main_panel(content: str, title: str, theme: str = "dark") -> Panel:
14
+
15
+ def create_main_panel(content: str, title: str, theme: str = "dark") -> AIPPanel:
15
16
  """Create a main content panel.
16
17
 
17
18
  Args:
@@ -23,7 +24,7 @@ def create_main_panel(content: str, title: str, theme: str = "dark") -> Panel:
23
24
  Rich Panel instance
24
25
  """
25
26
  if content.strip():
26
- return Panel(
27
+ return AIPPanel(
27
28
  Markdown(content, code_theme=("monokai" if theme == "dark" else "github")),
28
29
  title=title,
29
30
  border_style="green",
@@ -31,7 +32,7 @@ def create_main_panel(content: str, title: str, theme: str = "dark") -> Panel:
31
32
  else:
32
33
  # Placeholder panel
33
34
  placeholder = Text("Processing...", style="dim")
34
- return Panel(
35
+ return AIPPanel(
35
36
  placeholder,
36
37
  title=title,
37
38
  border_style="green",
@@ -44,7 +45,7 @@ def create_tool_panel(
44
45
  status: str = "running",
45
46
  theme: str = "dark",
46
47
  is_delegation: bool = False,
47
- ) -> Panel:
48
+ ) -> AIPPanel:
48
49
  """Create a tool execution panel.
49
50
 
50
51
  Args:
@@ -60,7 +61,7 @@ def create_tool_panel(
60
61
  mark = "✓" if status == "finished" else "⟳"
61
62
  border_style = "magenta" if is_delegation else "blue"
62
63
 
63
- return Panel(
64
+ return AIPPanel(
64
65
  Markdown(
65
66
  content or "Processing...",
66
67
  code_theme=("monokai" if theme == "dark" else "github"),
@@ -76,7 +77,7 @@ def create_context_panel(
76
77
  status: str = "running",
77
78
  theme: str = "dark",
78
79
  is_delegation: bool = False,
79
- ) -> Panel:
80
+ ) -> AIPPanel:
80
81
  """Create a context/sub-agent panel.
81
82
 
82
83
  Args:
@@ -92,7 +93,7 @@ def create_context_panel(
92
93
  mark = "✓" if status == "finished" else "⟳"
93
94
  border_style = "magenta" if is_delegation else "cyan"
94
95
 
95
- return Panel(
96
+ return AIPPanel(
96
97
  Markdown(
97
98
  content,
98
99
  code_theme=("monokai" if theme == "dark" else "github"),
@@ -104,7 +105,7 @@ def create_context_panel(
104
105
 
105
106
  def create_final_panel(
106
107
  content: str, title: str = "Final Result", theme: str = "dark"
107
- ) -> Panel:
108
+ ) -> AIPPanel:
108
109
  """Create a final result panel.
109
110
 
110
111
  Args:
@@ -115,7 +116,7 @@ def create_final_panel(
115
116
  Returns:
116
117
  Rich Panel instance
117
118
  """
118
- return Panel(
119
+ return AIPPanel(
119
120
  Markdown(content, code_theme=("monokai" if theme == "dark" else "github")),
120
121
  title=title,
121
122
  border_style="green",
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from collections.abc import Iterator
10
10
 
11
- from .models import Step
11
+ from glaip_sdk.utils.rendering.models import Step
12
12
 
13
13
 
14
14
  class StepManager:
@@ -0,0 +1,192 @@
1
+ """Resource reference utilities for ID/name extraction and UUID detection.
2
+
3
+ This module provides normalized helpers for working with resource references
4
+ across the SDK, consolidating logic that was previously duplicated between
5
+ CLI and SDK layers.
6
+
7
+ Authors:
8
+ Raymond Christopher (raymond.christopher@gdplabs.id)
9
+ """
10
+
11
+ import re
12
+ from typing import Any
13
+ from uuid import UUID
14
+
15
+
16
+ def is_uuid(value: str) -> bool:
17
+ """Check if a string is a valid UUID.
18
+
19
+ Args:
20
+ value: String to check
21
+
22
+ Returns:
23
+ True if value is a valid UUID, False otherwise
24
+ """
25
+ try:
26
+ UUID(value)
27
+ return True
28
+ except (ValueError, TypeError):
29
+ return False
30
+
31
+
32
+ def extract_ids(items: list[str | Any] | None) -> list[str]:
33
+ """Extract IDs from a list of objects or strings.
34
+
35
+ This function unifies the behavior between CLI and SDK layers, always
36
+ returning a list (empty list for None/empty input) rather than None.
37
+
38
+ Args:
39
+ items: List of items that may be strings, objects with .id, or other types
40
+
41
+ Returns:
42
+ List of extracted IDs (empty list if items is None/empty)
43
+
44
+ Examples:
45
+ extract_ids([{"id": "123"}, "456"]) -> ["123", "456"]
46
+ extract_ids(None) -> []
47
+ extract_ids([]) -> []
48
+ """
49
+ if not items:
50
+ return []
51
+
52
+ ids = []
53
+ for item in items:
54
+ if isinstance(item, str):
55
+ ids.append(item)
56
+ elif hasattr(item, "id"):
57
+ ids.append(str(item.id))
58
+ elif isinstance(item, dict) and "id" in item:
59
+ ids.append(str(item["id"]))
60
+ else:
61
+ # Fallback: convert to string
62
+ ids.append(str(item))
63
+
64
+ return ids
65
+
66
+
67
+ def extract_names(items: list[str | Any] | None) -> list[str]:
68
+ """Extract names from a list of objects or strings.
69
+
70
+ Args:
71
+ items: List of items that may be strings, objects with .name, or other types
72
+
73
+ Returns:
74
+ List of extracted names (empty list if items is None/empty)
75
+
76
+ Examples:
77
+ extract_names([{"name": "tool1"}, "tool2"]) -> ["tool1", "tool2"]
78
+ extract_names(None) -> []
79
+ """
80
+ if not items:
81
+ return []
82
+
83
+ names = []
84
+ for item in items:
85
+ if isinstance(item, str):
86
+ names.append(item)
87
+ elif hasattr(item, "name"):
88
+ names.append(str(item.name))
89
+ elif isinstance(item, dict) and "name" in item:
90
+ names.append(str(item["name"]))
91
+ else:
92
+ # Fallback: convert to string
93
+ names.append(str(item))
94
+
95
+ return names
96
+
97
+
98
+ def find_by_name(
99
+ items: list[Any], name: str, case_sensitive: bool = False
100
+ ) -> list[Any]:
101
+ """Filter items by name with optional case sensitivity.
102
+
103
+ This is a common pattern used across different clients for client-side
104
+ filtering when the backend doesn't support name query parameters.
105
+
106
+ Args:
107
+ items: List of items to filter
108
+ name: Name to search for
109
+ case_sensitive: Whether the search should be case sensitive
110
+
111
+ Returns:
112
+ Filtered list of items matching the name
113
+ """
114
+ if not name:
115
+ return items
116
+
117
+ if case_sensitive:
118
+ return [item for item in items if name in item.name]
119
+ else:
120
+ return [item for item in items if name.lower() in item.name.lower()]
121
+
122
+
123
+ def sanitize_name(name: str) -> str:
124
+ """Sanitize a name for resource creation.
125
+
126
+ Args:
127
+ name: Raw name input
128
+
129
+ Returns:
130
+ Sanitized name suitable for resource creation
131
+ """
132
+ # Remove special characters and normalize
133
+ sanitized = re.sub(r"[^a-zA-Z0-9\-_]", "-", name.strip())
134
+ sanitized = re.sub(r"-+", "-", sanitized) # Collapse multiple dashes
135
+ return sanitized.lower().strip("-")
136
+
137
+
138
+ def validate_name_format(name: str, resource_type: str = "resource") -> str:
139
+ """Validate resource name format and return cleaned version.
140
+
141
+ Args:
142
+ name: Name to validate
143
+ resource_type: Type of resource (for error messages)
144
+
145
+ Returns:
146
+ Cleaned name
147
+
148
+ Raises:
149
+ ValueError: If name format is invalid
150
+ """
151
+ # Map resource types to proper display names
152
+ type_display = {"agent": "Agent", "tool": "Tool", "mcp": "MCP"}
153
+ display_type = type_display.get(resource_type.lower(), resource_type.title())
154
+
155
+ if not name or not name.strip():
156
+ raise ValueError(f"{display_type} name cannot be empty")
157
+
158
+ cleaned_name = name.strip()
159
+
160
+ if len(cleaned_name) < 1:
161
+ raise ValueError(f"{display_type} name cannot be empty")
162
+
163
+ if len(cleaned_name) > 100:
164
+ raise ValueError(f"{display_type} name cannot be longer than 100 characters")
165
+
166
+ # Check for valid characters (alphanumeric, hyphens, underscores)
167
+ if not re.match(r"^[a-zA-Z0-9_-]+$", cleaned_name):
168
+ raise ValueError(
169
+ f"{display_type} name can only contain letters, numbers, hyphens, and underscores"
170
+ )
171
+
172
+ return cleaned_name
173
+
174
+
175
+ def validate_name_uniqueness(
176
+ name: str, existing_names: list[str], resource_type: str = "resource"
177
+ ) -> None:
178
+ """Validate that a resource name is unique.
179
+
180
+ Args:
181
+ name: Name to validate
182
+ existing_names: List of existing names to check against
183
+ resource_type: Type of resource (for error messages)
184
+
185
+ Raises:
186
+ ValueError: If name is not unique
187
+ """
188
+ if name.lower() in [existing.lower() for existing in existing_names]:
189
+ raise ValueError(
190
+ f"A {resource_type.lower()} named '{name}' already exists. "
191
+ "Please choose a unique name."
192
+ )
@@ -0,0 +1,29 @@
1
+ """Rich utility functions and availability checking.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+
8
+ def _check_rich_available():
9
+ """Check if Rich is available by attempting imports."""
10
+ try:
11
+ import importlib.util
12
+
13
+ # Check if rich modules are available without importing them
14
+ if (
15
+ importlib.util.find_spec("rich.console") is None
16
+ or importlib.util.find_spec("rich.text") is None
17
+ ):
18
+ return False
19
+
20
+ # Check if our rich components are available
21
+ if importlib.util.find_spec("glaip_sdk.rich_components") is None:
22
+ return False
23
+
24
+ return True
25
+ except Exception:
26
+ return False
27
+
28
+
29
+ RICH_AVAILABLE = _check_rich_available()
@@ -0,0 +1,285 @@
1
+ """Serialization utilities for JSON/YAML read/write and resource attribute collection.
2
+
3
+ This module provides pure functions for file I/O operations and data serialization
4
+ that can be used by both CLI and SDK layers without coupling to Click or Rich.
5
+
6
+ Authors:
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ import json
11
+ from collections.abc import Iterable
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import yaml
16
+
17
+
18
+ def read_json(file_path: Path) -> dict[str, Any]:
19
+ """Read data from JSON file.
20
+
21
+ Args:
22
+ file_path: Path to JSON file
23
+
24
+ Returns:
25
+ Parsed JSON data as dictionary
26
+
27
+ Raises:
28
+ FileNotFoundError: If file doesn't exist
29
+ ValueError: If file is not valid JSON
30
+ """
31
+ with open(file_path, encoding="utf-8") as f:
32
+ return json.load(f)
33
+
34
+
35
+ def write_json(file_path: Path, data: dict[str, Any], indent: int = 2) -> None:
36
+ """Write data to JSON file.
37
+
38
+ Args:
39
+ file_path: Path to write JSON file
40
+ data: Data to write
41
+ indent: JSON indentation level (default: 2)
42
+ """
43
+ with open(file_path, "w", encoding="utf-8") as f:
44
+ json.dump(data, f, indent=indent, default=str)
45
+
46
+
47
+ def read_yaml(file_path: Path) -> dict[str, Any]:
48
+ """Read data from YAML file.
49
+
50
+ Args:
51
+ file_path: Path to YAML file
52
+
53
+ Returns:
54
+ Parsed YAML data as dictionary
55
+
56
+ Raises:
57
+ FileNotFoundError: If file doesn't exist
58
+ ValueError: If file is not valid YAML
59
+ """
60
+ with open(file_path, encoding="utf-8") as f:
61
+ data = yaml.safe_load(f)
62
+
63
+ # Handle instruction_lines array format for user-friendly YAML
64
+ if (
65
+ isinstance(data, dict)
66
+ and "instruction_lines" in data
67
+ and isinstance(data["instruction_lines"], list)
68
+ ):
69
+ data["instruction"] = "\n\n".join(data["instruction_lines"])
70
+ del data["instruction_lines"]
71
+
72
+ # Handle instruction as list from YAML export (convert back to string)
73
+ if (
74
+ isinstance(data, dict)
75
+ and "instruction" in data
76
+ and isinstance(data["instruction"], list)
77
+ ):
78
+ data["instruction"] = "\n\n".join(data["instruction"])
79
+
80
+ return data
81
+
82
+
83
+ def write_yaml(file_path: Path, data: dict[str, Any]) -> None:
84
+ """Write data to YAML file with user-friendly formatting.
85
+
86
+ Args:
87
+ file_path: Path to write YAML file
88
+ data: Data to write
89
+ """
90
+
91
+ # Custom YAML dumper for user-friendly instruction formatting
92
+ class LiteralString(str):
93
+ pass
94
+
95
+ def literal_string_representer(dumper, data):
96
+ # Use literal block scalar (|) for multiline strings to preserve formatting
97
+ if "\n" in data:
98
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
99
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data)
100
+
101
+ # Add custom representer to the YAML dumper
102
+ yaml.add_representer(LiteralString, literal_string_representer)
103
+
104
+ # Convert instruction to LiteralString for proper formatting
105
+ if isinstance(data, dict) and "instruction" in data and data["instruction"]:
106
+ data = data.copy() # Don't modify original
107
+ data["instruction"] = LiteralString(data["instruction"])
108
+
109
+ with open(file_path, "w", encoding="utf-8") as f:
110
+ yaml.dump(
111
+ data, f, default_flow_style=False, allow_unicode=True, sort_keys=False
112
+ )
113
+
114
+
115
+ def load_resource_from_file(file_path: Path) -> dict[str, Any]:
116
+ """Load resource data from JSON or YAML file.
117
+
118
+ Args:
119
+ file_path: Path to the file
120
+
121
+ Returns:
122
+ Dictionary with resource data
123
+
124
+ Raises:
125
+ ValueError: If file format is not supported
126
+ """
127
+ if file_path.suffix.lower() in [".yaml", ".yml"]:
128
+ return read_yaml(file_path)
129
+ elif file_path.suffix.lower() == ".json":
130
+ return read_json(file_path)
131
+ else:
132
+ raise ValueError(
133
+ f"Unsupported file format: {file_path.suffix}. Only JSON and YAML files are supported."
134
+ )
135
+
136
+
137
+ def write_resource_export(
138
+ file_path: Path, data: dict[str, Any], format: str = "json"
139
+ ) -> None:
140
+ """Write resource export data to file.
141
+
142
+ Args:
143
+ file_path: Path to export file
144
+ data: Resource data to export
145
+ format: Export format ("json" or "yaml")
146
+ """
147
+ if format.lower() == "yaml" or file_path.suffix.lower() in [".yaml", ".yml"]:
148
+ write_yaml(file_path, data)
149
+ else:
150
+ write_json(file_path, data)
151
+
152
+
153
+ _EXCLUDED_ATTRS = {
154
+ "id",
155
+ "created_at",
156
+ "updated_at",
157
+ "_client",
158
+ "_raw_data",
159
+ }
160
+ _EXCLUDED_NAMES = {
161
+ "model_dump",
162
+ "dict",
163
+ "json",
164
+ "get",
165
+ "post",
166
+ "put",
167
+ "delete",
168
+ "save",
169
+ "refresh",
170
+ "update",
171
+ }
172
+ _PREFERRED_MAPPERS: tuple[str, ...] = ("model_dump", "dict", "to_dict")
173
+
174
+
175
+ def collect_attributes_for_export(resource: Any) -> dict[str, Any]:
176
+ """Collect resource attributes suitable for export.
177
+
178
+ The helper prefers structured dump methods when available and gracefully
179
+ falls back to the object's attribute space. Internal fields, identifiers,
180
+ and callables are filtered out so the result only contains user-configurable
181
+ data.
182
+ """
183
+
184
+ mapping = _coerce_resource_to_mapping(resource)
185
+ if mapping is None:
186
+ items = (
187
+ (name, _safe_getattr(resource, name))
188
+ for name in _iter_public_attribute_names(resource)
189
+ )
190
+ else:
191
+ items = mapping.items()
192
+
193
+ export: dict[str, Any] = {}
194
+ for key, value in items:
195
+ if _should_include_attribute(key, value):
196
+ export[key] = value
197
+ return export
198
+
199
+
200
+ def _coerce_resource_to_mapping(resource: Any) -> dict[str, Any] | None:
201
+ """Return a mapping representation of ``resource`` when possible."""
202
+
203
+ for attr in _PREFERRED_MAPPERS:
204
+ method = getattr(resource, attr, None)
205
+ if callable(method):
206
+ try:
207
+ data = method()
208
+ except Exception:
209
+ continue
210
+ if isinstance(data, dict):
211
+ return data
212
+
213
+ if isinstance(resource, dict):
214
+ return resource
215
+
216
+ if hasattr(resource, "__dict__"):
217
+ try:
218
+ return dict(resource.__dict__)
219
+ except Exception:
220
+ return None
221
+
222
+ return None
223
+
224
+
225
+ def _iter_public_attribute_names(resource: Any) -> Iterable[str]:
226
+ """Yield attribute names we should inspect on ``resource``."""
227
+
228
+ seen: set[str] = set()
229
+ names: list[str] = []
230
+
231
+ def _collect(candidates: Iterable[str] | None) -> None:
232
+ if candidates is None:
233
+ return
234
+ for candidate in candidates:
235
+ if candidate not in seen:
236
+ seen.add(candidate)
237
+ names.append(candidate)
238
+
239
+ _collect(
240
+ getattr(resource, "__dict__", {}).keys()
241
+ if hasattr(resource, "__dict__")
242
+ else None
243
+ )
244
+ _collect(getattr(resource, "__annotations__", {}).keys())
245
+ _collect(getattr(resource, "__slots__", ()))
246
+
247
+ if not names:
248
+ _collect(name for name in dir(resource) if not name.startswith("__"))
249
+
250
+ return iter(names)
251
+
252
+
253
+ def _safe_getattr(resource: Any, name: str) -> Any:
254
+ try:
255
+ return getattr(resource, name)
256
+ except Exception:
257
+ return None
258
+
259
+
260
+ def _should_include_attribute(key: str, value: Any) -> bool:
261
+ if key in _EXCLUDED_ATTRS or key in _EXCLUDED_NAMES:
262
+ return False
263
+ if key.startswith("_"):
264
+ return False
265
+ if callable(value):
266
+ return False
267
+ return True
268
+
269
+
270
+ def validate_json_string(json_str: str) -> dict[str, Any]:
271
+ """Validate JSON string and return parsed data.
272
+
273
+ Args:
274
+ json_str: JSON string to validate
275
+
276
+ Returns:
277
+ Parsed JSON data
278
+
279
+ Raises:
280
+ ValueError: If JSON is invalid
281
+ """
282
+ try:
283
+ return json.loads(json_str)
284
+ except json.JSONDecodeError as e:
285
+ raise ValueError(f"Invalid JSON: {e}")