glaip-sdk 0.6.5b3__py3-none-any.whl → 0.7.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 (145) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +362 -39
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  8. glaip_sdk/cli/commands/agents/_common.py +562 -0
  9. glaip_sdk/cli/commands/agents/create.py +155 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +375 -25
  55. glaip_sdk/cli/slash/tui/__init__.py +28 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1107 -126
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +92 -0
  60. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  61. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  62. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  63. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  64. glaip_sdk/cli/slash/tui/loading.py +43 -21
  65. glaip_sdk/cli/slash/tui/remote_runs_app.py +152 -20
  66. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  67. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  68. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  69. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  70. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  71. glaip_sdk/cli/slash/tui/toast.py +388 -0
  72. glaip_sdk/cli/transcript/history.py +1 -1
  73. glaip_sdk/cli/transcript/viewer.py +5 -3
  74. glaip_sdk/cli/tui_settings.py +125 -0
  75. glaip_sdk/cli/update_notifier.py +215 -7
  76. glaip_sdk/cli/validators.py +1 -1
  77. glaip_sdk/client/__init__.py +2 -1
  78. glaip_sdk/client/_schedule_payloads.py +89 -0
  79. glaip_sdk/client/agents.py +290 -16
  80. glaip_sdk/client/base.py +25 -0
  81. glaip_sdk/client/hitl.py +136 -0
  82. glaip_sdk/client/main.py +7 -5
  83. glaip_sdk/client/mcps.py +44 -13
  84. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  85. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  86. glaip_sdk/client/payloads/agent/responses.py +43 -0
  87. glaip_sdk/client/run_rendering.py +414 -3
  88. glaip_sdk/client/schedules.py +439 -0
  89. glaip_sdk/client/tools.py +57 -26
  90. glaip_sdk/config/constants.py +22 -2
  91. glaip_sdk/guardrails/__init__.py +80 -0
  92. glaip_sdk/guardrails/serializer.py +89 -0
  93. glaip_sdk/hitl/__init__.py +48 -0
  94. glaip_sdk/hitl/base.py +64 -0
  95. glaip_sdk/hitl/callback.py +43 -0
  96. glaip_sdk/hitl/local.py +121 -0
  97. glaip_sdk/hitl/remote.py +523 -0
  98. glaip_sdk/models/__init__.py +47 -1
  99. glaip_sdk/models/_provider_mappings.py +101 -0
  100. glaip_sdk/models/_validation.py +97 -0
  101. glaip_sdk/models/agent.py +2 -1
  102. glaip_sdk/models/agent_runs.py +2 -1
  103. glaip_sdk/models/constants.py +141 -0
  104. glaip_sdk/models/model.py +170 -0
  105. glaip_sdk/models/schedule.py +224 -0
  106. glaip_sdk/payload_schemas/agent.py +1 -0
  107. glaip_sdk/payload_schemas/guardrails.py +34 -0
  108. glaip_sdk/registry/tool.py +273 -66
  109. glaip_sdk/runner/__init__.py +76 -0
  110. glaip_sdk/runner/base.py +84 -0
  111. glaip_sdk/runner/deps.py +115 -0
  112. glaip_sdk/runner/langgraph.py +1055 -0
  113. glaip_sdk/runner/logging_config.py +77 -0
  114. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  115. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  116. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  117. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  118. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  119. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  120. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  121. glaip_sdk/schedules/__init__.py +22 -0
  122. glaip_sdk/schedules/base.py +291 -0
  123. glaip_sdk/tools/base.py +67 -14
  124. glaip_sdk/utils/__init__.py +1 -0
  125. glaip_sdk/utils/a2a/__init__.py +34 -0
  126. glaip_sdk/utils/a2a/event_processor.py +188 -0
  127. glaip_sdk/utils/agent_config.py +8 -2
  128. glaip_sdk/utils/bundler.py +138 -2
  129. glaip_sdk/utils/import_resolver.py +43 -11
  130. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  131. glaip_sdk/utils/runtime_config.py +120 -0
  132. glaip_sdk/utils/sync.py +31 -11
  133. glaip_sdk/utils/tool_detection.py +301 -0
  134. glaip_sdk/utils/tool_storage_provider.py +140 -0
  135. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +49 -38
  136. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  137. {glaip_sdk-0.6.5b3.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  138. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  139. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  140. glaip_sdk/cli/commands/agents.py +0 -1509
  141. glaip_sdk/cli/commands/mcps.py +0 -1356
  142. glaip_sdk/cli/commands/tools.py +0 -576
  143. glaip_sdk/cli/utils.py +0 -263
  144. glaip_sdk-0.6.5b3.dist-info/RECORD +0 -145
  145. glaip_sdk-0.6.5b3.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,439 @@
1
+ """Schedule client for AIP SDK.
2
+
3
+ Authors:
4
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from glaip_sdk.client._schedule_payloads import ScheduleListParams, normalize_schedule
11
+ from glaip_sdk.client.base import BaseClient
12
+ from glaip_sdk.exceptions import APIError, NotFoundError
13
+ from glaip_sdk.models.agent_runs import RunStatus
14
+ from glaip_sdk.models.schedule import (
15
+ ScheduleConfig,
16
+ ScheduleResponse,
17
+ ScheduleRunResponse,
18
+ ScheduleRunResult,
19
+ )
20
+ from glaip_sdk.schedules import (
21
+ Schedule,
22
+ ScheduleListResult,
23
+ ScheduleRun,
24
+ ScheduleRunListResult,
25
+ )
26
+
27
+ if TYPE_CHECKING:
28
+ from glaip_sdk.models import Agent
29
+
30
+
31
+ class ScheduleClient(BaseClient):
32
+ """Client for managing agent schedules.
33
+
34
+ Provides CRUD operations for scheduled agent executions.
35
+ Schedules allow agents to run automatically at specified times
36
+ using cron-like configurations.
37
+
38
+ Example:
39
+ >>> from glaip_sdk import Client
40
+ >>> from glaip_sdk.models.schedule import ScheduleConfig
41
+ >>>
42
+ >>> client = Client()
43
+ >>> # List all schedules
44
+ >>> result = client.schedules.list()
45
+ >>> for schedule in result:
46
+ ... print(f"{schedule.id}: {schedule.next_run_time}")
47
+ >>>
48
+ >>> # Get a specific schedule
49
+ >>> schedule = client.schedules.get("schedule-id")
50
+ >>>
51
+ >>> # Create a schedule for an agent
52
+ >>> schedule = client.schedules.create(
53
+ ... agent_id="agent-id",
54
+ ... input="Generate daily report",
55
+ ... schedule=ScheduleConfig(minute="0", hour="9", day_of_week="1-5")
56
+ ... )
57
+ >>>
58
+ >>> # Update a schedule
59
+ >>> schedule = client.schedules.update(
60
+ ... schedule_id="schedule-id",
61
+ ... input="Updated report input"
62
+ ... )
63
+ >>>
64
+ >>> # Delete a schedule
65
+ >>> client.schedules.delete("schedule-id")
66
+ """
67
+
68
+ def list(
69
+ self,
70
+ *,
71
+ limit: int | None = None,
72
+ page: int | None = None,
73
+ agent_id: str | None = None,
74
+ ) -> ScheduleListResult:
75
+ """List schedules with optional filtering and pagination.
76
+
77
+ Args:
78
+ limit: Maximum number of schedules to return (1-100)
79
+ page: Page number for pagination
80
+ agent_id: Filter schedules by agent ID
81
+
82
+ Returns:
83
+ ScheduleListResult containing schedules and pagination metadata
84
+ """
85
+ params = ScheduleListParams(limit=limit, page=page, agent_id=agent_id)
86
+
87
+ response = self._request_with_envelope(
88
+ "GET",
89
+ "/agents/schedules",
90
+ params=params.to_query_params(),
91
+ )
92
+
93
+ # Parse schedules from response data
94
+ schedules = [
95
+ Schedule.from_response(ScheduleResponse.model_validate(item), client=self)
96
+ for item in (response.get("data") or [])
97
+ ]
98
+
99
+ return ScheduleListResult(
100
+ items=schedules,
101
+ total=response.get("total"),
102
+ page=response.get("page"),
103
+ limit=response.get("limit"),
104
+ has_next=response.get("has_next"),
105
+ has_prev=response.get("has_prev"),
106
+ )
107
+
108
+ def get(self, schedule_id: str) -> Schedule:
109
+ """Get a schedule by ID.
110
+
111
+ Args:
112
+ schedule_id: The schedule ID to retrieve
113
+
114
+ Returns:
115
+ Schedule instance
116
+
117
+ Raises:
118
+ NotFoundError: If schedule is not found
119
+ AuthenticationError: If API key is invalid
120
+ APIError: If the API request fails
121
+ """
122
+ data = self._request("GET", f"/agents/schedules/{schedule_id}")
123
+
124
+ if data is None:
125
+ raise NotFoundError(f"Schedule not found: {schedule_id}")
126
+ return Schedule.from_response(ScheduleResponse.model_validate(data), client=self)
127
+
128
+ def create(
129
+ self,
130
+ *,
131
+ agent_id: str,
132
+ input: str,
133
+ schedule: ScheduleConfig | dict[str, str] | str,
134
+ ) -> Schedule:
135
+ """Create a new schedule for an agent.
136
+
137
+ Args:
138
+ agent_id: The agent ID to schedule
139
+ input: Input text for scheduled execution
140
+ schedule: Schedule configuration (ScheduleConfig, dict, or cron string)
141
+
142
+ Returns:
143
+ Created Schedule instance
144
+
145
+ Raises:
146
+ ValueError: If schedule format is invalid
147
+ NotFoundError: If agent is not found
148
+ ValidationError: If schedule configuration is invalid
149
+ AuthenticationError: If API key is invalid
150
+ APIError: If the API request fails
151
+ """
152
+ schedule_dict = normalize_schedule(schedule)
153
+ if schedule_dict is None:
154
+ raise ValueError("schedule is required")
155
+
156
+ payload = {
157
+ "input": input,
158
+ "schedule": schedule_dict,
159
+ }
160
+
161
+ response = self._request("POST", f"/agents/{agent_id}/schedule", json=payload)
162
+
163
+ # Response contains schedule_id, fetch the full schedule
164
+ schedule_id = response.get("schedule_id")
165
+ if not schedule_id:
166
+ raise APIError("Missing schedule_id in create response")
167
+
168
+ return self.get(schedule_id)
169
+
170
+ def update(
171
+ self,
172
+ schedule_id: str,
173
+ *,
174
+ input: str | None = None,
175
+ schedule: ScheduleConfig | dict[str, str] | str | None = None,
176
+ ) -> Schedule:
177
+ """Update an existing schedule.
178
+
179
+ Args:
180
+ schedule_id: The schedule ID to update
181
+ input: New input text for scheduled execution
182
+ schedule: New schedule configuration (ScheduleConfig, dict, or cron string)
183
+
184
+ Returns:
185
+ Updated Schedule instance
186
+
187
+ Raises:
188
+ NotFoundError: If schedule is not found
189
+ ValueError: If schedule config is required but not provided
190
+ ValidationError: If schedule configuration is invalid
191
+ AuthenticationError: If API key is invalid
192
+ APIError: If the API request fails
193
+
194
+ Note:
195
+ Updates use explicit replacement (not merge). The SDK normalizes partial
196
+ schedule dicts by filling missing cron fields with "*". This is intentional
197
+ for predictability - what you provide is what you get (plus wildcard defaults).
198
+ If the current schedule metadata is missing and no schedule parameter is
199
+ provided, a ValueError is raised.
200
+ """
201
+ # Get current schedule to merge with updates
202
+ current = self.get(schedule_id)
203
+
204
+ # Handle input - ensure we have valid input data
205
+ if current.input is None and input is None:
206
+ raise ValueError(
207
+ f"Schedule {schedule_id} has missing input metadata and no input parameter provided. "
208
+ "Please provide an input value to update this schedule."
209
+ )
210
+
211
+ # Handle schedule config - ensure we have complete schedule data
212
+ if current.schedule_config is None and schedule is None:
213
+ raise ValueError(
214
+ f"Schedule {schedule_id} has missing metadata and no schedule parameter provided. "
215
+ "Please provide a full schedule configuration to update this schedule."
216
+ )
217
+
218
+ current_input = current.input or ""
219
+ current_schedule = current.schedule_config.model_dump() if current.schedule_config else {}
220
+
221
+ payload: dict[str, Any] = {
222
+ "input": input if input is not None else current_input,
223
+ "schedule": normalize_schedule(schedule) or current_schedule,
224
+ }
225
+
226
+ data = self._request("PUT", f"/agents/schedules/{schedule_id}", json=payload)
227
+ return Schedule.from_response(ScheduleResponse.model_validate(data), client=self)
228
+
229
+ def delete(self, schedule_id: str) -> None:
230
+ """Delete a schedule.
231
+
232
+ Args:
233
+ schedule_id: The schedule ID to delete
234
+
235
+ Raises:
236
+ NotFoundError: If schedule is not found
237
+ AuthenticationError: If API key is invalid
238
+ APIError: If the API request fails
239
+ """
240
+ self._request("DELETE", f"/agents/schedules/{schedule_id}")
241
+
242
+ def list_runs(
243
+ self,
244
+ agent_id: str,
245
+ *,
246
+ schedule_id: str | None = None,
247
+ status: RunStatus | None = None,
248
+ limit: int | None = None,
249
+ page: int | None = None,
250
+ ) -> ScheduleRunListResult:
251
+ """List runs for an agent, optionally filtered by schedule ID.
252
+
253
+ Args:
254
+ agent_id: The agent ID to list runs for
255
+ schedule_id: Optional schedule ID to filter by
256
+ status: Optional status filter
257
+ limit: Maximum number of runs to return (1-100)
258
+ page: Page number for pagination
259
+
260
+ Returns:
261
+ ScheduleRunListResult containing runs and pagination metadata
262
+ """
263
+ params: dict[str, Any] = {"run_type": "schedule"}
264
+ if schedule_id is not None:
265
+ params["schedule_id"] = schedule_id
266
+ if status is not None:
267
+ params["status"] = status
268
+ if limit is not None:
269
+ params["limit"] = limit
270
+ if page is not None:
271
+ params["page"] = page
272
+
273
+ response = self._request_with_envelope(
274
+ "GET",
275
+ f"/agents/{agent_id}/runs",
276
+ params=params,
277
+ )
278
+
279
+ # Parse runs from response data
280
+ runs = [
281
+ ScheduleRun.from_response(ScheduleRunResponse.model_validate(item), client=self)
282
+ for item in (response.get("data") or [])
283
+ ]
284
+
285
+ return ScheduleRunListResult(
286
+ items=runs,
287
+ total=response.get("total"),
288
+ page=response.get("page"),
289
+ limit=response.get("limit"),
290
+ has_next=response.get("has_next"),
291
+ has_prev=response.get("has_prev"),
292
+ )
293
+
294
+ def get_run_result(self, agent_id: str, run_id: str) -> ScheduleRunResult:
295
+ """Get the full output payload for an agent run.
296
+
297
+ Args:
298
+ agent_id: The agent ID the run belongs to
299
+ run_id: The run ID to retrieve
300
+
301
+ Returns:
302
+ ScheduleRunResult containing run details and optional output
303
+ """
304
+ data = self._request("GET", f"/agents/{agent_id}/runs/{run_id}")
305
+ return ScheduleRunResult.model_validate(data)
306
+
307
+
308
+ class AgentScheduleManager:
309
+ """Facade for agent-scoped schedule operations.
310
+
311
+ Provides a convenient interface for managing schedules through
312
+ an Agent instance, automatically scoping operations to that agent.
313
+
314
+ Example:
315
+ >>> agent = client.get_agent_by_id("agent-id")
316
+ >>> # List schedules for this agent
317
+ >>> schedules = agent.schedule.list()
318
+ >>> # Create a schedule for this agent
319
+ >>> schedule = agent.schedule.create(
320
+ ... input="Daily task",
321
+ ... schedule="0 9 * * 1-5"
322
+ ... )
323
+ """
324
+
325
+ def __init__(self, agent: "Agent", client: ScheduleClient) -> None:
326
+ """Initialize the schedule manager.
327
+
328
+ Args:
329
+ agent: The agent to manage schedules for
330
+ client: The ScheduleClient for API operations
331
+ """
332
+ self._agent = agent
333
+ self._client = client
334
+
335
+ def list(
336
+ self,
337
+ *,
338
+ limit: int | None = None,
339
+ page: int | None = None,
340
+ ) -> ScheduleListResult:
341
+ """List schedules for this agent.
342
+
343
+ Args:
344
+ limit: Maximum number of schedules to return (1-100)
345
+ page: Page number for pagination
346
+
347
+ Returns:
348
+ ScheduleListResult containing schedules for this agent
349
+ """
350
+ return self._client.list(limit=limit, page=page, agent_id=self._agent.id)
351
+
352
+ def create(
353
+ self,
354
+ *,
355
+ input: str,
356
+ schedule: ScheduleConfig | dict[str, str] | str,
357
+ ) -> Schedule:
358
+ """Create a schedule for this agent.
359
+
360
+ Args:
361
+ input: Input text for scheduled execution
362
+ schedule: Schedule configuration (ScheduleConfig, dict, or cron string)
363
+
364
+ Returns:
365
+ Created Schedule instance
366
+ """
367
+ return self._client.create(
368
+ agent_id=self._agent.id,
369
+ input=input,
370
+ schedule=schedule,
371
+ )
372
+
373
+ def get(self, schedule_id: str) -> Schedule:
374
+ """Get a schedule by ID.
375
+
376
+ Args:
377
+ schedule_id: The schedule ID to retrieve
378
+
379
+ Returns:
380
+ Schedule instance
381
+
382
+ Raises:
383
+ NotFoundError: If schedule is not found
384
+ """
385
+ return self._client.get(schedule_id)
386
+
387
+ def update(
388
+ self,
389
+ schedule_id: str,
390
+ *,
391
+ input: str | None = None,
392
+ schedule: ScheduleConfig | dict[str, str] | str | None = None,
393
+ ) -> Schedule:
394
+ """Update a schedule.
395
+
396
+ Args:
397
+ schedule_id: The schedule ID to update
398
+ input: New input text for scheduled execution
399
+ schedule: New schedule configuration
400
+
401
+ Returns:
402
+ Updated Schedule instance
403
+ """
404
+ return self._client.update(schedule_id, input=input, schedule=schedule)
405
+
406
+ def delete(self, schedule_id: str) -> None:
407
+ """Delete a schedule.
408
+
409
+ Args:
410
+ schedule_id: The schedule ID to delete
411
+ """
412
+ self._client.delete(schedule_id)
413
+
414
+ def list_runs(
415
+ self,
416
+ schedule_id: str | None = None,
417
+ *,
418
+ status: RunStatus | None = None,
419
+ limit: int | None = None,
420
+ page: int | None = None,
421
+ ) -> ScheduleRunListResult:
422
+ """List runs for this agent.
423
+
424
+ Args:
425
+ schedule_id: Optional schedule ID to filter by
426
+ status: Optional status filter
427
+ limit: Maximum number of runs to return (1-100)
428
+ page: Page number for pagination
429
+
430
+ Returns:
431
+ ScheduleRunListResult containing runs for this agent
432
+ """
433
+ return self._client.list_runs(
434
+ self._agent.id,
435
+ schedule_id=schedule_id,
436
+ status=status,
437
+ limit=limit,
438
+ page=page,
439
+ )
glaip_sdk/client/tools.py CHANGED
@@ -103,6 +103,9 @@ class ToolClient(BaseClient):
103
103
  def _prepare_upload_data(self, name: str, framework: str, description: str | None = None, **kwargs) -> dict:
104
104
  """Prepare upload data dictionary.
105
105
 
106
+ Uses the same payload building logic as _build_create_payload to ensure
107
+ consistency between upload and metadata-only tool creation.
108
+
106
109
  Args:
107
110
  name: Tool name
108
111
  framework: Tool framework
@@ -112,28 +115,19 @@ class ToolClient(BaseClient):
112
115
  Returns:
113
116
  dict: Upload data dictionary
114
117
  """
115
- data = {
116
- "name": name,
117
- "framework": framework,
118
- "type": kwargs.pop("tool_type", DEFAULT_TOOL_TYPE), # Default to custom
119
- }
120
-
121
- if description:
122
- data["description"] = description
123
-
124
- # Handle tags if provided in kwargs
125
- if kwargs.get("tags"):
126
- if isinstance(kwargs["tags"], list):
127
- data["tags"] = ",".join(kwargs["tags"])
128
- else:
129
- data["tags"] = kwargs["tags"]
118
+ # Extract tool_type from kwargs if present, defaulting to DEFAULT_TOOL_TYPE
119
+ tool_type = kwargs.pop("tool_type", DEFAULT_TOOL_TYPE)
130
120
 
131
- # Include any other kwargs in the upload data
132
- for key, value in kwargs.items():
133
- if key not in ["tags"]: # tags already handled above
134
- data[key] = value
121
+ # Use _build_create_payload to build the payload consistently
122
+ payload = self._build_create_payload(
123
+ name=name,
124
+ description=description,
125
+ framework=framework,
126
+ tool_type=tool_type,
127
+ **kwargs,
128
+ )
135
129
 
136
- return data
130
+ return payload
137
131
 
138
132
  def _upload_tool_file(self, file_path: str, upload_data: dict) -> Tool:
139
133
  """Upload tool file to server.
@@ -439,9 +433,44 @@ class ToolClient(BaseClient):
439
433
  except OSError:
440
434
  pass # Ignore cleanup errors
441
435
 
442
- def update_tool(self, tool_id: str, **kwargs) -> Tool:
443
- """Update an existing tool."""
444
- data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
436
+ def update_tool(self, tool_id: str | Tool, **kwargs) -> Tool:
437
+ """Update an existing tool.
438
+
439
+ Notes:
440
+ - Payload construction is centralized via ``_build_update_payload`` to keep metadata
441
+ update and upload update flows consistent.
442
+ - Accepts either a tool ID or a ``Tool`` instance (avoids an extra fetch when callers
443
+ already have the current tool).
444
+ """
445
+ # Backward-compatible: allow passing a Tool instance to avoid an extra fetch.
446
+ if isinstance(tool_id, Tool):
447
+ current_tool = tool_id
448
+ if not current_tool.id:
449
+ raise ValueError("Tool instance has no id; cannot update.")
450
+ tool_id_value = str(current_tool.id)
451
+ else:
452
+ current_tool = None
453
+ tool_id_value = tool_id
454
+
455
+ if not kwargs:
456
+ data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id_value}", json={})
457
+ response = ToolResponse(**data)
458
+ return Tool.from_response(response, client=self)
459
+
460
+ if current_tool is None:
461
+ current_tool = self.get_tool_by_id(tool_id_value)
462
+
463
+ payload_kwargs = kwargs.copy()
464
+ name = payload_kwargs.pop("name", None)
465
+ description = payload_kwargs.pop("description", None)
466
+ update_payload = self._build_update_payload(
467
+ current_tool=current_tool,
468
+ name=name,
469
+ description=description,
470
+ **payload_kwargs,
471
+ )
472
+
473
+ data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id_value}", json=update_payload)
445
474
  response = ToolResponse(**data)
446
475
  return Tool.from_response(response, client=self)
447
476
 
@@ -562,12 +591,14 @@ class ToolClient(BaseClient):
562
591
  ) -> Tool:
563
592
  """Find tool by name and update, or create if not found."""
564
593
  existing = self.find_tools(name)
594
+ name_lower = name.lower()
595
+ exact_matches = [tool for tool in existing if tool.name and tool.name.lower() == name_lower]
565
596
 
566
- if len(existing) == 1:
597
+ if len(exact_matches) == 1:
567
598
  logger.info("Updating existing tool: %s", name)
568
- return self._do_tool_upsert_update(existing[0].id, name, code, description, framework, **kwargs)
599
+ return self._do_tool_upsert_update(exact_matches[0].id, name, code, description, framework, **kwargs)
569
600
 
570
- if len(existing) > 1:
601
+ if len(exact_matches) > 1:
571
602
  raise ValueError(f"Multiple tools found with name '{name}'")
572
603
 
573
604
  # Create new tool - code is required
@@ -4,8 +4,28 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
- # Default language model configuration
8
- DEFAULT_MODEL = "gpt-5-nano"
7
+ # Lazy import cache for DEFAULT_MODEL to avoid circular dependency
8
+ _DEFAULT_MODEL: str | None = None
9
+
10
+
11
+ def __getattr__(name: str) -> str:
12
+ """Lazy import DEFAULT_MODEL from models.constants to avoid circular dependency.
13
+
14
+ Note: Prefer importing DEFAULT_MODEL directly from glaip_sdk.models.constants
15
+ as it is the canonical source. This re-export exists for backward compatibility.
16
+ """
17
+ if name in ("DEFAULT_MODEL", "SDK_DEFAULT_MODEL"):
18
+ global _DEFAULT_MODEL
19
+ if _DEFAULT_MODEL is None:
20
+ from glaip_sdk.models.constants import ( # noqa: PLC0415
21
+ DEFAULT_MODEL as _MODEL,
22
+ )
23
+
24
+ _DEFAULT_MODEL = _MODEL
25
+ return _DEFAULT_MODEL
26
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
27
+
28
+
9
29
  DEFAULT_AGENT_RUN_TIMEOUT = 300
10
30
 
11
31
  # User agent and version
@@ -0,0 +1,80 @@
1
+ """Guardrails package for content filtering and safety checks.
2
+
3
+ This package provides modular guardrail engines and managers for filtering
4
+ harmful content in AI agent interactions. All components support lazy loading
5
+ from aip-agents to maintain Principle VII compliance.
6
+
7
+ Authors:
8
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
9
+ """
10
+
11
+ from enum import StrEnum
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ if TYPE_CHECKING:
15
+ from aip_agents.guardrails.engines.nemo import NemoGuardrailEngine
16
+ from aip_agents.guardrails.engines.phrase_matcher import PhraseMatcherEngine
17
+ from aip_agents.guardrails.manager import GuardrailManager
18
+ from aip_agents.guardrails.schemas import GuardrailMode
19
+
20
+
21
+ class ImportableName(StrEnum):
22
+ """Names of the importable attributes."""
23
+
24
+ GUARDRAIL_MANAGER = "GuardrailManager"
25
+ PHRASE_MATCHER_ENGINE = "PhraseMatcherEngine"
26
+ NEMO_GUARDRAIL_ENGINE = "NemoGuardrailEngine"
27
+ GUARDRAIL_MODE = "GuardrailMode"
28
+
29
+
30
+ # Lazy loading support - components are only imported when actually used
31
+ _LAZY_IMPORTS = {}
32
+
33
+
34
+ def __getattr__(name: str) -> Any:
35
+ """Lazy import to avoid eager loading of optional aip-agents dependency.
36
+
37
+ This function is called by Python when an attribute is not found in the module.
38
+ It performs the import from aip_agents.guardrails at runtime.
39
+
40
+ Args:
41
+ name: The name of the attribute to get.
42
+
43
+ Returns:
44
+ The attribute value from aip_agents.
45
+
46
+ Raises:
47
+ AttributeError: If the attribute doesn't exist.
48
+ ImportError: If aip-agents is not installed but a component is accessed.
49
+ """
50
+ if name in _LAZY_IMPORTS:
51
+ return _LAZY_IMPORTS[name]
52
+
53
+ if name == ImportableName.GUARDRAIL_MANAGER:
54
+ from aip_agents.guardrails.manager import GuardrailManager # noqa: PLC0415
55
+
56
+ _LAZY_IMPORTS[name] = GuardrailManager
57
+ return GuardrailManager
58
+
59
+ if name == ImportableName.PHRASE_MATCHER_ENGINE:
60
+ from aip_agents.guardrails.engines.phrase_matcher import ( # noqa: PLC0415
61
+ PhraseMatcherEngine,
62
+ )
63
+
64
+ _LAZY_IMPORTS[name] = PhraseMatcherEngine
65
+ return PhraseMatcherEngine
66
+
67
+ if name == ImportableName.NEMO_GUARDRAIL_ENGINE:
68
+ from aip_agents.guardrails.engines.nemo import NemoGuardrailEngine # noqa: PLC0415
69
+
70
+ _LAZY_IMPORTS[name] = NemoGuardrailEngine
71
+ return NemoGuardrailEngine
72
+
73
+ if name == ImportableName.GUARDRAIL_MODE:
74
+ from aip_agents.guardrails.schemas import GuardrailMode # noqa: PLC0415
75
+
76
+ _LAZY_IMPORTS[name] = GuardrailMode
77
+ return GuardrailMode
78
+
79
+ msg = f"module {__name__!r} has no attribute {name!r}"
80
+ raise AttributeError(msg)