glaip-sdk 0.6.15b2__py3-none-any.whl → 0.6.15b3__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/__init__.py +27 -0
- glaip_sdk/agents/base.py +1196 -0
- glaip_sdk/cli/__init__.py +9 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +78 -0
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +5 -0
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +1509 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +896 -0
- glaip_sdk/cli/commands/mcps.py +1356 -0
- glaip_sdk/cli/commands/models.py +69 -0
- glaip_sdk/cli/commands/tools.py +576 -0
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +61 -0
- glaip_sdk/cli/config.py +95 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +150 -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 +355 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +112 -0
- glaip_sdk/cli/main.py +615 -0
- glaip_sdk/cli/masking.py +136 -0
- glaip_sdk/cli/mcp_validators.py +287 -0
- glaip_sdk/cli/pager.py +266 -0
- glaip_sdk/cli/parsers/__init__.py +7 -0
- glaip_sdk/cli/parsers/json_input.py +177 -0
- glaip_sdk/cli/resolution.py +67 -0
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +285 -0
- glaip_sdk/cli/slash/prompt.py +256 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1708 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +31 -0
- glaip_sdk/cli/transcript/cache.py +536 -0
- glaip_sdk/cli/transcript/capture.py +329 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +77 -0
- glaip_sdk/cli/transcript/viewer.py +374 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +263 -0
- glaip_sdk/cli/validators.py +238 -0
- glaip_sdk/client/__init__.py +11 -0
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +1335 -0
- glaip_sdk/client/base.py +502 -0
- glaip_sdk/client/main.py +249 -0
- glaip_sdk/client/mcps.py +370 -0
- glaip_sdk/client/run_rendering.py +700 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +661 -0
- glaip_sdk/client/validators.py +198 -0
- glaip_sdk/config/constants.py +52 -0
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +7 -0
- glaip_sdk/payload_schemas/agent.py +85 -0
- 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 +232 -0
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +782 -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 +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +86 -0
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +194 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +486 -0
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +135 -0
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +61 -0
- glaip_sdk/utils/import_export.py +168 -0
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -0
- glaip_sdk/utils/rendering/formatting.py +264 -0
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/layout/panels.py +156 -0
- glaip_sdk/utils/rendering/layout/progress.py +202 -0
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +85 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
- glaip_sdk/utils/rendering/renderer/base.py +1024 -0
- glaip_sdk/utils/rendering/renderer/config.py +27 -0
- glaip_sdk/utils/rendering/renderer/console.py +55 -0
- glaip_sdk/utils/rendering/renderer/debug.py +178 -0
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +202 -0
- 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/manager.py +387 -0
- 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 +195 -0
- glaip_sdk/utils/run_renderer.py +41 -0
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +424 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +264 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/METADATA +1 -1
- glaip_sdk-0.6.15b3.dist-info/RECORD +160 -0
- glaip_sdk-0.6.15b2.dist-info/RECORD +0 -12
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.15b3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1509 @@
|
|
|
1
|
+
"""Agent CLI commands for AIP SDK.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
from glaip_sdk.branding import (
|
|
20
|
+
ACCENT_STYLE,
|
|
21
|
+
ERROR_STYLE,
|
|
22
|
+
HINT_PREFIX_STYLE,
|
|
23
|
+
INFO,
|
|
24
|
+
SUCCESS,
|
|
25
|
+
SUCCESS_STYLE,
|
|
26
|
+
WARNING_STYLE,
|
|
27
|
+
)
|
|
28
|
+
from glaip_sdk.cli.agent_config import (
|
|
29
|
+
merge_agent_config_with_cli_args as merge_import_with_cli_args,
|
|
30
|
+
)
|
|
31
|
+
from glaip_sdk.cli.agent_config import (
|
|
32
|
+
resolve_agent_language_model_selection as resolve_language_model_selection,
|
|
33
|
+
)
|
|
34
|
+
from glaip_sdk.cli.agent_config import (
|
|
35
|
+
sanitize_agent_config_for_cli as sanitize_agent_config,
|
|
36
|
+
)
|
|
37
|
+
from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
|
|
38
|
+
from glaip_sdk.cli.context import get_ctx_value, output_flags
|
|
39
|
+
from glaip_sdk.cli.display import (
|
|
40
|
+
build_resource_result_data,
|
|
41
|
+
display_agent_run_suggestions,
|
|
42
|
+
display_confirmation_prompt,
|
|
43
|
+
display_creation_success,
|
|
44
|
+
display_deletion_success,
|
|
45
|
+
display_update_success,
|
|
46
|
+
handle_json_output,
|
|
47
|
+
handle_rich_output,
|
|
48
|
+
print_api_error,
|
|
49
|
+
)
|
|
50
|
+
from glaip_sdk.cli.hints import in_slash_mode
|
|
51
|
+
from glaip_sdk.cli.io import (
|
|
52
|
+
fetch_raw_resource_details,
|
|
53
|
+
)
|
|
54
|
+
from glaip_sdk.cli.io import (
|
|
55
|
+
load_resource_from_file_with_validation as load_resource_from_file,
|
|
56
|
+
)
|
|
57
|
+
from glaip_sdk.cli.resolution import resolve_resource_reference
|
|
58
|
+
from glaip_sdk.cli.rich_helpers import markup_text, print_markup
|
|
59
|
+
from glaip_sdk.cli.transcript import (
|
|
60
|
+
maybe_launch_post_run_viewer,
|
|
61
|
+
store_transcript_for_session,
|
|
62
|
+
)
|
|
63
|
+
from glaip_sdk.cli.utils import (
|
|
64
|
+
_fuzzy_pick_for_resources,
|
|
65
|
+
build_renderer,
|
|
66
|
+
coerce_to_row,
|
|
67
|
+
get_client,
|
|
68
|
+
handle_resource_export,
|
|
69
|
+
output_list,
|
|
70
|
+
output_result,
|
|
71
|
+
spinner_context,
|
|
72
|
+
with_client_and_spinner,
|
|
73
|
+
)
|
|
74
|
+
from glaip_sdk.cli.validators import (
|
|
75
|
+
validate_agent_instruction_cli as validate_agent_instruction,
|
|
76
|
+
)
|
|
77
|
+
from glaip_sdk.cli.validators import (
|
|
78
|
+
validate_agent_name_cli as validate_agent_name,
|
|
79
|
+
)
|
|
80
|
+
from glaip_sdk.cli.validators import (
|
|
81
|
+
validate_timeout_cli as validate_timeout,
|
|
82
|
+
)
|
|
83
|
+
from glaip_sdk.config.constants import AGENT_CONFIG_FIELDS, DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
|
|
84
|
+
from glaip_sdk.exceptions import AgentTimeoutError
|
|
85
|
+
from glaip_sdk.icons import ICON_AGENT
|
|
86
|
+
from glaip_sdk.utils import format_datetime, is_uuid
|
|
87
|
+
from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
|
|
88
|
+
from glaip_sdk.utils.import_export import convert_export_to_import_format
|
|
89
|
+
from glaip_sdk.utils.rendering.renderer.toggle import TranscriptToggleController
|
|
90
|
+
from glaip_sdk.utils.validation import coerce_timeout
|
|
91
|
+
|
|
92
|
+
console = Console()
|
|
93
|
+
|
|
94
|
+
# Error message constants
|
|
95
|
+
AGENT_NOT_FOUND_ERROR = "Agent not found"
|
|
96
|
+
|
|
97
|
+
# Instruction preview controls
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _safe_agent_attribute(agent: Any, name: str) -> Any:
|
|
101
|
+
"""Return attribute value for ``name`` while filtering Mock sentinels."""
|
|
102
|
+
try:
|
|
103
|
+
value = getattr(agent, name)
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
if hasattr(value, "_mock_name"):
|
|
108
|
+
return None
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _coerce_mapping_candidate(candidate: Any) -> dict[str, Any] | None:
|
|
113
|
+
"""Convert a mapping-like candidate to a plain dict when possible."""
|
|
114
|
+
if candidate is None:
|
|
115
|
+
return None
|
|
116
|
+
if isinstance(candidate, Mapping):
|
|
117
|
+
return dict(candidate)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _call_agent_method(agent: Any, method_name: str) -> dict[str, Any] | None:
|
|
122
|
+
"""Attempt to call the named method and coerce its output to a dict."""
|
|
123
|
+
method = getattr(agent, method_name, None)
|
|
124
|
+
if not callable(method):
|
|
125
|
+
return None
|
|
126
|
+
try:
|
|
127
|
+
candidate = method()
|
|
128
|
+
except Exception:
|
|
129
|
+
return None
|
|
130
|
+
return _coerce_mapping_candidate(candidate)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _coerce_agent_via_methods(agent: Any) -> dict[str, Any] | None:
|
|
134
|
+
"""Try standard serialisation helpers to produce a mapping."""
|
|
135
|
+
for attr in ("model_dump", "dict", "to_dict"):
|
|
136
|
+
mapping = _call_agent_method(agent, attr)
|
|
137
|
+
if mapping is not None:
|
|
138
|
+
return mapping
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
|
|
143
|
+
"""Construct a minimal mapping from well-known agent attributes."""
|
|
144
|
+
fallback_fields = (
|
|
145
|
+
"id",
|
|
146
|
+
"name",
|
|
147
|
+
"instruction",
|
|
148
|
+
"description",
|
|
149
|
+
"model",
|
|
150
|
+
"agent_config",
|
|
151
|
+
*[field for field in AGENT_CONFIG_FIELDS if field not in ("name", "instruction", "model")],
|
|
152
|
+
"tool_configs",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
fallback: dict[str, Any] = {}
|
|
156
|
+
for field in fallback_fields:
|
|
157
|
+
value = _safe_agent_attribute(agent, field)
|
|
158
|
+
if value is not None:
|
|
159
|
+
fallback[field] = value
|
|
160
|
+
|
|
161
|
+
return fallback or {"name": str(agent)}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _prepare_agent_output(agent: Any) -> dict[str, Any]:
|
|
165
|
+
"""Build a JSON-serialisable mapping for CLI output."""
|
|
166
|
+
method_mapping = _coerce_agent_via_methods(agent)
|
|
167
|
+
if method_mapping is not None:
|
|
168
|
+
return method_mapping
|
|
169
|
+
|
|
170
|
+
intrinsic = _coerce_mapping_candidate(agent)
|
|
171
|
+
if intrinsic is not None:
|
|
172
|
+
return intrinsic
|
|
173
|
+
|
|
174
|
+
return _build_fallback_agent_mapping(agent)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _fetch_full_agent_details(client: Any, agent: Any) -> Any | None:
|
|
178
|
+
"""Fetch full agent details by ID to ensure all fields are populated."""
|
|
179
|
+
try:
|
|
180
|
+
agent_id = str(getattr(agent, "id", "")).strip()
|
|
181
|
+
if agent_id:
|
|
182
|
+
return client.agents.get_agent_by_id(agent_id)
|
|
183
|
+
except Exception:
|
|
184
|
+
# If fetching full details fails, continue with the resolved object
|
|
185
|
+
pass
|
|
186
|
+
return agent
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _normalise_model_name(value: Any) -> str | None:
|
|
190
|
+
"""Return a cleaned model name or None when not usable."""
|
|
191
|
+
if value is None:
|
|
192
|
+
return None
|
|
193
|
+
if isinstance(value, str):
|
|
194
|
+
cleaned = value.strip()
|
|
195
|
+
return cleaned or None
|
|
196
|
+
if isinstance(value, bool):
|
|
197
|
+
return None
|
|
198
|
+
return str(value)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _model_from_config(agent: Any) -> str | None:
|
|
202
|
+
"""Extract a usable model name from an agent's configuration mapping."""
|
|
203
|
+
config = getattr(agent, "agent_config", None)
|
|
204
|
+
if not config or not isinstance(config, dict):
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
for key in ("lm_name", "model"):
|
|
208
|
+
normalised = _normalise_model_name(config.get(key))
|
|
209
|
+
if normalised:
|
|
210
|
+
return normalised
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _get_agent_model_name(agent: Any) -> str | None:
|
|
215
|
+
"""Extract model name from agent configuration."""
|
|
216
|
+
config_model = _model_from_config(agent)
|
|
217
|
+
if config_model:
|
|
218
|
+
return config_model
|
|
219
|
+
|
|
220
|
+
normalised_attr = _normalise_model_name(getattr(agent, "model", None))
|
|
221
|
+
if normalised_attr:
|
|
222
|
+
return normalised_attr
|
|
223
|
+
|
|
224
|
+
return DEFAULT_MODEL
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _resolve_resources_by_name(
|
|
228
|
+
_client: Any, items: tuple[str, ...], resource_type: str, find_func: Any, label: str
|
|
229
|
+
) -> list[str]:
|
|
230
|
+
"""Resolve resource names/IDs to IDs, handling ambiguity.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
client: API client
|
|
234
|
+
items: Tuple of resource names/IDs
|
|
235
|
+
resource_type: Type of resource ("tool" or "agent")
|
|
236
|
+
find_func: Function to find resources by name
|
|
237
|
+
label: Label for error messages
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List of resolved resource IDs
|
|
241
|
+
"""
|
|
242
|
+
out = []
|
|
243
|
+
for ref in items or ():
|
|
244
|
+
if is_uuid(ref):
|
|
245
|
+
out.append(ref)
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
matches = find_func(name=ref)
|
|
249
|
+
if not matches:
|
|
250
|
+
raise click.ClickException(f"{label} not found: {ref}")
|
|
251
|
+
if len(matches) > 1:
|
|
252
|
+
raise click.ClickException(f"Multiple {resource_type}s named '{ref}'. Use ID instead.")
|
|
253
|
+
out.append(str(matches[0].id))
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _fetch_and_format_raw_agent_data(client: Any, agent: Any) -> dict | None:
|
|
258
|
+
"""Fetch raw agent data and format it for display."""
|
|
259
|
+
try:
|
|
260
|
+
raw_agent_data = fetch_raw_resource_details(client, agent, "agents")
|
|
261
|
+
if not raw_agent_data:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
# Format dates for better display
|
|
265
|
+
formatted_data = raw_agent_data.copy()
|
|
266
|
+
if "created_at" in formatted_data:
|
|
267
|
+
formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
|
|
268
|
+
if "updated_at" in formatted_data:
|
|
269
|
+
formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
|
|
270
|
+
|
|
271
|
+
return formatted_data
|
|
272
|
+
except Exception:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _format_fallback_agent_data(client: Any, agent: Any) -> dict:
|
|
277
|
+
"""Format fallback agent data using Pydantic model."""
|
|
278
|
+
full_agent = _fetch_full_agent_details(client, agent)
|
|
279
|
+
|
|
280
|
+
# Define fields to extract
|
|
281
|
+
fields = [
|
|
282
|
+
"id",
|
|
283
|
+
"name",
|
|
284
|
+
"type",
|
|
285
|
+
"framework",
|
|
286
|
+
"version",
|
|
287
|
+
"description",
|
|
288
|
+
"instruction",
|
|
289
|
+
"created_at",
|
|
290
|
+
"updated_at",
|
|
291
|
+
"metadata",
|
|
292
|
+
"language_model_id",
|
|
293
|
+
"agent_config",
|
|
294
|
+
"tools",
|
|
295
|
+
"agents",
|
|
296
|
+
"mcps",
|
|
297
|
+
"a2a_profile",
|
|
298
|
+
"tool_configs",
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
result_data = build_resource_result_data(full_agent, fields)
|
|
302
|
+
|
|
303
|
+
# Handle missing instruction
|
|
304
|
+
if result_data.get("instruction") in ["N/A", None, ""]:
|
|
305
|
+
result_data["instruction"] = "-"
|
|
306
|
+
|
|
307
|
+
# Format dates for better display
|
|
308
|
+
for date_field in ["created_at", "updated_at"]:
|
|
309
|
+
if result_data.get(date_field) and result_data[date_field] not in ["N/A", None]:
|
|
310
|
+
result_data[date_field] = format_datetime(result_data[date_field])
|
|
311
|
+
|
|
312
|
+
return result_data
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _clamp_instruction_preview_limit(limit: int | None) -> int:
|
|
316
|
+
"""Normalise preview limit; 0 disables trimming."""
|
|
317
|
+
default = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
|
|
318
|
+
if limit is None: # pragma: no cover
|
|
319
|
+
return default
|
|
320
|
+
try:
|
|
321
|
+
limit_value = int(limit)
|
|
322
|
+
except (TypeError, ValueError): # pragma: no cover - defensive parsing
|
|
323
|
+
return default
|
|
324
|
+
|
|
325
|
+
if limit_value <= 0:
|
|
326
|
+
return 0
|
|
327
|
+
|
|
328
|
+
return limit_value
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _build_instruction_preview(value: Any, limit: int) -> tuple[Any, bool]:
|
|
332
|
+
"""Return a trimmed preview for long instruction strings."""
|
|
333
|
+
if not isinstance(value, str) or limit <= 0: # pragma: no cover
|
|
334
|
+
return value, False
|
|
335
|
+
|
|
336
|
+
if len(value) <= limit:
|
|
337
|
+
return value, False
|
|
338
|
+
|
|
339
|
+
trimmed_value = value[:limit].rstrip()
|
|
340
|
+
preview = f"{trimmed_value}\n\n... (preview trimmed)"
|
|
341
|
+
return preview, True
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _prepare_agent_details_payload(
|
|
345
|
+
data: dict[str, Any],
|
|
346
|
+
*,
|
|
347
|
+
instruction_preview_limit: int,
|
|
348
|
+
) -> tuple[dict[str, Any], bool]:
|
|
349
|
+
"""Return payload ready for rendering plus trim indicator."""
|
|
350
|
+
payload = deepcopy(data)
|
|
351
|
+
trimmed = False
|
|
352
|
+
if instruction_preview_limit > 0:
|
|
353
|
+
preview, trimmed = _build_instruction_preview(payload.get("instruction"), instruction_preview_limit)
|
|
354
|
+
if trimmed:
|
|
355
|
+
payload["instruction"] = preview
|
|
356
|
+
return payload, trimmed
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _show_instruction_trim_hint(
|
|
360
|
+
ctx: Any,
|
|
361
|
+
*,
|
|
362
|
+
trimmed: bool,
|
|
363
|
+
preview_limit: int,
|
|
364
|
+
) -> None:
|
|
365
|
+
"""Render hint describing how to expand or collapse the instruction preview."""
|
|
366
|
+
if not trimmed or preview_limit <= 0:
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
view = get_ctx_value(ctx, "view", "rich") if ctx is not None else "rich"
|
|
370
|
+
if view != "rich": # pragma: no cover - non-rich view handling
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
suffix = f"[dim](preview: {preview_limit:,} chars)[/]"
|
|
374
|
+
if in_slash_mode(ctx):
|
|
375
|
+
console.print(
|
|
376
|
+
f"[{HINT_PREFIX_STYLE}]Tip:[/] Use '/details' again to toggle between trimmed and full prompts {suffix}"
|
|
377
|
+
)
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
console.print( # pragma: no cover - fallback hint rendering
|
|
381
|
+
f"[{HINT_PREFIX_STYLE}]Tip:[/] Run 'aip agents get <agent> --instruction-preview <n>' "
|
|
382
|
+
f"to control prompt preview length {suffix}"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _display_agent_details(
|
|
387
|
+
ctx: Any,
|
|
388
|
+
client: Any,
|
|
389
|
+
agent: Any,
|
|
390
|
+
*,
|
|
391
|
+
instruction_preview_limit: int | None = None,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Display full agent details using raw API data to preserve ALL fields."""
|
|
394
|
+
if agent is None:
|
|
395
|
+
handle_rich_output(ctx, markup_text(f"[{ERROR_STYLE}]❌ No agent provided[/]"))
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
preview_limit = _clamp_instruction_preview_limit(instruction_preview_limit)
|
|
399
|
+
trimmed_instruction = False
|
|
400
|
+
|
|
401
|
+
# Try to fetch and format raw agent data first
|
|
402
|
+
with spinner_context(
|
|
403
|
+
ctx,
|
|
404
|
+
"[bold blue]Loading agent details…[/bold blue]",
|
|
405
|
+
console_override=console,
|
|
406
|
+
):
|
|
407
|
+
formatted_data = _fetch_and_format_raw_agent_data(client, agent)
|
|
408
|
+
|
|
409
|
+
if formatted_data:
|
|
410
|
+
# Use raw API data - this preserves ALL fields including account_id
|
|
411
|
+
panel_title = f"{ICON_AGENT} {formatted_data.get('name', 'Unknown')}"
|
|
412
|
+
payload, trimmed_instruction = _prepare_agent_details_payload(
|
|
413
|
+
formatted_data,
|
|
414
|
+
instruction_preview_limit=preview_limit,
|
|
415
|
+
)
|
|
416
|
+
output_result(
|
|
417
|
+
ctx,
|
|
418
|
+
payload,
|
|
419
|
+
title=panel_title,
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
# Fall back to Pydantic model data if raw fetch fails
|
|
423
|
+
handle_rich_output(
|
|
424
|
+
ctx,
|
|
425
|
+
markup_text(f"[{WARNING_STYLE}]Falling back to Pydantic model data[/]"),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
with spinner_context(
|
|
429
|
+
ctx,
|
|
430
|
+
"[bold blue]Preparing fallback agent details…[/bold blue]",
|
|
431
|
+
console_override=console,
|
|
432
|
+
):
|
|
433
|
+
result_data = _format_fallback_agent_data(client, agent)
|
|
434
|
+
|
|
435
|
+
# Display using output_result
|
|
436
|
+
payload, trimmed_instruction = _prepare_agent_details_payload(
|
|
437
|
+
result_data,
|
|
438
|
+
instruction_preview_limit=preview_limit,
|
|
439
|
+
)
|
|
440
|
+
output_result(
|
|
441
|
+
ctx,
|
|
442
|
+
payload,
|
|
443
|
+
title="Agent Details",
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
_show_instruction_trim_hint(
|
|
447
|
+
ctx,
|
|
448
|
+
trimmed=trimmed_instruction,
|
|
449
|
+
preview_limit=preview_limit,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@click.group(name="agents", no_args_is_help=True)
|
|
454
|
+
def agents_group() -> None:
|
|
455
|
+
"""Agent management operations."""
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _resolve_agent(
|
|
460
|
+
ctx: Any,
|
|
461
|
+
client: Any,
|
|
462
|
+
ref: str,
|
|
463
|
+
select: int | None = None,
|
|
464
|
+
interface_preference: str = "fuzzy",
|
|
465
|
+
) -> Any | None:
|
|
466
|
+
"""Resolve an agent by ID or name, supporting fuzzy and questionary interfaces.
|
|
467
|
+
|
|
468
|
+
This function provides agent-specific resolution with flexible UI options.
|
|
469
|
+
It wraps resolve_resource_reference with agent-specific configuration, allowing
|
|
470
|
+
users to choose between fuzzy search and traditional questionary selection.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
ctx: Click context for CLI command execution.
|
|
474
|
+
client: AIP SDK client instance.
|
|
475
|
+
ref: Agent identifier (UUID or name string).
|
|
476
|
+
select: Pre-selected index for non-interactive resolution (1-based).
|
|
477
|
+
interface_preference: UI preference - "fuzzy" for search or "questionary" for list.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Agent object when found, None when resolution fails.
|
|
481
|
+
"""
|
|
482
|
+
# Configure agent-specific resolution parameters
|
|
483
|
+
resolution_config = {
|
|
484
|
+
"resource_type": "agent",
|
|
485
|
+
"get_by_id": client.agents.get_agent_by_id,
|
|
486
|
+
"find_by_name": client.agents.find_agents,
|
|
487
|
+
"label": "Agent",
|
|
488
|
+
}
|
|
489
|
+
# Use agent-specific resolution with flexible interface preference
|
|
490
|
+
return resolve_resource_reference(
|
|
491
|
+
ctx,
|
|
492
|
+
client,
|
|
493
|
+
ref,
|
|
494
|
+
resolution_config["resource_type"],
|
|
495
|
+
resolution_config["get_by_id"],
|
|
496
|
+
resolution_config["find_by_name"],
|
|
497
|
+
resolution_config["label"],
|
|
498
|
+
select=select,
|
|
499
|
+
interface_preference=interface_preference,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@agents_group.command(name="list")
|
|
504
|
+
@click.option("--simple", is_flag=True, help="Show simple table without interactive picker")
|
|
505
|
+
@click.option("--type", "agent_type", help="Filter by agent type (config, code, a2a, langflow)")
|
|
506
|
+
@click.option("--framework", help="Filter by framework (langchain, langgraph, google_adk)")
|
|
507
|
+
@click.option("--name", help="Filter by partial name match (case-insensitive)")
|
|
508
|
+
@click.option("--version", help="Filter by exact version match")
|
|
509
|
+
@click.option(
|
|
510
|
+
"--sync-langflow",
|
|
511
|
+
is_flag=True,
|
|
512
|
+
help="Sync with LangFlow server before listing (only applies when filtering by langflow type)",
|
|
513
|
+
)
|
|
514
|
+
@output_flags()
|
|
515
|
+
@click.pass_context
|
|
516
|
+
def list_agents(
|
|
517
|
+
ctx: Any,
|
|
518
|
+
simple: bool,
|
|
519
|
+
agent_type: str | None,
|
|
520
|
+
framework: str | None,
|
|
521
|
+
name: str | None,
|
|
522
|
+
version: str | None,
|
|
523
|
+
sync_langflow: bool,
|
|
524
|
+
) -> None:
|
|
525
|
+
"""List agents with optional filtering."""
|
|
526
|
+
try:
|
|
527
|
+
with with_client_and_spinner(
|
|
528
|
+
ctx,
|
|
529
|
+
"[bold blue]Fetching agents…[/bold blue]",
|
|
530
|
+
console_override=console,
|
|
531
|
+
) as client:
|
|
532
|
+
# Query agents with specified filters
|
|
533
|
+
filter_params = {
|
|
534
|
+
"agent_type": agent_type,
|
|
535
|
+
"framework": framework,
|
|
536
|
+
"name": name,
|
|
537
|
+
"version": version,
|
|
538
|
+
"sync_langflow_agents": sync_langflow,
|
|
539
|
+
}
|
|
540
|
+
agents = client.agents.list_agents(**filter_params)
|
|
541
|
+
|
|
542
|
+
# Define table columns: (data_key, header, style, width)
|
|
543
|
+
columns = [
|
|
544
|
+
("id", "ID", "dim", 36),
|
|
545
|
+
("name", "Name", ACCENT_STYLE, None),
|
|
546
|
+
("type", "Type", WARNING_STYLE, None),
|
|
547
|
+
("framework", "Framework", INFO, None),
|
|
548
|
+
("version", "Version", SUCCESS, None),
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
# Transform function for safe attribute access
|
|
552
|
+
def transform_agent(agent: Any) -> dict[str, Any]:
|
|
553
|
+
"""Transform an agent object to a display row dictionary.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
agent: Agent object to transform.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Dictionary with id, name, type, framework, and version fields.
|
|
560
|
+
"""
|
|
561
|
+
row = coerce_to_row(agent, ["id", "name", "type", "framework", "version"])
|
|
562
|
+
# Ensure id is always a string
|
|
563
|
+
row["id"] = str(row["id"])
|
|
564
|
+
return row
|
|
565
|
+
|
|
566
|
+
# Use fuzzy picker for interactive agent selection and details (default behavior)
|
|
567
|
+
# Skip if --simple flag is used, a name filter is applied, or non-rich output is requested
|
|
568
|
+
ctx_obj = ctx.obj if isinstance(getattr(ctx, "obj", None), dict) else {}
|
|
569
|
+
current_view = ctx_obj.get("view")
|
|
570
|
+
interactive_enabled = (
|
|
571
|
+
not simple
|
|
572
|
+
and name is None
|
|
573
|
+
and current_view not in {"json", "plain", "md"}
|
|
574
|
+
and console.is_terminal
|
|
575
|
+
and os.isatty(1)
|
|
576
|
+
and len(agents) > 0
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Track picker attempt so the fallback table doesn't re-open the palette
|
|
580
|
+
picker_attempted = False
|
|
581
|
+
if interactive_enabled:
|
|
582
|
+
picker_attempted = True
|
|
583
|
+
picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
|
|
584
|
+
if picked_agent:
|
|
585
|
+
_display_agent_details(ctx, client, picked_agent)
|
|
586
|
+
# Show run suggestions via centralized display helper
|
|
587
|
+
handle_rich_output(ctx, display_agent_run_suggestions(picked_agent))
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
# Show simple table (either --simple flag or non-interactive)
|
|
591
|
+
output_list(
|
|
592
|
+
ctx,
|
|
593
|
+
agents,
|
|
594
|
+
f"{ICON_AGENT} Available Agents",
|
|
595
|
+
columns,
|
|
596
|
+
transform_agent,
|
|
597
|
+
skip_picker=(
|
|
598
|
+
not interactive_enabled
|
|
599
|
+
or picker_attempted
|
|
600
|
+
or simple
|
|
601
|
+
or any(param is not None for param in (agent_type, framework, name, version))
|
|
602
|
+
),
|
|
603
|
+
use_pager=False,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
except Exception as e:
|
|
607
|
+
raise click.ClickException(str(e)) from e
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@agents_group.command()
|
|
611
|
+
@click.argument("agent_ref")
|
|
612
|
+
@click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
|
|
613
|
+
@click.option(
|
|
614
|
+
"--export",
|
|
615
|
+
type=click.Path(dir_okay=False, writable=True),
|
|
616
|
+
help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
|
|
617
|
+
)
|
|
618
|
+
@click.option(
|
|
619
|
+
"--instruction-preview",
|
|
620
|
+
type=int,
|
|
621
|
+
default=0,
|
|
622
|
+
show_default=True,
|
|
623
|
+
help="Instruction preview length when printing instructions (0 shows full prompt).",
|
|
624
|
+
)
|
|
625
|
+
@output_flags()
|
|
626
|
+
@click.pass_context
|
|
627
|
+
def get(
|
|
628
|
+
ctx: Any,
|
|
629
|
+
agent_ref: str,
|
|
630
|
+
select: int | None,
|
|
631
|
+
export: str | None,
|
|
632
|
+
instruction_preview: int,
|
|
633
|
+
) -> None:
|
|
634
|
+
r"""Get agent details.
|
|
635
|
+
|
|
636
|
+
\b
|
|
637
|
+
Examples:
|
|
638
|
+
aip agents get my-agent
|
|
639
|
+
aip agents get my-agent --export agent.json # Exports complete configuration as JSON
|
|
640
|
+
aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
|
|
641
|
+
"""
|
|
642
|
+
try:
|
|
643
|
+
# Initialize API client for agent retrieval
|
|
644
|
+
api_client = get_client(ctx)
|
|
645
|
+
|
|
646
|
+
# Resolve agent reference using questionary interface for better UX
|
|
647
|
+
agent = _resolve_agent(ctx, api_client, agent_ref, select, interface_preference="questionary")
|
|
648
|
+
|
|
649
|
+
if not agent:
|
|
650
|
+
raise click.ClickException(f"Agent '{agent_ref}' not found")
|
|
651
|
+
|
|
652
|
+
# Handle export option if requested
|
|
653
|
+
if export:
|
|
654
|
+
handle_resource_export(
|
|
655
|
+
ctx,
|
|
656
|
+
agent,
|
|
657
|
+
Path(export),
|
|
658
|
+
resource_type="agent",
|
|
659
|
+
get_by_id_func=api_client.agents.get_agent_by_id,
|
|
660
|
+
console_override=console,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# Display full agent details using the standardized helper
|
|
664
|
+
_display_agent_details(
|
|
665
|
+
ctx,
|
|
666
|
+
api_client,
|
|
667
|
+
agent,
|
|
668
|
+
instruction_preview_limit=instruction_preview,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# Show run suggestions via centralized display helper
|
|
672
|
+
handle_rich_output(ctx, display_agent_run_suggestions(agent))
|
|
673
|
+
|
|
674
|
+
except click.ClickException:
|
|
675
|
+
raise
|
|
676
|
+
except Exception as e:
|
|
677
|
+
raise click.ClickException(str(e)) from e
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _validate_run_input(input_option: str | None, input_text: str | None) -> str:
|
|
681
|
+
"""Validate and determine the final input text for agent run."""
|
|
682
|
+
final_input_text = input_option if input_option else input_text
|
|
683
|
+
|
|
684
|
+
if not final_input_text:
|
|
685
|
+
raise click.ClickException("Input text is required. Use either positional argument or --input option.")
|
|
686
|
+
|
|
687
|
+
return final_input_text
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _parse_chat_history(chat_history: str | None) -> list[dict[str, Any]] | None:
|
|
691
|
+
"""Parse chat history JSON if provided."""
|
|
692
|
+
if not chat_history:
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
return json.loads(chat_history)
|
|
697
|
+
except json.JSONDecodeError as err:
|
|
698
|
+
raise click.ClickException("Invalid JSON in chat history") from err
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _setup_run_renderer(ctx: Any, save: str | None, verbose: bool) -> Any:
|
|
702
|
+
"""Set up renderer and working console for agent run."""
|
|
703
|
+
tty_enabled = bool(get_ctx_value(ctx, "tty", True))
|
|
704
|
+
return build_renderer(
|
|
705
|
+
ctx,
|
|
706
|
+
save_path=save,
|
|
707
|
+
verbose=verbose,
|
|
708
|
+
_tty_enabled=tty_enabled,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _maybe_attach_transcript_toggle(ctx: Any, renderer: Any) -> None:
|
|
713
|
+
"""Attach transcript toggle controller when interactive TTY is available."""
|
|
714
|
+
if renderer is None:
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
console_obj = getattr(renderer, "console", None)
|
|
718
|
+
if console_obj is None or not getattr(console_obj, "is_terminal", False):
|
|
719
|
+
return
|
|
720
|
+
|
|
721
|
+
tty_enabled = bool(get_ctx_value(ctx, "tty", True))
|
|
722
|
+
if not tty_enabled:
|
|
723
|
+
return
|
|
724
|
+
|
|
725
|
+
controller = TranscriptToggleController(enabled=True)
|
|
726
|
+
renderer.transcript_controller = controller
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _prepare_run_kwargs(
|
|
730
|
+
agent: Any,
|
|
731
|
+
final_input_text: str,
|
|
732
|
+
files: list[str] | None,
|
|
733
|
+
parsed_chat_history: list[dict[str, Any]] | None,
|
|
734
|
+
renderer: Any,
|
|
735
|
+
tty_enabled: bool,
|
|
736
|
+
) -> dict[str, Any]:
|
|
737
|
+
"""Prepare kwargs for agent run."""
|
|
738
|
+
run_kwargs = {
|
|
739
|
+
"agent_id": agent.id,
|
|
740
|
+
"message": final_input_text,
|
|
741
|
+
"files": list(files),
|
|
742
|
+
"agent_name": agent.name,
|
|
743
|
+
"tty": tty_enabled,
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if parsed_chat_history:
|
|
747
|
+
run_kwargs["chat_history"] = parsed_chat_history
|
|
748
|
+
|
|
749
|
+
if renderer is not None:
|
|
750
|
+
run_kwargs["renderer"] = renderer
|
|
751
|
+
|
|
752
|
+
return run_kwargs
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _handle_run_output(ctx: Any, result: Any, renderer: Any) -> None:
|
|
756
|
+
"""Handle output formatting for agent run results."""
|
|
757
|
+
printed_by_renderer = bool(renderer)
|
|
758
|
+
selected_view = get_ctx_value(ctx, "view", "rich")
|
|
759
|
+
|
|
760
|
+
if not printed_by_renderer:
|
|
761
|
+
if selected_view == "json":
|
|
762
|
+
handle_json_output(ctx, {"output": result})
|
|
763
|
+
elif selected_view == "md":
|
|
764
|
+
click.echo(f"# Assistant\n\n{result}")
|
|
765
|
+
elif selected_view == "plain":
|
|
766
|
+
click.echo(result)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _save_run_transcript(save: str | None, result: Any, working_console: Any) -> None:
|
|
770
|
+
"""Save transcript to file if requested."""
|
|
771
|
+
if not save:
|
|
772
|
+
return
|
|
773
|
+
|
|
774
|
+
ext = (save.rsplit(".", 1)[-1] or "").lower()
|
|
775
|
+
if ext == "json":
|
|
776
|
+
save_data = {
|
|
777
|
+
"output": result or "",
|
|
778
|
+
"full_debug_output": getattr(working_console, "get_captured_output", lambda: "")(),
|
|
779
|
+
"timestamp": "captured during agent execution",
|
|
780
|
+
}
|
|
781
|
+
content = json.dumps(save_data, indent=2)
|
|
782
|
+
else:
|
|
783
|
+
full_output = getattr(working_console, "get_captured_output", lambda: "")()
|
|
784
|
+
if full_output:
|
|
785
|
+
content = f"# Agent Debug Log\n\n{full_output}\n\n---\n\n## Final Result\n\n{result or ''}\n"
|
|
786
|
+
else:
|
|
787
|
+
content = f"# Assistant\n\n{result or ''}\n"
|
|
788
|
+
|
|
789
|
+
with open(save, "w", encoding="utf-8") as f:
|
|
790
|
+
f.write(content)
|
|
791
|
+
print_markup(f"[{SUCCESS_STYLE}]Full debug output saved to: {save}[/]", console=console)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@agents_group.command()
|
|
795
|
+
@click.argument("agent_ref")
|
|
796
|
+
@click.argument("input_text", required=False)
|
|
797
|
+
@click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
|
|
798
|
+
@click.option("--input", "input_option", help="Input text for the agent")
|
|
799
|
+
@click.option("--chat-history", help="JSON string of chat history")
|
|
800
|
+
@click.option(
|
|
801
|
+
"--timeout",
|
|
802
|
+
default=DEFAULT_AGENT_RUN_TIMEOUT,
|
|
803
|
+
type=int,
|
|
804
|
+
help="Agent execution timeout in seconds (default: 300s)",
|
|
805
|
+
)
|
|
806
|
+
@click.option(
|
|
807
|
+
"--save",
|
|
808
|
+
type=click.Path(dir_okay=False, writable=True),
|
|
809
|
+
help="Save transcript to file (md or json)",
|
|
810
|
+
)
|
|
811
|
+
@click.option(
|
|
812
|
+
"--file",
|
|
813
|
+
"files",
|
|
814
|
+
multiple=True,
|
|
815
|
+
type=click.Path(exists=True),
|
|
816
|
+
help="Attach file(s)",
|
|
817
|
+
)
|
|
818
|
+
@click.option(
|
|
819
|
+
"--verbose/--no-verbose",
|
|
820
|
+
default=False,
|
|
821
|
+
help="Show detailed SSE events during streaming",
|
|
822
|
+
)
|
|
823
|
+
@output_flags()
|
|
824
|
+
@click.pass_context
|
|
825
|
+
def run(
|
|
826
|
+
ctx: Any,
|
|
827
|
+
agent_ref: str,
|
|
828
|
+
select: int | None,
|
|
829
|
+
input_text: str | None,
|
|
830
|
+
input_option: str | None,
|
|
831
|
+
chat_history: str | None,
|
|
832
|
+
timeout: float | None,
|
|
833
|
+
save: str | None,
|
|
834
|
+
files: tuple[str, ...] | None,
|
|
835
|
+
verbose: bool,
|
|
836
|
+
) -> None:
|
|
837
|
+
r"""Run an agent with input text.
|
|
838
|
+
|
|
839
|
+
Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
|
|
840
|
+
|
|
841
|
+
\b
|
|
842
|
+
Examples:
|
|
843
|
+
aip agents run my-agent "Hello world"
|
|
844
|
+
aip agents run agent-123 "Process this data" --timeout 600
|
|
845
|
+
aip agents run my-agent --input "Hello world" # Legacy style
|
|
846
|
+
"""
|
|
847
|
+
final_input_text = _validate_run_input(input_option, input_text)
|
|
848
|
+
|
|
849
|
+
if verbose:
|
|
850
|
+
_emit_verbose_guidance(ctx)
|
|
851
|
+
return
|
|
852
|
+
|
|
853
|
+
try:
|
|
854
|
+
client = get_client(ctx)
|
|
855
|
+
agent = _resolve_agent(ctx, client, agent_ref, select, interface_preference="fuzzy")
|
|
856
|
+
|
|
857
|
+
parsed_chat_history = _parse_chat_history(chat_history)
|
|
858
|
+
renderer, working_console = _setup_run_renderer(ctx, save, verbose)
|
|
859
|
+
_maybe_attach_transcript_toggle(ctx, renderer)
|
|
860
|
+
|
|
861
|
+
try:
|
|
862
|
+
client.timeout = float(timeout)
|
|
863
|
+
except Exception:
|
|
864
|
+
pass
|
|
865
|
+
|
|
866
|
+
run_kwargs = _prepare_run_kwargs(
|
|
867
|
+
agent,
|
|
868
|
+
final_input_text,
|
|
869
|
+
files,
|
|
870
|
+
parsed_chat_history,
|
|
871
|
+
renderer,
|
|
872
|
+
bool(get_ctx_value(ctx, "tty", True)),
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
result = client.agents.run_agent(**run_kwargs, timeout=timeout)
|
|
876
|
+
|
|
877
|
+
slash_mode = _running_in_slash_mode(ctx)
|
|
878
|
+
agent_id = str(_safe_agent_attribute(agent, "id") or "") or None
|
|
879
|
+
agent_name = _safe_agent_attribute(agent, "name")
|
|
880
|
+
model_hint = _get_agent_model_name(agent)
|
|
881
|
+
|
|
882
|
+
transcript_context = store_transcript_for_session(
|
|
883
|
+
ctx,
|
|
884
|
+
renderer,
|
|
885
|
+
final_result=result,
|
|
886
|
+
agent_id=agent_id,
|
|
887
|
+
agent_name=agent_name,
|
|
888
|
+
model=model_hint,
|
|
889
|
+
source="slash" if slash_mode else "cli",
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
_handle_run_output(ctx, result, renderer)
|
|
893
|
+
_save_run_transcript(save, result, working_console)
|
|
894
|
+
maybe_launch_post_run_viewer(
|
|
895
|
+
ctx,
|
|
896
|
+
transcript_context,
|
|
897
|
+
console=console,
|
|
898
|
+
slash_mode=slash_mode,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
except AgentTimeoutError as e:
|
|
902
|
+
error_msg = str(e)
|
|
903
|
+
handle_json_output(ctx, error=Exception(error_msg))
|
|
904
|
+
raise click.ClickException(error_msg) from e
|
|
905
|
+
except Exception as e:
|
|
906
|
+
_handle_command_exception(ctx, e)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _running_in_slash_mode(ctx: Any) -> bool:
|
|
910
|
+
"""Return True if the command is executing inside the slash session."""
|
|
911
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
912
|
+
return isinstance(ctx_obj, dict) and bool(ctx_obj.get("_slash_session"))
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _emit_verbose_guidance(ctx: Any) -> None:
|
|
916
|
+
"""Explain the modern alternative to the deprecated --verbose flag."""
|
|
917
|
+
if _running_in_slash_mode(ctx):
|
|
918
|
+
message = (
|
|
919
|
+
"[dim]Tip:[/] Verbose streaming has been retired in the command palette. Run the agent normally and open "
|
|
920
|
+
"the post-run viewer (Ctrl+T) to inspect the transcript."
|
|
921
|
+
)
|
|
922
|
+
else:
|
|
923
|
+
message = (
|
|
924
|
+
"[dim]Tip:[/] `--verbose` is no longer supported. Re-run without the flag and toggle the post-run viewer "
|
|
925
|
+
"(Ctrl+T) for detailed output."
|
|
926
|
+
)
|
|
927
|
+
handle_rich_output(ctx, markup_text(message))
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _handle_import_file_logic(
|
|
931
|
+
import_file: str,
|
|
932
|
+
model: str | None,
|
|
933
|
+
name: str,
|
|
934
|
+
instruction: str,
|
|
935
|
+
tools: tuple[str, ...],
|
|
936
|
+
agents: tuple[str, ...],
|
|
937
|
+
mcps: tuple[str, ...],
|
|
938
|
+
timeout: float | None,
|
|
939
|
+
) -> dict[str, Any]:
|
|
940
|
+
"""Handle import file logic and merge with CLI args."""
|
|
941
|
+
import_data = load_resource_from_file(Path(import_file), "agent")
|
|
942
|
+
import_data = convert_export_to_import_format(import_data)
|
|
943
|
+
import_data = normalize_agent_config_for_import(import_data, model)
|
|
944
|
+
|
|
945
|
+
cli_args = {
|
|
946
|
+
"name": name,
|
|
947
|
+
"instruction": instruction,
|
|
948
|
+
"model": model,
|
|
949
|
+
"tools": tools or (),
|
|
950
|
+
"agents": agents or (),
|
|
951
|
+
"mcps": mcps or (),
|
|
952
|
+
"timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return merge_import_with_cli_args(import_data, cli_args)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _build_cli_args_data(
|
|
959
|
+
name: str,
|
|
960
|
+
instruction: str,
|
|
961
|
+
model: str | None,
|
|
962
|
+
tools: tuple[str, ...],
|
|
963
|
+
agents: tuple[str, ...],
|
|
964
|
+
mcps: tuple[str, ...],
|
|
965
|
+
timeout: float | None,
|
|
966
|
+
) -> dict[str, Any]:
|
|
967
|
+
"""Build merged data from CLI arguments."""
|
|
968
|
+
return {
|
|
969
|
+
"name": name,
|
|
970
|
+
"instruction": instruction,
|
|
971
|
+
"model": model,
|
|
972
|
+
"tools": tools or (),
|
|
973
|
+
"agents": agents or (),
|
|
974
|
+
"mcps": mcps or (),
|
|
975
|
+
"timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _extract_and_validate_fields(
|
|
980
|
+
merged_data: dict[str, Any],
|
|
981
|
+
) -> tuple[str, str, str | None, tuple, tuple, tuple, Any]:
|
|
982
|
+
"""Extract and validate required fields from merged data."""
|
|
983
|
+
name = merged_data.get("name")
|
|
984
|
+
instruction = merged_data.get("instruction")
|
|
985
|
+
model = merged_data.get("model")
|
|
986
|
+
tools = tuple(merged_data.get("tools", ()))
|
|
987
|
+
agents = tuple(merged_data.get("agents", ()))
|
|
988
|
+
mcps = tuple(merged_data.get("mcps", ()))
|
|
989
|
+
timeout = merged_data.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
990
|
+
|
|
991
|
+
# Validate required fields
|
|
992
|
+
if not name:
|
|
993
|
+
raise click.ClickException("Agent name is required (--name or --import)")
|
|
994
|
+
if not instruction:
|
|
995
|
+
raise click.ClickException("Agent instruction is required (--instruction or --import)")
|
|
996
|
+
|
|
997
|
+
return name, instruction, model, tools, agents, mcps, timeout
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _validate_and_coerce_fields(name: str, instruction: str, timeout: Any) -> tuple[str, str, Any]:
|
|
1001
|
+
"""Validate and coerce field values."""
|
|
1002
|
+
name = validate_agent_name(name)
|
|
1003
|
+
instruction = validate_agent_instruction(instruction)
|
|
1004
|
+
timeout = coerce_timeout(timeout)
|
|
1005
|
+
if timeout is not None:
|
|
1006
|
+
timeout = validate_timeout(timeout)
|
|
1007
|
+
|
|
1008
|
+
return name, instruction, timeout
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def _resolve_resources(client: Any, tools: tuple, agents: tuple, mcps: tuple) -> tuple[list, list, list]:
|
|
1012
|
+
"""Resolve tool, agent, and MCP references."""
|
|
1013
|
+
resolved_tools = _resolve_resources_by_name(client, tools, "tool", client.find_tools, "Tool")
|
|
1014
|
+
resolved_agents = _resolve_resources_by_name(client, agents, "agent", client.find_agents, "Agent")
|
|
1015
|
+
resolved_mcps = _resolve_resources_by_name(client, mcps, "mcp", client.find_mcps, "MCP")
|
|
1016
|
+
|
|
1017
|
+
return resolved_tools, resolved_agents, resolved_mcps
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def _build_create_kwargs(
|
|
1021
|
+
name: str,
|
|
1022
|
+
instruction: str,
|
|
1023
|
+
resolved_tools: list,
|
|
1024
|
+
resolved_agents: list,
|
|
1025
|
+
resolved_mcps: list,
|
|
1026
|
+
timeout: Any,
|
|
1027
|
+
merged_data: dict[str, Any],
|
|
1028
|
+
model: str | None,
|
|
1029
|
+
import_file: str | None,
|
|
1030
|
+
) -> dict[str, Any]:
|
|
1031
|
+
"""Build create_agent kwargs with all necessary parameters."""
|
|
1032
|
+
create_kwargs = {
|
|
1033
|
+
"name": name,
|
|
1034
|
+
"instruction": instruction,
|
|
1035
|
+
"tools": resolved_tools or None,
|
|
1036
|
+
"agents": resolved_agents or None,
|
|
1037
|
+
"mcps": resolved_mcps or None,
|
|
1038
|
+
"timeout": timeout,
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
# Handle language model selection
|
|
1042
|
+
lm_selection_dict, should_strip_lm_identity = resolve_language_model_selection(merged_data, model)
|
|
1043
|
+
create_kwargs.update(lm_selection_dict)
|
|
1044
|
+
|
|
1045
|
+
# Handle import file specific logic
|
|
1046
|
+
if import_file:
|
|
1047
|
+
_add_import_file_attributes(create_kwargs, merged_data, should_strip_lm_identity)
|
|
1048
|
+
|
|
1049
|
+
return create_kwargs
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _add_import_file_attributes(
|
|
1053
|
+
create_kwargs: dict[str, Any],
|
|
1054
|
+
merged_data: dict[str, Any],
|
|
1055
|
+
should_strip_lm_identity: bool,
|
|
1056
|
+
) -> None:
|
|
1057
|
+
"""Add import file specific attributes to create_kwargs."""
|
|
1058
|
+
agent_config_raw = merged_data.get("agent_config")
|
|
1059
|
+
if isinstance(agent_config_raw, dict):
|
|
1060
|
+
create_kwargs["agent_config"] = sanitize_agent_config(
|
|
1061
|
+
agent_config_raw, strip_lm_identity=should_strip_lm_identity
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
# Add other attributes from import data
|
|
1065
|
+
excluded_fields = {
|
|
1066
|
+
"name",
|
|
1067
|
+
"instruction",
|
|
1068
|
+
"model",
|
|
1069
|
+
"language_model_id",
|
|
1070
|
+
"tools",
|
|
1071
|
+
"agents",
|
|
1072
|
+
"timeout",
|
|
1073
|
+
"agent_config",
|
|
1074
|
+
"id",
|
|
1075
|
+
"created_at",
|
|
1076
|
+
"updated_at",
|
|
1077
|
+
"type",
|
|
1078
|
+
"framework",
|
|
1079
|
+
"version",
|
|
1080
|
+
"mcps",
|
|
1081
|
+
"a2a_profile",
|
|
1082
|
+
}
|
|
1083
|
+
for key, value in merged_data.items():
|
|
1084
|
+
if key not in excluded_fields and value is not None:
|
|
1085
|
+
create_kwargs[key] = value
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def _get_language_model_display_name(agent: Any, model: str | None) -> str:
|
|
1089
|
+
"""Get display name for the language model."""
|
|
1090
|
+
lm_display = getattr(agent, "model", None)
|
|
1091
|
+
if not lm_display:
|
|
1092
|
+
cfg = getattr(agent, "agent_config", {}) or {}
|
|
1093
|
+
lm_display = cfg.get("lm_name") or cfg.get("model") or model or f"{DEFAULT_MODEL} (backend default)"
|
|
1094
|
+
return lm_display
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _handle_successful_creation(ctx: Any, agent: Any, model: str | None) -> None:
|
|
1098
|
+
"""Handle successful agent creation output."""
|
|
1099
|
+
handle_json_output(ctx, _prepare_agent_output(agent))
|
|
1100
|
+
|
|
1101
|
+
lm_display = _get_language_model_display_name(agent, model)
|
|
1102
|
+
|
|
1103
|
+
handle_rich_output(
|
|
1104
|
+
ctx,
|
|
1105
|
+
display_creation_success(
|
|
1106
|
+
"Agent",
|
|
1107
|
+
agent.name,
|
|
1108
|
+
agent.id,
|
|
1109
|
+
Model=lm_display,
|
|
1110
|
+
Type=getattr(agent, "type", "config"),
|
|
1111
|
+
Framework=getattr(agent, "framework", "langchain"),
|
|
1112
|
+
Version=getattr(agent, "version", "1.0"),
|
|
1113
|
+
),
|
|
1114
|
+
)
|
|
1115
|
+
handle_rich_output(ctx, display_agent_run_suggestions(agent))
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _handle_command_exception(ctx: Any, e: Exception) -> None:
|
|
1119
|
+
"""Handle exceptions during command execution with consistent error handling."""
|
|
1120
|
+
if isinstance(e, click.ClickException):
|
|
1121
|
+
if get_ctx_value(ctx, "view") == "json":
|
|
1122
|
+
handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
|
|
1123
|
+
raise
|
|
1124
|
+
|
|
1125
|
+
handle_json_output(ctx, error=e)
|
|
1126
|
+
if get_ctx_value(ctx, "view") != "json":
|
|
1127
|
+
print_api_error(e)
|
|
1128
|
+
raise click.exceptions.Exit(1) from e
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _handle_creation_exception(ctx: Any, e: Exception) -> None:
|
|
1132
|
+
"""Handle exceptions during agent creation."""
|
|
1133
|
+
_handle_command_exception(ctx, e)
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
@agents_group.command()
|
|
1137
|
+
@click.option("--name", help="Agent name")
|
|
1138
|
+
@click.option("--instruction", help="Agent instruction (prompt)")
|
|
1139
|
+
@click.option(
|
|
1140
|
+
"--model",
|
|
1141
|
+
help=f"Language model to use (e.g., {DEFAULT_MODEL}, default: {DEFAULT_MODEL})",
|
|
1142
|
+
)
|
|
1143
|
+
@click.option("--tools", multiple=True, help="Tool names or IDs to attach")
|
|
1144
|
+
@click.option("--agents", multiple=True, help="Sub-agent names or IDs to attach")
|
|
1145
|
+
@click.option("--mcps", multiple=True, help="MCP names or IDs to attach")
|
|
1146
|
+
@click.option(
|
|
1147
|
+
"--timeout",
|
|
1148
|
+
default=DEFAULT_AGENT_RUN_TIMEOUT,
|
|
1149
|
+
type=int,
|
|
1150
|
+
help="Agent execution timeout in seconds (default: 300s)",
|
|
1151
|
+
)
|
|
1152
|
+
@click.option(
|
|
1153
|
+
"--import",
|
|
1154
|
+
"import_file",
|
|
1155
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
1156
|
+
help="Import agent configuration from JSON file",
|
|
1157
|
+
)
|
|
1158
|
+
@output_flags()
|
|
1159
|
+
@click.pass_context
|
|
1160
|
+
def create(
|
|
1161
|
+
ctx: Any,
|
|
1162
|
+
name: str,
|
|
1163
|
+
instruction: str,
|
|
1164
|
+
model: str | None,
|
|
1165
|
+
tools: tuple[str, ...] | None,
|
|
1166
|
+
agents: tuple[str, ...] | None,
|
|
1167
|
+
mcps: tuple[str, ...] | None,
|
|
1168
|
+
timeout: float | None,
|
|
1169
|
+
import_file: str | None,
|
|
1170
|
+
) -> None:
|
|
1171
|
+
r"""Create a new agent.
|
|
1172
|
+
|
|
1173
|
+
\b
|
|
1174
|
+
Examples:
|
|
1175
|
+
aip agents create --name "My Agent" --instruction "You are a helpful assistant"
|
|
1176
|
+
aip agents create --import agent.json
|
|
1177
|
+
"""
|
|
1178
|
+
try:
|
|
1179
|
+
client = get_client(ctx)
|
|
1180
|
+
|
|
1181
|
+
# Handle import file or CLI args
|
|
1182
|
+
if import_file:
|
|
1183
|
+
merged_data = _handle_import_file_logic(import_file, model, name, instruction, tools, agents, mcps, timeout)
|
|
1184
|
+
else:
|
|
1185
|
+
merged_data = _build_cli_args_data(name, instruction, model, tools, agents, mcps, timeout)
|
|
1186
|
+
|
|
1187
|
+
# Extract and validate fields
|
|
1188
|
+
(
|
|
1189
|
+
name,
|
|
1190
|
+
instruction,
|
|
1191
|
+
model,
|
|
1192
|
+
tools,
|
|
1193
|
+
agents,
|
|
1194
|
+
mcps,
|
|
1195
|
+
timeout,
|
|
1196
|
+
) = _extract_and_validate_fields(merged_data)
|
|
1197
|
+
name, instruction, timeout = _validate_and_coerce_fields(name, instruction, timeout)
|
|
1198
|
+
|
|
1199
|
+
# Resolve resources
|
|
1200
|
+
resolved_tools, resolved_agents, resolved_mcps = _resolve_resources(client, tools, agents, mcps)
|
|
1201
|
+
|
|
1202
|
+
# Build create kwargs
|
|
1203
|
+
create_kwargs = _build_create_kwargs(
|
|
1204
|
+
name,
|
|
1205
|
+
instruction,
|
|
1206
|
+
resolved_tools,
|
|
1207
|
+
resolved_agents,
|
|
1208
|
+
resolved_mcps,
|
|
1209
|
+
timeout,
|
|
1210
|
+
merged_data,
|
|
1211
|
+
model,
|
|
1212
|
+
import_file,
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
# Create agent
|
|
1216
|
+
agent = client.agents.create_agent(**create_kwargs)
|
|
1217
|
+
|
|
1218
|
+
# Handle successful creation
|
|
1219
|
+
_handle_successful_creation(ctx, agent, model)
|
|
1220
|
+
|
|
1221
|
+
except Exception as e:
|
|
1222
|
+
_handle_creation_exception(ctx, e)
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _get_agent_for_update(client: Any, agent_id: str) -> Any:
|
|
1226
|
+
"""Retrieve agent by ID for update operation."""
|
|
1227
|
+
try:
|
|
1228
|
+
return client.agents.get_agent_by_id(agent_id)
|
|
1229
|
+
except Exception as e:
|
|
1230
|
+
raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}") from e
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
def _handle_update_import_file(
|
|
1234
|
+
import_file: str | None,
|
|
1235
|
+
name: str | None,
|
|
1236
|
+
instruction: str | None,
|
|
1237
|
+
tools: tuple[str, ...] | None,
|
|
1238
|
+
agents: tuple[str, ...] | None,
|
|
1239
|
+
mcps: tuple[str, ...] | None,
|
|
1240
|
+
timeout: float | None,
|
|
1241
|
+
) -> tuple[
|
|
1242
|
+
Any | None,
|
|
1243
|
+
str | None,
|
|
1244
|
+
str | None,
|
|
1245
|
+
tuple[str, ...] | None,
|
|
1246
|
+
tuple[str, ...] | None,
|
|
1247
|
+
tuple[str, ...] | None,
|
|
1248
|
+
float | None,
|
|
1249
|
+
]:
|
|
1250
|
+
"""Handle import file processing for agent update."""
|
|
1251
|
+
if not import_file:
|
|
1252
|
+
return None, name, instruction, tools, agents, mcps, timeout
|
|
1253
|
+
|
|
1254
|
+
import_data = load_resource_from_file(Path(import_file), "agent")
|
|
1255
|
+
import_data = convert_export_to_import_format(import_data)
|
|
1256
|
+
import_data = normalize_agent_config_for_import(import_data, None)
|
|
1257
|
+
|
|
1258
|
+
cli_args = {
|
|
1259
|
+
"name": name,
|
|
1260
|
+
"instruction": instruction,
|
|
1261
|
+
"tools": tools or (),
|
|
1262
|
+
"agents": agents or (),
|
|
1263
|
+
"mcps": mcps or (),
|
|
1264
|
+
"timeout": timeout,
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
merged_data = merge_import_with_cli_args(import_data, cli_args)
|
|
1268
|
+
|
|
1269
|
+
return (
|
|
1270
|
+
merged_data,
|
|
1271
|
+
merged_data.get("name"),
|
|
1272
|
+
merged_data.get("instruction"),
|
|
1273
|
+
tuple(merged_data.get("tools", ())),
|
|
1274
|
+
tuple(merged_data.get("agents", ())),
|
|
1275
|
+
tuple(merged_data.get("mcps", ())),
|
|
1276
|
+
coerce_timeout(merged_data.get("timeout")),
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def _build_update_data(
|
|
1281
|
+
name: str | None,
|
|
1282
|
+
instruction: str | None,
|
|
1283
|
+
tools: tuple[str, ...] | None,
|
|
1284
|
+
agents: tuple[str, ...] | None,
|
|
1285
|
+
mcps: tuple[str, ...] | None,
|
|
1286
|
+
timeout: float | None,
|
|
1287
|
+
) -> dict[str, Any]:
|
|
1288
|
+
"""Build the update data dictionary from provided parameters."""
|
|
1289
|
+
update_data = {}
|
|
1290
|
+
if name is not None:
|
|
1291
|
+
update_data["name"] = name
|
|
1292
|
+
if instruction is not None:
|
|
1293
|
+
update_data["instruction"] = instruction
|
|
1294
|
+
if tools:
|
|
1295
|
+
update_data["tools"] = list(tools)
|
|
1296
|
+
if agents:
|
|
1297
|
+
update_data["agents"] = list(agents)
|
|
1298
|
+
if mcps:
|
|
1299
|
+
update_data["mcps"] = list(mcps)
|
|
1300
|
+
if timeout is not None:
|
|
1301
|
+
update_data["timeout"] = timeout
|
|
1302
|
+
return update_data
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _handle_update_import_config(
|
|
1306
|
+
import_file: str | None, merged_data: dict[str, Any], update_data: dict[str, Any]
|
|
1307
|
+
) -> None:
|
|
1308
|
+
"""Handle agent config and additional attributes for import-based updates."""
|
|
1309
|
+
if not import_file:
|
|
1310
|
+
return
|
|
1311
|
+
|
|
1312
|
+
lm_selection, should_strip_lm_identity = resolve_language_model_selection(merged_data, None)
|
|
1313
|
+
update_data.update(lm_selection)
|
|
1314
|
+
|
|
1315
|
+
raw_cfg = merged_data.get("agent_config") if isinstance(merged_data, dict) else None
|
|
1316
|
+
if isinstance(raw_cfg, dict):
|
|
1317
|
+
update_data["agent_config"] = sanitize_agent_config(raw_cfg, strip_lm_identity=should_strip_lm_identity)
|
|
1318
|
+
|
|
1319
|
+
excluded_fields = {
|
|
1320
|
+
"name",
|
|
1321
|
+
"instruction",
|
|
1322
|
+
"tools",
|
|
1323
|
+
"agents",
|
|
1324
|
+
"timeout",
|
|
1325
|
+
"agent_config",
|
|
1326
|
+
"language_model_id",
|
|
1327
|
+
"id",
|
|
1328
|
+
"created_at",
|
|
1329
|
+
"updated_at",
|
|
1330
|
+
"type",
|
|
1331
|
+
"framework",
|
|
1332
|
+
"version",
|
|
1333
|
+
"a2a_profile",
|
|
1334
|
+
}
|
|
1335
|
+
for key, value in merged_data.items():
|
|
1336
|
+
if key not in excluded_fields and value is not None:
|
|
1337
|
+
update_data[key] = value
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
@agents_group.command()
|
|
1341
|
+
@click.argument("agent_id")
|
|
1342
|
+
@click.option("--name", help="New agent name")
|
|
1343
|
+
@click.option("--instruction", help="New instruction")
|
|
1344
|
+
@click.option("--tools", multiple=True, help="New tool names or IDs")
|
|
1345
|
+
@click.option("--agents", multiple=True, help="New sub-agent names")
|
|
1346
|
+
@click.option("--mcps", multiple=True, help="New MCP names or IDs")
|
|
1347
|
+
@click.option("--timeout", type=int, help="New timeout value")
|
|
1348
|
+
@click.option(
|
|
1349
|
+
"--import",
|
|
1350
|
+
"import_file",
|
|
1351
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
1352
|
+
help="Import agent configuration from JSON file",
|
|
1353
|
+
)
|
|
1354
|
+
@output_flags()
|
|
1355
|
+
@click.pass_context
|
|
1356
|
+
def update(
|
|
1357
|
+
ctx: Any,
|
|
1358
|
+
agent_id: str,
|
|
1359
|
+
name: str | None,
|
|
1360
|
+
instruction: str | None,
|
|
1361
|
+
tools: tuple[str, ...] | None,
|
|
1362
|
+
agents: tuple[str, ...] | None,
|
|
1363
|
+
mcps: tuple[str, ...] | None,
|
|
1364
|
+
timeout: float | None,
|
|
1365
|
+
import_file: str | None,
|
|
1366
|
+
) -> None:
|
|
1367
|
+
r"""Update an existing agent.
|
|
1368
|
+
|
|
1369
|
+
\b
|
|
1370
|
+
Examples:
|
|
1371
|
+
aip agents update my-agent --instruction "New instruction"
|
|
1372
|
+
aip agents update my-agent --import agent.json
|
|
1373
|
+
"""
|
|
1374
|
+
try:
|
|
1375
|
+
client = get_client(ctx)
|
|
1376
|
+
agent = _get_agent_for_update(client, agent_id)
|
|
1377
|
+
|
|
1378
|
+
# Handle import file processing
|
|
1379
|
+
(
|
|
1380
|
+
merged_data,
|
|
1381
|
+
name,
|
|
1382
|
+
instruction,
|
|
1383
|
+
tools,
|
|
1384
|
+
agents,
|
|
1385
|
+
mcps,
|
|
1386
|
+
timeout,
|
|
1387
|
+
) = _handle_update_import_file(import_file, name, instruction, tools, agents, mcps, timeout)
|
|
1388
|
+
|
|
1389
|
+
update_data = _build_update_data(name, instruction, tools, agents, mcps, timeout)
|
|
1390
|
+
|
|
1391
|
+
if merged_data:
|
|
1392
|
+
_handle_update_import_config(import_file, merged_data, update_data)
|
|
1393
|
+
# Ensure instruction from import file is included if not already set via CLI
|
|
1394
|
+
# This handles the case where instruction is None in CLI args but exists in import file
|
|
1395
|
+
if import_file and (instruction is None or "instruction" not in update_data):
|
|
1396
|
+
import_instruction = merged_data.get("instruction")
|
|
1397
|
+
if import_instruction is not None:
|
|
1398
|
+
update_data["instruction"] = import_instruction
|
|
1399
|
+
|
|
1400
|
+
if not update_data:
|
|
1401
|
+
raise click.ClickException("No update fields specified")
|
|
1402
|
+
|
|
1403
|
+
updated_agent = client.agents.update_agent(agent.id, **update_data)
|
|
1404
|
+
|
|
1405
|
+
handle_json_output(ctx, _prepare_agent_output(updated_agent))
|
|
1406
|
+
handle_rich_output(ctx, display_update_success("Agent", updated_agent.name))
|
|
1407
|
+
handle_rich_output(ctx, display_agent_run_suggestions(updated_agent))
|
|
1408
|
+
|
|
1409
|
+
except click.ClickException:
|
|
1410
|
+
# Handle JSON output for ClickExceptions if view is JSON
|
|
1411
|
+
if get_ctx_value(ctx, "view") == "json":
|
|
1412
|
+
handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
|
|
1413
|
+
# Re-raise ClickExceptions without additional processing
|
|
1414
|
+
raise
|
|
1415
|
+
except Exception as e:
|
|
1416
|
+
_handle_command_exception(ctx, e)
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
@agents_group.command()
|
|
1420
|
+
@click.argument("agent_id")
|
|
1421
|
+
@click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
|
|
1422
|
+
@output_flags()
|
|
1423
|
+
@click.pass_context
|
|
1424
|
+
def delete(ctx: Any, agent_id: str, yes: bool) -> None:
|
|
1425
|
+
"""Delete an agent."""
|
|
1426
|
+
try:
|
|
1427
|
+
client = get_client(ctx)
|
|
1428
|
+
|
|
1429
|
+
# Get agent by ID (no ambiguity handling needed)
|
|
1430
|
+
try:
|
|
1431
|
+
agent = client.agents.get_agent_by_id(agent_id)
|
|
1432
|
+
except Exception as e:
|
|
1433
|
+
raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}") from e
|
|
1434
|
+
|
|
1435
|
+
# Confirm deletion when not forced
|
|
1436
|
+
if not yes and not display_confirmation_prompt("Agent", agent.name):
|
|
1437
|
+
return
|
|
1438
|
+
|
|
1439
|
+
client.agents.delete_agent(agent.id)
|
|
1440
|
+
|
|
1441
|
+
handle_json_output(
|
|
1442
|
+
ctx,
|
|
1443
|
+
{
|
|
1444
|
+
"success": True,
|
|
1445
|
+
"message": f"Agent '{agent.name}' deleted",
|
|
1446
|
+
},
|
|
1447
|
+
)
|
|
1448
|
+
handle_rich_output(ctx, display_deletion_success("Agent", agent.name))
|
|
1449
|
+
|
|
1450
|
+
except click.ClickException:
|
|
1451
|
+
# Handle JSON output for ClickExceptions if view is JSON
|
|
1452
|
+
if get_ctx_value(ctx, "view") == "json":
|
|
1453
|
+
handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
|
|
1454
|
+
# Re-raise ClickExceptions without additional processing
|
|
1455
|
+
raise
|
|
1456
|
+
except Exception as e:
|
|
1457
|
+
_handle_command_exception(ctx, e)
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
@agents_group.command()
|
|
1461
|
+
@click.option(
|
|
1462
|
+
"--base-url",
|
|
1463
|
+
help="Custom LangFlow server base URL (overrides LANGFLOW_BASE_URL env var)",
|
|
1464
|
+
)
|
|
1465
|
+
@click.option("--api-key", help="Custom LangFlow API key (overrides LANGFLOW_API_KEY env var)")
|
|
1466
|
+
@output_flags()
|
|
1467
|
+
@click.pass_context
|
|
1468
|
+
def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
|
|
1469
|
+
r"""Sync agents with LangFlow server flows.
|
|
1470
|
+
|
|
1471
|
+
This command fetches all flows from the configured LangFlow server and
|
|
1472
|
+
creates/updates corresponding agents in the platform.
|
|
1473
|
+
|
|
1474
|
+
The LangFlow server configuration can be provided via:
|
|
1475
|
+
- Command options (--base-url, --api-key)
|
|
1476
|
+
- Environment variables (LANGFLOW_BASE_URL, LANGFLOW_API_KEY)
|
|
1477
|
+
|
|
1478
|
+
\b
|
|
1479
|
+
Examples:
|
|
1480
|
+
aip agents sync-langflow
|
|
1481
|
+
aip agents sync-langflow --base-url https://my-langflow.com --api-key my-key
|
|
1482
|
+
"""
|
|
1483
|
+
try:
|
|
1484
|
+
client = get_client(ctx)
|
|
1485
|
+
|
|
1486
|
+
# Perform the sync
|
|
1487
|
+
result = client.sync_langflow_agents(base_url=base_url, api_key=api_key)
|
|
1488
|
+
|
|
1489
|
+
# Handle output format
|
|
1490
|
+
handle_json_output(ctx, result)
|
|
1491
|
+
|
|
1492
|
+
# Show success message for non-JSON output
|
|
1493
|
+
if get_ctx_value(ctx, "view") != "json":
|
|
1494
|
+
# Extract some useful info from the result
|
|
1495
|
+
success_count = result.get("data", {}).get("created_count", 0) + result.get("data", {}).get(
|
|
1496
|
+
"updated_count", 0
|
|
1497
|
+
)
|
|
1498
|
+
total_count = result.get("data", {}).get("total_processed", 0)
|
|
1499
|
+
|
|
1500
|
+
handle_rich_output(
|
|
1501
|
+
ctx,
|
|
1502
|
+
markup_text(
|
|
1503
|
+
f"[{SUCCESS_STYLE}]✅ Successfully synced {success_count} LangFlow agents "
|
|
1504
|
+
f"({total_count} total processed)[/]"
|
|
1505
|
+
),
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
except Exception as e:
|
|
1509
|
+
_handle_command_exception(ctx, e)
|