glaip-sdk 0.5.3__py3-none-any.whl → 0.6.0__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 +4 -1
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +989 -0
- glaip_sdk/cli/commands/accounts.py +210 -23
- glaip_sdk/cli/commands/tools.py +2 -5
- glaip_sdk/client/_agent_payloads.py +10 -9
- glaip_sdk/client/agents.py +70 -8
- glaip_sdk/client/base.py +1 -0
- glaip_sdk/client/main.py +12 -4
- glaip_sdk/client/mcps.py +112 -10
- glaip_sdk/client/tools.py +151 -7
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +65 -31
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +0 -1
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -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 +251 -0
- glaip_sdk/registry/tool.py +238 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +50 -9
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +26 -7
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/import_resolver.py +500 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/sync.py +142 -0
- {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/METADATA +5 -3
- {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/RECORD +38 -18
- glaip_sdk/models.py +0 -241
- {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.5.3.dist-info → glaip_sdk-0.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,9 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
import getpass
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
9
10
|
import sys
|
|
11
|
+
from pathlib import Path
|
|
10
12
|
|
|
11
13
|
import click
|
|
12
14
|
from rich.console import Console
|
|
@@ -110,6 +112,153 @@ def list_accounts(output_json: bool) -> None:
|
|
|
110
112
|
console.print(f"\n[{INFO}]💡 Tip[/]: To update an account's URL or key, use: [bold]aip accounts edit <name>[/bold]")
|
|
111
113
|
|
|
112
114
|
|
|
115
|
+
def _build_account_json_payload(
|
|
116
|
+
name: str,
|
|
117
|
+
api_url: str,
|
|
118
|
+
masked_key: str,
|
|
119
|
+
config_path: str,
|
|
120
|
+
is_active: bool,
|
|
121
|
+
env_lock: bool,
|
|
122
|
+
metadata: dict[str, str | None],
|
|
123
|
+
) -> dict[str, str | bool | None]:
|
|
124
|
+
"""Build JSON payload for account display.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
name: Account name.
|
|
128
|
+
api_url: API URL.
|
|
129
|
+
masked_key: Masked API key.
|
|
130
|
+
config_path: Config file path.
|
|
131
|
+
is_active: Whether account is active.
|
|
132
|
+
env_lock: Whether env credentials are set.
|
|
133
|
+
metadata: Account metadata dict.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
JSON payload dict.
|
|
137
|
+
"""
|
|
138
|
+
payload: dict[str, str | bool | None] = {
|
|
139
|
+
"name": name,
|
|
140
|
+
"api_url": api_url,
|
|
141
|
+
"api_key_masked": masked_key,
|
|
142
|
+
"config_path": config_path,
|
|
143
|
+
"active": is_active,
|
|
144
|
+
"env_lock": env_lock,
|
|
145
|
+
}
|
|
146
|
+
for key, value in metadata.items():
|
|
147
|
+
if value:
|
|
148
|
+
payload[key] = value
|
|
149
|
+
return payload
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _format_config_path(config_path: str) -> str:
|
|
153
|
+
"""Format config path for display, shortening under home."""
|
|
154
|
+
path_obj = Path(config_path).expanduser()
|
|
155
|
+
try:
|
|
156
|
+
home = Path.home().expanduser()
|
|
157
|
+
resolved = path_obj.resolve(strict=False)
|
|
158
|
+
relative = resolved.relative_to(home).as_posix()
|
|
159
|
+
return f"~/{relative}"
|
|
160
|
+
except ValueError:
|
|
161
|
+
# Not under home; return expanded path
|
|
162
|
+
return str(path_obj)
|
|
163
|
+
except OSError:
|
|
164
|
+
# Fall back to original string on resolution errors
|
|
165
|
+
return config_path
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_account_display_lines(
|
|
169
|
+
name: str,
|
|
170
|
+
api_url: str,
|
|
171
|
+
masked_key: str,
|
|
172
|
+
config_path: str,
|
|
173
|
+
is_active: bool,
|
|
174
|
+
env_lock: bool,
|
|
175
|
+
metadata: dict[str, str | None],
|
|
176
|
+
) -> list[str]:
|
|
177
|
+
"""Build display lines for account information.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
name: Account name.
|
|
181
|
+
api_url: API URL.
|
|
182
|
+
masked_key: Masked API key.
|
|
183
|
+
config_path: Config file path.
|
|
184
|
+
is_active: Whether account is active.
|
|
185
|
+
env_lock: Whether env credentials are set.
|
|
186
|
+
metadata: Account metadata dict.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of formatted display lines.
|
|
190
|
+
"""
|
|
191
|
+
lines = [
|
|
192
|
+
f"[{SUCCESS_STYLE}]Name[/]: {name}{' (active)' if is_active else ''}",
|
|
193
|
+
f"[{SUCCESS_STYLE}]API URL[/]: {api_url or 'not set'}",
|
|
194
|
+
f"[{SUCCESS_STYLE}]Key[/]: {masked_key or 'not set'}",
|
|
195
|
+
f"[{SUCCESS_STYLE}]Config[/]: {config_path}",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
label_map = {
|
|
199
|
+
"notes": "Notes",
|
|
200
|
+
"last_used_at": "Last used",
|
|
201
|
+
"last_validated_at": "Last validated",
|
|
202
|
+
"created_with": "Created with",
|
|
203
|
+
}
|
|
204
|
+
for key, label in label_map.items():
|
|
205
|
+
value = metadata.get(key)
|
|
206
|
+
if value:
|
|
207
|
+
lines.append(f"[{SUCCESS_STYLE}]{label}[/]: {value}")
|
|
208
|
+
|
|
209
|
+
if env_lock:
|
|
210
|
+
lines.append(
|
|
211
|
+
f"[{WARNING_STYLE}]Env credentials detected (AIP_API_URL/AIP_API_KEY); stored profile may be ignored.[/]"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return lines
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@accounts_group.command("show")
|
|
218
|
+
@click.argument("name")
|
|
219
|
+
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
|
|
220
|
+
def show_account(name: str, output_json: bool) -> None:
|
|
221
|
+
"""Show details for a single account profile."""
|
|
222
|
+
store = get_account_store()
|
|
223
|
+
account = store.get_account(name)
|
|
224
|
+
|
|
225
|
+
if not account:
|
|
226
|
+
console.print(f"[{ERROR_STYLE}]Error: Account '{name}' not found.[/]")
|
|
227
|
+
raise click.Abort()
|
|
228
|
+
|
|
229
|
+
api_url = account.get("api_url", "")
|
|
230
|
+
api_key = account.get("api_key")
|
|
231
|
+
masked_key = _mask_api_key(api_key or "")
|
|
232
|
+
active_account = store.get_active_account()
|
|
233
|
+
is_active = active_account == name
|
|
234
|
+
env_lock = bool(os.getenv("AIP_API_URL") or os.getenv("AIP_API_KEY"))
|
|
235
|
+
config_path_raw = str(store.config_file)
|
|
236
|
+
config_path_display = _format_config_path(config_path_raw)
|
|
237
|
+
|
|
238
|
+
metadata = {
|
|
239
|
+
"notes": account.get("notes"),
|
|
240
|
+
"last_used_at": account.get("last_used_at"),
|
|
241
|
+
"last_validated_at": account.get("last_validated_at"),
|
|
242
|
+
"created_with": account.get("created_with"),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if output_json:
|
|
246
|
+
payload = _build_account_json_payload(name, api_url, masked_key, config_path_raw, is_active, env_lock, metadata)
|
|
247
|
+
click.echo(json.dumps(payload, indent=2))
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
lines = _build_account_display_lines(name, api_url, masked_key, config_path_display, is_active, env_lock, metadata)
|
|
251
|
+
|
|
252
|
+
lock_badge = " 🔒 Env lock" if env_lock else ""
|
|
253
|
+
console.print(
|
|
254
|
+
AIPPanel(
|
|
255
|
+
"\n".join(lines),
|
|
256
|
+
title=f"AIP Account{lock_badge}",
|
|
257
|
+
border_style=ACCENT_STYLE,
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
113
262
|
def _check_account_overwrite(name: str, store: AccountStore, overwrite: bool) -> dict[str, str] | None:
|
|
114
263
|
"""Check if account exists and handle overwrite logic.
|
|
115
264
|
|
|
@@ -155,8 +304,8 @@ def _get_credentials_non_interactive(
|
|
|
155
304
|
if not sys.stdin.isatty():
|
|
156
305
|
return url, sys.stdin.read().strip()
|
|
157
306
|
console.print(
|
|
158
|
-
f"[{ERROR_STYLE}]Error: --key
|
|
159
|
-
f"Use: cat key.txt | {command_name} {name} --url {url} --key[/]",
|
|
307
|
+
f"[{ERROR_STYLE}]Error: --key expects stdin or an explicit value. "
|
|
308
|
+
f"Use '--key <value>' or pipe: cat key.txt | {command_name} {name} --url {url} --key[/]",
|
|
160
309
|
)
|
|
161
310
|
raise click.Abort()
|
|
162
311
|
# URL provided, prompt for key
|
|
@@ -179,7 +328,8 @@ def _get_credentials_interactive(read_key_from_stdin: bool, existing: dict[str,
|
|
|
179
328
|
"""
|
|
180
329
|
if read_key_from_stdin:
|
|
181
330
|
console.print(
|
|
182
|
-
f"[{ERROR_STYLE}]Error: --key requires --url.
|
|
331
|
+
f"[{ERROR_STYLE}]Error: --key requires --url. "
|
|
332
|
+
f"Provide --url with --key <value|-> for non-interactive use or omit --key to be prompted.[/]",
|
|
183
333
|
)
|
|
184
334
|
raise click.Abort()
|
|
185
335
|
# Fully interactive
|
|
@@ -237,7 +387,7 @@ def _preserve_existing_values(
|
|
|
237
387
|
|
|
238
388
|
def _collect_credentials_from_inputs(
|
|
239
389
|
url: str | None,
|
|
240
|
-
|
|
390
|
+
api_key_input: str | None,
|
|
241
391
|
name: str,
|
|
242
392
|
existing: dict[str, str] | None,
|
|
243
393
|
command_name: str,
|
|
@@ -247,7 +397,7 @@ def _collect_credentials_from_inputs(
|
|
|
247
397
|
|
|
248
398
|
Args:
|
|
249
399
|
url: Optional URL from flag.
|
|
250
|
-
|
|
400
|
+
api_key_input: API key value from flag (or "-" when stdin requested).
|
|
251
401
|
name: Account name (for error messages).
|
|
252
402
|
existing: Existing account data.
|
|
253
403
|
command_name: Command name for error messages.
|
|
@@ -256,6 +406,29 @@ def _collect_credentials_from_inputs(
|
|
|
256
406
|
Returns:
|
|
257
407
|
Tuple of (api_url, api_key).
|
|
258
408
|
"""
|
|
409
|
+
provided_key = api_key_input if api_key_input not in (None, "-") else None
|
|
410
|
+
read_key_from_stdin = api_key_input == "-"
|
|
411
|
+
|
|
412
|
+
if provided_key and url:
|
|
413
|
+
# Fully non-interactive: URL and key provided via flags
|
|
414
|
+
return url, provided_key
|
|
415
|
+
|
|
416
|
+
if provided_key:
|
|
417
|
+
# Reuse stored URL if present; otherwise require --url
|
|
418
|
+
if existing_url:
|
|
419
|
+
return existing_url, provided_key
|
|
420
|
+
if existing:
|
|
421
|
+
console.print(
|
|
422
|
+
f"[{ERROR_STYLE}]Error: Account '{name}' is missing an API URL. "
|
|
423
|
+
f"Provide --url to set it when rotating the key.[/]"
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
console.print(
|
|
427
|
+
f"[{ERROR_STYLE}]Error: --key requires --url for new accounts. "
|
|
428
|
+
f"Run without --key for prompts or pass both flags for non-interactive setup.[/]",
|
|
429
|
+
)
|
|
430
|
+
raise click.Abort()
|
|
431
|
+
|
|
259
432
|
if url and read_key_from_stdin:
|
|
260
433
|
# Non-interactive: URL from flag, key from stdin
|
|
261
434
|
return _get_credentials_non_interactive(url, True, name, command_name)
|
|
@@ -271,15 +444,25 @@ def _collect_credentials_from_inputs(
|
|
|
271
444
|
|
|
272
445
|
def _collect_account_credentials(
|
|
273
446
|
url: str | None,
|
|
274
|
-
|
|
447
|
+
api_key_input: str | None,
|
|
275
448
|
name: str,
|
|
276
449
|
existing: dict[str, str] | None,
|
|
277
450
|
) -> tuple[str, str]:
|
|
278
451
|
"""Collect account credentials from various input methods.
|
|
279
452
|
|
|
453
|
+
Examples:
|
|
454
|
+
# Inline key
|
|
455
|
+
aip accounts add prod --url https://api.example.com --key sk-abc123
|
|
456
|
+
|
|
457
|
+
# Stdin (useful for scripts)
|
|
458
|
+
echo "sk-abc123" | aip accounts add prod --url https://api.example.com --key
|
|
459
|
+
|
|
460
|
+
# Fully interactive
|
|
461
|
+
aip accounts add prod
|
|
462
|
+
|
|
280
463
|
Args:
|
|
281
464
|
url: Optional URL from flag.
|
|
282
|
-
|
|
465
|
+
api_key_input: API key value from flag (or "-" when stdin requested).
|
|
283
466
|
name: Account name (for error messages).
|
|
284
467
|
existing: Existing account data.
|
|
285
468
|
|
|
@@ -293,9 +476,7 @@ def _collect_account_credentials(
|
|
|
293
476
|
existing_url = existing.get("api_url", "") if existing else ""
|
|
294
477
|
existing_key = existing.get("api_key", "") if existing else ""
|
|
295
478
|
|
|
296
|
-
api_url, api_key = _collect_credentials_from_inputs(
|
|
297
|
-
url, read_key_from_stdin, name, existing, command_name, existing_url
|
|
298
|
-
)
|
|
479
|
+
api_url, api_key = _collect_credentials_from_inputs(url, api_key_input, name, existing, command_name, existing_url)
|
|
299
480
|
|
|
300
481
|
# Preserve stored values when blank input is provided during edit
|
|
301
482
|
api_url, api_key = _preserve_existing_values(api_url, api_key, existing_url, existing_key)
|
|
@@ -311,9 +492,12 @@ def _collect_account_credentials(
|
|
|
311
492
|
@click.option("--url", help="API URL (required for non-interactive mode)")
|
|
312
493
|
@click.option(
|
|
313
494
|
"--key",
|
|
314
|
-
"
|
|
315
|
-
|
|
316
|
-
|
|
495
|
+
"api_key_input",
|
|
496
|
+
type=str,
|
|
497
|
+
is_flag=False,
|
|
498
|
+
flag_value="-",
|
|
499
|
+
default=None,
|
|
500
|
+
help="API key value. Pass without a value or '-' to read from stdin. Requires --url for non-interactive use.",
|
|
317
501
|
)
|
|
318
502
|
@click.option(
|
|
319
503
|
"--yes",
|
|
@@ -324,7 +508,7 @@ def _collect_account_credentials(
|
|
|
324
508
|
def add_account(
|
|
325
509
|
name: str,
|
|
326
510
|
url: str | None,
|
|
327
|
-
|
|
511
|
+
api_key_input: str | None,
|
|
328
512
|
overwrite: bool,
|
|
329
513
|
) -> None:
|
|
330
514
|
"""Add a new account profile.
|
|
@@ -332,7 +516,7 @@ def add_account(
|
|
|
332
516
|
NAME is the account name (1-32 chars, alphanumeric, dash, underscore).
|
|
333
517
|
|
|
334
518
|
By default, this command runs interactively, prompting for API URL and key.
|
|
335
|
-
For non-interactive use,
|
|
519
|
+
For non-interactive use, provide --url with --key <value> or --key - (stdin).
|
|
336
520
|
|
|
337
521
|
If the account already exists, use --yes to overwrite without prompting.
|
|
338
522
|
To update an existing account, use [bold]aip accounts edit <name>[/bold] instead.
|
|
@@ -343,7 +527,7 @@ def add_account(
|
|
|
343
527
|
existing = _check_account_overwrite(name, store, overwrite)
|
|
344
528
|
|
|
345
529
|
# Collect credentials
|
|
346
|
-
api_url, api_key = _collect_account_credentials(url,
|
|
530
|
+
api_url, api_key = _collect_account_credentials(url, api_key_input, name, existing)
|
|
347
531
|
|
|
348
532
|
# Save account
|
|
349
533
|
try:
|
|
@@ -363,14 +547,17 @@ def add_account(
|
|
|
363
547
|
@click.option("--url", help="API URL (optional, leave blank to keep current)")
|
|
364
548
|
@click.option(
|
|
365
549
|
"--key",
|
|
366
|
-
"
|
|
367
|
-
|
|
368
|
-
|
|
550
|
+
"api_key_input",
|
|
551
|
+
type=str,
|
|
552
|
+
is_flag=False,
|
|
553
|
+
flag_value="-",
|
|
554
|
+
default=None,
|
|
555
|
+
help="API key value. Pass without a value or '-' to read from stdin. Uses stored URL unless --url is provided.",
|
|
369
556
|
)
|
|
370
557
|
def edit_account(
|
|
371
558
|
name: str,
|
|
372
559
|
url: str | None,
|
|
373
|
-
|
|
560
|
+
api_key_input: str | None,
|
|
374
561
|
) -> None:
|
|
375
562
|
"""Edit an existing account profile's URL or key.
|
|
376
563
|
|
|
@@ -379,8 +566,8 @@ def edit_account(
|
|
|
379
566
|
By default, this command runs interactively, showing current values and
|
|
380
567
|
prompting for new ones. Leave fields blank to keep current values.
|
|
381
568
|
|
|
382
|
-
For non-interactive use, provide --url to change the URL, --key
|
|
383
|
-
or
|
|
569
|
+
For non-interactive use, provide --url to change the URL, --key <value> to rotate the key,
|
|
570
|
+
or --key - (stdin) for scripts. Stored values are reused for any fields not provided.
|
|
384
571
|
"""
|
|
385
572
|
store = get_account_store()
|
|
386
573
|
|
|
@@ -392,7 +579,7 @@ def edit_account(
|
|
|
392
579
|
raise click.Abort()
|
|
393
580
|
|
|
394
581
|
# Collect credentials (will pre-fill existing values in interactive mode)
|
|
395
|
-
api_url, api_key = _collect_account_credentials(url,
|
|
582
|
+
api_url, api_key = _collect_account_credentials(url, api_key_input, name, existing)
|
|
396
583
|
|
|
397
584
|
# Save account
|
|
398
585
|
try:
|
glaip_sdk/cli/commands/tools.py
CHANGED
|
@@ -10,8 +10,6 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
|
-
from rich.console import Console
|
|
14
|
-
|
|
15
13
|
from glaip_sdk.branding import (
|
|
16
14
|
ACCENT_STYLE,
|
|
17
15
|
ERROR_STYLE,
|
|
@@ -29,9 +27,7 @@ from glaip_sdk.cli.display import (
|
|
|
29
27
|
handle_json_output,
|
|
30
28
|
handle_rich_output,
|
|
31
29
|
)
|
|
32
|
-
from glaip_sdk.cli.io import
|
|
33
|
-
fetch_raw_resource_details,
|
|
34
|
-
)
|
|
30
|
+
from glaip_sdk.cli.io import fetch_raw_resource_details
|
|
35
31
|
from glaip_sdk.cli.io import (
|
|
36
32
|
load_resource_from_file_with_validation as load_resource_from_file,
|
|
37
33
|
)
|
|
@@ -49,6 +45,7 @@ from glaip_sdk.cli.utils import (
|
|
|
49
45
|
)
|
|
50
46
|
from glaip_sdk.icons import ICON_TOOL
|
|
51
47
|
from glaip_sdk.utils.import_export import merge_import_with_cli_args
|
|
48
|
+
from rich.console import Console
|
|
52
49
|
|
|
53
50
|
console = Console()
|
|
54
51
|
|
|
@@ -15,10 +15,7 @@ from glaip_sdk.config.constants import (
|
|
|
15
15
|
DEFAULT_AGENT_VERSION,
|
|
16
16
|
DEFAULT_MODEL,
|
|
17
17
|
)
|
|
18
|
-
from glaip_sdk.payload_schemas.agent import
|
|
19
|
-
AgentImportOperation,
|
|
20
|
-
get_import_field_plan,
|
|
21
|
-
)
|
|
18
|
+
from glaip_sdk.payload_schemas.agent import AgentImportOperation, get_import_field_plan
|
|
22
19
|
from glaip_sdk.utils.client_utils import extract_ids
|
|
23
20
|
|
|
24
21
|
_LM_CONFLICT_KEYS = {
|
|
@@ -437,12 +434,16 @@ __all__ = [
|
|
|
437
434
|
|
|
438
435
|
def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
|
|
439
436
|
"""Populate immutable agent update fields using request data or existing agent defaults."""
|
|
437
|
+
# Support both "agent_type" (runtime class) and "type" (API response) attributes
|
|
438
|
+
current_type = getattr(current_agent, "agent_type", None) or getattr(current_agent, "type", None)
|
|
440
439
|
return {
|
|
441
|
-
"name": request.name.strip() if request.name is not None else getattr(current_agent, "name", None),
|
|
442
|
-
"instruction":
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
440
|
+
"name": (request.name.strip() if request.name is not None else getattr(current_agent, "name", None)),
|
|
441
|
+
"instruction": (
|
|
442
|
+
request.instruction.strip()
|
|
443
|
+
if request.instruction is not None
|
|
444
|
+
else getattr(current_agent, "instruction", None)
|
|
445
|
+
),
|
|
446
|
+
"type": request.agent_type or current_type or DEFAULT_AGENT_TYPE,
|
|
446
447
|
"framework": request.framework or getattr(current_agent, "framework", None) or DEFAULT_AGENT_FRAMEWORK,
|
|
447
448
|
"version": request.version or getattr(current_agent, "version", None) or DEFAULT_AGENT_VERSION,
|
|
448
449
|
}
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import asyncio
|
|
@@ -15,7 +16,7 @@ from pathlib import Path
|
|
|
15
16
|
from typing import Any, BinaryIO
|
|
16
17
|
|
|
17
18
|
import httpx
|
|
18
|
-
|
|
19
|
+
from glaip_sdk.agents import Agent
|
|
19
20
|
from glaip_sdk.client._agent_payloads import (
|
|
20
21
|
AgentCreateRequest,
|
|
21
22
|
AgentListParams,
|
|
@@ -40,7 +41,7 @@ from glaip_sdk.config.constants import (
|
|
|
40
41
|
DEFAULT_MODEL,
|
|
41
42
|
)
|
|
42
43
|
from glaip_sdk.exceptions import NotFoundError, ValidationError
|
|
43
|
-
from glaip_sdk.models import
|
|
44
|
+
from glaip_sdk.models import AgentResponse
|
|
44
45
|
from glaip_sdk.payload_schemas.agent import list_server_only_fields
|
|
45
46
|
from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
|
|
46
47
|
from glaip_sdk.utils.client_utils import (
|
|
@@ -73,7 +74,9 @@ _DEFAULT_METADATA_TYPE = "custom"
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
@asynccontextmanager
|
|
76
|
-
async def _async_timeout_guard(
|
|
77
|
+
async def _async_timeout_guard(
|
|
78
|
+
timeout_seconds: float | None,
|
|
79
|
+
) -> AsyncGenerator[None, None]:
|
|
77
80
|
"""Apply an asyncio timeout when a custom timeout is provided."""
|
|
78
81
|
if timeout_seconds is None:
|
|
79
82
|
yield
|
|
@@ -260,7 +263,7 @@ class AgentClient(BaseClient):
|
|
|
260
263
|
self._renderer_manager = AgentRunRenderingManager(logger)
|
|
261
264
|
self._tool_client: ToolClient | None = None
|
|
262
265
|
self._mcp_client: MCPClient | None = None
|
|
263
|
-
self._runs_client:
|
|
266
|
+
self._runs_client: AgentRunsClient | None = None
|
|
264
267
|
|
|
265
268
|
def list_agents(
|
|
266
269
|
self,
|
|
@@ -359,7 +362,8 @@ class AgentClient(BaseClient):
|
|
|
359
362
|
status_code=404,
|
|
360
363
|
)
|
|
361
364
|
|
|
362
|
-
|
|
365
|
+
response = AgentResponse(**data)
|
|
366
|
+
return Agent.from_response(response, client=self)
|
|
363
367
|
|
|
364
368
|
def find_agents(self, name: str | None = None) -> list[Agent]:
|
|
365
369
|
"""Find agents by name."""
|
|
@@ -826,7 +830,8 @@ class AgentClient(BaseClient):
|
|
|
826
830
|
get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
|
|
827
831
|
json=payload_dict,
|
|
828
832
|
)
|
|
829
|
-
|
|
833
|
+
response = AgentResponse(**full_agent_data)
|
|
834
|
+
return Agent.from_response(response, client=self)
|
|
830
835
|
|
|
831
836
|
def create_agent(
|
|
832
837
|
self,
|
|
@@ -921,8 +926,9 @@ class AgentClient(BaseClient):
|
|
|
921
926
|
|
|
922
927
|
payload_dict = request.to_payload(current_agent)
|
|
923
928
|
|
|
924
|
-
|
|
925
|
-
|
|
929
|
+
api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
|
|
930
|
+
response = AgentResponse(**api_response)
|
|
931
|
+
return Agent.from_response(response, client=self)
|
|
926
932
|
|
|
927
933
|
def update_agent(
|
|
928
934
|
self,
|
|
@@ -969,6 +975,62 @@ class AgentClient(BaseClient):
|
|
|
969
975
|
"""Delete an agent."""
|
|
970
976
|
self._request("DELETE", f"/agents/{agent_id}")
|
|
971
977
|
|
|
978
|
+
def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
|
|
979
|
+
"""Create or update an agent by instance, ID, or name.
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
identifier: Agent instance, ID (UUID string), or name
|
|
983
|
+
**kwargs: Agent configuration (instruction, description, tools, etc.)
|
|
984
|
+
|
|
985
|
+
Returns:
|
|
986
|
+
The created or updated agent.
|
|
987
|
+
|
|
988
|
+
Example:
|
|
989
|
+
>>> # By name (creates if not exists)
|
|
990
|
+
>>> agent = client.agents.upsert_agent(
|
|
991
|
+
... "hello_agent",
|
|
992
|
+
... instruction="You are a helpful assistant.",
|
|
993
|
+
... description="A friendly agent",
|
|
994
|
+
... )
|
|
995
|
+
>>> # By instance
|
|
996
|
+
>>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
|
|
997
|
+
>>> # By ID
|
|
998
|
+
>>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
|
|
999
|
+
"""
|
|
1000
|
+
# Handle Agent instance
|
|
1001
|
+
if isinstance(identifier, Agent):
|
|
1002
|
+
if identifier.id:
|
|
1003
|
+
logger.info("Updating agent by instance: %s", identifier.name)
|
|
1004
|
+
return self.update_agent(identifier.id, name=identifier.name, **kwargs)
|
|
1005
|
+
identifier = identifier.name
|
|
1006
|
+
|
|
1007
|
+
# Handle string (ID or name)
|
|
1008
|
+
if isinstance(identifier, str):
|
|
1009
|
+
# Check if it's a UUID
|
|
1010
|
+
if is_uuid(identifier):
|
|
1011
|
+
logger.info("Updating agent by ID: %s", identifier)
|
|
1012
|
+
return self.update_agent(identifier, **kwargs)
|
|
1013
|
+
|
|
1014
|
+
# It's a name - find or create
|
|
1015
|
+
return self._upsert_agent_by_name(identifier, **kwargs)
|
|
1016
|
+
|
|
1017
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
1018
|
+
|
|
1019
|
+
def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
|
|
1020
|
+
"""Find agent by name and update, or create if not found."""
|
|
1021
|
+
existing = self.find_agents(name)
|
|
1022
|
+
|
|
1023
|
+
if len(existing) == 1:
|
|
1024
|
+
logger.info("Updating existing agent: %s", name)
|
|
1025
|
+
return self.update_agent(existing[0].id, name=name, **kwargs)
|
|
1026
|
+
|
|
1027
|
+
if len(existing) > 1:
|
|
1028
|
+
raise ValueError(f"Multiple agents found with name '{name}'")
|
|
1029
|
+
|
|
1030
|
+
# Create new agent
|
|
1031
|
+
logger.info("Creating new agent: %s", name)
|
|
1032
|
+
return self.create_agent(name=name, **kwargs)
|
|
1033
|
+
|
|
972
1034
|
def _prepare_sync_request_data(
|
|
973
1035
|
self,
|
|
974
1036
|
message: str,
|
glaip_sdk/client/base.py
CHANGED
glaip_sdk/client/main.py
CHANGED
|
@@ -3,16 +3,24 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
9
12
|
|
|
10
13
|
from glaip_sdk.client.agents import AgentClient
|
|
11
14
|
from glaip_sdk.client.base import BaseClient
|
|
12
15
|
from glaip_sdk.client.mcps import MCPClient
|
|
13
16
|
from glaip_sdk.client.shared import build_shared_config
|
|
14
17
|
from glaip_sdk.client.tools import ToolClient
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from glaip_sdk.agents import Agent
|
|
21
|
+
from glaip_sdk.client._agent_payloads import AgentListResult
|
|
22
|
+
from glaip_sdk.mcps import MCP
|
|
23
|
+
from glaip_sdk.tools import Tool
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
class Client(BaseClient):
|
|
@@ -49,7 +57,7 @@ class Client(BaseClient):
|
|
|
49
57
|
name: str | None = None,
|
|
50
58
|
version: str | None = None,
|
|
51
59
|
sync_langflow_agents: bool = False,
|
|
52
|
-
) ->
|
|
60
|
+
) -> AgentListResult:
|
|
53
61
|
"""List agents with optional filtering.
|
|
54
62
|
|
|
55
63
|
Args:
|
|
@@ -60,7 +68,7 @@ class Client(BaseClient):
|
|
|
60
68
|
sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
|
|
61
69
|
|
|
62
70
|
Returns:
|
|
63
|
-
|
|
71
|
+
AgentListResult with agents and pagination metadata. Supports iteration and indexing.
|
|
64
72
|
"""
|
|
65
73
|
return self.agents.list_agents(
|
|
66
74
|
agent_type=agent_type,
|