glaip-sdk 0.1.2__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 (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
glaip_sdk/client/tools.py CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
6
7
  """
7
8
 
8
9
  import logging
@@ -16,11 +17,14 @@ from glaip_sdk.config.constants import (
16
17
  DEFAULT_TOOL_TYPE,
17
18
  DEFAULT_TOOL_VERSION,
18
19
  )
19
- from glaip_sdk.models import Tool
20
+ from glaip_sdk.models import ToolResponse
21
+ from glaip_sdk.tools import Tool
20
22
  from glaip_sdk.utils.client_utils import (
23
+ add_kwargs_to_payload,
21
24
  create_model_instances,
22
25
  find_by_name,
23
26
  )
27
+ from glaip_sdk.utils.resource_refs import is_uuid
24
28
 
25
29
  # API endpoints
26
30
  TOOLS_ENDPOINT = "/tools/"
@@ -58,11 +62,11 @@ class ToolClient(BaseClient):
58
62
  def get_tool_by_id(self, tool_id: str) -> Tool:
59
63
  """Get tool by ID."""
60
64
  data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
61
- return Tool(**data)._set_client(self)
65
+ response = ToolResponse(**data)
66
+ return Tool.from_response(response, client=self)
62
67
 
63
68
  def find_tools(self, name: str | None = None) -> list[Tool]:
64
69
  """Find tools by name."""
65
- # Backend doesn't support name query parameter, so we fetch all and filter client-side
66
70
  data = self._request("GET", TOOLS_ENDPOINT)
67
71
  tools = create_model_instances(data, Tool, self)
68
72
  return find_by_name(tools, name, case_sensitive=False)
@@ -99,6 +103,9 @@ class ToolClient(BaseClient):
99
103
  def _prepare_upload_data(self, name: str, framework: str, description: str | None = None, **kwargs) -> dict:
100
104
  """Prepare upload data dictionary.
101
105
 
106
+ Uses the same payload building logic as _build_create_payload to ensure
107
+ consistency between upload and metadata-only tool creation.
108
+
102
109
  Args:
103
110
  name: Tool name
104
111
  framework: Tool framework
@@ -108,27 +115,19 @@ class ToolClient(BaseClient):
108
115
  Returns:
109
116
  dict: Upload data dictionary
110
117
  """
111
- data = {
112
- "name": name,
113
- "framework": framework,
114
- }
115
-
116
- if description:
117
- data["description"] = description
118
-
119
- # Handle tags if provided in kwargs
120
- if kwargs.get("tags"):
121
- if isinstance(kwargs["tags"], list):
122
- data["tags"] = ",".join(kwargs["tags"])
123
- else:
124
- 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)
125
120
 
126
- # Include any other kwargs in the upload data
127
- for key, value in kwargs.items():
128
- if key not in ["tags"]: # tags already handled above
129
- 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
+ )
130
129
 
131
- return data
130
+ return payload
132
131
 
133
132
  def _upload_tool_file(self, file_path: str, upload_data: dict) -> Tool:
134
133
  """Upload tool file to server.
@@ -152,7 +151,8 @@ class ToolClient(BaseClient):
152
151
  data=upload_data,
153
152
  )
154
153
 
155
- return Tool(**response)._set_client(self)
154
+ tool_response = ToolResponse(**response)
155
+ return Tool.from_response(tool_response, client=self)
156
156
 
157
157
  def _build_create_payload(
158
158
  self,
@@ -200,9 +200,7 @@ class ToolClient(BaseClient):
200
200
 
201
201
  # Add any other kwargs (excluding already handled ones)
202
202
  excluded_keys = {"tags", "version"}
203
- for key, value in kwargs.items():
204
- if key not in excluded_keys:
205
- payload[key] = value
203
+ add_kwargs_to_payload(payload, kwargs, excluded_keys)
206
204
 
207
205
  return payload
208
206
 
@@ -276,6 +274,9 @@ class ToolClient(BaseClient):
276
274
  or getattr(current_tool, "type", None)
277
275
  or DEFAULT_TOOL_TYPE
278
276
  )
277
+ # Convert enum to string value for API payload
278
+ if hasattr(current_type, "value"):
279
+ current_type = current_type.value
279
280
 
280
281
  update_data = {
281
282
  "name": name if name is not None else current_tool.name,
@@ -432,15 +433,187 @@ class ToolClient(BaseClient):
432
433
  except OSError:
433
434
  pass # Ignore cleanup errors
434
435
 
435
- def update_tool(self, tool_id: str, **kwargs) -> Tool:
436
- """Update an existing tool."""
437
- data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
438
- return Tool(**data)._set_client(self)
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)
474
+ response = ToolResponse(**data)
475
+ return Tool.from_response(response, client=self)
439
476
 
440
477
  def delete_tool(self, tool_id: str) -> None:
441
478
  """Delete a tool."""
442
479
  self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
443
480
 
481
+ def upsert_tool(
482
+ self,
483
+ identifier: str | Tool,
484
+ code: str | None = None,
485
+ description: str | None = None,
486
+ framework: str = "langchain",
487
+ **kwargs,
488
+ ) -> Tool:
489
+ """Create or update a tool by instance, ID, or name.
490
+
491
+ Args:
492
+ identifier: Tool instance, ID (UUID string), or name
493
+ code: Python code containing the tool plugin (required for create)
494
+ description: Tool description
495
+ framework: Tool framework (defaults to "langchain")
496
+ **kwargs: Additional parameters (tags, version, etc.)
497
+
498
+ Returns:
499
+ The created or updated tool.
500
+
501
+ Example:
502
+ >>> # By name with code (creates if not exists)
503
+ >>> tool = client.tools.upsert_tool(
504
+ ... "greeting",
505
+ ... code=bundled_source,
506
+ ... description="A greeting tool",
507
+ ... )
508
+ >>> # By instance
509
+ >>> tool = client.tools.upsert_tool(existing_tool, code=new_code)
510
+ >>> # By ID
511
+ >>> tool = client.tools.upsert_tool("uuid-here", code=new_code)
512
+ """
513
+ # Handle Tool instance
514
+ if isinstance(identifier, Tool):
515
+ if identifier.id:
516
+ logger.info("Updating tool by instance: %s", identifier.name)
517
+ return self._do_tool_upsert_update(
518
+ identifier.id,
519
+ identifier.name,
520
+ code,
521
+ description,
522
+ framework,
523
+ **kwargs,
524
+ )
525
+ identifier = identifier.name
526
+
527
+ # Handle string (ID or name)
528
+ if isinstance(identifier, str):
529
+ if is_uuid(identifier):
530
+ logger.info("Updating tool by ID: %s", identifier)
531
+ existing = self.get_tool_by_id(identifier)
532
+ return self._do_tool_upsert_update(identifier, existing.name, code, description, framework, **kwargs)
533
+
534
+ # It's a name - find or create
535
+ return self._upsert_tool_by_name(identifier, code, description, framework, **kwargs)
536
+
537
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
538
+
539
+ def _do_tool_upsert_update(
540
+ self,
541
+ tool_id: str,
542
+ name: str | None,
543
+ code: str | None,
544
+ description: str | None,
545
+ framework: str,
546
+ **kwargs,
547
+ ) -> Tool:
548
+ """Perform the update part of tool upsert."""
549
+ if code:
550
+ # Update via file upload
551
+ with tempfile.NamedTemporaryFile(
552
+ mode="w",
553
+ suffix=".py",
554
+ prefix=f"{name or 'tool'}_",
555
+ delete=False,
556
+ encoding="utf-8",
557
+ ) as temp_file:
558
+ temp_file.write(code)
559
+ temp_file_path = temp_file.name
560
+
561
+ try:
562
+ return self.update_tool_via_file(
563
+ tool_id,
564
+ temp_file_path,
565
+ name=name,
566
+ description=description,
567
+ framework=framework,
568
+ **kwargs,
569
+ )
570
+ finally:
571
+ try:
572
+ os.unlink(temp_file_path)
573
+ except OSError:
574
+ pass
575
+ else:
576
+ # Metadata-only update
577
+ update_kwargs = {"framework": framework, **kwargs}
578
+ if name:
579
+ update_kwargs["name"] = name
580
+ if description:
581
+ update_kwargs["description"] = description
582
+ return self.update_tool(tool_id, **update_kwargs)
583
+
584
+ def _upsert_tool_by_name(
585
+ self,
586
+ name: str,
587
+ code: str | None,
588
+ description: str | None,
589
+ framework: str,
590
+ **kwargs,
591
+ ) -> Tool:
592
+ """Find tool by name and update, or create if not found."""
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]
596
+
597
+ if len(exact_matches) == 1:
598
+ logger.info("Updating existing tool: %s", name)
599
+ return self._do_tool_upsert_update(exact_matches[0].id, name, code, description, framework, **kwargs)
600
+
601
+ if len(exact_matches) > 1:
602
+ raise ValueError(f"Multiple tools found with name '{name}'")
603
+
604
+ # Create new tool - code is required
605
+ if not code:
606
+ raise ValueError(f"Tool '{name}' not found and no code provided for creation")
607
+
608
+ logger.info("Creating new tool: %s", name)
609
+ return self.create_tool_from_code(
610
+ name=name,
611
+ code=code,
612
+ framework=framework,
613
+ description=description,
614
+ **kwargs,
615
+ )
616
+
444
617
  def get_tool_script(self, tool_id: str) -> str:
445
618
  """Get the tool script content.
446
619
 
@@ -509,8 +682,9 @@ class ToolClient(BaseClient):
509
682
  data=update_payload,
510
683
  )
511
684
 
512
- return Tool(**response)._set_client(self)
685
+ tool_response = ToolResponse(**response)
686
+ return Tool.from_response(tool_response, client=self)
513
687
 
514
688
  except Exception as e:
515
- logger.error(f"Failed to update tool {tool_id} via file: {e}")
689
+ logger.error("Failed to update tool %s via file: %s", tool_id, e)
516
690
  raise
@@ -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
@@ -39,3 +59,14 @@ DEFAULT_MCP_TRANSPORT = "stdio"
39
59
 
40
60
  # Default error messages
41
61
  DEFAULT_ERROR_MESSAGE = "Unknown error"
62
+
63
+ # Agent configuration fields used for CLI args and payload building
64
+ AGENT_CONFIG_FIELDS = (
65
+ "name",
66
+ "instruction",
67
+ "model",
68
+ "tools",
69
+ "agents",
70
+ "mcps",
71
+ "timeout",
72
+ )
@@ -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)
@@ -0,0 +1,89 @@
1
+ """Guardrail serialization logic.
2
+
3
+ This module provides functionality to serialize GuardrailManager and its engines
4
+ into the JSON format expected by the GL AIP backend. This keeps the serialization
5
+ logic within the SDK rather than polluting the core aip-agents logic.
6
+
7
+ Authors:
8
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ if TYPE_CHECKING:
16
+ from glaip_sdk.guardrails import (
17
+ GuardrailManager,
18
+ NemoGuardrailEngine,
19
+ PhraseMatcherEngine,
20
+ )
21
+
22
+
23
+ def _serialize_phrase_matcher(engine: PhraseMatcherEngine) -> dict[str, Any]:
24
+ """Serialize a PhraseMatcherEngine configuration."""
25
+ config: dict[str, Any] = {}
26
+
27
+ # Extract config from BaseGuardrailEngineConfig
28
+ if hasattr(engine, "config") and engine.config:
29
+ config.update(engine.config.model_dump())
30
+
31
+ # Extract specific fields
32
+ if hasattr(engine, "banned_phrases"):
33
+ config["banned_phrases"] = engine.banned_phrases
34
+
35
+ return config
36
+
37
+
38
+ def _serialize_nemo(engine: NemoGuardrailEngine) -> dict[str, Any]:
39
+ """Serialize a NemoGuardrailEngine configuration."""
40
+ config: dict[str, Any] = {}
41
+
42
+ # Extract config from BaseGuardrailEngineConfig
43
+ if hasattr(engine, "config") and engine.config:
44
+ config.update(engine.config.model_dump())
45
+
46
+ # Extract specific fields
47
+ nemo_fields = [
48
+ "topic_safety_mode",
49
+ "allowed_topics",
50
+ "denied_topics",
51
+ "include_core_restrictions",
52
+ "core_restriction_categories",
53
+ "config_dict",
54
+ "denial_phrases",
55
+ ]
56
+ for field in nemo_fields:
57
+ if hasattr(engine, field):
58
+ val = getattr(engine, field)
59
+ if val is not None:
60
+ config[field] = val
61
+
62
+ return config
63
+
64
+
65
+ def serialize_guardrail_manager(manager: GuardrailManager) -> dict[str, Any]:
66
+ """Serialize a GuardrailManager into the backend JSON format.
67
+
68
+ Args:
69
+ manager: The GuardrailManager instance to serialize.
70
+
71
+ Returns:
72
+ A dictionary matching the agent_config.guardrails schema.
73
+ """
74
+ from glaip_sdk.guardrails import NemoGuardrailEngine, PhraseMatcherEngine # noqa: PLC0415
75
+
76
+ engines_config = []
77
+
78
+ if hasattr(manager, "engines"):
79
+ for engine in manager.engines:
80
+ if isinstance(engine, PhraseMatcherEngine):
81
+ engines_config.append({"type": "phrase_matcher", "config": _serialize_phrase_matcher(engine)})
82
+ elif isinstance(engine, NemoGuardrailEngine):
83
+ engines_config.append({"type": "nemo", "config": _serialize_nemo(engine)})
84
+ else:
85
+ # Fallback for unknown engines
86
+ continue
87
+
88
+ enabled = getattr(manager, "enabled", True)
89
+ return {"enabled": enabled, "engines": engines_config}
@@ -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
+ ...
@@ -0,0 +1,43 @@
1
+ """Pause/resume callback for HITL renderer control.
2
+
3
+ This module provides PauseResumeCallback which allows HITL prompt handlers
4
+ to control the live renderer without directly coupling to the renderer implementation.
5
+
6
+ Author:
7
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
8
+ """
9
+
10
+ from typing import Any
11
+
12
+
13
+ class PauseResumeCallback:
14
+ """Simple callback object for pausing/resuming the live renderer.
15
+
16
+ This allows the LocalPromptHandler to control the renderer without
17
+ directly coupling to the renderer implementation.
18
+ """
19
+
20
+ def __init__(self) -> None:
21
+ """Initialize the callback."""
22
+ self._renderer: Any | None = None
23
+
24
+ def set_renderer(self, renderer: Any) -> None:
25
+ """Set the renderer instance.
26
+
27
+ Args:
28
+ renderer: RichStreamRenderer instance with pause_live() and resume_live() methods.
29
+ """
30
+ self._renderer = renderer
31
+
32
+ def pause(self) -> None:
33
+ """Pause the live renderer before prompting."""
34
+ if self._renderer and hasattr(self._renderer, "_shutdown_live"):
35
+ self._renderer._shutdown_live()
36
+
37
+ def resume(self) -> None:
38
+ """Resume the live renderer after prompting."""
39
+ if self._renderer and hasattr(self._renderer, "_ensure_live"):
40
+ self._renderer._ensure_live()
41
+
42
+
43
+ __all__ = ["PauseResumeCallback"]