glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.7__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 (116) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +156 -32
  3. glaip_sdk/cli/auth.py +14 -8
  4. glaip_sdk/cli/commands/accounts.py +1 -1
  5. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  6. glaip_sdk/cli/commands/agents/_common.py +561 -0
  7. glaip_sdk/cli/commands/agents/create.py +151 -0
  8. glaip_sdk/cli/commands/agents/delete.py +64 -0
  9. glaip_sdk/cli/commands/agents/get.py +89 -0
  10. glaip_sdk/cli/commands/agents/list.py +129 -0
  11. glaip_sdk/cli/commands/agents/run.py +264 -0
  12. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  13. glaip_sdk/cli/commands/agents/update.py +112 -0
  14. glaip_sdk/cli/commands/common_config.py +15 -12
  15. glaip_sdk/cli/commands/configure.py +2 -3
  16. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  17. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  18. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  19. glaip_sdk/cli/commands/mcps/create.py +152 -0
  20. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  21. glaip_sdk/cli/commands/mcps/get.py +212 -0
  22. glaip_sdk/cli/commands/mcps/list.py +69 -0
  23. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  24. glaip_sdk/cli/commands/mcps/update.py +190 -0
  25. glaip_sdk/cli/commands/models.py +2 -4
  26. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  27. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  28. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  29. glaip_sdk/cli/commands/tools/_common.py +80 -0
  30. glaip_sdk/cli/commands/tools/create.py +228 -0
  31. glaip_sdk/cli/commands/tools/delete.py +61 -0
  32. glaip_sdk/cli/commands/tools/get.py +103 -0
  33. glaip_sdk/cli/commands/tools/list.py +69 -0
  34. glaip_sdk/cli/commands/tools/script.py +49 -0
  35. glaip_sdk/cli/commands/tools/update.py +102 -0
  36. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  37. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  38. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  39. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  40. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  41. glaip_sdk/cli/commands/update.py +163 -17
  42. glaip_sdk/cli/core/output.py +12 -7
  43. glaip_sdk/cli/entrypoint.py +20 -0
  44. glaip_sdk/cli/main.py +127 -39
  45. glaip_sdk/cli/pager.py +3 -3
  46. glaip_sdk/cli/resolution.py +2 -1
  47. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  48. glaip_sdk/cli/slash/agent_session.py +5 -2
  49. glaip_sdk/cli/slash/prompt.py +11 -0
  50. glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
  51. glaip_sdk/cli/slash/session.py +58 -13
  52. glaip_sdk/cli/slash/tui/__init__.py +26 -1
  53. glaip_sdk/cli/slash/tui/accounts.tcss +7 -5
  54. glaip_sdk/cli/slash/tui/accounts_app.py +70 -9
  55. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  56. glaip_sdk/cli/slash/tui/context.py +59 -0
  57. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  58. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  59. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  60. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  61. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  62. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  63. glaip_sdk/cli/slash/tui/toast.py +123 -0
  64. glaip_sdk/cli/transcript/history.py +1 -1
  65. glaip_sdk/cli/transcript/viewer.py +5 -3
  66. glaip_sdk/cli/update_notifier.py +215 -7
  67. glaip_sdk/cli/validators.py +1 -1
  68. glaip_sdk/client/__init__.py +2 -1
  69. glaip_sdk/client/_schedule_payloads.py +89 -0
  70. glaip_sdk/client/agents.py +50 -8
  71. glaip_sdk/client/hitl.py +136 -0
  72. glaip_sdk/client/main.py +7 -1
  73. glaip_sdk/client/mcps.py +44 -13
  74. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  75. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
  76. glaip_sdk/client/payloads/agent/responses.py +43 -0
  77. glaip_sdk/client/run_rendering.py +367 -3
  78. glaip_sdk/client/schedules.py +439 -0
  79. glaip_sdk/client/tools.py +57 -26
  80. glaip_sdk/hitl/__init__.py +48 -0
  81. glaip_sdk/hitl/base.py +64 -0
  82. glaip_sdk/hitl/callback.py +43 -0
  83. glaip_sdk/hitl/local.py +121 -0
  84. glaip_sdk/hitl/remote.py +523 -0
  85. glaip_sdk/models/__init__.py +17 -0
  86. glaip_sdk/models/agent_runs.py +2 -1
  87. glaip_sdk/models/schedule.py +224 -0
  88. glaip_sdk/registry/tool.py +273 -59
  89. glaip_sdk/runner/__init__.py +20 -3
  90. glaip_sdk/runner/deps.py +5 -8
  91. glaip_sdk/runner/langgraph.py +317 -42
  92. glaip_sdk/runner/logging_config.py +77 -0
  93. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
  94. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
  95. glaip_sdk/schedules/__init__.py +22 -0
  96. glaip_sdk/schedules/base.py +291 -0
  97. glaip_sdk/tools/base.py +44 -11
  98. glaip_sdk/utils/__init__.py +1 -0
  99. glaip_sdk/utils/bundler.py +138 -2
  100. glaip_sdk/utils/import_resolver.py +43 -11
  101. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  102. glaip_sdk/utils/runtime_config.py +15 -12
  103. glaip_sdk/utils/sync.py +31 -11
  104. glaip_sdk/utils/tool_detection.py +274 -6
  105. glaip_sdk/utils/tool_storage_provider.py +140 -0
  106. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +47 -37
  107. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  108. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  109. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  110. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  111. glaip_sdk/cli/commands/agents.py +0 -1509
  112. glaip_sdk/cli/commands/mcps.py +0 -1356
  113. glaip_sdk/cli/commands/tools.py +0 -576
  114. glaip_sdk/cli/utils.py +0 -263
  115. glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
  116. glaip_sdk-0.6.5b6.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
@@ -0,0 +1,48 @@
1
+ """Human-in-the-Loop (HITL) utilities for glaip-sdk.
2
+
3
+ This package provides utilities for HITL approval workflows in both local
4
+ and remote agent execution modes.
5
+
6
+ For local development, LocalPromptHandler is automatically injected when
7
+ agent_config.hitl_enabled is True. No manual setup required.
8
+
9
+ For remote execution, use RemoteHITLHandler to handle HITL events programmatically.
10
+
11
+ Authors:
12
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
13
+ GLAIP SDK Team
14
+ """
15
+
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ # These don't require aip_agents, so import them directly
19
+ from glaip_sdk.hitl.base import HITLCallback, HITLDecision, HITLRequest, HITLResponse
20
+ from glaip_sdk.hitl.callback import PauseResumeCallback
21
+ from glaip_sdk.hitl.remote import RemoteHITLHandler
22
+
23
+ if TYPE_CHECKING:
24
+ from glaip_sdk.hitl.local import LocalPromptHandler
25
+
26
+ __all__ = [
27
+ "LocalPromptHandler",
28
+ "PauseResumeCallback",
29
+ "HITLCallback",
30
+ "HITLDecision",
31
+ "HITLRequest",
32
+ "HITLResponse",
33
+ "RemoteHITLHandler",
34
+ ]
35
+
36
+
37
+ def __getattr__(name: str) -> Any: # noqa: ANN401
38
+ """Lazy import for LocalPromptHandler.
39
+
40
+ This defers the import of aip_agents until LocalPromptHandler is actually accessed,
41
+ preventing ImportError when aip-agents is not installed but HITL is not being used.
42
+ """
43
+ if name == "LocalPromptHandler":
44
+ from glaip_sdk.hitl.local import LocalPromptHandler # noqa: PLC0415
45
+
46
+ globals()["LocalPromptHandler"] = LocalPromptHandler
47
+ return LocalPromptHandler
48
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
glaip_sdk/hitl/base.py ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ """Base types for HITL approval handling.
3
+
4
+ Authors:
5
+ GLAIP SDK Team
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Any, Protocol, runtime_checkable
11
+
12
+
13
+ class HITLDecision(str, Enum):
14
+ """HITL decision types."""
15
+
16
+ APPROVED = "approved"
17
+ REJECTED = "rejected"
18
+ SKIPPED = "skipped"
19
+
20
+
21
+ @dataclass
22
+ class HITLRequest:
23
+ """HITL approval request from SSE stream."""
24
+
25
+ request_id: str
26
+ tool_name: str
27
+ tool_args: dict[str, Any]
28
+ timeout_at: str # ISO 8601, authoritative deadline
29
+ timeout_seconds: int # Informational, fallback only
30
+
31
+ # Raw metadata for advanced use cases
32
+ hitl_metadata: dict[str, Any]
33
+ tool_metadata: dict[str, Any]
34
+
35
+
36
+ @dataclass
37
+ class HITLResponse:
38
+ """HITL decision response."""
39
+
40
+ decision: HITLDecision
41
+ operator_input: str | None = None
42
+
43
+
44
+ @runtime_checkable
45
+ class HITLCallback(Protocol):
46
+ """Protocol for HITL approval callbacks.
47
+
48
+ Callbacks should complete within the computed callback timeout.
49
+ Callbacks should handle exceptions internally or let them propagate.
50
+ """
51
+
52
+ def __call__(self, request: HITLRequest) -> HITLResponse:
53
+ """Handle HITL approval request.
54
+
55
+ Args:
56
+ request: HITL request with tool info and metadata
57
+
58
+ Returns:
59
+ HITLResponse with decision and optional operator input
60
+
61
+ Raises:
62
+ Any exception will be caught, logged, and treated as REJECTED.
63
+ """
64
+ ...