glaip-sdk 0.0.15__py3-none-any.whl → 0.0.17__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 (43) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/branding.py +28 -2
  3. glaip_sdk/cli/commands/agents.py +36 -27
  4. glaip_sdk/cli/commands/configure.py +46 -52
  5. glaip_sdk/cli/commands/mcps.py +19 -22
  6. glaip_sdk/cli/commands/tools.py +19 -13
  7. glaip_sdk/cli/config.py +42 -0
  8. glaip_sdk/cli/display.py +97 -30
  9. glaip_sdk/cli/main.py +141 -124
  10. glaip_sdk/cli/mcp_validators.py +2 -2
  11. glaip_sdk/cli/pager.py +3 -2
  12. glaip_sdk/cli/parsers/json_input.py +2 -2
  13. glaip_sdk/cli/resolution.py +12 -10
  14. glaip_sdk/cli/rich_helpers.py +29 -0
  15. glaip_sdk/cli/slash/agent_session.py +7 -0
  16. glaip_sdk/cli/slash/prompt.py +21 -2
  17. glaip_sdk/cli/slash/session.py +15 -21
  18. glaip_sdk/cli/update_notifier.py +8 -2
  19. glaip_sdk/cli/utils.py +115 -58
  20. glaip_sdk/client/_agent_payloads.py +504 -0
  21. glaip_sdk/client/agents.py +633 -559
  22. glaip_sdk/client/base.py +92 -20
  23. glaip_sdk/client/main.py +14 -0
  24. glaip_sdk/client/run_rendering.py +275 -0
  25. glaip_sdk/config/constants.py +4 -1
  26. glaip_sdk/exceptions.py +15 -0
  27. glaip_sdk/models.py +5 -0
  28. glaip_sdk/payload_schemas/__init__.py +19 -0
  29. glaip_sdk/payload_schemas/agent.py +87 -0
  30. glaip_sdk/rich_components.py +12 -0
  31. glaip_sdk/utils/client_utils.py +12 -0
  32. glaip_sdk/utils/import_export.py +2 -2
  33. glaip_sdk/utils/rendering/formatting.py +5 -0
  34. glaip_sdk/utils/rendering/models.py +22 -0
  35. glaip_sdk/utils/rendering/renderer/base.py +9 -1
  36. glaip_sdk/utils/rendering/renderer/panels.py +0 -1
  37. glaip_sdk/utils/rendering/steps.py +59 -0
  38. glaip_sdk/utils/serialization.py +24 -3
  39. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/METADATA +2 -2
  40. glaip_sdk-0.0.17.dist-info/RECORD +73 -0
  41. glaip_sdk-0.0.15.dist-info/RECORD +0 -67
  42. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/WHEEL +0 -0
  43. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/base.py CHANGED
@@ -14,7 +14,7 @@ from dotenv import load_dotenv
14
14
 
15
15
  import glaip_sdk
16
16
  from glaip_sdk._version import __version__ as SDK_VERSION
17
- from glaip_sdk.config.constants import SDK_NAME
17
+ from glaip_sdk.config.constants import DEFAULT_ERROR_MESSAGE, SDK_NAME
18
18
  from glaip_sdk.exceptions import (
19
19
  AuthenticationError,
20
20
  ConflictError,
@@ -230,8 +230,13 @@ class BaseClient:
230
230
  else:
231
231
  raise
232
232
 
233
- def _request(self, method: str, endpoint: str, **kwargs) -> Any:
234
- """Make HTTP request with error handling."""
233
+ def _perform_request(
234
+ self,
235
+ method: str,
236
+ endpoint: str,
237
+ **kwargs: Any,
238
+ ) -> httpx.Response:
239
+ """Execute a raw HTTP request with retry handling."""
235
240
  # Ensure client is alive before making request
236
241
  self._ensure_client_alive()
237
242
 
@@ -239,7 +244,7 @@ class BaseClient:
239
244
  try:
240
245
  response = self.http_client.request(method, endpoint, **kwargs)
241
246
  client_log.debug(f"Response status: {response.status_code}")
242
- return self._handle_response(response)
247
+ return response
243
248
  except httpx.ConnectError as e:
244
249
  client_log.warning(
245
250
  f"Connection error on {method} {endpoint}, retrying once: {e}"
@@ -249,11 +254,26 @@ class BaseClient:
249
254
  client_log.debug(
250
255
  f"Retry successful, response status: {response.status_code}"
251
256
  )
252
- return self._handle_response(response)
257
+ return response
253
258
  except httpx.ConnectError:
254
259
  client_log.error(f"Retry failed for {method} {endpoint}: {e}")
255
260
  raise e
256
261
 
262
+ def _request(self, method: str, endpoint: str, **kwargs) -> Any:
263
+ """Make HTTP request with error handling and unwrap success envelopes."""
264
+ response = self._perform_request(method, endpoint, **kwargs)
265
+ return self._handle_response(response, unwrap=True)
266
+
267
+ def _request_with_envelope(
268
+ self,
269
+ method: str,
270
+ endpoint: str,
271
+ **kwargs: Any,
272
+ ) -> Any:
273
+ """Make HTTP request but return the full success envelope."""
274
+ response = self._perform_request(method, endpoint, **kwargs)
275
+ return self._handle_response(response, unwrap=False)
276
+
257
277
  def _parse_response_content(self, response: httpx.Response) -> Any | None:
258
278
  """Parse response content based on content type."""
259
279
  if response.status_code == 204:
@@ -271,14 +291,14 @@ class BaseClient:
271
291
  else:
272
292
  return None # Let _handle_response deal with error status codes
273
293
 
274
- def _handle_success_response(self, parsed: Any) -> Any:
294
+ def _handle_success_response(self, parsed: Any, *, unwrap: bool) -> Any:
275
295
  """Handle successful response with success flag."""
276
296
  if isinstance(parsed, dict) and "success" in parsed:
277
297
  if parsed.get("success"):
278
- return parsed.get("data", parsed)
298
+ return parsed.get("data", parsed) if unwrap else parsed
279
299
  else:
280
300
  error_type = parsed.get("error", "UnknownError")
281
- message = parsed.get("message", "Unknown error")
301
+ message = parsed.get("message", DEFAULT_ERROR_MESSAGE)
282
302
  self._raise_api_error(
283
303
  400,
284
304
  message,
@@ -290,19 +310,67 @@ class BaseClient:
290
310
 
291
311
  def _get_error_message(self, response: httpx.Response) -> str:
292
312
  """Extract error message from response, preferring parsed content."""
293
- # Try to get error message from parsed content if available
294
- error_message = response.text
313
+ parsed = self._parse_error_json(response)
314
+ if parsed is None:
315
+ return response.text
316
+
317
+ formatted = self._format_parsed_error(parsed)
318
+ return formatted if formatted is not None else response.text
319
+
320
+ def _parse_error_json(self, response: httpx.Response) -> Any | None:
321
+ """Safely parse JSON from an error response."""
295
322
  try:
296
- parsed = response.json()
297
- if isinstance(parsed, dict) and "message" in parsed:
298
- error_message = parsed["message"]
299
- elif isinstance(parsed, str):
300
- error_message = parsed
323
+ return response.json()
301
324
  except (ValueError, TypeError):
302
- pass # Use response.text as fallback
303
- return error_message
325
+ return None
326
+
327
+ def _format_parsed_error(self, parsed: Any) -> str | None:
328
+ """Build a readable error message from parsed JSON payloads."""
329
+ if isinstance(parsed, dict):
330
+ return self._format_error_dict(parsed)
331
+ if isinstance(parsed, str):
332
+ return parsed
333
+ return str(parsed) if parsed else None
334
+
335
+ def _format_error_dict(self, parsed: dict[str, Any]) -> str:
336
+ """Format structured API error payloads."""
337
+ detail = parsed.get("detail")
338
+ if isinstance(detail, list):
339
+ validation_message = self._format_validation_errors(detail)
340
+ if validation_message:
341
+ return validation_message
342
+ return f"Validation error: {parsed}"
343
+
344
+ message = parsed.get("message")
345
+ if message:
346
+ return message
347
+
348
+ return str(parsed) if parsed else DEFAULT_ERROR_MESSAGE
349
+
350
+ def _format_validation_errors(self, errors: list[Any]) -> str | None:
351
+ """Render validation errors into a human-readable string."""
352
+ entries: list[str] = []
353
+ for error in errors:
354
+ if isinstance(error, dict):
355
+ loc = " -> ".join(str(x) for x in error.get("loc", []))
356
+ msg = error.get("msg", DEFAULT_ERROR_MESSAGE)
357
+ error_type = error.get("type", "unknown")
358
+ prefix = loc if loc else "Field"
359
+ entries.append(f" {prefix}: {msg} ({error_type})")
360
+ else:
361
+ entries.append(f" {error}")
362
+
363
+ if not entries:
364
+ return None
304
365
 
305
- def _handle_response(self, response: httpx.Response) -> Any:
366
+ return "Validation errors:\n" + "\n".join(entries)
367
+
368
+ def _handle_response(
369
+ self,
370
+ response: httpx.Response,
371
+ *,
372
+ unwrap: bool = True,
373
+ ) -> Any:
306
374
  """Handle HTTP response with proper error handling."""
307
375
  # Handle no-content success before general error handling
308
376
  if response.status_code == 204:
@@ -311,14 +379,18 @@ class BaseClient:
311
379
  # Handle error status codes
312
380
  if not (200 <= response.status_code < 300):
313
381
  error_message = self._get_error_message(response)
314
- self._raise_api_error(response.status_code, error_message)
382
+ # Try to parse response content for payload
383
+ parsed_content = self._parse_response_content(response)
384
+ self._raise_api_error(
385
+ response.status_code, error_message, payload=parsed_content
386
+ )
315
387
  return None # Won't be reached but helps with type checking
316
388
 
317
389
  parsed = self._parse_response_content(response)
318
390
  if parsed is None:
319
391
  return None
320
392
 
321
- return self._handle_success_response(parsed)
393
+ return self._handle_success_response(parsed, unwrap=unwrap)
322
394
 
323
395
  def _raise_api_error(
324
396
  self,
glaip_sdk/client/main.py CHANGED
@@ -18,6 +18,11 @@ class Client(BaseClient):
18
18
  """Main client that composes all specialized clients and shares one HTTP session."""
19
19
 
20
20
  def __init__(self, **kwargs):
21
+ """Initialize the main client.
22
+
23
+ Args:
24
+ **kwargs: Client configuration arguments (api_url, api_key, timeout, etc.)
25
+ """
21
26
  super().__init__(**kwargs)
22
27
  # Share the single httpx.Client + config with sub-clients
23
28
  shared_config = {
@@ -37,6 +42,10 @@ class Client(BaseClient):
37
42
  """Create a new agent."""
38
43
  return self.agents.create_agent(**kwargs)
39
44
 
45
+ def create_agent_from_file(self, *args, **kwargs) -> Agent:
46
+ """Create a new agent from a JSON or YAML configuration file."""
47
+ return self.agents.create_agent_from_file(*args, **kwargs)
48
+
40
49
  def list_agents(
41
50
  self,
42
51
  agent_type: str | None = None,
@@ -81,6 +90,10 @@ class Client(BaseClient):
81
90
  """Update an existing agent."""
82
91
  return self.agents.update_agent(agent_id, **kwargs)
83
92
 
93
+ def update_agent_from_file(self, agent_id: str, *args, **kwargs) -> Agent:
94
+ """Update an existing agent using a JSON or YAML configuration file."""
95
+ return self.agents.update_agent_from_file(agent_id, *args, **kwargs)
96
+
84
97
  def delete_agent(self, agent_id: str) -> bool:
85
98
  """Delete an agent."""
86
99
  return self.agents.delete_agent(agent_id)
@@ -199,6 +212,7 @@ class Client(BaseClient):
199
212
  # ---- Timeout propagation ----
200
213
  @property
201
214
  def timeout(self) -> float: # type: ignore[override]
215
+ """Get the client timeout value."""
202
216
  return super().timeout
203
217
 
204
218
  @timeout.setter
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env python3
2
+ """Rendering helpers for agent streaming flows.
3
+
4
+ Authors:
5
+ Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import io
11
+ import json
12
+ import logging
13
+ from time import monotonic
14
+ from typing import Any
15
+
16
+ import httpx
17
+ from rich.console import Console as _Console
18
+
19
+ from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
20
+ from glaip_sdk.utils.client_utils import iter_sse_events
21
+ from glaip_sdk.utils.rendering.models import RunStats
22
+ from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
23
+ from glaip_sdk.utils.rendering.renderer.config import RendererConfig
24
+
25
+
26
+ class AgentRunRenderingManager:
27
+ """Coordinate renderer creation and streaming event handling."""
28
+
29
+ def __init__(self, logger: logging.Logger | None = None) -> None:
30
+ """Initialize the rendering manager.
31
+
32
+ Args:
33
+ logger: Optional logger instance, creates default if None
34
+ """
35
+ self._logger = logger or logging.getLogger(__name__)
36
+
37
+ # --------------------------------------------------------------------- #
38
+ # Renderer setup helpers
39
+ # --------------------------------------------------------------------- #
40
+ def create_renderer(
41
+ self,
42
+ renderer_spec: RichStreamRenderer | str | None,
43
+ *,
44
+ verbose: bool = False,
45
+ ) -> RichStreamRenderer:
46
+ """Create an appropriate renderer based on the supplied spec."""
47
+ if isinstance(renderer_spec, RichStreamRenderer):
48
+ return renderer_spec
49
+
50
+ if isinstance(renderer_spec, str):
51
+ if renderer_spec == "silent":
52
+ return self._create_silent_renderer()
53
+ if renderer_spec == "minimal":
54
+ return self._create_minimal_renderer()
55
+ return self._create_default_renderer(verbose)
56
+
57
+ return self._create_default_renderer(verbose)
58
+
59
+ def build_initial_metadata(
60
+ self,
61
+ agent_id: str,
62
+ message: str,
63
+ kwargs: dict[str, Any],
64
+ ) -> dict[str, Any]:
65
+ """Construct the initial renderer metadata payload."""
66
+ return {
67
+ "agent_name": kwargs.get("agent_name", agent_id),
68
+ "model": kwargs.get("model"),
69
+ "run_id": None,
70
+ "input_message": message,
71
+ }
72
+
73
+ @staticmethod
74
+ def start_renderer(renderer: RichStreamRenderer, meta: dict[str, Any]) -> None:
75
+ """Notify renderer that streaming is starting."""
76
+ renderer.on_start(meta)
77
+
78
+ def _create_silent_renderer(self) -> RichStreamRenderer:
79
+ silent_config = RendererConfig(
80
+ live=False,
81
+ persist_live=False,
82
+ show_delegate_tool_panels=False,
83
+ render_thinking=False,
84
+ )
85
+ return RichStreamRenderer(
86
+ console=_Console(file=io.StringIO(), force_terminal=False),
87
+ cfg=silent_config,
88
+ verbose=False,
89
+ )
90
+
91
+ def _create_minimal_renderer(self) -> RichStreamRenderer:
92
+ minimal_config = RendererConfig(
93
+ live=False,
94
+ persist_live=False,
95
+ show_delegate_tool_panels=False,
96
+ render_thinking=False,
97
+ )
98
+ return RichStreamRenderer(
99
+ console=_Console(),
100
+ cfg=minimal_config,
101
+ verbose=False,
102
+ )
103
+
104
+ def _create_verbose_renderer(self) -> RichStreamRenderer:
105
+ verbose_config = RendererConfig(
106
+ theme="dark",
107
+ style="debug",
108
+ live=False,
109
+ show_delegate_tool_panels=True,
110
+ append_finished_snapshots=False,
111
+ )
112
+ return RichStreamRenderer(
113
+ console=_Console(),
114
+ cfg=verbose_config,
115
+ verbose=True,
116
+ )
117
+
118
+ def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
119
+ if verbose:
120
+ return self._create_verbose_renderer()
121
+ default_config = RendererConfig(show_delegate_tool_panels=True)
122
+ return RichStreamRenderer(console=_Console(), cfg=default_config)
123
+
124
+ # --------------------------------------------------------------------- #
125
+ # Streaming event handling
126
+ # --------------------------------------------------------------------- #
127
+ def process_stream_events(
128
+ self,
129
+ stream_response: httpx.Response,
130
+ renderer: RichStreamRenderer,
131
+ timeout_seconds: float,
132
+ agent_name: str | None,
133
+ meta: dict[str, Any],
134
+ ) -> tuple[str, dict[str, Any], float | None, float | None]:
135
+ """Process streaming events and accumulate response."""
136
+ final_text = ""
137
+ stats_usage: dict[str, Any] = {}
138
+ started_monotonic: float | None = None
139
+
140
+ self._capture_request_id(stream_response, meta, renderer)
141
+
142
+ for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
143
+ if started_monotonic is None:
144
+ started_monotonic = self._maybe_start_timer(event)
145
+
146
+ final_text, stats_usage = self._process_single_event(
147
+ event,
148
+ renderer,
149
+ final_text,
150
+ stats_usage,
151
+ meta,
152
+ )
153
+
154
+ finished_monotonic = monotonic()
155
+ return final_text, stats_usage, started_monotonic, finished_monotonic
156
+
157
+ def _capture_request_id(
158
+ self,
159
+ stream_response: httpx.Response,
160
+ meta: dict[str, Any],
161
+ renderer: RichStreamRenderer,
162
+ ) -> None:
163
+ req_id = stream_response.headers.get(
164
+ "x-request-id"
165
+ ) or stream_response.headers.get("x-run-id")
166
+ if req_id:
167
+ meta["run_id"] = req_id
168
+ renderer.on_start(meta)
169
+
170
+ def _maybe_start_timer(self, event: dict[str, Any]) -> float | None:
171
+ try:
172
+ ev = json.loads(event["data"])
173
+ except json.JSONDecodeError:
174
+ return None
175
+
176
+ if "content" in ev or "status" in ev or ev.get("metadata"):
177
+ return monotonic()
178
+ return None
179
+
180
+ def _process_single_event(
181
+ self,
182
+ event: dict[str, Any],
183
+ renderer: RichStreamRenderer,
184
+ final_text: str,
185
+ stats_usage: dict[str, Any],
186
+ meta: dict[str, Any],
187
+ ) -> tuple[str, dict[str, Any]]:
188
+ try:
189
+ ev = json.loads(event["data"])
190
+ except json.JSONDecodeError:
191
+ self._logger.debug("Non-JSON SSE fragment skipped")
192
+ return final_text, stats_usage
193
+
194
+ kind = (ev.get("metadata") or {}).get("kind")
195
+ renderer.on_event(ev)
196
+
197
+ if kind == "artifact":
198
+ return final_text, stats_usage
199
+
200
+ if kind == "final_response" and ev.get("content"):
201
+ final_text = ev.get("content", "")
202
+ elif ev.get("content"):
203
+ final_text = self._handle_content_event(ev, final_text)
204
+ elif kind == "usage":
205
+ stats_usage.update(ev.get("usage") or {})
206
+ elif kind == "run_info":
207
+ self._handle_run_info_event(ev, meta, renderer)
208
+
209
+ return final_text, stats_usage
210
+
211
+ def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
212
+ content = ev.get("content", "")
213
+ if not content.startswith("Artifact received:"):
214
+ return content
215
+ return final_text
216
+
217
+ def _handle_run_info_event(
218
+ self,
219
+ ev: dict[str, Any],
220
+ meta: dict[str, Any],
221
+ renderer: RichStreamRenderer,
222
+ ) -> None:
223
+ if ev.get("model"):
224
+ meta["model"] = ev["model"]
225
+ renderer.on_start(meta)
226
+ if ev.get("run_id"):
227
+ meta["run_id"] = ev["run_id"]
228
+ renderer.on_start(meta)
229
+
230
+ # --------------------------------------------------------------------- #
231
+ # Finalisation helpers
232
+ # --------------------------------------------------------------------- #
233
+ def finalize_renderer(
234
+ self,
235
+ renderer: RichStreamRenderer,
236
+ final_text: str,
237
+ stats_usage: dict[str, Any],
238
+ started_monotonic: float | None,
239
+ finished_monotonic: float | None,
240
+ ) -> str:
241
+ """Complete rendering and return the textual result."""
242
+ st = RunStats()
243
+ st.started_at = started_monotonic or st.started_at
244
+ st.finished_at = finished_monotonic or st.started_at
245
+ st.usage = stats_usage
246
+
247
+ rendered_text = ""
248
+ buffer_values: Any | None = None
249
+
250
+ if hasattr(renderer, "state") and hasattr(renderer.state, "buffer"):
251
+ buffer_values = renderer.state.buffer
252
+ elif hasattr(renderer, "buffer"):
253
+ buffer_values = getattr(renderer, "buffer")
254
+
255
+ if buffer_values is not None:
256
+ try:
257
+ rendered_text = "".join(buffer_values)
258
+ except TypeError:
259
+ rendered_text = ""
260
+
261
+ renderer.on_complete(st)
262
+ return final_text or rendered_text or "No response content received."
263
+
264
+
265
+ def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
266
+ """Determine the execution timeout for agent runs.
267
+
268
+ Args:
269
+ kwargs: Dictionary containing execution parameters, including timeout.
270
+
271
+ Returns:
272
+ The timeout value in seconds, defaulting to DEFAULT_AGENT_RUN_TIMEOUT
273
+ if not specified in kwargs.
274
+ """
275
+ return kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
@@ -5,7 +5,7 @@ Authors:
5
5
  """
6
6
 
7
7
  # Default language model configuration
8
- DEFAULT_MODEL = "gpt-4.1"
8
+ DEFAULT_MODEL = "gpt-5-nano"
9
9
  DEFAULT_AGENT_RUN_TIMEOUT = 300
10
10
 
11
11
  # User agent and version
@@ -36,3 +36,6 @@ DEFAULT_TOOL_VERSION = "1.0"
36
36
  # MCP creation/update constants
37
37
  DEFAULT_MCP_TYPE = "server"
38
38
  DEFAULT_MCP_TRANSPORT = "stdio"
39
+
40
+ # Default error messages
41
+ DEFAULT_ERROR_MESSAGE = "Unknown error"
glaip_sdk/exceptions.py CHANGED
@@ -26,6 +26,15 @@ class APIError(AIPError):
26
26
  payload: Any = None,
27
27
  request_id: str | None = None,
28
28
  ):
29
+ """Initialize the API error.
30
+
31
+ Args:
32
+ message: The error message
33
+ status_code: HTTP status code
34
+ error_type: Type of error
35
+ payload: Additional error payload
36
+ request_id: Request identifier
37
+ """
29
38
  super().__init__(message)
30
39
  self.status_code = status_code
31
40
  self.error_type = error_type
@@ -91,6 +100,12 @@ class AgentTimeoutError(TimeoutError):
91
100
  """Agent execution timeout with specific duration information."""
92
101
 
93
102
  def __init__(self, timeout_seconds: float, agent_name: str = None):
103
+ """Initialize the agent timeout error.
104
+
105
+ Args:
106
+ timeout_seconds: The timeout duration in seconds
107
+ agent_name: Optional name of the agent that timed out
108
+ """
94
109
  agent_info = f" for agent '{agent_name}'" if agent_name else ""
95
110
  message = (
96
111
  f"Agent execution timed out after {timeout_seconds} seconds{agent_info}"
glaip_sdk/models.py CHANGED
@@ -236,6 +236,11 @@ class TTYRenderer:
236
236
  """Simple TTY renderer for non-Rich environments."""
237
237
 
238
238
  def __init__(self, use_color: bool = True):
239
+ """Initialize the TTY renderer.
240
+
241
+ Args:
242
+ use_color: Whether to use color output
243
+ """
239
244
  self.use_color = use_color
240
245
 
241
246
  def render_message(self, message: str, event_type: str = "message") -> None:
@@ -0,0 +1,19 @@
1
+ """Payload schema metadata for AIP resources.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from glaip_sdk.payload_schemas.agent import (
8
+ AgentImportOperation,
9
+ ImportFieldPlan,
10
+ get_import_field_plan,
11
+ list_server_only_fields,
12
+ )
13
+
14
+ __all__ = [
15
+ "AgentImportOperation",
16
+ "ImportFieldPlan",
17
+ "get_import_field_plan",
18
+ "list_server_only_fields",
19
+ ]
@@ -0,0 +1,87 @@
1
+ """Agent payload schema metadata derived from agent_payloads.md.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+
6
+ This module encodes which agent fields are mutable, server-managed, or require
7
+ additional sanitisation so that CLI and SDK flows can share the same rules.
8
+ """
9
+
10
+ from collections.abc import Collection, Mapping
11
+ from dataclasses import dataclass
12
+ from typing import Literal
13
+
14
+ AgentImportOperation = Literal["create", "update"]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class FieldRule:
19
+ """Schema rule defining how a field should be treated."""
20
+
21
+ server_only: bool = False
22
+ cli_managed_create: bool = False
23
+ cli_managed_update: bool = False
24
+ requires_sanitization: bool = False
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ImportFieldPlan:
29
+ """Plan for how an import pipeline should treat a specific field."""
30
+
31
+ copy: bool
32
+ sanitize: bool = False
33
+
34
+
35
+ _DEFAULT_RULE = FieldRule()
36
+
37
+
38
+ AGENT_FIELD_RULES: Mapping[str, FieldRule] = {
39
+ # Server-provided metadata (never send back)
40
+ "id": FieldRule(server_only=True),
41
+ "created_at": FieldRule(server_only=True),
42
+ "updated_at": FieldRule(server_only=True),
43
+ "deleted_at": FieldRule(server_only=True),
44
+ "success": FieldRule(server_only=True),
45
+ "message": FieldRule(server_only=True),
46
+ # Fields handled explicitly by CLI/SDK helpers for language model selection
47
+ "language_model_id": FieldRule(cli_managed_create=True, cli_managed_update=True),
48
+ "provider": FieldRule(cli_managed_create=True, cli_managed_update=True),
49
+ "model_name": FieldRule(cli_managed_create=True, cli_managed_update=True),
50
+ "model": FieldRule(cli_managed_create=True, cli_managed_update=True),
51
+ # Fields collected via CLI flags / explicit logic
52
+ "name": FieldRule(cli_managed_create=True, cli_managed_update=True),
53
+ "instruction": FieldRule(cli_managed_create=True, cli_managed_update=True),
54
+ "tools": FieldRule(cli_managed_create=True, cli_managed_update=True),
55
+ "agents": FieldRule(cli_managed_create=True, cli_managed_update=True),
56
+ "mcps": FieldRule(cli_managed_create=True, cli_managed_update=True),
57
+ "timeout": FieldRule(cli_managed_create=True, cli_managed_update=True),
58
+ # Fields requiring sanitisation before sending to the API
59
+ "agent_config": FieldRule(requires_sanitization=True),
60
+ }
61
+
62
+
63
+ def get_import_field_plan(
64
+ field_name: str, operation: AgentImportOperation
65
+ ) -> ImportFieldPlan:
66
+ """Return the import handling plan for ``field_name`` under ``operation``.
67
+
68
+ Unknown fields default to being copied as-is so new API fields propagate
69
+ without additional code changes.
70
+ """
71
+ rule = AGENT_FIELD_RULES.get(field_name, _DEFAULT_RULE)
72
+
73
+ if rule.server_only:
74
+ return ImportFieldPlan(copy=False)
75
+
76
+ if operation == "create" and rule.cli_managed_create:
77
+ return ImportFieldPlan(copy=False)
78
+
79
+ if operation == "update" and rule.cli_managed_update:
80
+ return ImportFieldPlan(copy=False)
81
+
82
+ return ImportFieldPlan(copy=True, sanitize=rule.requires_sanitization)
83
+
84
+
85
+ def list_server_only_fields() -> Collection[str]:
86
+ """Expose the set of server-only fields for other tooling."""
87
+ return {name for name, rule in AGENT_FIELD_RULES.items() if rule.server_only}
@@ -11,6 +11,12 @@ class AIPPanel(Panel):
11
11
  """Rich Panel configured without vertical borders by default."""
12
12
 
13
13
  def __init__(self, *args, **kwargs):
14
+ """Initialize AIPPanel with default settings for horizontal borders and padding.
15
+
16
+ Args:
17
+ *args: Positional arguments passed to Panel
18
+ **kwargs: Keyword arguments passed to Panel
19
+ """
14
20
  kwargs.setdefault("box", box.HORIZONTALS)
15
21
  kwargs.setdefault("padding", (0, 1))
16
22
  super().__init__(*args, **kwargs)
@@ -20,6 +26,12 @@ class AIPTable(Table):
20
26
  """Rich Table configured without vertical borders by default."""
21
27
 
22
28
  def __init__(self, *args, **kwargs):
29
+ """Initialize AIPTable with default settings for horizontal borders and no edge padding.
30
+
31
+ Args:
32
+ *args: Positional arguments passed to Table
33
+ **kwargs: Keyword arguments passed to Table
34
+ """
23
35
  kwargs.setdefault("box", box.HORIZONTALS)
24
36
  kwargs.setdefault("show_edge", False)
25
37
  kwargs.setdefault("pad_edge", False)