glaip-sdk 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/__init__.py +2 -2
- glaip_sdk/_version.py +51 -0
- glaip_sdk/branding.py +145 -0
- glaip_sdk/cli/commands/agents.py +876 -166
- glaip_sdk/cli/commands/configure.py +46 -104
- glaip_sdk/cli/commands/init.py +43 -118
- glaip_sdk/cli/commands/mcps.py +86 -161
- glaip_sdk/cli/commands/tools.py +196 -57
- glaip_sdk/cli/main.py +43 -29
- glaip_sdk/cli/utils.py +258 -27
- glaip_sdk/client/__init__.py +54 -2
- glaip_sdk/client/agents.py +196 -237
- glaip_sdk/client/base.py +62 -2
- glaip_sdk/client/mcps.py +63 -20
- glaip_sdk/client/tools.py +236 -81
- glaip_sdk/config/constants.py +10 -3
- glaip_sdk/exceptions.py +13 -0
- glaip_sdk/models.py +21 -5
- glaip_sdk/utils/__init__.py +116 -18
- glaip_sdk/utils/client_utils.py +284 -0
- glaip_sdk/utils/rendering/__init__.py +1 -0
- glaip_sdk/utils/rendering/formatting.py +211 -0
- glaip_sdk/utils/rendering/models.py +53 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +38 -0
- glaip_sdk/utils/rendering/renderer/base.py +827 -0
- glaip_sdk/utils/rendering/renderer/config.py +33 -0
- glaip_sdk/utils/rendering/renderer/console.py +54 -0
- glaip_sdk/utils/rendering/renderer/debug.py +82 -0
- glaip_sdk/utils/rendering/renderer/panels.py +123 -0
- glaip_sdk/utils/rendering/renderer/progress.py +118 -0
- glaip_sdk/utils/rendering/renderer/stream.py +198 -0
- glaip_sdk/utils/rendering/steps.py +168 -0
- glaip_sdk/utils/run_renderer.py +22 -1086
- {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/METADATA +8 -36
- glaip_sdk-0.0.4.dist-info/RECORD +41 -0
- glaip_sdk/cli/config.py +0 -592
- glaip_sdk/utils.py +0 -167
- glaip_sdk-0.0.2.dist-info/RECORD +0 -28
- {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/commands/agents.py
CHANGED
|
@@ -1,63 +1,538 @@
|
|
|
1
|
-
"""Agent
|
|
1
|
+
"""Agent CLI commands for AIP SDK.
|
|
2
2
|
|
|
3
3
|
Authors:
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
+
import os
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
8
12
|
|
|
9
13
|
import click
|
|
10
14
|
from rich.console import Console
|
|
11
15
|
from rich.panel import Panel
|
|
12
16
|
from rich.text import Text
|
|
13
17
|
|
|
18
|
+
from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
|
|
19
|
+
from glaip_sdk.exceptions import AgentTimeoutError
|
|
14
20
|
from glaip_sdk.utils import is_uuid
|
|
15
21
|
|
|
16
22
|
from ..utils import (
|
|
23
|
+
_fuzzy_pick_for_resources,
|
|
24
|
+
build_renderer,
|
|
25
|
+
coerce_to_row,
|
|
17
26
|
get_client,
|
|
18
|
-
handle_ambiguous_resource,
|
|
19
27
|
output_flags,
|
|
20
28
|
output_list,
|
|
21
29
|
output_result,
|
|
22
|
-
|
|
30
|
+
resolve_resource,
|
|
23
31
|
)
|
|
24
32
|
|
|
25
33
|
console = Console()
|
|
26
34
|
|
|
27
35
|
|
|
36
|
+
def _display_run_suggestions(agent):
|
|
37
|
+
"""Display helpful suggestions for running the agent."""
|
|
38
|
+
console.print()
|
|
39
|
+
console.print(
|
|
40
|
+
Panel(
|
|
41
|
+
f"[bold blue]💡 Next Steps:[/bold blue]\n\n"
|
|
42
|
+
f"🚀 Run this agent:\n"
|
|
43
|
+
f' [green]aip agents run {agent.id} "Your message here"[/green]\n\n'
|
|
44
|
+
f"📋 Or use the agent name:\n"
|
|
45
|
+
f' [green]aip agents run "{agent.name}" "Your message here"[/green]\n\n'
|
|
46
|
+
f"🔧 Available options:\n"
|
|
47
|
+
f" [dim]--chat-history[/dim] Include previous conversation\n"
|
|
48
|
+
f" [dim]--file[/dim] Attach files\n"
|
|
49
|
+
f" [dim]--input[/dim] Alternative input method\n"
|
|
50
|
+
f" [dim]--timeout[/dim] Set execution timeout\n"
|
|
51
|
+
f" [dim]--save[/dim] Save transcript to file\n"
|
|
52
|
+
f" [dim]--verbose[/dim] Show detailed execution\n\n"
|
|
53
|
+
f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]",
|
|
54
|
+
title="🤖 Ready to Run Agent",
|
|
55
|
+
border_style="blue",
|
|
56
|
+
padding=(0, 1),
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _format_datetime(dt):
|
|
62
|
+
"""Format datetime object to readable string."""
|
|
63
|
+
if isinstance(dt, datetime):
|
|
64
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
65
|
+
elif dt is None:
|
|
66
|
+
return "N/A"
|
|
67
|
+
return dt
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _fetch_full_agent_details(client, agent):
|
|
71
|
+
"""Fetch full agent details by ID to ensure all fields are populated."""
|
|
72
|
+
try:
|
|
73
|
+
agent_id = str(getattr(agent, "id", "")).strip()
|
|
74
|
+
if agent_id:
|
|
75
|
+
return client.agents.get_agent_by_id(agent_id)
|
|
76
|
+
except Exception:
|
|
77
|
+
# If fetching full details fails, continue with the resolved object
|
|
78
|
+
pass
|
|
79
|
+
return agent
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _build_agent_result_data(agent):
|
|
83
|
+
"""Build standardized result data for agent display."""
|
|
84
|
+
return {
|
|
85
|
+
"id": str(getattr(agent, "id", "N/A")),
|
|
86
|
+
"name": getattr(agent, "name", "N/A"),
|
|
87
|
+
"type": getattr(agent, "type", "N/A"),
|
|
88
|
+
"framework": getattr(agent, "framework", "N/A"),
|
|
89
|
+
"version": getattr(agent, "version", "N/A"),
|
|
90
|
+
"description": getattr(agent, "description", "N/A"),
|
|
91
|
+
"instruction": getattr(agent, "instruction", "") or "-",
|
|
92
|
+
"created_at": _format_datetime(getattr(agent, "created_at", "N/A")),
|
|
93
|
+
"updated_at": _format_datetime(getattr(agent, "updated_at", "N/A")),
|
|
94
|
+
"metadata": getattr(agent, "metadata", "N/A"),
|
|
95
|
+
"language_model_id": getattr(agent, "language_model_id", "N/A"),
|
|
96
|
+
"agent_config": getattr(agent, "agent_config", "N/A"),
|
|
97
|
+
"tool_configs": getattr(agent, "tool_configs", {}),
|
|
98
|
+
"tools": getattr(agent, "tools", []),
|
|
99
|
+
"agents": getattr(agent, "agents", []),
|
|
100
|
+
"mcps": getattr(agent, "mcps", []),
|
|
101
|
+
"a2a_profile": getattr(agent, "a2a_profile", "N/A"),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _get_agent_attributes(agent):
|
|
106
|
+
"""Dynamically get all relevant agent attributes for export."""
|
|
107
|
+
# Exclude these attributes from export (methods, private attrs, computed properties)
|
|
108
|
+
exclude_attrs = {
|
|
109
|
+
"id",
|
|
110
|
+
"created_at",
|
|
111
|
+
"updated_at", # System-managed fields
|
|
112
|
+
"_client",
|
|
113
|
+
"_raw_data", # Internal fields
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Methods and callable attributes to exclude
|
|
117
|
+
exclude_callables = {
|
|
118
|
+
"model_dump",
|
|
119
|
+
"dict",
|
|
120
|
+
"json", # Pydantic methods
|
|
121
|
+
"get",
|
|
122
|
+
"post",
|
|
123
|
+
"put",
|
|
124
|
+
"delete", # HTTP methods
|
|
125
|
+
"save",
|
|
126
|
+
"refresh",
|
|
127
|
+
"update", # ORM methods
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export_data = {}
|
|
131
|
+
|
|
132
|
+
# Method 1: Try Pydantic model_dump() if available (best for structured data)
|
|
133
|
+
if hasattr(agent, "model_dump") and callable(agent.model_dump):
|
|
134
|
+
try:
|
|
135
|
+
# Get all model fields
|
|
136
|
+
all_data = agent.model_dump()
|
|
137
|
+
# Filter out excluded attributes
|
|
138
|
+
for key, value in all_data.items():
|
|
139
|
+
if key not in exclude_attrs and key not in exclude_callables:
|
|
140
|
+
export_data[key] = value
|
|
141
|
+
return export_data
|
|
142
|
+
except Exception:
|
|
143
|
+
# Fall back to manual attribute detection
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# Method 2: Manual attribute inspection with filtering
|
|
147
|
+
# Get all non-private, non-method attributes
|
|
148
|
+
all_attrs = []
|
|
149
|
+
if hasattr(agent, "__dict__"):
|
|
150
|
+
all_attrs.extend(agent.__dict__.keys())
|
|
151
|
+
if hasattr(agent, "__annotations__"):
|
|
152
|
+
all_attrs.extend(agent.__annotations__.keys())
|
|
153
|
+
|
|
154
|
+
# Remove duplicates and filter
|
|
155
|
+
all_attrs = list(set(all_attrs))
|
|
156
|
+
|
|
157
|
+
for attr in all_attrs:
|
|
158
|
+
# Skip excluded attributes
|
|
159
|
+
if (
|
|
160
|
+
attr in exclude_attrs
|
|
161
|
+
or attr.startswith("_") # Private attributes
|
|
162
|
+
or attr in exclude_callables
|
|
163
|
+
):
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Skip callable attributes (methods, functions)
|
|
167
|
+
attr_value = getattr(agent, attr, None)
|
|
168
|
+
if callable(attr_value):
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Add the attribute to export data
|
|
172
|
+
export_data[attr] = attr_value
|
|
173
|
+
|
|
174
|
+
return export_data
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _build_agent_export_data(agent):
|
|
178
|
+
"""Build comprehensive export data for agent (always includes all fields)."""
|
|
179
|
+
# Get all available agent attributes dynamically
|
|
180
|
+
all_agent_data = _get_agent_attributes(agent)
|
|
181
|
+
|
|
182
|
+
# Always include all detected attributes for comprehensive export
|
|
183
|
+
# Add default timeout if not present
|
|
184
|
+
if "timeout" not in all_agent_data:
|
|
185
|
+
all_agent_data["timeout"] = DEFAULT_AGENT_RUN_TIMEOUT
|
|
186
|
+
return all_agent_data
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _get_agent_model_name(agent):
|
|
190
|
+
"""Extract model name from agent configuration."""
|
|
191
|
+
# Try different possible locations for model name
|
|
192
|
+
if hasattr(agent, "agent_config") and agent.agent_config:
|
|
193
|
+
if isinstance(agent.agent_config, dict):
|
|
194
|
+
return agent.agent_config.get("lm_name") or agent.agent_config.get("model")
|
|
195
|
+
|
|
196
|
+
if hasattr(agent, "model") and agent.model:
|
|
197
|
+
return agent.model
|
|
198
|
+
|
|
199
|
+
# Default fallback
|
|
200
|
+
return DEFAULT_MODEL
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _extract_tool_ids(agent):
|
|
204
|
+
"""Extract tool IDs from agent tools list for import compatibility."""
|
|
205
|
+
tools = getattr(agent, "tools", [])
|
|
206
|
+
if not tools:
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
ids = []
|
|
210
|
+
for tool in tools:
|
|
211
|
+
if isinstance(tool, dict):
|
|
212
|
+
tool_id = tool.get("id")
|
|
213
|
+
if tool_id:
|
|
214
|
+
ids.append(tool_id)
|
|
215
|
+
else:
|
|
216
|
+
# Fallback to name if ID not available
|
|
217
|
+
name = tool.get("name", "")
|
|
218
|
+
if name:
|
|
219
|
+
ids.append(name)
|
|
220
|
+
elif hasattr(tool, "id"):
|
|
221
|
+
ids.append(tool.id)
|
|
222
|
+
elif hasattr(tool, "name"):
|
|
223
|
+
ids.append(tool.name)
|
|
224
|
+
else:
|
|
225
|
+
ids.append(str(tool))
|
|
226
|
+
return ids
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _extract_agent_ids(agent):
|
|
230
|
+
"""Extract agent IDs from agent agents list for import compatibility."""
|
|
231
|
+
agents = getattr(agent, "agents", [])
|
|
232
|
+
if not agents:
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
ids = []
|
|
236
|
+
for sub_agent in agents:
|
|
237
|
+
if isinstance(sub_agent, dict):
|
|
238
|
+
agent_id = sub_agent.get("id")
|
|
239
|
+
if agent_id:
|
|
240
|
+
ids.append(agent_id)
|
|
241
|
+
else:
|
|
242
|
+
# Fallback to name if ID not available
|
|
243
|
+
name = sub_agent.get("name", "")
|
|
244
|
+
if name:
|
|
245
|
+
ids.append(name)
|
|
246
|
+
elif hasattr(sub_agent, "id"):
|
|
247
|
+
ids.append(sub_agent.id)
|
|
248
|
+
elif hasattr(sub_agent, "name"):
|
|
249
|
+
ids.append(sub_agent.name)
|
|
250
|
+
else:
|
|
251
|
+
ids.append(str(sub_agent))
|
|
252
|
+
return ids
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _export_agent_to_file(agent, file_path: Path, format: str = "json"):
|
|
256
|
+
"""Export agent to file (JSON or YAML) with comprehensive data."""
|
|
257
|
+
export_data = _build_agent_export_data(agent)
|
|
258
|
+
|
|
259
|
+
if format.lower() == "yaml" or file_path.suffix.lower() in [".yaml", ".yml"]:
|
|
260
|
+
_write_yaml_to_file(file_path, export_data)
|
|
261
|
+
else:
|
|
262
|
+
_write_json_to_file(file_path, export_data)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _load_agent_from_file(file_path: Path) -> dict[str, Any]:
|
|
266
|
+
"""Load agent data from JSON or YAML file."""
|
|
267
|
+
if file_path.suffix.lower() in [".yaml", ".yml"]:
|
|
268
|
+
return _read_yaml_from_file(file_path)
|
|
269
|
+
else:
|
|
270
|
+
return _read_json_from_file(file_path)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _read_yaml_from_file(file_path: Path) -> dict[str, Any]:
|
|
274
|
+
"""Read YAML data from file."""
|
|
275
|
+
try:
|
|
276
|
+
import yaml
|
|
277
|
+
except ImportError:
|
|
278
|
+
raise click.ClickException(
|
|
279
|
+
"PyYAML is required for YAML import. Install with: pip install PyYAML"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if not file_path.exists():
|
|
283
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
284
|
+
|
|
285
|
+
with open(file_path, encoding="utf-8") as f:
|
|
286
|
+
data = yaml.safe_load(f)
|
|
287
|
+
|
|
288
|
+
# Handle instruction_lines array format for user-friendly YAML
|
|
289
|
+
if "instruction_lines" in data and isinstance(data["instruction_lines"], list):
|
|
290
|
+
data["instruction"] = "\n\n".join(data["instruction_lines"])
|
|
291
|
+
del data["instruction_lines"]
|
|
292
|
+
|
|
293
|
+
# Handle instruction as list from YAML export (convert back to string)
|
|
294
|
+
if "instruction" in data and isinstance(data["instruction"], list):
|
|
295
|
+
data["instruction"] = "\n\n".join(data["instruction"])
|
|
296
|
+
|
|
297
|
+
return data
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _extract_ids_from_export(items: list) -> list[str]:
|
|
301
|
+
"""Extract IDs from export format (list of dicts with id/name fields)."""
|
|
302
|
+
ids = []
|
|
303
|
+
for item in items:
|
|
304
|
+
if isinstance(item, dict):
|
|
305
|
+
item_id = item.get("id")
|
|
306
|
+
if item_id:
|
|
307
|
+
ids.append(item_id)
|
|
308
|
+
elif isinstance(item, str):
|
|
309
|
+
ids.append(item)
|
|
310
|
+
return ids
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _convert_export_to_import_format(data: dict[str, Any]) -> dict[str, Any]:
|
|
314
|
+
"""Convert export format to import-compatible format (extract IDs from objects)."""
|
|
315
|
+
import_data = data.copy()
|
|
316
|
+
|
|
317
|
+
# Convert tools from dicts to IDs
|
|
318
|
+
if "tools" in import_data and isinstance(import_data["tools"], list):
|
|
319
|
+
import_data["tools"] = _extract_ids_from_export(import_data["tools"])
|
|
320
|
+
|
|
321
|
+
# Convert agents from dicts to IDs
|
|
322
|
+
if "agents" in import_data and isinstance(import_data["agents"], list):
|
|
323
|
+
import_data["agents"] = _extract_ids_from_export(import_data["agents"])
|
|
324
|
+
|
|
325
|
+
return import_data
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _write_json_to_file(file_path: Path, data: dict[str, Any], indent: int = 2) -> None:
|
|
329
|
+
"""Write data to JSON file with consistent formatting."""
|
|
330
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
331
|
+
json.dump(data, f, indent=indent)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _write_yaml_to_file(file_path: Path, data: dict[str, Any]) -> None:
|
|
335
|
+
"""Write data to YAML file with user-friendly formatting."""
|
|
336
|
+
try:
|
|
337
|
+
import yaml
|
|
338
|
+
except ImportError:
|
|
339
|
+
raise click.ClickException(
|
|
340
|
+
"PyYAML is required for YAML export. Install with: pip install PyYAML"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Custom YAML dumper for user-friendly instruction formatting
|
|
344
|
+
class LiteralString(str):
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
def literal_string_representer(dumper, data):
|
|
348
|
+
# Use literal block scalar (|) for multiline strings to preserve formatting
|
|
349
|
+
if "\n" in data:
|
|
350
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
|
|
351
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
|
352
|
+
|
|
353
|
+
# Add custom representer to the YAML dumper
|
|
354
|
+
yaml.add_representer(LiteralString, literal_string_representer)
|
|
355
|
+
|
|
356
|
+
# Convert instruction to LiteralString for proper formatting
|
|
357
|
+
if "instruction" in data and data["instruction"]:
|
|
358
|
+
data["instruction"] = LiteralString(data["instruction"])
|
|
359
|
+
|
|
360
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
361
|
+
yaml.dump(
|
|
362
|
+
data, f, default_flow_style=False, allow_unicode=True, sort_keys=False
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _read_json_from_file(file_path: Path) -> dict[str, Any]:
|
|
367
|
+
"""Read JSON data from file with validation."""
|
|
368
|
+
if not file_path.exists():
|
|
369
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
370
|
+
|
|
371
|
+
if file_path.suffix.lower() != ".json":
|
|
372
|
+
raise ValueError(
|
|
373
|
+
f"Unsupported file format: {file_path.suffix}. Only JSON files are supported."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
with open(file_path, encoding="utf-8") as f:
|
|
377
|
+
return json.load(f)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _merge_import_with_cli_args(
|
|
381
|
+
import_data: dict[str, Any],
|
|
382
|
+
cli_args: dict[str, Any],
|
|
383
|
+
array_fields: list[str] = None,
|
|
384
|
+
) -> dict[str, Any]:
|
|
385
|
+
"""Merge imported data with CLI arguments, preferring CLI args.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
import_data: Data loaded from import file
|
|
389
|
+
cli_args: Arguments passed via CLI
|
|
390
|
+
array_fields: Fields that should be combined (merged) rather than replaced
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Merged data dictionary
|
|
394
|
+
"""
|
|
395
|
+
if array_fields is None:
|
|
396
|
+
array_fields = ["tools", "agents"]
|
|
397
|
+
|
|
398
|
+
merged = {}
|
|
399
|
+
|
|
400
|
+
for key, cli_value in cli_args.items():
|
|
401
|
+
if cli_value is not None and (
|
|
402
|
+
not isinstance(cli_value, list | tuple) or len(cli_value) > 0
|
|
403
|
+
):
|
|
404
|
+
# CLI value takes precedence (for non-empty values)
|
|
405
|
+
if key in array_fields and key in import_data:
|
|
406
|
+
# For array fields, combine CLI and imported values
|
|
407
|
+
import_value = import_data[key]
|
|
408
|
+
if isinstance(import_value, list):
|
|
409
|
+
merged[key] = list(cli_value) + import_value
|
|
410
|
+
else:
|
|
411
|
+
merged[key] = cli_value
|
|
412
|
+
else:
|
|
413
|
+
merged[key] = cli_value
|
|
414
|
+
elif key in import_data:
|
|
415
|
+
# Use imported value if no CLI value
|
|
416
|
+
merged[key] = import_data[key]
|
|
417
|
+
|
|
418
|
+
# Add any import-only fields
|
|
419
|
+
for key, import_value in import_data.items():
|
|
420
|
+
if key not in merged:
|
|
421
|
+
merged[key] = import_value
|
|
422
|
+
|
|
423
|
+
return merged
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _resolve_resources_by_name(
|
|
427
|
+
client, items: tuple[str, ...], resource_type: str, find_func, label: str
|
|
428
|
+
) -> list[str]:
|
|
429
|
+
"""Resolve resource names/IDs to IDs, handling ambiguity.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
client: API client
|
|
433
|
+
items: Tuple of resource names/IDs
|
|
434
|
+
resource_type: Type of resource ("tool" or "agent")
|
|
435
|
+
find_func: Function to find resources by name
|
|
436
|
+
label: Label for error messages
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
List of resolved resource IDs
|
|
440
|
+
"""
|
|
441
|
+
out = []
|
|
442
|
+
for ref in list(items or ()):
|
|
443
|
+
if is_uuid(ref):
|
|
444
|
+
out.append(ref)
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
matches = find_func(name=ref)
|
|
448
|
+
if not matches:
|
|
449
|
+
raise click.ClickException(f"{label} not found: {ref}")
|
|
450
|
+
if len(matches) > 1:
|
|
451
|
+
raise click.ClickException(
|
|
452
|
+
f"Multiple {resource_type}s named '{ref}'. Use ID instead."
|
|
453
|
+
)
|
|
454
|
+
out.append(str(matches[0].id))
|
|
455
|
+
return out
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _display_agent_creation_success(agent, model=None, default_model=DEFAULT_MODEL):
|
|
459
|
+
"""Display success message for agent creation/update."""
|
|
460
|
+
lm = getattr(agent, "model", None)
|
|
461
|
+
if not lm:
|
|
462
|
+
cfg = getattr(agent, "agent_config", {}) or {}
|
|
463
|
+
lm = (
|
|
464
|
+
cfg.get("lm_name")
|
|
465
|
+
or cfg.get("model")
|
|
466
|
+
or model # Use provided model if specified
|
|
467
|
+
or f"{default_model} (backend default)"
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
panel = Panel(
|
|
471
|
+
f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
|
|
472
|
+
f"ID: {agent.id}\n"
|
|
473
|
+
f"Model: {lm}\n"
|
|
474
|
+
f"Type: {getattr(agent, 'type', 'config')}\n"
|
|
475
|
+
f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
|
|
476
|
+
f"Version: {getattr(agent, 'version', '1.0')}",
|
|
477
|
+
title="🤖 Agent Created",
|
|
478
|
+
border_style="green",
|
|
479
|
+
padding=(0, 1),
|
|
480
|
+
)
|
|
481
|
+
console.print(panel)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _display_agent_update_success(agent):
|
|
485
|
+
"""Display success message for agent update."""
|
|
486
|
+
console.print(Text(f"[green]✅ Agent '{agent.name}' updated successfully[/green]"))
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _display_agent_details(ctx, client, agent):
|
|
490
|
+
"""Display full agent details in a standardized format."""
|
|
491
|
+
# Fetch full details to ensure all fields are populated
|
|
492
|
+
full_agent = _fetch_full_agent_details(client, agent)
|
|
493
|
+
|
|
494
|
+
# Build result data
|
|
495
|
+
result_data = _build_agent_result_data(full_agent)
|
|
496
|
+
|
|
497
|
+
# Display using output_result
|
|
498
|
+
output_result(
|
|
499
|
+
ctx,
|
|
500
|
+
result_data,
|
|
501
|
+
title="Agent Details",
|
|
502
|
+
panel_title=f"🤖 {full_agent.name}",
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
28
506
|
@click.group(name="agents", no_args_is_help=True)
|
|
29
507
|
def agents_group():
|
|
30
508
|
"""Agent management operations."""
|
|
31
509
|
pass
|
|
32
510
|
|
|
33
511
|
|
|
34
|
-
def _resolve_agent(ctx, client, ref, select=None):
|
|
35
|
-
"""Resolve agent reference (ID or name) with ambiguity handling.
|
|
36
|
-
if is_uuid(ref):
|
|
37
|
-
return client.agents.get_agent_by_id(ref)
|
|
38
|
-
|
|
39
|
-
# Find agents by name
|
|
40
|
-
matches = client.agents.find_agents(name=ref)
|
|
41
|
-
if not matches:
|
|
42
|
-
raise click.ClickException(f"Agent '{ref}' not found")
|
|
43
|
-
|
|
44
|
-
if len(matches) == 1:
|
|
45
|
-
return matches[0]
|
|
46
|
-
|
|
47
|
-
# Multiple matches - handle ambiguity
|
|
48
|
-
if select:
|
|
49
|
-
idx = int(select) - 1
|
|
50
|
-
if not (0 <= idx < len(matches)):
|
|
51
|
-
raise click.ClickException(f"--select must be 1..{len(matches)}")
|
|
52
|
-
return matches[idx]
|
|
512
|
+
def _resolve_agent(ctx, client, ref, select=None, interface_preference="fuzzy"):
|
|
513
|
+
"""Resolve agent reference (ID or name) with ambiguity handling.
|
|
53
514
|
|
|
54
|
-
|
|
515
|
+
Args:
|
|
516
|
+
interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
|
|
517
|
+
"""
|
|
518
|
+
return resolve_resource(
|
|
519
|
+
ctx,
|
|
520
|
+
ref,
|
|
521
|
+
get_by_id=client.agents.get_agent_by_id,
|
|
522
|
+
find_by_name=client.agents.find_agents,
|
|
523
|
+
label="Agent",
|
|
524
|
+
select=select,
|
|
525
|
+
interface_preference=interface_preference,
|
|
526
|
+
)
|
|
55
527
|
|
|
56
528
|
|
|
57
529
|
@agents_group.command(name="list")
|
|
530
|
+
@click.option(
|
|
531
|
+
"--simple", is_flag=True, help="Show simple table without interactive picker"
|
|
532
|
+
)
|
|
58
533
|
@output_flags()
|
|
59
534
|
@click.pass_context
|
|
60
|
-
def list_agents(ctx):
|
|
535
|
+
def list_agents(ctx, simple):
|
|
61
536
|
"""List all agents."""
|
|
62
537
|
try:
|
|
63
538
|
client = get_client(ctx)
|
|
@@ -74,14 +549,20 @@ def list_agents(ctx):
|
|
|
74
549
|
|
|
75
550
|
# Transform function for safe attribute access
|
|
76
551
|
def transform_agent(agent):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"framework": safe_getattr(agent, "framework") or "N/A",
|
|
82
|
-
"version": safe_getattr(agent, "version") or "N/A",
|
|
83
|
-
}
|
|
552
|
+
row = coerce_to_row(agent, ["id", "name", "type", "framework", "version"])
|
|
553
|
+
# Ensure id is always a string
|
|
554
|
+
row["id"] = str(row["id"])
|
|
555
|
+
return row
|
|
84
556
|
|
|
557
|
+
# Use fuzzy picker for interactive agent selection and details (default behavior)
|
|
558
|
+
# Skip if --simple flag is used
|
|
559
|
+
if not simple and console.is_terminal and os.isatty(1) and len(agents) > 0:
|
|
560
|
+
picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
|
|
561
|
+
if picked_agent:
|
|
562
|
+
_display_agent_details(ctx, client, picked_agent)
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
# Show simple table (either --simple flag or non-interactive)
|
|
85
566
|
output_list(ctx, agents, "🤖 Available Agents", columns, transform_agent)
|
|
86
567
|
|
|
87
568
|
except Exception as e:
|
|
@@ -91,64 +572,84 @@ def list_agents(ctx):
|
|
|
91
572
|
@agents_group.command()
|
|
92
573
|
@click.argument("agent_ref")
|
|
93
574
|
@click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
|
|
575
|
+
@click.option(
|
|
576
|
+
"--export",
|
|
577
|
+
type=click.Path(dir_okay=False, writable=True),
|
|
578
|
+
help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
|
|
579
|
+
)
|
|
94
580
|
@output_flags()
|
|
95
581
|
@click.pass_context
|
|
96
|
-
def get(ctx, agent_ref, select):
|
|
97
|
-
"""Get agent details.
|
|
582
|
+
def get(ctx, agent_ref, select, export):
|
|
583
|
+
"""Get agent details.
|
|
584
|
+
|
|
585
|
+
Examples:
|
|
586
|
+
aip agents get my-agent
|
|
587
|
+
aip agents get my-agent --export agent.json # Exports complete configuration as JSON
|
|
588
|
+
aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
|
|
589
|
+
"""
|
|
98
590
|
try:
|
|
99
591
|
client = get_client(ctx)
|
|
100
592
|
|
|
101
|
-
# Resolve agent with ambiguity handling
|
|
102
|
-
agent = _resolve_agent(
|
|
103
|
-
|
|
104
|
-
# Create result data with all available fields from backend
|
|
105
|
-
result_data = {
|
|
106
|
-
"id": str(getattr(agent, "id", "N/A")),
|
|
107
|
-
"name": getattr(agent, "name", "N/A"),
|
|
108
|
-
"type": getattr(agent, "type", "N/A"),
|
|
109
|
-
"framework": getattr(agent, "framework", "N/A"),
|
|
110
|
-
"version": getattr(agent, "version", "N/A"),
|
|
111
|
-
"description": getattr(agent, "description", "N/A"),
|
|
112
|
-
"instruction": getattr(agent, "instruction", "") or "-",
|
|
113
|
-
"metadata": getattr(agent, "metadata", "N/A"),
|
|
114
|
-
"language_model_id": getattr(agent, "language_model_id", "N/A"),
|
|
115
|
-
"agent_config": getattr(agent, "agent_config", "N/A"),
|
|
116
|
-
"tools": getattr(agent, "tools", []),
|
|
117
|
-
"agents": getattr(agent, "agents", []),
|
|
118
|
-
"mcps": getattr(agent, "mcps", []),
|
|
119
|
-
"a2a_profile": getattr(agent, "a2a_profile", "N/A"),
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
output_result(
|
|
123
|
-
ctx, result_data, title="Agent Details", panel_title=f"🤖 {agent.name}"
|
|
593
|
+
# Resolve agent with ambiguity handling - use questionary interface for traditional UX
|
|
594
|
+
agent = _resolve_agent(
|
|
595
|
+
ctx, client, agent_ref, select, interface_preference="questionary"
|
|
124
596
|
)
|
|
125
597
|
|
|
598
|
+
# Handle export option
|
|
599
|
+
if export:
|
|
600
|
+
export_path = Path(export)
|
|
601
|
+
# Auto-detect format from file extension
|
|
602
|
+
if export_path.suffix.lower() in [".yaml", ".yml"]:
|
|
603
|
+
detected_format = "yaml"
|
|
604
|
+
else:
|
|
605
|
+
detected_format = "json"
|
|
606
|
+
|
|
607
|
+
# Always export comprehensive data - re-fetch agent with full details
|
|
608
|
+
try:
|
|
609
|
+
agent = client.agents.get_agent_by_id(agent.id)
|
|
610
|
+
except Exception as e:
|
|
611
|
+
console.print(
|
|
612
|
+
Text(f"[yellow]⚠️ Could not fetch full agent details: {e}[/yellow]")
|
|
613
|
+
)
|
|
614
|
+
console.print(
|
|
615
|
+
Text("[yellow]⚠️ Proceeding with available data[/yellow]")
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
_export_agent_to_file(agent, export_path, detected_format)
|
|
619
|
+
console.print(
|
|
620
|
+
Text(
|
|
621
|
+
f"[green]✅ Complete agent configuration exported to: {export_path} (format: {detected_format})[/green]"
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Display full agent details using the standardized helper
|
|
626
|
+
_display_agent_details(ctx, client, agent)
|
|
627
|
+
|
|
628
|
+
# Show run suggestions (only in rich mode, not JSON)
|
|
629
|
+
if ctx.obj.get("view") != "json":
|
|
630
|
+
_display_run_suggestions(agent)
|
|
631
|
+
|
|
126
632
|
except Exception as e:
|
|
127
633
|
raise click.ClickException(str(e))
|
|
128
634
|
|
|
129
635
|
|
|
130
636
|
@agents_group.command()
|
|
131
|
-
@click.argument("
|
|
132
|
-
@click.
|
|
637
|
+
@click.argument("agent_ref")
|
|
638
|
+
@click.argument("input_text", required=False)
|
|
639
|
+
@click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
|
|
640
|
+
@click.option("--input", "input_option", help="Input text for the agent")
|
|
133
641
|
@click.option("--chat-history", help="JSON string of chat history")
|
|
134
|
-
@click.option("--timeout", default=600, type=int, help="Request timeout in seconds")
|
|
135
642
|
@click.option(
|
|
136
|
-
"--
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
help="
|
|
140
|
-
)
|
|
141
|
-
@click.option(
|
|
142
|
-
"--compact/--verbose", default=True, help="Collapse tool steps (default: compact)"
|
|
643
|
+
"--timeout",
|
|
644
|
+
default=DEFAULT_AGENT_RUN_TIMEOUT,
|
|
645
|
+
type=int,
|
|
646
|
+
help="Agent execution timeout in seconds (default: 300s)",
|
|
143
647
|
)
|
|
144
648
|
@click.option(
|
|
145
649
|
"--save",
|
|
146
650
|
type=click.Path(dir_okay=False, writable=True),
|
|
147
651
|
help="Save transcript to file (md or json)",
|
|
148
652
|
)
|
|
149
|
-
@click.option(
|
|
150
|
-
"--theme", type=click.Choice(["dark", "light"]), default="dark", help="Color theme"
|
|
151
|
-
)
|
|
152
653
|
@click.option(
|
|
153
654
|
"--file",
|
|
154
655
|
"files",
|
|
@@ -156,28 +657,50 @@ def get(ctx, agent_ref, select):
|
|
|
156
657
|
type=click.Path(exists=True),
|
|
157
658
|
help="Attach file(s)",
|
|
158
659
|
)
|
|
660
|
+
@click.option(
|
|
661
|
+
"--verbose/--no-verbose",
|
|
662
|
+
default=False,
|
|
663
|
+
help="Show detailed SSE events during streaming",
|
|
664
|
+
)
|
|
665
|
+
@output_flags()
|
|
159
666
|
@click.pass_context
|
|
160
667
|
def run(
|
|
161
668
|
ctx,
|
|
162
|
-
|
|
669
|
+
agent_ref,
|
|
670
|
+
select,
|
|
163
671
|
input_text,
|
|
672
|
+
input_option,
|
|
164
673
|
chat_history,
|
|
165
674
|
timeout,
|
|
166
|
-
view,
|
|
167
|
-
compact,
|
|
168
675
|
save,
|
|
169
|
-
theme,
|
|
170
676
|
files,
|
|
677
|
+
verbose,
|
|
171
678
|
):
|
|
172
|
-
"""Run an agent with input text
|
|
679
|
+
"""Run an agent with input text.
|
|
680
|
+
|
|
681
|
+
Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
|
|
682
|
+
|
|
683
|
+
Examples:
|
|
684
|
+
aip agents run my-agent "Hello world"
|
|
685
|
+
aip agents run agent-123 "Process this data" --timeout 600
|
|
686
|
+
aip agents run my-agent --input "Hello world" # Legacy style
|
|
687
|
+
"""
|
|
688
|
+
# Handle input precedence: --input option overrides positional argument
|
|
689
|
+
final_input_text = input_option if input_option else input_text
|
|
690
|
+
|
|
691
|
+
# Validate that we have input text from either positional argument or --input option
|
|
692
|
+
if not final_input_text:
|
|
693
|
+
raise click.ClickException(
|
|
694
|
+
"Input text is required. Use either positional argument or --input option."
|
|
695
|
+
)
|
|
696
|
+
|
|
173
697
|
try:
|
|
174
698
|
client = get_client(ctx)
|
|
175
699
|
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
|
|
700
|
+
# Resolve agent by ID or name (align with other commands) - use fuzzy interface
|
|
701
|
+
agent = _resolve_agent(
|
|
702
|
+
ctx, client, agent_ref, select, interface_preference="fuzzy"
|
|
703
|
+
)
|
|
181
704
|
|
|
182
705
|
# Parse chat history if provided
|
|
183
706
|
parsed_chat_history = None
|
|
@@ -187,131 +710,255 @@ def run(
|
|
|
187
710
|
except json.JSONDecodeError:
|
|
188
711
|
raise click.ClickException("Invalid JSON in chat history")
|
|
189
712
|
|
|
190
|
-
#
|
|
191
|
-
|
|
713
|
+
# Create custom renderer with CLI flags
|
|
714
|
+
tty_enabled = bool((ctx.obj or {}).get("tty", True))
|
|
192
715
|
|
|
193
|
-
#
|
|
194
|
-
renderer =
|
|
195
|
-
|
|
196
|
-
|
|
716
|
+
# Build renderer and capturing console
|
|
717
|
+
renderer, working_console = build_renderer(
|
|
718
|
+
ctx,
|
|
719
|
+
save_path=save,
|
|
720
|
+
verbose=verbose,
|
|
721
|
+
tty_enabled=tty_enabled,
|
|
722
|
+
)
|
|
197
723
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
724
|
+
# Set HTTP timeout to match agent timeout exactly
|
|
725
|
+
# This ensures the agent timeout controls the HTTP timeout
|
|
726
|
+
try:
|
|
727
|
+
client.timeout = float(timeout)
|
|
728
|
+
except Exception:
|
|
729
|
+
pass
|
|
203
730
|
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
agent_id
|
|
207
|
-
message
|
|
208
|
-
files
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
731
|
+
# Ensure timeout is applied to the root client and subclients share its session
|
|
732
|
+
run_kwargs = {
|
|
733
|
+
"agent_id": agent.id,
|
|
734
|
+
"message": final_input_text,
|
|
735
|
+
"files": list(files),
|
|
736
|
+
"agent_name": agent.name, # Pass agent name for better display
|
|
737
|
+
"tty": tty_enabled,
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
# Add optional parameters
|
|
741
|
+
if parsed_chat_history:
|
|
742
|
+
run_kwargs["chat_history"] = parsed_chat_history
|
|
743
|
+
|
|
744
|
+
# Pass custom renderer if available
|
|
745
|
+
if renderer is not None:
|
|
746
|
+
run_kwargs["renderer"] = renderer
|
|
747
|
+
|
|
748
|
+
# Pass timeout to client (verbose mode is handled by the renderer)
|
|
749
|
+
result = client.agents.run_agent(**run_kwargs, timeout=timeout)
|
|
214
750
|
|
|
215
751
|
# Check if renderer already printed output (for streaming renderers)
|
|
216
|
-
|
|
752
|
+
# Note: Auto-paging is handled by the renderer when view=="rich"
|
|
753
|
+
printed_by_renderer = bool(renderer)
|
|
217
754
|
|
|
218
|
-
#
|
|
755
|
+
# Resolve selected view from context (output_flags() stores it here)
|
|
756
|
+
selected_view = (ctx.obj or {}).get("view", "rich")
|
|
757
|
+
|
|
758
|
+
# Handle output format for fallback
|
|
219
759
|
# Only print here if nothing was printed by the renderer
|
|
220
760
|
if not printed_by_renderer:
|
|
221
|
-
if
|
|
761
|
+
if selected_view == "json":
|
|
222
762
|
click.echo(json.dumps({"output": result}, indent=2))
|
|
223
|
-
elif
|
|
763
|
+
elif selected_view == "md":
|
|
224
764
|
click.echo(f"# Assistant\n\n{result}")
|
|
225
|
-
elif
|
|
765
|
+
elif selected_view == "plain":
|
|
226
766
|
click.echo(result)
|
|
227
|
-
elif not stream:
|
|
228
|
-
# Rich output for non-streaming
|
|
229
|
-
panel = Panel(
|
|
230
|
-
Text(result, style="green"),
|
|
231
|
-
title="Agent Output",
|
|
232
|
-
border_style="green",
|
|
233
|
-
)
|
|
234
|
-
console.print(panel)
|
|
235
767
|
|
|
236
768
|
# Save transcript if requested
|
|
237
|
-
if save
|
|
769
|
+
if save:
|
|
238
770
|
ext = (save.rsplit(".", 1)[-1] or "").lower()
|
|
239
771
|
if ext == "json":
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
772
|
+
# Save both the result and captured output
|
|
773
|
+
save_data = {
|
|
774
|
+
"output": result or "",
|
|
775
|
+
"full_debug_output": getattr(
|
|
776
|
+
working_console, "get_captured_output", lambda: ""
|
|
777
|
+
)(),
|
|
778
|
+
"timestamp": "captured during agent execution",
|
|
779
|
+
}
|
|
780
|
+
content = json.dumps(save_data, indent=2)
|
|
243
781
|
else:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
782
|
+
# For markdown/text files, save the full captured output if available
|
|
783
|
+
# Get the full captured output including all tool panels and debug info (if available)
|
|
784
|
+
full_output = getattr(
|
|
785
|
+
working_console, "get_captured_output", lambda: ""
|
|
786
|
+
)()
|
|
787
|
+
if full_output:
|
|
788
|
+
content = f"# Agent Debug Log\n\n{full_output}\n\n---\n\n## Final Result\n\n{result or ''}\n"
|
|
789
|
+
else:
|
|
790
|
+
# Fallback to simple format
|
|
791
|
+
content = f"# Assistant\n\n{result or ''}\n"
|
|
792
|
+
|
|
793
|
+
with open(save, "w", encoding="utf-8") as f:
|
|
794
|
+
f.write(content)
|
|
795
|
+
console.print(Text(f"[green]Full debug output saved to: {save}[/green]"))
|
|
248
796
|
|
|
797
|
+
except AgentTimeoutError as e:
|
|
798
|
+
# Handle agent timeout errors with specific messages
|
|
799
|
+
error_msg = str(e)
|
|
800
|
+
if ctx.obj.get("view") == "json":
|
|
801
|
+
click.echo(json.dumps({"error": error_msg}, indent=2))
|
|
802
|
+
# Don't print the error message here - Click.ClickException will handle it
|
|
803
|
+
raise click.ClickException(error_msg)
|
|
249
804
|
except Exception as e:
|
|
250
805
|
if ctx.obj.get("view") == "json":
|
|
251
806
|
click.echo(json.dumps({"error": str(e)}, indent=2))
|
|
252
|
-
|
|
253
|
-
console.print(f"[red]Error running agent: {e}[/red]")
|
|
807
|
+
# Don't print the error message here - Click.ClickException will handle it
|
|
254
808
|
raise click.ClickException(str(e))
|
|
255
809
|
|
|
256
810
|
|
|
257
811
|
@agents_group.command()
|
|
258
|
-
@click.option("--name",
|
|
259
|
-
@click.option("--instruction",
|
|
812
|
+
@click.option("--name", help="Agent name")
|
|
813
|
+
@click.option("--instruction", help="Agent instruction (prompt)")
|
|
814
|
+
@click.option(
|
|
815
|
+
"--model",
|
|
816
|
+
help=f"Language model to use (e.g., {DEFAULT_MODEL}, default: {DEFAULT_MODEL})",
|
|
817
|
+
)
|
|
260
818
|
@click.option("--tools", multiple=True, help="Tool names or IDs to attach")
|
|
261
|
-
@click.option("--agents", multiple=True, help="Sub-agent names to attach")
|
|
262
|
-
@click.option(
|
|
819
|
+
@click.option("--agents", multiple=True, help="Sub-agent names or IDs to attach")
|
|
820
|
+
@click.option(
|
|
821
|
+
"--timeout",
|
|
822
|
+
default=DEFAULT_AGENT_RUN_TIMEOUT,
|
|
823
|
+
type=int,
|
|
824
|
+
help="Agent execution timeout in seconds (default: 300s)",
|
|
825
|
+
)
|
|
826
|
+
@click.option(
|
|
827
|
+
"--import",
|
|
828
|
+
"import_file",
|
|
829
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
830
|
+
help="Import agent configuration from JSON file",
|
|
831
|
+
)
|
|
263
832
|
@output_flags()
|
|
264
833
|
@click.pass_context
|
|
265
834
|
def create(
|
|
266
835
|
ctx,
|
|
267
836
|
name,
|
|
268
837
|
instruction,
|
|
838
|
+
model,
|
|
269
839
|
tools,
|
|
270
840
|
agents,
|
|
271
841
|
timeout,
|
|
842
|
+
import_file,
|
|
272
843
|
):
|
|
273
|
-
"""Create a new agent.
|
|
844
|
+
"""Create a new agent.
|
|
845
|
+
|
|
846
|
+
Examples:
|
|
847
|
+
aip agents create --name "My Agent" --instruction "You are a helpful assistant"
|
|
848
|
+
aip agents create --import agent.json
|
|
849
|
+
"""
|
|
274
850
|
try:
|
|
275
851
|
client = get_client(ctx)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
852
|
+
|
|
853
|
+
# Handle import from file
|
|
854
|
+
if import_file:
|
|
855
|
+
import_data = _load_agent_from_file(Path(import_file))
|
|
856
|
+
|
|
857
|
+
# Convert export format to import-compatible format
|
|
858
|
+
import_data = _convert_export_to_import_format(import_data)
|
|
859
|
+
|
|
860
|
+
# Merge CLI args with imported data
|
|
861
|
+
cli_args = {
|
|
862
|
+
"name": name,
|
|
863
|
+
"instruction": instruction,
|
|
864
|
+
"model": model,
|
|
865
|
+
"tools": tools or (),
|
|
866
|
+
"agents": agents or (),
|
|
867
|
+
"timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
merged_data = _merge_import_with_cli_args(import_data, cli_args)
|
|
871
|
+
|
|
872
|
+
# Extract merged values
|
|
873
|
+
name = merged_data.get("name")
|
|
874
|
+
instruction = merged_data.get("instruction")
|
|
875
|
+
model = merged_data.get("model")
|
|
876
|
+
tools = tuple(merged_data.get("tools", ()))
|
|
877
|
+
agents = tuple(merged_data.get("agents", ()))
|
|
878
|
+
timeout = merged_data.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
879
|
+
|
|
880
|
+
# Validate required fields
|
|
881
|
+
if not name:
|
|
882
|
+
raise click.ClickException("Agent name is required (--name or --import)")
|
|
883
|
+
if not instruction:
|
|
884
|
+
raise click.ClickException(
|
|
885
|
+
"Agent instruction is required (--instruction or --import)"
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
# Resolve tool and agent references: accept names or IDs
|
|
889
|
+
resolved_tools = _resolve_resources_by_name(
|
|
890
|
+
client, tools, "tool", client.find_tools, "Tool"
|
|
891
|
+
)
|
|
892
|
+
resolved_agents = _resolve_resources_by_name(
|
|
893
|
+
client, agents, "agent", client.find_agents, "Agent"
|
|
283
894
|
)
|
|
284
895
|
|
|
896
|
+
# Create agent with comprehensive attribute support
|
|
897
|
+
create_kwargs = {
|
|
898
|
+
"name": name,
|
|
899
|
+
"instruction": instruction,
|
|
900
|
+
"tools": resolved_tools or None,
|
|
901
|
+
"agents": resolved_agents or None,
|
|
902
|
+
"timeout": timeout,
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
# Add model if specified (prioritize CLI model over imported model)
|
|
906
|
+
if model:
|
|
907
|
+
create_kwargs["model"] = model
|
|
908
|
+
elif (
|
|
909
|
+
import_file
|
|
910
|
+
and "agent_config" in merged_data
|
|
911
|
+
and merged_data["agent_config"]
|
|
912
|
+
):
|
|
913
|
+
# Use lm_name from agent_config for cloning
|
|
914
|
+
agent_config = merged_data["agent_config"]
|
|
915
|
+
if isinstance(agent_config, dict) and "lm_name" in agent_config:
|
|
916
|
+
create_kwargs["model"] = agent_config["lm_name"]
|
|
917
|
+
|
|
918
|
+
# If importing from file, include all other detected attributes
|
|
919
|
+
if import_file:
|
|
920
|
+
# Add all other attributes from import data (excluding already handled ones and system-only fields)
|
|
921
|
+
excluded_fields = {
|
|
922
|
+
"name",
|
|
923
|
+
"instruction",
|
|
924
|
+
"model",
|
|
925
|
+
"tools",
|
|
926
|
+
"agents",
|
|
927
|
+
"timeout",
|
|
928
|
+
# System-only fields that shouldn't be passed to create_agent
|
|
929
|
+
"id",
|
|
930
|
+
"created_at",
|
|
931
|
+
"updated_at",
|
|
932
|
+
"agent_config",
|
|
933
|
+
"language_model_id",
|
|
934
|
+
"type",
|
|
935
|
+
"framework",
|
|
936
|
+
"version",
|
|
937
|
+
"tool_configs",
|
|
938
|
+
"mcps",
|
|
939
|
+
"a2a_profile",
|
|
940
|
+
}
|
|
941
|
+
for key, value in merged_data.items():
|
|
942
|
+
if key not in excluded_fields and value is not None:
|
|
943
|
+
create_kwargs[key] = value
|
|
944
|
+
|
|
945
|
+
agent = client.agents.create_agent(**create_kwargs)
|
|
946
|
+
|
|
285
947
|
if ctx.obj.get("view") == "json":
|
|
286
948
|
click.echo(json.dumps(agent.model_dump(), indent=2))
|
|
287
949
|
else:
|
|
288
950
|
# Rich output
|
|
289
|
-
|
|
290
|
-
if not lm:
|
|
291
|
-
cfg = getattr(agent, "agent_config", {}) or {}
|
|
292
|
-
lm = (
|
|
293
|
-
cfg.get("lm_name")
|
|
294
|
-
or cfg.get("model")
|
|
295
|
-
or "gpt-4.1 (backend default)"
|
|
296
|
-
)
|
|
951
|
+
_display_agent_creation_success(agent, model)
|
|
297
952
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
f"Model: {lm}\n"
|
|
302
|
-
f"Type: {getattr(agent, 'type', 'config')}\n"
|
|
303
|
-
f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
|
|
304
|
-
f"Version: {getattr(agent, 'version', '1.0')}",
|
|
305
|
-
title="🤖 Agent Created",
|
|
306
|
-
border_style="green",
|
|
307
|
-
)
|
|
308
|
-
console.print(panel)
|
|
953
|
+
# Show run suggestions (only in rich mode, not JSON)
|
|
954
|
+
if ctx.obj.get("view") != "json":
|
|
955
|
+
_display_run_suggestions(agent)
|
|
309
956
|
|
|
310
957
|
except Exception as e:
|
|
311
958
|
if ctx.obj.get("view") == "json":
|
|
312
959
|
click.echo(json.dumps({"error": str(e)}, indent=2))
|
|
313
960
|
else:
|
|
314
|
-
console.print(f"[red]Error creating agent: {e}[/red]")
|
|
961
|
+
console.print(Text(f"[red]Error creating agent: {e}[/red]"))
|
|
315
962
|
raise click.ClickException(str(e))
|
|
316
963
|
|
|
317
964
|
|
|
@@ -322,10 +969,21 @@ def create(
|
|
|
322
969
|
@click.option("--tools", multiple=True, help="New tool names or IDs")
|
|
323
970
|
@click.option("--agents", multiple=True, help="New sub-agent names")
|
|
324
971
|
@click.option("--timeout", type=int, help="New timeout value")
|
|
972
|
+
@click.option(
|
|
973
|
+
"--import",
|
|
974
|
+
"import_file",
|
|
975
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
976
|
+
help="Import agent configuration from JSON file",
|
|
977
|
+
)
|
|
325
978
|
@output_flags()
|
|
326
979
|
@click.pass_context
|
|
327
|
-
def update(ctx, agent_id, name, instruction, tools, agents, timeout):
|
|
328
|
-
"""Update an existing agent.
|
|
980
|
+
def update(ctx, agent_id, name, instruction, tools, agents, timeout, import_file):
|
|
981
|
+
"""Update an existing agent.
|
|
982
|
+
|
|
983
|
+
Examples:
|
|
984
|
+
aip agents update my-agent --instruction "New instruction"
|
|
985
|
+
aip agents update my-agent --import agent.json
|
|
986
|
+
"""
|
|
329
987
|
try:
|
|
330
988
|
client = get_client(ctx)
|
|
331
989
|
|
|
@@ -335,7 +993,32 @@ def update(ctx, agent_id, name, instruction, tools, agents, timeout):
|
|
|
335
993
|
except Exception as e:
|
|
336
994
|
raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
|
|
337
995
|
|
|
338
|
-
#
|
|
996
|
+
# Handle import from file
|
|
997
|
+
if import_file:
|
|
998
|
+
import_data = _load_agent_from_file(Path(import_file))
|
|
999
|
+
|
|
1000
|
+
# Convert export format to import-compatible format
|
|
1001
|
+
import_data = _convert_export_to_import_format(import_data)
|
|
1002
|
+
|
|
1003
|
+
# Merge CLI args with imported data
|
|
1004
|
+
cli_args = {
|
|
1005
|
+
"name": name,
|
|
1006
|
+
"instruction": instruction,
|
|
1007
|
+
"tools": tools or (),
|
|
1008
|
+
"agents": agents or (),
|
|
1009
|
+
"timeout": timeout,
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
merged_data = _merge_import_with_cli_args(import_data, cli_args)
|
|
1013
|
+
|
|
1014
|
+
# Extract merged values
|
|
1015
|
+
name = merged_data.get("name")
|
|
1016
|
+
instruction = merged_data.get("instruction")
|
|
1017
|
+
tools = tuple(merged_data.get("tools", ()))
|
|
1018
|
+
agents = tuple(merged_data.get("agents", ()))
|
|
1019
|
+
timeout = merged_data.get("timeout")
|
|
1020
|
+
|
|
1021
|
+
# Build update data with comprehensive attribute support
|
|
339
1022
|
update_data = {}
|
|
340
1023
|
if name is not None:
|
|
341
1024
|
update_data["name"] = name
|
|
@@ -348,24 +1031,51 @@ def update(ctx, agent_id, name, instruction, tools, agents, timeout):
|
|
|
348
1031
|
if timeout is not None:
|
|
349
1032
|
update_data["timeout"] = timeout
|
|
350
1033
|
|
|
1034
|
+
# If importing from file, include all other detected attributes
|
|
1035
|
+
if import_file:
|
|
1036
|
+
# Add all other attributes from import data (excluding already handled ones and system-only fields)
|
|
1037
|
+
excluded_fields = {
|
|
1038
|
+
"name",
|
|
1039
|
+
"instruction",
|
|
1040
|
+
"tools",
|
|
1041
|
+
"agents",
|
|
1042
|
+
"timeout",
|
|
1043
|
+
# System-only fields that shouldn't be passed to update_agent
|
|
1044
|
+
"id",
|
|
1045
|
+
"created_at",
|
|
1046
|
+
"updated_at",
|
|
1047
|
+
"agent_config",
|
|
1048
|
+
"type",
|
|
1049
|
+
"framework",
|
|
1050
|
+
"version",
|
|
1051
|
+
"tool_configs",
|
|
1052
|
+
"mcps",
|
|
1053
|
+
"a2a_profile",
|
|
1054
|
+
}
|
|
1055
|
+
for key, value in merged_data.items():
|
|
1056
|
+
if key not in excluded_fields and value is not None:
|
|
1057
|
+
update_data[key] = value
|
|
1058
|
+
|
|
351
1059
|
if not update_data:
|
|
352
1060
|
raise click.ClickException("No update fields specified")
|
|
353
1061
|
|
|
354
1062
|
# Update agent
|
|
355
|
-
updated_agent = client.agents.update_agent(agent.id, update_data)
|
|
1063
|
+
updated_agent = client.agents.update_agent(agent.id, **update_data)
|
|
356
1064
|
|
|
357
1065
|
if ctx.obj.get("view") == "json":
|
|
358
1066
|
click.echo(json.dumps(updated_agent.model_dump(), indent=2))
|
|
359
1067
|
else:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
)
|
|
1068
|
+
_display_agent_update_success(updated_agent)
|
|
1069
|
+
|
|
1070
|
+
# Show run suggestions (only in rich mode, not JSON)
|
|
1071
|
+
if ctx.obj.get("view") != "json":
|
|
1072
|
+
_display_run_suggestions(updated_agent)
|
|
363
1073
|
|
|
364
1074
|
except Exception as e:
|
|
365
1075
|
if ctx.obj.get("view") == "json":
|
|
366
1076
|
click.echo(json.dumps({"error": str(e)}, indent=2))
|
|
367
1077
|
else:
|
|
368
|
-
console.print(f"[red]Error updating agent: {e}[/red]")
|
|
1078
|
+
console.print(Text(f"[red]Error updating agent: {e}[/red]"))
|
|
369
1079
|
raise click.ClickException(str(e))
|
|
370
1080
|
|
|
371
1081
|
|
|
@@ -390,7 +1100,7 @@ def delete(ctx, agent_id, yes):
|
|
|
390
1100
|
f"Are you sure you want to delete agent '{agent.name}'?"
|
|
391
1101
|
):
|
|
392
1102
|
if ctx.obj.get("view") != "json":
|
|
393
|
-
console.print("Deletion cancelled.")
|
|
1103
|
+
console.print(Text("Deletion cancelled."))
|
|
394
1104
|
return
|
|
395
1105
|
|
|
396
1106
|
client.agents.delete_agent(agent.id)
|
|
@@ -404,12 +1114,12 @@ def delete(ctx, agent_id, yes):
|
|
|
404
1114
|
)
|
|
405
1115
|
else:
|
|
406
1116
|
console.print(
|
|
407
|
-
f"[green]✅ Agent '{agent.name}' deleted successfully[/green]"
|
|
1117
|
+
Text(f"[green]✅ Agent '{agent.name}' deleted successfully[/green]")
|
|
408
1118
|
)
|
|
409
1119
|
|
|
410
1120
|
except Exception as e:
|
|
411
1121
|
if ctx.obj.get("view") == "json":
|
|
412
1122
|
click.echo(json.dumps({"error": str(e)}, indent=2))
|
|
413
1123
|
else:
|
|
414
|
-
console.print(f"[red]Error deleting agent: {e}[/red]")
|
|
1124
|
+
console.print(Text(f"[red]Error deleting agent: {e}[/red]"))
|
|
415
1125
|
raise click.ClickException(str(e))
|