glaip-sdk 0.0.20__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.
- glaip_sdk/__init__.py +44 -4
- glaip_sdk/_version.py +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1250 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +271 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +734 -143
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +14 -12
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/transcripts_original.py +756 -0
- glaip_sdk/cli/commands/update.py +164 -23
- glaip_sdk/cli/config.py +49 -7
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +45 -32
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +344 -167
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +15 -22
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +5 -10
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +580 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +65 -29
- glaip_sdk/cli/slash/prompt.py +24 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +827 -232
- glaip_sdk/cli/slash/tui/__init__.py +34 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/clipboard.py +147 -0
- glaip_sdk/cli/slash/tui/context.py +59 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/slash/tui/terminal.py +402 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +123 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +79 -329
- glaip_sdk/cli/update_notifier.py +385 -24
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +3 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +370 -100
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +25 -10
- glaip_sdk/client/mcps.py +166 -27
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +583 -79
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +214 -56
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/icons.py +9 -3
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +107 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +445 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +76 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +872 -0
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +468 -0
- glaip_sdk/utils/__init__.py +59 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/bundler.py +403 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- glaip_sdk/utils/import_resolver.py +524 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +38 -23
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +18 -8
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
- glaip_sdk/utils/rendering/renderer/base.py +534 -882
- glaip_sdk/utils/rendering/renderer/config.py +4 -10
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +13 -54
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +29 -26
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +32 -46
- glaip_sdk/utils/sync.py +162 -0
- glaip_sdk/utils/tool_detection.py +301 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- glaip_sdk/utils/validation.py +20 -28
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
- glaip_sdk-0.7.7.dist-info/RECORD +213 -0
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1412
- glaip_sdk/cli/commands/mcps.py +0 -1225
- glaip_sdk/cli/commands/tools.py +0 -597
- glaip_sdk/cli/utils.py +0 -1330
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.0.20.dist-info/RECORD +0 -80
- glaip_sdk-0.0.20.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
|
|
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
|
-
|
|
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)
|
|
@@ -96,11 +100,12 @@ class ToolClient(BaseClient):
|
|
|
96
100
|
"""
|
|
97
101
|
return os.path.splitext(os.path.basename(file_path))[0]
|
|
98
102
|
|
|
99
|
-
def _prepare_upload_data(
|
|
100
|
-
self, name: str, framework: str, description: str | None = None, **kwargs
|
|
101
|
-
) -> dict:
|
|
103
|
+
def _prepare_upload_data(self, name: str, framework: str, description: str | None = None, **kwargs) -> dict:
|
|
102
104
|
"""Prepare upload data dictionary.
|
|
103
105
|
|
|
106
|
+
Uses the same payload building logic as _build_create_payload to ensure
|
|
107
|
+
consistency between upload and metadata-only tool creation.
|
|
108
|
+
|
|
104
109
|
Args:
|
|
105
110
|
name: Tool name
|
|
106
111
|
framework: Tool framework
|
|
@@ -110,27 +115,19 @@ class ToolClient(BaseClient):
|
|
|
110
115
|
Returns:
|
|
111
116
|
dict: Upload data dictionary
|
|
112
117
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"framework": framework,
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if description:
|
|
119
|
-
data["description"] = description
|
|
118
|
+
# Extract tool_type from kwargs if present, defaulting to DEFAULT_TOOL_TYPE
|
|
119
|
+
tool_type = kwargs.pop("tool_type", DEFAULT_TOOL_TYPE)
|
|
120
120
|
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for key, value in kwargs.items():
|
|
130
|
-
if key not in ["tags"]: # tags already handled above
|
|
131
|
-
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
|
+
)
|
|
132
129
|
|
|
133
|
-
return
|
|
130
|
+
return payload
|
|
134
131
|
|
|
135
132
|
def _upload_tool_file(self, file_path: str, upload_data: dict) -> Tool:
|
|
136
133
|
"""Upload tool file to server.
|
|
@@ -154,7 +151,8 @@ class ToolClient(BaseClient):
|
|
|
154
151
|
data=upload_data,
|
|
155
152
|
)
|
|
156
153
|
|
|
157
|
-
|
|
154
|
+
tool_response = ToolResponse(**response)
|
|
155
|
+
return Tool.from_response(tool_response, client=self)
|
|
158
156
|
|
|
159
157
|
def _build_create_payload(
|
|
160
158
|
self,
|
|
@@ -202,9 +200,7 @@ class ToolClient(BaseClient):
|
|
|
202
200
|
|
|
203
201
|
# Add any other kwargs (excluding already handled ones)
|
|
204
202
|
excluded_keys = {"tags", "version"}
|
|
205
|
-
|
|
206
|
-
if key not in excluded_keys:
|
|
207
|
-
payload[key] = value
|
|
203
|
+
add_kwargs_to_payload(payload, kwargs, excluded_keys)
|
|
208
204
|
|
|
209
205
|
return payload
|
|
210
206
|
|
|
@@ -217,29 +213,21 @@ class ToolClient(BaseClient):
|
|
|
217
213
|
elif hasattr(current_tool, "description") and current_tool.description:
|
|
218
214
|
update_data["description"] = current_tool.description
|
|
219
215
|
|
|
220
|
-
def _handle_tags_update(
|
|
221
|
-
self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool
|
|
222
|
-
) -> None:
|
|
216
|
+
def _handle_tags_update(self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool) -> None:
|
|
223
217
|
"""Handle tags field in update payload."""
|
|
224
218
|
if kwargs.get("tags"):
|
|
225
219
|
if isinstance(kwargs["tags"], list):
|
|
226
|
-
update_data["tags"] = ",".join(
|
|
227
|
-
str(tag).strip() for tag in kwargs["tags"]
|
|
228
|
-
)
|
|
220
|
+
update_data["tags"] = ",".join(str(tag).strip() for tag in kwargs["tags"])
|
|
229
221
|
else:
|
|
230
222
|
update_data["tags"] = str(kwargs["tags"])
|
|
231
223
|
elif hasattr(current_tool, "tags") and current_tool.tags:
|
|
232
224
|
# Preserve existing tags if present
|
|
233
225
|
if isinstance(current_tool.tags, list):
|
|
234
|
-
update_data["tags"] = ",".join(
|
|
235
|
-
str(tag).strip() for tag in current_tool.tags
|
|
236
|
-
)
|
|
226
|
+
update_data["tags"] = ",".join(str(tag).strip() for tag in current_tool.tags)
|
|
237
227
|
else:
|
|
238
228
|
update_data["tags"] = str(current_tool.tags)
|
|
239
229
|
|
|
240
|
-
def _handle_additional_kwargs(
|
|
241
|
-
self, update_data: dict[str, Any], kwargs: dict[str, Any]
|
|
242
|
-
) -> None:
|
|
230
|
+
def _handle_additional_kwargs(self, update_data: dict[str, Any], kwargs: dict[str, Any]) -> None:
|
|
243
231
|
"""Handle additional kwargs in update payload."""
|
|
244
232
|
excluded_keys = {
|
|
245
233
|
"tags",
|
|
@@ -286,16 +274,15 @@ class ToolClient(BaseClient):
|
|
|
286
274
|
or getattr(current_tool, "type", None)
|
|
287
275
|
or DEFAULT_TOOL_TYPE
|
|
288
276
|
)
|
|
277
|
+
# Convert enum to string value for API payload
|
|
278
|
+
if hasattr(current_type, "value"):
|
|
279
|
+
current_type = current_type.value
|
|
289
280
|
|
|
290
281
|
update_data = {
|
|
291
282
|
"name": name if name is not None else current_tool.name,
|
|
292
283
|
"type": current_type,
|
|
293
|
-
"framework": kwargs.get(
|
|
294
|
-
|
|
295
|
-
),
|
|
296
|
-
"version": kwargs.get(
|
|
297
|
-
"version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)
|
|
298
|
-
),
|
|
284
|
+
"framework": kwargs.get("framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)),
|
|
285
|
+
"version": kwargs.get("version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)),
|
|
299
286
|
}
|
|
300
287
|
|
|
301
288
|
# Handle description update
|
|
@@ -355,9 +342,7 @@ class ToolClient(BaseClient):
|
|
|
355
342
|
|
|
356
343
|
try:
|
|
357
344
|
# Prepare upload data
|
|
358
|
-
upload_data = self._prepare_upload_data(
|
|
359
|
-
name=name, framework=framework, description=description, **kwargs
|
|
360
|
-
)
|
|
345
|
+
upload_data = self._prepare_upload_data(name=name, framework=framework, description=description, **kwargs)
|
|
361
346
|
|
|
362
347
|
# Upload file
|
|
363
348
|
return self._upload_tool_file(temp_file_path, upload_data)
|
|
@@ -448,15 +433,187 @@ class ToolClient(BaseClient):
|
|
|
448
433
|
except OSError:
|
|
449
434
|
pass # Ignore cleanup errors
|
|
450
435
|
|
|
451
|
-
def update_tool(self, tool_id: str, **kwargs) -> Tool:
|
|
452
|
-
"""Update an existing tool.
|
|
453
|
-
|
|
454
|
-
|
|
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)
|
|
455
476
|
|
|
456
477
|
def delete_tool(self, tool_id: str) -> None:
|
|
457
478
|
"""Delete a tool."""
|
|
458
479
|
self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
|
|
459
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
|
+
|
|
460
617
|
def get_tool_script(self, tool_id: str) -> str:
|
|
461
618
|
"""Get the tool script content.
|
|
462
619
|
|
|
@@ -525,8 +682,9 @@ class ToolClient(BaseClient):
|
|
|
525
682
|
data=update_payload,
|
|
526
683
|
)
|
|
527
684
|
|
|
528
|
-
|
|
685
|
+
tool_response = ToolResponse(**response)
|
|
686
|
+
return Tool.from_response(tool_response, client=self)
|
|
529
687
|
|
|
530
688
|
except Exception as e:
|
|
531
|
-
logger.error(
|
|
689
|
+
logger.error("Failed to update tool %s via file: %s", tool_id, e)
|
|
532
690
|
raise
|
glaip_sdk/client/validators.py
CHANGED
|
@@ -39,9 +39,7 @@ class ResourceValidator:
|
|
|
39
39
|
if len(found_tools) == 1:
|
|
40
40
|
return str(found_tools[0].id)
|
|
41
41
|
elif len(found_tools) > 1:
|
|
42
|
-
raise AmbiguousResourceError(
|
|
43
|
-
f"Multiple tools found with name '{tool_name}': {[t.id for t in found_tools]}"
|
|
44
|
-
)
|
|
42
|
+
raise AmbiguousResourceError(f"Multiple tools found with name '{tool_name}': {[t.id for t in found_tools]}")
|
|
45
43
|
else:
|
|
46
44
|
raise NotFoundError(f"Tool not found: {tool_name}")
|
|
47
45
|
|
|
@@ -51,9 +49,7 @@ class ResourceValidator:
|
|
|
51
49
|
if len(found_tools) == 1:
|
|
52
50
|
return str(found_tools[0].id)
|
|
53
51
|
elif len(found_tools) > 1:
|
|
54
|
-
raise AmbiguousResourceError(
|
|
55
|
-
f"Multiple tools found with name '{tool.name}': {[t.id for t in found_tools]}"
|
|
56
|
-
)
|
|
52
|
+
raise AmbiguousResourceError(f"Multiple tools found with name '{tool.name}': {[t.id for t in found_tools]}")
|
|
57
53
|
else:
|
|
58
54
|
raise NotFoundError(f"Tool not found: {tool.name}")
|
|
59
55
|
|
|
@@ -73,9 +69,7 @@ class ResourceValidator:
|
|
|
73
69
|
elif hasattr(tool, "name") and tool.name is not None:
|
|
74
70
|
return self._resolve_tool_by_name_attribute(tool, client)
|
|
75
71
|
else:
|
|
76
|
-
raise ValidationError(
|
|
77
|
-
f"Invalid tool reference: {tool} - must have 'id' or 'name' attribute"
|
|
78
|
-
)
|
|
72
|
+
raise ValidationError(f"Invalid tool reference: {tool} - must have 'id' or 'name' attribute")
|
|
79
73
|
|
|
80
74
|
def _process_single_tool(self, tool: str | Tool, client: Any) -> str:
|
|
81
75
|
"""Process a single tool reference and return its ID."""
|
|
@@ -99,22 +93,14 @@ class ResourceValidator:
|
|
|
99
93
|
try:
|
|
100
94
|
tool_id = cls()._process_single_tool(tool, client)
|
|
101
95
|
tool_ids.append(tool_id)
|
|
102
|
-
except (AmbiguousResourceError, NotFoundError) as
|
|
96
|
+
except (AmbiguousResourceError, NotFoundError) as err:
|
|
103
97
|
# Determine the tool name for the error message
|
|
104
|
-
tool_name = (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
raise ValidationError(
|
|
108
|
-
f"Failed to resolve tool name '{tool_name}' to ID: {e}"
|
|
109
|
-
)
|
|
110
|
-
except Exception as e:
|
|
98
|
+
tool_name = tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
|
|
99
|
+
raise ValidationError(f"Failed to resolve tool name '{tool_name}' to ID: {err}") from err
|
|
100
|
+
except Exception as err:
|
|
111
101
|
# For other exceptions, wrap them appropriately
|
|
112
|
-
tool_name = (
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
raise ValidationError(
|
|
116
|
-
f"Failed to resolve tool name '{tool_name}' to ID: {e}"
|
|
117
|
-
)
|
|
102
|
+
tool_name = tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
|
|
103
|
+
raise ValidationError(f"Failed to resolve tool name '{tool_name}' to ID: {err}") from err
|
|
118
104
|
|
|
119
105
|
return tool_ids
|
|
120
106
|
|
|
@@ -158,9 +144,7 @@ class ResourceValidator:
|
|
|
158
144
|
elif hasattr(agent, "name") and agent.name is not None:
|
|
159
145
|
return self._resolve_agent_by_name_attribute(agent, client)
|
|
160
146
|
else:
|
|
161
|
-
raise ValidationError(
|
|
162
|
-
f"Invalid agent reference: {agent} - must have 'id' or 'name' attribute"
|
|
163
|
-
)
|
|
147
|
+
raise ValidationError(f"Invalid agent reference: {agent} - must have 'id' or 'name' attribute")
|
|
164
148
|
|
|
165
149
|
def _process_single_agent(self, agent: str | Any, client: Any) -> str:
|
|
166
150
|
"""Process a single agent reference and return its ID."""
|
|
@@ -184,26 +168,14 @@ class ResourceValidator:
|
|
|
184
168
|
try:
|
|
185
169
|
agent_id = cls()._process_single_agent(agent, client)
|
|
186
170
|
agent_ids.append(agent_id)
|
|
187
|
-
except (AmbiguousResourceError, NotFoundError) as
|
|
171
|
+
except (AmbiguousResourceError, NotFoundError) as err:
|
|
188
172
|
# Determine the agent name for the error message
|
|
189
|
-
agent_name = (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
else getattr(agent, "name", str(agent))
|
|
193
|
-
)
|
|
194
|
-
raise ValidationError(
|
|
195
|
-
f"Failed to resolve agent name '{agent_name}' to ID: {e}"
|
|
196
|
-
)
|
|
197
|
-
except Exception as e:
|
|
173
|
+
agent_name = agent if isinstance(agent, str) else getattr(agent, "name", str(agent))
|
|
174
|
+
raise ValidationError(f"Failed to resolve agent name '{agent_name}' to ID: {err}") from err
|
|
175
|
+
except Exception as err:
|
|
198
176
|
# For other exceptions, wrap them appropriately
|
|
199
|
-
agent_name = (
|
|
200
|
-
|
|
201
|
-
if isinstance(agent, str)
|
|
202
|
-
else getattr(agent, "name", str(agent))
|
|
203
|
-
)
|
|
204
|
-
raise ValidationError(
|
|
205
|
-
f"Failed to resolve agent name '{agent_name}' to ID: {e}"
|
|
206
|
-
)
|
|
177
|
+
agent_name = agent if isinstance(agent, str) else getattr(agent, "name", str(agent))
|
|
178
|
+
raise ValidationError(f"Failed to resolve agent name '{agent_name}' to ID: {err}") from err
|
|
207
179
|
|
|
208
180
|
return agent_ids
|
|
209
181
|
|
|
@@ -213,8 +185,8 @@ class ResourceValidator:
|
|
|
213
185
|
for tool_id in tool_ids:
|
|
214
186
|
try:
|
|
215
187
|
client.get_tool_by_id(tool_id)
|
|
216
|
-
except NotFoundError:
|
|
217
|
-
raise ValidationError(f"Tool not found: {tool_id}")
|
|
188
|
+
except NotFoundError as err:
|
|
189
|
+
raise ValidationError(f"Tool not found: {tool_id}") from err
|
|
218
190
|
|
|
219
191
|
@classmethod
|
|
220
192
|
def validate_agents_exist(cls, agent_ids: list[str], client: Any) -> None:
|
|
@@ -222,5 +194,5 @@ class ResourceValidator:
|
|
|
222
194
|
for agent_id in agent_ids:
|
|
223
195
|
try:
|
|
224
196
|
client.get_agent_by_id(agent_id)
|
|
225
|
-
except NotFoundError:
|
|
226
|
-
raise ValidationError(f"Agent not found: {agent_id}")
|
|
197
|
+
except NotFoundError as err:
|
|
198
|
+
raise ValidationError(f"Agent not found: {agent_id}") from err
|
glaip_sdk/config/constants.py
CHANGED
|
@@ -39,3 +39,14 @@ DEFAULT_MCP_TRANSPORT = "stdio"
|
|
|
39
39
|
|
|
40
40
|
# Default error messages
|
|
41
41
|
DEFAULT_ERROR_MESSAGE = "Unknown error"
|
|
42
|
+
|
|
43
|
+
# Agent configuration fields used for CLI args and payload building
|
|
44
|
+
AGENT_CONFIG_FIELDS = (
|
|
45
|
+
"name",
|
|
46
|
+
"instruction",
|
|
47
|
+
"model",
|
|
48
|
+
"tools",
|
|
49
|
+
"agents",
|
|
50
|
+
"mcps",
|
|
51
|
+
"timeout",
|
|
52
|
+
)
|
glaip_sdk/exceptions.py
CHANGED
|
@@ -107,9 +107,7 @@ class AgentTimeoutError(TimeoutError):
|
|
|
107
107
|
agent_name: Optional name of the agent that timed out
|
|
108
108
|
"""
|
|
109
109
|
agent_info = f" for agent '{agent_name}'" if agent_name else ""
|
|
110
|
-
message =
|
|
111
|
-
f"Agent execution timed out after {timeout_seconds} seconds{agent_info}"
|
|
112
|
-
)
|
|
110
|
+
message = f"Agent execution timed out after {timeout_seconds} seconds{agent_info}"
|
|
113
111
|
super().__init__(message)
|
|
114
112
|
self.timeout_seconds = timeout_seconds
|
|
115
113
|
self.agent_name = agent_name
|
|
@@ -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
|
+
...
|