agent-devkit 0.0.3 → 0.1.0
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.
- package/README.md +170 -1
- package/package.json +1 -1
- package/runtime/README.md +192 -14
- package/runtime/agent +29 -13
- package/runtime/agents/README.md +3 -0
- package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
- package/runtime/agents/github-pr-reviewer/README.md +7 -0
- package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
- package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
- package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
- package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
- package/runtime/cli/README.md +93 -0
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/aliases.py +196 -0
- package/runtime/cli/aikit/app_home.py +78 -0
- package/runtime/cli/aikit/audit.py +344 -0
- package/runtime/cli/aikit/calendar.py +163 -0
- package/runtime/cli/aikit/diagnostics.py +9 -0
- package/runtime/cli/aikit/github_pr.py +254 -0
- package/runtime/cli/aikit/identity.py +75 -0
- package/runtime/cli/aikit/llm.py +240 -48
- package/runtime/cli/aikit/main.py +924 -24
- package/runtime/cli/aikit/memory.py +148 -8
- package/runtime/cli/aikit/notifications.py +9 -0
- package/runtime/cli/aikit/permissions.py +345 -0
- package/runtime/cli/aikit/personality.py +123 -0
- package/runtime/cli/aikit/providers.py +4 -5
- package/runtime/cli/aikit/scheduler.py +18 -0
- package/runtime/cli/aikit/sessions.py +396 -0
- package/runtime/cli/aikit/setup_wizard.py +12 -0
- package/runtime/cli/aikit/tasks.py +351 -0
- package/runtime/cli/aikit/toolchain.py +274 -0
- package/runtime/plugins/claude-code-ai-devkit/plugin.json +1 -1
- package/runtime/plugins/claude-skill-ai-devkit/plugin.json +1 -1
- package/runtime/providers/calendar.yaml +28 -0
- package/runtime/providers/github.yaml +42 -0
- package/runtime/providers/local-notification.yaml +19 -0
- package/runtime/providers/local-scheduler.yaml +24 -0
- package/runtime/scripts/README.md +1 -1
- package/runtime/scripts/verify-release-alignment.mjs +1 -1
- package/runtime/vendor/skills/napkin/napkin.md +13 -3
package/runtime/cli/aikit/llm.py
CHANGED
|
@@ -13,6 +13,9 @@ from dataclasses import dataclass
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
+
from cli.aikit.app_home import app_home, config_path as app_config_path, ensure_app_home
|
|
17
|
+
from cli.aikit.identity import host_cli_prompt, identity_system_prompt
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
@dataclass(frozen=True)
|
|
18
21
|
class LlmBackend:
|
|
@@ -97,17 +100,15 @@ BACKENDS: dict[str, LlmBackend] = {
|
|
|
97
100
|
|
|
98
101
|
ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
99
102
|
DEFAULT_AGENT_TIMEOUT_SECONDS = 120
|
|
103
|
+
DEFAULT_FALLBACK_ORDER = ("claude-code", "codex-cli", "openai", "anthropic", "openrouter", "ollama")
|
|
100
104
|
|
|
101
105
|
|
|
102
106
|
def config_home() -> Path:
|
|
103
|
-
|
|
104
|
-
if raw:
|
|
105
|
-
return Path(raw).expanduser().resolve()
|
|
106
|
-
return (Path.home() / ".ai-devkit").resolve()
|
|
107
|
+
return app_home()
|
|
107
108
|
|
|
108
109
|
|
|
109
110
|
def config_path() -> Path:
|
|
110
|
-
return
|
|
111
|
+
return app_config_path()
|
|
111
112
|
|
|
112
113
|
|
|
113
114
|
def empty_config() -> dict[str, Any]:
|
|
@@ -137,6 +138,7 @@ def load_config() -> dict[str, Any]:
|
|
|
137
138
|
|
|
138
139
|
|
|
139
140
|
def save_config(config: dict[str, Any]) -> Path:
|
|
141
|
+
ensure_app_home()
|
|
140
142
|
path = config_path()
|
|
141
143
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
142
144
|
with path.open("w", encoding="utf-8") as file:
|
|
@@ -156,10 +158,12 @@ def list_backends() -> dict[str, Any]:
|
|
|
156
158
|
config = load_config()
|
|
157
159
|
default_backend = config.get("llm", {}).get("default")
|
|
158
160
|
configured = set(config.get("llm", {}).get("backends", {}))
|
|
161
|
+
preference = llm_preference(config)
|
|
159
162
|
return {
|
|
160
163
|
"kind": "llm-backends",
|
|
161
164
|
"config_path": str(config_path()),
|
|
162
165
|
"default": default_backend,
|
|
166
|
+
"preference": preference,
|
|
163
167
|
"items": [
|
|
164
168
|
{
|
|
165
169
|
"id": backend.id,
|
|
@@ -252,6 +256,68 @@ def set_default_backend(backend_id: str) -> dict[str, Any]:
|
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
|
|
259
|
+
def llm_preference(config: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
260
|
+
config = config or load_config()
|
|
261
|
+
llm = config.get("llm") if isinstance(config.get("llm"), dict) else {}
|
|
262
|
+
raw_preference = llm.get("preference") if isinstance(llm.get("preference"), dict) else {}
|
|
263
|
+
primary = raw_preference.get("primary") or llm.get("default")
|
|
264
|
+
order = raw_preference.get("order") if isinstance(raw_preference.get("order"), list) else None
|
|
265
|
+
normalized_order = normalize_backend_order(order or DEFAULT_FALLBACK_ORDER)
|
|
266
|
+
if primary and primary in BACKENDS:
|
|
267
|
+
normalized_order = [primary, *[item for item in normalized_order if item != primary]]
|
|
268
|
+
return {
|
|
269
|
+
"kind": "llm-preference",
|
|
270
|
+
"status": "ok",
|
|
271
|
+
"config_path": str(config_path()),
|
|
272
|
+
"primary": primary,
|
|
273
|
+
"order": normalized_order,
|
|
274
|
+
"fallback_enabled": bool(raw_preference.get("fallback_enabled", True)),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def set_llm_preference(
|
|
279
|
+
*,
|
|
280
|
+
primary: str | None = None,
|
|
281
|
+
order: str | list[str] | tuple[str, ...] | None = None,
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
config = load_config()
|
|
284
|
+
llm = config.setdefault("llm", {"default": None, "backends": {}})
|
|
285
|
+
backends = llm.setdefault("backends", {})
|
|
286
|
+
preference = llm.setdefault("preference", {})
|
|
287
|
+
if not isinstance(preference, dict):
|
|
288
|
+
preference = {}
|
|
289
|
+
llm["preference"] = preference
|
|
290
|
+
|
|
291
|
+
if primary:
|
|
292
|
+
backend = backend_or_error(primary)
|
|
293
|
+
backends.setdefault(backend.id, default_backend_config(backend))
|
|
294
|
+
llm["default"] = backend.id
|
|
295
|
+
preference["primary"] = backend.id
|
|
296
|
+
if order is not None:
|
|
297
|
+
preference["order"] = normalize_backend_order(order)
|
|
298
|
+
preference.setdefault("fallback_enabled", True)
|
|
299
|
+
written_path = save_config(config)
|
|
300
|
+
payload = llm_preference(config)
|
|
301
|
+
payload.update({"status": "configured", "config_path": str(written_path), "stored_secret": False})
|
|
302
|
+
return payload
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def normalize_backend_order(order: str | list[str] | tuple[str, ...]) -> list[str]:
|
|
306
|
+
raw_items = order.split(",") if isinstance(order, str) else list(order)
|
|
307
|
+
normalized: list[str] = []
|
|
308
|
+
for raw_item in raw_items:
|
|
309
|
+
backend_id = str(raw_item).strip()
|
|
310
|
+
if not backend_id:
|
|
311
|
+
continue
|
|
312
|
+
backend_or_error(backend_id)
|
|
313
|
+
if backend_id not in normalized:
|
|
314
|
+
normalized.append(backend_id)
|
|
315
|
+
for backend_id in DEFAULT_FALLBACK_ORDER:
|
|
316
|
+
if backend_id not in normalized:
|
|
317
|
+
normalized.append(backend_id)
|
|
318
|
+
return normalized
|
|
319
|
+
|
|
320
|
+
|
|
255
321
|
def default_backend_config(backend: LlmBackend) -> dict[str, Any]:
|
|
256
322
|
entry: dict[str, Any] = {"kind": backend.kind, "auth": backend.auth}
|
|
257
323
|
if backend.auth == "api-key-env":
|
|
@@ -359,31 +425,54 @@ def resolve_backend(requested: str | None = None) -> dict[str, Any] | None:
|
|
|
359
425
|
backend_or_error(requested)
|
|
360
426
|
return doctor_backend(BACKENDS[requested], config)
|
|
361
427
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
configured = config.get("llm", {}).get("backends", {})
|
|
368
|
-
if isinstance(configured, dict):
|
|
369
|
-
for backend_id in configured:
|
|
370
|
-
if backend_id in BACKENDS:
|
|
371
|
-
check = doctor_backend(BACKENDS[backend_id], config)
|
|
372
|
-
if check.get("status") == "ok":
|
|
373
|
-
return check
|
|
428
|
+
for backend_id in candidate_backend_ids(config=config, allow_fallback=True):
|
|
429
|
+
check = doctor_backend(BACKENDS[backend_id], config)
|
|
430
|
+
if check.get("status") == "ok":
|
|
431
|
+
return check
|
|
374
432
|
|
|
375
433
|
return None
|
|
376
434
|
|
|
377
435
|
|
|
378
|
-
def
|
|
379
|
-
|
|
380
|
-
|
|
436
|
+
def candidate_backend_ids(
|
|
437
|
+
*,
|
|
438
|
+
config: dict[str, Any],
|
|
439
|
+
requested: str | None = None,
|
|
440
|
+
allow_fallback: bool = True,
|
|
441
|
+
) -> list[str]:
|
|
442
|
+
if requested:
|
|
443
|
+
backend_or_error(requested)
|
|
444
|
+
return [requested]
|
|
445
|
+
llm = config.get("llm") if isinstance(config.get("llm"), dict) else {}
|
|
446
|
+
configured = llm.get("backends") if isinstance(llm.get("backends"), dict) else {}
|
|
447
|
+
configured_ids = [backend_id for backend_id in configured if backend_id in BACKENDS]
|
|
448
|
+
if not configured_ids:
|
|
449
|
+
return []
|
|
450
|
+
preference = llm_preference(config)
|
|
451
|
+
ordered = [backend_id for backend_id in preference["order"] if backend_id in configured_ids]
|
|
452
|
+
for backend_id in configured_ids:
|
|
453
|
+
if backend_id not in ordered:
|
|
454
|
+
ordered.append(backend_id)
|
|
455
|
+
return ordered if allow_fallback else ordered[:1]
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def invoke_agent_prompt(
|
|
459
|
+
prompt: str,
|
|
460
|
+
requested: str | None = None,
|
|
461
|
+
*,
|
|
462
|
+
public_name: str = "Agent DevKit",
|
|
463
|
+
allow_fallback: bool = True,
|
|
464
|
+
) -> dict[str, Any]:
|
|
465
|
+
config = load_config()
|
|
466
|
+
candidate_ids = candidate_backend_ids(config=config, requested=requested, allow_fallback=allow_fallback)
|
|
467
|
+
if not candidate_ids:
|
|
381
468
|
return {
|
|
382
469
|
"kind": "agent",
|
|
383
470
|
"status": "blocked",
|
|
384
471
|
"ok": False,
|
|
385
472
|
"requires_llm": True,
|
|
386
473
|
"llm_backend": requested,
|
|
474
|
+
"llm_backend_attempts": [],
|
|
475
|
+
"llm_fallback_enabled": allow_fallback,
|
|
387
476
|
"prompt_received": True,
|
|
388
477
|
"prompt_length": len(prompt),
|
|
389
478
|
"message": "agent requires a configured LLM backend for natural-language tasks.",
|
|
@@ -395,36 +484,107 @@ def invoke_agent_prompt(prompt: str, requested: str | None = None) -> dict[str,
|
|
|
395
484
|
"exit_code": 2,
|
|
396
485
|
}
|
|
397
486
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
487
|
+
attempts: list[dict[str, Any]] = []
|
|
488
|
+
last_error: str | None = None
|
|
489
|
+
for backend_id in candidate_ids:
|
|
490
|
+
backend = doctor_backend(BACKENDS[backend_id], config)
|
|
491
|
+
attempt = {"id": backend_id, "status": backend.get("status")}
|
|
492
|
+
attempts.append(attempt)
|
|
493
|
+
if backend.get("status") != "ok":
|
|
494
|
+
attempt["message"] = backend.get("message")
|
|
495
|
+
continue
|
|
496
|
+
try:
|
|
497
|
+
response = invoke_resolved_backend(backend, prompt, public_name=public_name)
|
|
498
|
+
except LlmPolicyError as exc:
|
|
499
|
+
attempt["status"] = "policy-blocked"
|
|
500
|
+
attempt["message"] = str(exc)
|
|
501
|
+
return failed_agent_payload(
|
|
502
|
+
prompt,
|
|
503
|
+
backend=backend,
|
|
504
|
+
attempts=attempts,
|
|
505
|
+
message=str(exc),
|
|
506
|
+
policy_error=True,
|
|
507
|
+
allow_fallback=allow_fallback,
|
|
508
|
+
)
|
|
509
|
+
except LlmInvocationError as exc:
|
|
510
|
+
attempt["status"] = "failed"
|
|
511
|
+
attempt["message"] = str(exc)
|
|
512
|
+
last_error = str(exc)
|
|
513
|
+
if requested or not allow_fallback:
|
|
514
|
+
break
|
|
515
|
+
continue
|
|
516
|
+
attempt["status"] = "ok"
|
|
401
517
|
return {
|
|
402
518
|
"kind": "agent",
|
|
403
|
-
"status": "
|
|
404
|
-
"ok":
|
|
519
|
+
"status": "ok",
|
|
520
|
+
"ok": True,
|
|
405
521
|
"requires_llm": True,
|
|
406
522
|
"llm_backend": backend["id"],
|
|
407
523
|
"llm_backend_status": backend["status"],
|
|
524
|
+
"llm_backend_attempts": attempts,
|
|
525
|
+
"llm_fallback_enabled": allow_fallback,
|
|
526
|
+
"llm_fallback_used": attempts[0]["id"] != backend["id"],
|
|
408
527
|
"prompt_received": True,
|
|
409
528
|
"prompt_length": len(prompt),
|
|
410
|
-
"
|
|
411
|
-
"next_steps": [
|
|
412
|
-
"Run `agent llm doctor` to verify the selected backend.",
|
|
413
|
-
"Use `agent run <agent> <capability>` when deterministic execution is enough.",
|
|
414
|
-
],
|
|
415
|
-
"exit_code": 1,
|
|
529
|
+
"response": response,
|
|
416
530
|
}
|
|
417
531
|
|
|
532
|
+
if last_error:
|
|
533
|
+
return failed_agent_payload(
|
|
534
|
+
prompt,
|
|
535
|
+
backend={"id": attempts[-1]["id"], "status": attempts[-1].get("status")},
|
|
536
|
+
attempts=attempts,
|
|
537
|
+
message=last_error,
|
|
538
|
+
policy_error=False,
|
|
539
|
+
allow_fallback=allow_fallback,
|
|
540
|
+
)
|
|
418
541
|
return {
|
|
419
542
|
"kind": "agent",
|
|
420
|
-
"status": "
|
|
421
|
-
"ok":
|
|
543
|
+
"status": "blocked",
|
|
544
|
+
"ok": False,
|
|
422
545
|
"requires_llm": True,
|
|
423
|
-
"llm_backend":
|
|
424
|
-
"
|
|
546
|
+
"llm_backend": requested,
|
|
547
|
+
"llm_backend_attempts": attempts,
|
|
548
|
+
"llm_fallback_enabled": allow_fallback,
|
|
425
549
|
"prompt_received": True,
|
|
426
550
|
"prompt_length": len(prompt),
|
|
427
|
-
"
|
|
551
|
+
"message": "agent did not find an available configured LLM backend.",
|
|
552
|
+
"next_steps": [
|
|
553
|
+
"Run `agent llm doctor` to verify configured backends.",
|
|
554
|
+
"Configure a backend with `agent llm configure <backend> --set-default`.",
|
|
555
|
+
"Adjust fallback order with `agent llm preference set --primary <backend>`.",
|
|
556
|
+
],
|
|
557
|
+
"exit_code": 2,
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def failed_agent_payload(
|
|
562
|
+
prompt: str,
|
|
563
|
+
*,
|
|
564
|
+
backend: dict[str, Any],
|
|
565
|
+
attempts: list[dict[str, Any]],
|
|
566
|
+
message: str,
|
|
567
|
+
policy_error: bool,
|
|
568
|
+
allow_fallback: bool,
|
|
569
|
+
) -> dict[str, Any]:
|
|
570
|
+
return {
|
|
571
|
+
"kind": "agent",
|
|
572
|
+
"status": "failed",
|
|
573
|
+
"ok": False,
|
|
574
|
+
"requires_llm": True,
|
|
575
|
+
"llm_backend": backend.get("id"),
|
|
576
|
+
"llm_backend_status": backend.get("status"),
|
|
577
|
+
"llm_backend_attempts": attempts,
|
|
578
|
+
"llm_fallback_enabled": allow_fallback,
|
|
579
|
+
"llm_policy_error": policy_error,
|
|
580
|
+
"prompt_received": True,
|
|
581
|
+
"prompt_length": len(prompt),
|
|
582
|
+
"message": message,
|
|
583
|
+
"next_steps": [
|
|
584
|
+
"Run `agent llm doctor` to verify the selected backend.",
|
|
585
|
+
"Use `agent run <agent> <capability>` when deterministic execution is enough.",
|
|
586
|
+
],
|
|
587
|
+
"exit_code": 1,
|
|
428
588
|
}
|
|
429
589
|
|
|
430
590
|
|
|
@@ -432,21 +592,33 @@ class LlmInvocationError(RuntimeError):
|
|
|
432
592
|
"""Raised for safe, user-facing LLM invocation errors."""
|
|
433
593
|
|
|
434
594
|
|
|
435
|
-
|
|
595
|
+
class LlmPolicyError(LlmInvocationError):
|
|
596
|
+
"""Raised when the backend rejects content for policy or safety reasons."""
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def invoke_resolved_backend(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
|
|
436
600
|
kind = backend.get("kind")
|
|
437
601
|
backend_id = backend.get("id")
|
|
438
602
|
if kind == "openai-compatible":
|
|
439
|
-
return invoke_openai_compatible(backend, prompt)
|
|
603
|
+
return invoke_openai_compatible(backend, prompt, public_name=public_name)
|
|
440
604
|
if kind == "anthropic":
|
|
441
|
-
return invoke_anthropic(backend, prompt)
|
|
605
|
+
return invoke_anthropic(backend, prompt, public_name=public_name)
|
|
442
606
|
if kind == "host-cli" and backend_id == "codex-cli":
|
|
443
|
-
return invoke_host_cli(
|
|
607
|
+
return invoke_host_cli(
|
|
608
|
+
[
|
|
609
|
+
str(backend.get("command") or "codex"),
|
|
610
|
+
"exec",
|
|
611
|
+
"--skip-git-repo-check",
|
|
612
|
+
"--ephemeral",
|
|
613
|
+
host_cli_prompt(prompt, name=public_name),
|
|
614
|
+
]
|
|
615
|
+
)
|
|
444
616
|
if kind == "host-cli" and backend_id == "claude-code":
|
|
445
|
-
return invoke_host_cli([str(backend.get("command") or "claude"), "--print", "--permission-mode", "plan", prompt])
|
|
617
|
+
return invoke_host_cli([str(backend.get("command") or "claude"), "--print", "--permission-mode", "plan", host_cli_prompt(prompt, name=public_name)])
|
|
446
618
|
raise LlmInvocationError(f"LLM backend is not invokable by agent yet: {backend_id}")
|
|
447
619
|
|
|
448
620
|
|
|
449
|
-
def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
|
|
621
|
+
def invoke_openai_compatible(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
|
|
450
622
|
base_url = str(backend.get("base_url") or "").rstrip("/")
|
|
451
623
|
model = str(backend.get("model") or "")
|
|
452
624
|
if not base_url or not model:
|
|
@@ -460,10 +632,7 @@ def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
|
|
|
460
632
|
"messages": [
|
|
461
633
|
{
|
|
462
634
|
"role": "system",
|
|
463
|
-
"content": (
|
|
464
|
-
"You are AI DevKit's natural-language router. Answer concisely, "
|
|
465
|
-
"prefer deterministic agent/capability commands when useful, and never ask for all credentials upfront."
|
|
466
|
-
),
|
|
635
|
+
"content": identity_system_prompt(name=public_name),
|
|
467
636
|
},
|
|
468
637
|
{"role": "user", "content": prompt},
|
|
469
638
|
],
|
|
@@ -475,7 +644,7 @@ def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
|
|
|
475
644
|
raise LlmInvocationError("OpenAI-compatible backend returned an unexpected response shape.") from exc
|
|
476
645
|
|
|
477
646
|
|
|
478
|
-
def invoke_anthropic(backend: dict[str, Any], prompt: str) -> str:
|
|
647
|
+
def invoke_anthropic(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
|
|
479
648
|
api_key = api_key_from_ref(backend.get("api_key_ref"))
|
|
480
649
|
if not api_key:
|
|
481
650
|
raise LlmInvocationError("Anthropic backend is missing an API key environment variable.")
|
|
@@ -491,7 +660,7 @@ def invoke_anthropic(backend: dict[str, Any], prompt: str) -> str:
|
|
|
491
660
|
body = {
|
|
492
661
|
"model": model,
|
|
493
662
|
"max_tokens": 1024,
|
|
494
|
-
"system":
|
|
663
|
+
"system": identity_system_prompt(name=public_name),
|
|
495
664
|
"messages": [{"role": "user", "content": prompt}],
|
|
496
665
|
}
|
|
497
666
|
payload = post_json(f"{base_url}/messages", headers, body)
|
|
@@ -527,6 +696,13 @@ def post_json(url: str, headers: dict[str, str], body: dict[str, Any]) -> dict[s
|
|
|
527
696
|
with urllib.request.urlopen(request, timeout=DEFAULT_AGENT_TIMEOUT_SECONDS) as response: # noqa: S310 - user-configured backend URL.
|
|
528
697
|
raw = response.read().decode("utf-8")
|
|
529
698
|
except urllib.error.HTTPError as exc:
|
|
699
|
+
raw_error = ""
|
|
700
|
+
try:
|
|
701
|
+
raw_error = exc.read().decode("utf-8", errors="replace")
|
|
702
|
+
except OSError:
|
|
703
|
+
raw_error = ""
|
|
704
|
+
if is_policy_error(exc.code, raw_error):
|
|
705
|
+
raise LlmPolicyError(f"LLM backend policy error: {exc.code}") from exc
|
|
530
706
|
raise LlmInvocationError(f"LLM backend HTTP error: {exc.code}") from exc
|
|
531
707
|
except urllib.error.URLError as exc:
|
|
532
708
|
raise LlmInvocationError(f"LLM backend connection failed: {exc.reason}") from exc
|
|
@@ -536,9 +712,25 @@ def post_json(url: str, headers: dict[str, str], body: dict[str, Any]) -> dict[s
|
|
|
536
712
|
raise LlmInvocationError("LLM backend returned invalid JSON.") from exc
|
|
537
713
|
if not isinstance(payload, dict):
|
|
538
714
|
raise LlmInvocationError("LLM backend returned a non-object JSON payload.")
|
|
715
|
+
if payload_has_policy_error(payload):
|
|
716
|
+
raise LlmPolicyError("LLM backend returned a policy error.")
|
|
539
717
|
return payload
|
|
540
718
|
|
|
541
719
|
|
|
720
|
+
def is_policy_error(status_code: int, body: str) -> bool:
|
|
721
|
+
if status_code not in {400, 403}:
|
|
722
|
+
return False
|
|
723
|
+
return any(marker in body.lower() for marker in ("policy", "safety", "content_filter", "content filter", "not allowed"))
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def payload_has_policy_error(payload: dict[str, Any]) -> bool:
|
|
727
|
+
error = payload.get("error")
|
|
728
|
+
if not isinstance(error, dict):
|
|
729
|
+
return False
|
|
730
|
+
text = json.dumps(error, ensure_ascii=False).lower()
|
|
731
|
+
return any(marker in text for marker in ("policy", "safety", "content_filter", "content filter", "not allowed"))
|
|
732
|
+
|
|
733
|
+
|
|
542
734
|
def api_key_from_ref(value: Any) -> str | None:
|
|
543
735
|
ref = str(value or "")
|
|
544
736
|
if not ref.startswith("env:"):
|