glaip-sdk 0.6.19__py3-none-any.whl → 0.7.27__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/agents/base.py +283 -30
- glaip_sdk/agents/component.py +233 -0
- glaip_sdk/branding.py +113 -2
- glaip_sdk/cli/account_store.py +15 -0
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -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 +1 -1
- glaip_sdk/cli/commands/configure.py +1 -2
- 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 +2 -4
- 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.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +1 -0
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +112 -35
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +3 -1
- glaip_sdk/cli/slash/agent_session.py +1 -1
- glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +343 -20
- glaip_sdk/cli/slash/tui/__init__.py +29 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
- glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
- glaip_sdk/cli/slash/tui/clipboard.py +316 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
- glaip_sdk/cli/slash/tui/loading.py +43 -21
- glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
- glaip_sdk/cli/slash/tui/terminal.py +407 -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 +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +388 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +1 -1
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +293 -17
- glaip_sdk/client/base.py +25 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -5
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +109 -30
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +52 -23
- glaip_sdk/config/constants.py +22 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +91 -0
- glaip_sdk/hitl/__init__.py +35 -2
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +1 -31
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +47 -1
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +2 -1
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/model.py +170 -0
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/ptc.py +145 -0
- glaip_sdk/registry/tool.py +270 -57
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +4 -1
- glaip_sdk/runner/langgraph.py +251 -27
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
- glaip_sdk/runner/ptc_adapter.py +98 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +67 -14
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +427 -49
- glaip_sdk/utils/runtime_config.py +3 -2
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +22 -8
- glaip_sdk-0.7.27.dist-info/RECORD +227 -0
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +1 -1
- glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.19.dist-info/RECORD +0 -163
- glaip_sdk-0.6.19.dist-info/entry_points.txt +0 -2
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Update MCP command.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from glaip_sdk.cli.context import output_flags
|
|
14
|
+
from glaip_sdk.cli.display import display_update_success, handle_json_output, handle_rich_output
|
|
15
|
+
from glaip_sdk.cli.core.context import get_client
|
|
16
|
+
from glaip_sdk.cli.core.rendering import spinner_context
|
|
17
|
+
|
|
18
|
+
from ._common import (
|
|
19
|
+
_handle_cli_error,
|
|
20
|
+
_handle_update_preview_and_confirmation,
|
|
21
|
+
_load_import_ready_payload,
|
|
22
|
+
_parse_and_validate_config_auth,
|
|
23
|
+
_resolve_mcp,
|
|
24
|
+
_validate_import_payload_fields,
|
|
25
|
+
_validate_update_inputs,
|
|
26
|
+
console,
|
|
27
|
+
mcps_group,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _merge_update_kwargs(
|
|
32
|
+
import_payload: dict[str, Any] | None,
|
|
33
|
+
name: str | None,
|
|
34
|
+
transport: str | None,
|
|
35
|
+
description: str | None,
|
|
36
|
+
config: str | None,
|
|
37
|
+
auth: str | None,
|
|
38
|
+
mcp: Any,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Merge import payload and CLI options into kwargs for SDK builder.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
import_payload: Import payload dictionary or None
|
|
44
|
+
name: MCP name option
|
|
45
|
+
transport: Transport option
|
|
46
|
+
description: Description option
|
|
47
|
+
config: Config option
|
|
48
|
+
auth: Auth option
|
|
49
|
+
mcp: Current MCP object
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dictionary with merged update kwargs
|
|
53
|
+
"""
|
|
54
|
+
update_kwargs: dict[str, Any] = {}
|
|
55
|
+
|
|
56
|
+
# Start with import payload fields
|
|
57
|
+
if import_payload:
|
|
58
|
+
for field in ("name", "transport", "description", "config", "authentication"):
|
|
59
|
+
if field in import_payload:
|
|
60
|
+
update_kwargs[field] = import_payload[field]
|
|
61
|
+
|
|
62
|
+
# Override with CLI options (CLI takes precedence)
|
|
63
|
+
if name is not None:
|
|
64
|
+
update_kwargs["name"] = name
|
|
65
|
+
if transport is not None:
|
|
66
|
+
update_kwargs["transport"] = transport
|
|
67
|
+
if description is not None:
|
|
68
|
+
update_kwargs["description"] = description
|
|
69
|
+
_parse_and_validate_config_auth(update_kwargs, config, auth, transport, import_payload, mcp)
|
|
70
|
+
|
|
71
|
+
return update_kwargs
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcps_group.command()
|
|
75
|
+
@click.argument("mcp_ref")
|
|
76
|
+
@click.option("--name", help="New MCP name")
|
|
77
|
+
@click.option("--transport", type=click.Choice(["http", "sse"]), help="New transport protocol")
|
|
78
|
+
@click.option("--description", help="New description")
|
|
79
|
+
@click.option(
|
|
80
|
+
"--config",
|
|
81
|
+
help="JSON configuration string or @file reference (e.g., @config.json)",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--auth",
|
|
85
|
+
"--authentication",
|
|
86
|
+
"auth",
|
|
87
|
+
help="JSON authentication object or @file reference (e.g., @auth.json)",
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"--import",
|
|
91
|
+
"import_file",
|
|
92
|
+
type=click.Path(exists=True, dir_okay=False, readable=True),
|
|
93
|
+
help="Import MCP configuration from JSON or YAML export",
|
|
94
|
+
)
|
|
95
|
+
@click.option("-y", is_flag=True, help="Skip confirmation prompt when using --import")
|
|
96
|
+
@output_flags()
|
|
97
|
+
@click.pass_context
|
|
98
|
+
def update(
|
|
99
|
+
ctx: Any,
|
|
100
|
+
mcp_ref: str,
|
|
101
|
+
name: str | None,
|
|
102
|
+
transport: str | None,
|
|
103
|
+
description: str | None,
|
|
104
|
+
config: str | None,
|
|
105
|
+
auth: str | None,
|
|
106
|
+
import_file: str | None,
|
|
107
|
+
y: bool,
|
|
108
|
+
) -> None:
|
|
109
|
+
r"""Update an existing MCP with new configuration values.
|
|
110
|
+
|
|
111
|
+
You can update an MCP by providing individual fields via CLI options, or by
|
|
112
|
+
importing from a file and optionally overriding specific fields.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
ctx: Click context containing output format preferences
|
|
116
|
+
mcp_ref: MCP reference (ID or name)
|
|
117
|
+
name: New MCP name (optional)
|
|
118
|
+
transport: New transport protocol (optional)
|
|
119
|
+
description: New description (optional)
|
|
120
|
+
config: New JSON configuration string or @file reference (optional)
|
|
121
|
+
auth: New JSON authentication object or @file reference (optional)
|
|
122
|
+
import_file: Optional path to import configuration from export file.
|
|
123
|
+
CLI options override imported values.
|
|
124
|
+
y: Skip confirmation prompt when using --import
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ClickException: If MCP not found, JSON invalid, or no fields specified
|
|
128
|
+
|
|
129
|
+
Note:
|
|
130
|
+
Must specify either --import OR at least one CLI field.
|
|
131
|
+
CLI options override imported values when both are specified.
|
|
132
|
+
Method selection (PATCH vs PUT) is handled automatically by the SDK client
|
|
133
|
+
based on the fields provided.
|
|
134
|
+
|
|
135
|
+
\b
|
|
136
|
+
Examples:
|
|
137
|
+
Update with CLI options:
|
|
138
|
+
aip mcps update my-mcp --name new-name --transport sse
|
|
139
|
+
|
|
140
|
+
Import from file:
|
|
141
|
+
aip mcps update my-mcp --import mcp-export.json
|
|
142
|
+
|
|
143
|
+
Import with overrides:
|
|
144
|
+
aip mcps update my-mcp --import mcp-export.json --name new-name -y
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
client = get_client(ctx)
|
|
148
|
+
|
|
149
|
+
# Validate that at least one update method is provided
|
|
150
|
+
_validate_update_inputs(name, transport, description, config, auth, import_file)
|
|
151
|
+
|
|
152
|
+
# Resolve MCP using helper function
|
|
153
|
+
mcp = _resolve_mcp(ctx, client, mcp_ref)
|
|
154
|
+
|
|
155
|
+
# Load and validate import data if provided
|
|
156
|
+
import_payload = None
|
|
157
|
+
if import_file:
|
|
158
|
+
import_payload = _load_import_ready_payload(import_file)
|
|
159
|
+
if not _validate_import_payload_fields(import_payload):
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Merge import payload and CLI options into kwargs for SDK builder
|
|
163
|
+
update_kwargs = _merge_update_kwargs(import_payload, name, transport, description, config, auth, mcp)
|
|
164
|
+
|
|
165
|
+
if not update_kwargs:
|
|
166
|
+
raise click.ClickException("No update fields specified")
|
|
167
|
+
|
|
168
|
+
# Build preview data for confirmation (using the same structure as before)
|
|
169
|
+
preview_data = update_kwargs.copy()
|
|
170
|
+
|
|
171
|
+
# Show confirmation preview for import-based updates (unless -y flag)
|
|
172
|
+
if not _handle_update_preview_and_confirmation(
|
|
173
|
+
import_payload, y, mcp, preview_data, name, transport, description, config, auth
|
|
174
|
+
):
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Use SDK client method to update MCP
|
|
178
|
+
# Pass mcp object (not mcp.id) to avoid extra fetch; SDK accepts str | MCP
|
|
179
|
+
with spinner_context(
|
|
180
|
+
ctx,
|
|
181
|
+
"[bold blue]Updating MCP…[/bold blue]",
|
|
182
|
+
console_override=console,
|
|
183
|
+
):
|
|
184
|
+
updated_mcp = client.mcps.update_mcp(mcp, **update_kwargs)
|
|
185
|
+
|
|
186
|
+
handle_json_output(ctx, updated_mcp.model_dump())
|
|
187
|
+
handle_rich_output(ctx, display_update_success("MCP", updated_mcp.name))
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
_handle_cli_error(ctx, e, "MCP update")
|
glaip_sdk/cli/commands/models.py
CHANGED
|
@@ -11,10 +11,8 @@ from rich.console import Console
|
|
|
11
11
|
|
|
12
12
|
from glaip_sdk.branding import ACCENT_STYLE, INFO, SUCCESS
|
|
13
13
|
from glaip_sdk.cli.context import output_flags
|
|
14
|
-
from glaip_sdk.cli.
|
|
15
|
-
|
|
16
|
-
with_client_and_spinner,
|
|
17
|
-
)
|
|
14
|
+
from glaip_sdk.cli.core.output import output_list
|
|
15
|
+
from glaip_sdk.cli.core.rendering import with_client_and_spinner
|
|
18
16
|
|
|
19
17
|
console = Console()
|
|
20
18
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared CLI command helpers.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from glaip_sdk.cli.commands.shared.formatters import (
|
|
8
|
+
_format_empty_override_warnings,
|
|
9
|
+
_format_dict_value,
|
|
10
|
+
_format_preview_value,
|
|
11
|
+
_is_sensitive_data,
|
|
12
|
+
_redact_sensitive_dict,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"_format_empty_override_warnings",
|
|
17
|
+
"_format_dict_value",
|
|
18
|
+
"_format_preview_value",
|
|
19
|
+
"_is_sensitive_data",
|
|
20
|
+
"_redact_sensitive_dict",
|
|
21
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Shared formatting helpers for CLI commands.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_sensitive_data(val: Any) -> bool:
|
|
12
|
+
"""Check if value contains sensitive authentication data.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
val: Value to check for sensitive information
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
True if the value appears to contain sensitive data
|
|
19
|
+
"""
|
|
20
|
+
if not isinstance(val, dict):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
sensitive_patterns = {"token", "password", "secret", "key", "credential"}
|
|
24
|
+
return any(pattern in str(k).lower() for k in val.keys() for pattern in sensitive_patterns)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _redact_sensitive_dict(val: dict[str, Any]) -> dict[str, Any]:
|
|
28
|
+
"""Redact sensitive fields from a dictionary.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
val: Dictionary to redact
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Redacted dictionary
|
|
35
|
+
"""
|
|
36
|
+
redacted = val.copy()
|
|
37
|
+
sensitive_patterns = {"token", "password", "secret", "key", "credential"}
|
|
38
|
+
for k in redacted.keys():
|
|
39
|
+
if any(pattern in k.lower() for pattern in sensitive_patterns):
|
|
40
|
+
redacted[k] = "<REDACTED>"
|
|
41
|
+
return redacted
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _format_dict_value(val: dict[str, Any]) -> str:
|
|
45
|
+
"""Format a dictionary value for display.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
val: Dictionary to format
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Formatted string representation
|
|
52
|
+
"""
|
|
53
|
+
if _is_sensitive_data(val):
|
|
54
|
+
redacted = _redact_sensitive_dict(val)
|
|
55
|
+
return json.dumps(redacted, indent=2)
|
|
56
|
+
return json.dumps(val, indent=2)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _format_preview_value(val: Any) -> str:
|
|
60
|
+
"""Format a value for display in update preview with sensitive data redaction.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
val: Value to format
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Formatted string representation of the value
|
|
67
|
+
"""
|
|
68
|
+
if val is None:
|
|
69
|
+
return "[dim]None[/dim]"
|
|
70
|
+
if isinstance(val, dict):
|
|
71
|
+
return _format_dict_value(val)
|
|
72
|
+
if isinstance(val, str):
|
|
73
|
+
return f'"{val}"' if val else '""'
|
|
74
|
+
return str(val)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _format_empty_override_warnings(empty_fields: list[str]) -> list[str]:
|
|
78
|
+
"""Format warning lines for empty CLI overrides.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
empty_fields: List of field names with empty string overrides
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of formatted warning lines
|
|
85
|
+
"""
|
|
86
|
+
if not empty_fields:
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
warnings = ["\n[yellow]⚠️ Warning: Empty values provided via CLI will override import values[/yellow]"]
|
|
90
|
+
warnings.extend(f"- [yellow]{field}: will be set to empty string[/yellow]" for field in empty_fields)
|
|
91
|
+
return warnings
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Tool CLI commands package.
|
|
2
|
+
|
|
3
|
+
This package contains tool management commands split by operation.
|
|
4
|
+
The package is the canonical import surface.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# pylint: disable=duplicate-code
|
|
11
|
+
from glaip_sdk.cli.commands.tools._common import tools_group, _resolve_tool, console
|
|
12
|
+
from glaip_sdk.cli.commands.tools.create import create # noqa: E402
|
|
13
|
+
from glaip_sdk.cli.commands.tools.delete import delete # noqa: E402
|
|
14
|
+
from glaip_sdk.cli.commands.tools.get import get # noqa: E402
|
|
15
|
+
from glaip_sdk.cli.commands.tools.list import list_tools # noqa: E402
|
|
16
|
+
from glaip_sdk.cli.commands.tools.script import script # noqa: E402
|
|
17
|
+
from glaip_sdk.cli.commands.tools.update import update # noqa: E402
|
|
18
|
+
|
|
19
|
+
# Import helper functions from create module for backward compatibility
|
|
20
|
+
from glaip_sdk.cli.commands.tools.create import ( # noqa: E402
|
|
21
|
+
_check_duplicate_name,
|
|
22
|
+
_create_tool_from_file,
|
|
23
|
+
_extract_internal_name,
|
|
24
|
+
_handle_import_file,
|
|
25
|
+
_parse_tags,
|
|
26
|
+
_validate_name_match,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Import core functions for test compatibility
|
|
30
|
+
from glaip_sdk.cli.core.context import get_client # noqa: E402
|
|
31
|
+
from glaip_sdk.cli.core.output import ( # noqa: E402
|
|
32
|
+
output_list,
|
|
33
|
+
output_result,
|
|
34
|
+
)
|
|
35
|
+
from glaip_sdk.cli.core.rendering import spinner_context # noqa: E402
|
|
36
|
+
|
|
37
|
+
# Import IO functions for test compatibility
|
|
38
|
+
from glaip_sdk.cli.io import ( # noqa: E402
|
|
39
|
+
fetch_raw_resource_details,
|
|
40
|
+
load_resource_from_file_with_validation,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Alias for backward compatibility (used in create.py)
|
|
44
|
+
load_resource_from_file = load_resource_from_file_with_validation
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"tools_group",
|
|
48
|
+
"create",
|
|
49
|
+
"delete",
|
|
50
|
+
"get",
|
|
51
|
+
"list_tools",
|
|
52
|
+
"script",
|
|
53
|
+
"update",
|
|
54
|
+
"_check_duplicate_name",
|
|
55
|
+
"_create_tool_from_file",
|
|
56
|
+
"_extract_internal_name",
|
|
57
|
+
"_handle_import_file",
|
|
58
|
+
"_parse_tags",
|
|
59
|
+
"_resolve_tool",
|
|
60
|
+
"_validate_name_match",
|
|
61
|
+
"console",
|
|
62
|
+
"get_client",
|
|
63
|
+
"output_list",
|
|
64
|
+
"output_result",
|
|
65
|
+
"spinner_context",
|
|
66
|
+
"fetch_raw_resource_details",
|
|
67
|
+
"load_resource_from_file_with_validation",
|
|
68
|
+
"load_resource_from_file",
|
|
69
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Common helpers and group definition for tool commands.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from glaip_sdk.cli.core.context import get_client
|
|
15
|
+
from glaip_sdk.cli.core.rendering import spinner_context
|
|
16
|
+
from glaip_sdk.cli.resolution import resolve_resource_reference
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group(name="tools", no_args_is_help=True)
|
|
22
|
+
def tools_group() -> None:
|
|
23
|
+
"""Tool management operations."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_tool(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
|
|
28
|
+
"""Resolve a tool by ID or name, handling ambiguous matches interactively.
|
|
29
|
+
|
|
30
|
+
This function provides tool-specific resolution logic. It uses
|
|
31
|
+
resolve_resource_reference to find tools by UUID or name, with interactive
|
|
32
|
+
selection when multiple matches are found.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
ctx: Click context for CLI operations.
|
|
36
|
+
client: API client instance.
|
|
37
|
+
ref: Tool reference (UUID string or name).
|
|
38
|
+
select: Pre-selected index for non-interactive mode (1-based).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tool object if found, None otherwise.
|
|
42
|
+
"""
|
|
43
|
+
# Configure tool-specific resolution with standard fuzzy matching
|
|
44
|
+
get_by_id = client.get_tool
|
|
45
|
+
find_by_name = client.find_tools
|
|
46
|
+
return resolve_resource_reference(
|
|
47
|
+
ctx,
|
|
48
|
+
client,
|
|
49
|
+
ref,
|
|
50
|
+
"tool",
|
|
51
|
+
get_by_id,
|
|
52
|
+
find_by_name,
|
|
53
|
+
"Tool",
|
|
54
|
+
select=select,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_tool_by_id_with_spinner(ctx: Any, tool_id: str) -> Any:
|
|
59
|
+
"""Get tool by ID with spinner context and error handling.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
ctx: Click context.
|
|
63
|
+
tool_id: Tool ID to fetch.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tool object.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
click.ClickException: If tool not found.
|
|
70
|
+
"""
|
|
71
|
+
client = get_client(ctx)
|
|
72
|
+
try:
|
|
73
|
+
with spinner_context(
|
|
74
|
+
ctx,
|
|
75
|
+
"[bold blue]Fetching tool…[/bold blue]",
|
|
76
|
+
console_override=console,
|
|
77
|
+
):
|
|
78
|
+
return client.get_tool_by_id(tool_id)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}") from e
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Create tool command.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from glaip_sdk.cli.context import get_ctx_value, output_flags
|
|
16
|
+
from glaip_sdk.cli.core.context import get_client, handle_best_effort_check
|
|
17
|
+
from glaip_sdk.cli.core.rendering import spinner_context
|
|
18
|
+
from glaip_sdk.cli.display import (
|
|
19
|
+
display_api_error,
|
|
20
|
+
display_creation_success,
|
|
21
|
+
handle_json_output,
|
|
22
|
+
handle_rich_output,
|
|
23
|
+
)
|
|
24
|
+
from glaip_sdk.cli.io import load_resource_from_file_with_validation as load_resource_from_file
|
|
25
|
+
from glaip_sdk.utils.import_export import merge_import_with_cli_args
|
|
26
|
+
|
|
27
|
+
from ._common import console, tools_group
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_internal_name(code: str) -> str:
|
|
31
|
+
"""Extract plugin class name attribute from tool code."""
|
|
32
|
+
m = re.search(r'^\s*name\s*:\s*str\s*=\s*"([^"]+)"', code, re.M)
|
|
33
|
+
if not m:
|
|
34
|
+
m = re.search(r'^\s*name\s*=\s*"([^"]+)"', code, re.M)
|
|
35
|
+
if not m:
|
|
36
|
+
raise click.ClickException(
|
|
37
|
+
"Could not find plugin 'name' attribute in the tool file. "
|
|
38
|
+
'Ensure your plugin class defines e.g. name: str = "my_tool".'
|
|
39
|
+
)
|
|
40
|
+
return m.group(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _validate_name_match(provided: str | None, internal: str) -> str:
|
|
44
|
+
"""Validate provided --name against internal name; return effective name."""
|
|
45
|
+
if provided and provided != internal:
|
|
46
|
+
raise click.ClickException(
|
|
47
|
+
f"--name '{provided}' does not match plugin internal name '{internal}'. "
|
|
48
|
+
"Either update the code or pass a matching --name."
|
|
49
|
+
)
|
|
50
|
+
return provided or internal
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _check_duplicate_name(client: Any, tool_name: str) -> None:
|
|
54
|
+
"""Raise if a tool with the same name already exists."""
|
|
55
|
+
|
|
56
|
+
def _check_duplicate() -> None:
|
|
57
|
+
existing = client.find_tools(name=tool_name)
|
|
58
|
+
if existing:
|
|
59
|
+
raise click.ClickException(
|
|
60
|
+
f"A tool named '{tool_name}' already exists. "
|
|
61
|
+
"Please change your plugin's 'name' to a unique value, then re-run."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
handle_best_effort_check(_check_duplicate)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_tags(tags: str | None) -> list[str]:
|
|
68
|
+
"""Return a cleaned list of tag strings from a comma-separated input."""
|
|
69
|
+
return [t.strip() for t in (tags.split(",") if tags else []) if t.strip()]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _handle_import_file(
|
|
73
|
+
import_file: str | None,
|
|
74
|
+
name: str | None,
|
|
75
|
+
description: str | None,
|
|
76
|
+
tags: str | None,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""Handle import file logic and merge with CLI arguments."""
|
|
79
|
+
if import_file:
|
|
80
|
+
import_data = load_resource_from_file(Path(import_file), "tool")
|
|
81
|
+
|
|
82
|
+
# Merge CLI args with imported data
|
|
83
|
+
cli_args = {
|
|
84
|
+
"name": name,
|
|
85
|
+
"description": description,
|
|
86
|
+
"tags": tags,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return merge_import_with_cli_args(import_data, cli_args)
|
|
90
|
+
else:
|
|
91
|
+
# No import file - use CLI args directly
|
|
92
|
+
return {
|
|
93
|
+
"name": name,
|
|
94
|
+
"description": description,
|
|
95
|
+
"tags": tags,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _create_tool_from_file(
|
|
100
|
+
client: Any,
|
|
101
|
+
file_path: str,
|
|
102
|
+
name: str | None,
|
|
103
|
+
description: str | None,
|
|
104
|
+
tags: str | None,
|
|
105
|
+
) -> Any:
|
|
106
|
+
"""Create tool from file upload."""
|
|
107
|
+
with open(file_path, encoding="utf-8") as f:
|
|
108
|
+
code_content = f.read()
|
|
109
|
+
|
|
110
|
+
internal_name = _extract_internal_name(code_content)
|
|
111
|
+
tool_name = _validate_name_match(name, internal_name)
|
|
112
|
+
_check_duplicate_name(client, tool_name)
|
|
113
|
+
|
|
114
|
+
# Upload the plugin code as-is (no rewrite)
|
|
115
|
+
return client.create_tool_from_code(
|
|
116
|
+
name=tool_name,
|
|
117
|
+
code=code_content,
|
|
118
|
+
framework="langchain", # Always langchain
|
|
119
|
+
description=description,
|
|
120
|
+
tags=_parse_tags(tags) if tags else None,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _validate_creation_parameters(
|
|
125
|
+
file: str | None,
|
|
126
|
+
import_file: str | None,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Validate required parameters for tool creation."""
|
|
129
|
+
if not file and not import_file:
|
|
130
|
+
raise click.ClickException("A tool file must be provided. Use --file to specify the tool file to upload.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@tools_group.command()
|
|
134
|
+
@click.argument("file_arg", required=False, type=click.Path(exists=True))
|
|
135
|
+
@click.option(
|
|
136
|
+
"--file",
|
|
137
|
+
type=click.Path(exists=True),
|
|
138
|
+
help="Tool file to upload",
|
|
139
|
+
)
|
|
140
|
+
@click.option(
|
|
141
|
+
"--name",
|
|
142
|
+
help="Tool name (extracted from script if file provided)",
|
|
143
|
+
)
|
|
144
|
+
@click.option(
|
|
145
|
+
"--description",
|
|
146
|
+
help="Tool description (extracted from script if file provided)",
|
|
147
|
+
)
|
|
148
|
+
@click.option(
|
|
149
|
+
"--tags",
|
|
150
|
+
help="Comma-separated tags for the tool",
|
|
151
|
+
)
|
|
152
|
+
@click.option(
|
|
153
|
+
"--import",
|
|
154
|
+
"import_file",
|
|
155
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
156
|
+
help="Import tool configuration from JSON file",
|
|
157
|
+
)
|
|
158
|
+
@output_flags()
|
|
159
|
+
@click.pass_context
|
|
160
|
+
def create(
|
|
161
|
+
ctx: Any,
|
|
162
|
+
file_arg: str | None,
|
|
163
|
+
file: str | None,
|
|
164
|
+
name: str | None,
|
|
165
|
+
description: str | None,
|
|
166
|
+
tags: str | None,
|
|
167
|
+
import_file: str | None,
|
|
168
|
+
) -> None:
|
|
169
|
+
r"""Create a new tool.
|
|
170
|
+
|
|
171
|
+
\b
|
|
172
|
+
Examples:
|
|
173
|
+
aip tools create tool.py # Create from file
|
|
174
|
+
aip tools create --import tool.json # Create from exported configuration
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
client = get_client(ctx)
|
|
178
|
+
|
|
179
|
+
# Allow positional file argument for better DX (matches examples)
|
|
180
|
+
if not file and file_arg:
|
|
181
|
+
file = file_arg
|
|
182
|
+
|
|
183
|
+
# Handle import file and merge with CLI arguments
|
|
184
|
+
merged_data = _handle_import_file(import_file, name, description, tags)
|
|
185
|
+
|
|
186
|
+
# Extract merged values
|
|
187
|
+
name = merged_data.get("name")
|
|
188
|
+
description = merged_data.get("description")
|
|
189
|
+
tags_raw = merged_data.get("tags")
|
|
190
|
+
# Convert tags to string format (for _create_tool_from_file which expects str | None)
|
|
191
|
+
# Import data may have tags as list, CLI provides string
|
|
192
|
+
if isinstance(tags_raw, list):
|
|
193
|
+
tags = ",".join(str(tag).strip() for tag in tags_raw) if tags_raw else None
|
|
194
|
+
else:
|
|
195
|
+
tags = tags_raw # Already string or None
|
|
196
|
+
|
|
197
|
+
# Validate required parameters
|
|
198
|
+
_validate_creation_parameters(file, import_file)
|
|
199
|
+
|
|
200
|
+
# Create tool from file (either direct file or import file)
|
|
201
|
+
with spinner_context(
|
|
202
|
+
ctx,
|
|
203
|
+
"[bold blue]Creating tool…[/bold blue]",
|
|
204
|
+
console_override=console,
|
|
205
|
+
):
|
|
206
|
+
tool = _create_tool_from_file(client, file, name, description, tags)
|
|
207
|
+
|
|
208
|
+
# Handle JSON output
|
|
209
|
+
handle_json_output(ctx, tool.model_dump())
|
|
210
|
+
|
|
211
|
+
# Handle Rich output
|
|
212
|
+
creation_method = "file upload (custom)"
|
|
213
|
+
rich_panel = display_creation_success(
|
|
214
|
+
"Tool",
|
|
215
|
+
tool.name,
|
|
216
|
+
tool.id,
|
|
217
|
+
Framework=getattr(tool, "framework", "N/A"),
|
|
218
|
+
Type=getattr(tool, "tool_type", "N/A"),
|
|
219
|
+
Description=getattr(tool, "description", "No description"),
|
|
220
|
+
Method=creation_method,
|
|
221
|
+
)
|
|
222
|
+
handle_rich_output(ctx, rich_panel)
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
handle_json_output(ctx, error=e)
|
|
226
|
+
if get_ctx_value(ctx, "view") != "json":
|
|
227
|
+
display_api_error(e, "tool creation")
|
|
228
|
+
raise click.ClickException(str(e)) from e
|