agent-devkit 0.0.4 → 0.1.5
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 +42 -2
- package/package.json +1 -1
- package/runtime/README.md +50 -2
- 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 +37 -1
- 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/configuration_orchestrator.py +291 -0
- package/runtime/cli/aikit/decision_store.py +158 -0
- package/runtime/cli/aikit/diagnostics.py +9 -0
- package/runtime/cli/aikit/fallback.py +48 -2
- package/runtime/cli/aikit/github_pr.py +254 -0
- package/runtime/cli/aikit/identity.py +75 -0
- package/runtime/cli/aikit/llm.py +249 -48
- package/runtime/cli/aikit/main.py +1240 -32
- package/runtime/cli/aikit/memory.py +148 -8
- package/runtime/cli/aikit/model_router.py +70 -0
- package/runtime/cli/aikit/notifications.py +9 -0
- package/runtime/cli/aikit/ollama.py +237 -0
- package/runtime/cli/aikit/permissions.py +345 -0
- package/runtime/cli/aikit/personality.py +123 -0
- package/runtime/cli/aikit/provider_wizard.py +19 -0
- package/runtime/cli/aikit/providers.py +4 -5
- package/runtime/cli/aikit/review_gate.py +40 -0
- 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/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/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,14 @@ 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)
|
|
162
|
+
from cli.aikit.decision_store import get_decision
|
|
163
|
+
|
|
159
164
|
return {
|
|
160
165
|
"kind": "llm-backends",
|
|
161
166
|
"config_path": str(config_path()),
|
|
162
167
|
"default": default_backend,
|
|
168
|
+
"preference": preference,
|
|
163
169
|
"items": [
|
|
164
170
|
{
|
|
165
171
|
"id": backend.id,
|
|
@@ -175,6 +181,8 @@ def list_backends() -> dict[str, Any]:
|
|
|
175
181
|
"default_model": backend.default_model,
|
|
176
182
|
"command": backend.command,
|
|
177
183
|
"notes": backend.notes,
|
|
184
|
+
"decision": get_decision("llms", backend.id),
|
|
185
|
+
"enabled": not bool((get_decision("llms", backend.id) or {}).get("state") in {"disabled_by_user", "denied_by_user"}),
|
|
178
186
|
}
|
|
179
187
|
for backend in BACKENDS.values()
|
|
180
188
|
],
|
|
@@ -252,6 +260,68 @@ def set_default_backend(backend_id: str) -> dict[str, Any]:
|
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
|
|
263
|
+
def llm_preference(config: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
264
|
+
config = config or load_config()
|
|
265
|
+
llm = config.get("llm") if isinstance(config.get("llm"), dict) else {}
|
|
266
|
+
raw_preference = llm.get("preference") if isinstance(llm.get("preference"), dict) else {}
|
|
267
|
+
primary = raw_preference.get("primary") or llm.get("default")
|
|
268
|
+
order = raw_preference.get("order") if isinstance(raw_preference.get("order"), list) else None
|
|
269
|
+
normalized_order = normalize_backend_order(order or DEFAULT_FALLBACK_ORDER)
|
|
270
|
+
if primary and primary in BACKENDS:
|
|
271
|
+
normalized_order = [primary, *[item for item in normalized_order if item != primary]]
|
|
272
|
+
return {
|
|
273
|
+
"kind": "llm-preference",
|
|
274
|
+
"status": "ok",
|
|
275
|
+
"config_path": str(config_path()),
|
|
276
|
+
"primary": primary,
|
|
277
|
+
"order": normalized_order,
|
|
278
|
+
"fallback_enabled": bool(raw_preference.get("fallback_enabled", True)),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def set_llm_preference(
|
|
283
|
+
*,
|
|
284
|
+
primary: str | None = None,
|
|
285
|
+
order: str | list[str] | tuple[str, ...] | None = None,
|
|
286
|
+
) -> dict[str, Any]:
|
|
287
|
+
config = load_config()
|
|
288
|
+
llm = config.setdefault("llm", {"default": None, "backends": {}})
|
|
289
|
+
backends = llm.setdefault("backends", {})
|
|
290
|
+
preference = llm.setdefault("preference", {})
|
|
291
|
+
if not isinstance(preference, dict):
|
|
292
|
+
preference = {}
|
|
293
|
+
llm["preference"] = preference
|
|
294
|
+
|
|
295
|
+
if primary:
|
|
296
|
+
backend = backend_or_error(primary)
|
|
297
|
+
backends.setdefault(backend.id, default_backend_config(backend))
|
|
298
|
+
llm["default"] = backend.id
|
|
299
|
+
preference["primary"] = backend.id
|
|
300
|
+
if order is not None:
|
|
301
|
+
preference["order"] = normalize_backend_order(order)
|
|
302
|
+
preference.setdefault("fallback_enabled", True)
|
|
303
|
+
written_path = save_config(config)
|
|
304
|
+
payload = llm_preference(config)
|
|
305
|
+
payload.update({"status": "configured", "config_path": str(written_path), "stored_secret": False})
|
|
306
|
+
return payload
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def normalize_backend_order(order: str | list[str] | tuple[str, ...]) -> list[str]:
|
|
310
|
+
raw_items = order.split(",") if isinstance(order, str) else list(order)
|
|
311
|
+
normalized: list[str] = []
|
|
312
|
+
for raw_item in raw_items:
|
|
313
|
+
backend_id = str(raw_item).strip()
|
|
314
|
+
if not backend_id:
|
|
315
|
+
continue
|
|
316
|
+
backend_or_error(backend_id)
|
|
317
|
+
if backend_id not in normalized:
|
|
318
|
+
normalized.append(backend_id)
|
|
319
|
+
for backend_id in DEFAULT_FALLBACK_ORDER:
|
|
320
|
+
if backend_id not in normalized:
|
|
321
|
+
normalized.append(backend_id)
|
|
322
|
+
return normalized
|
|
323
|
+
|
|
324
|
+
|
|
255
325
|
def default_backend_config(backend: LlmBackend) -> dict[str, Any]:
|
|
256
326
|
entry: dict[str, Any] = {"kind": backend.kind, "auth": backend.auth}
|
|
257
327
|
if backend.auth == "api-key-env":
|
|
@@ -359,31 +429,59 @@ def resolve_backend(requested: str | None = None) -> dict[str, Any] | None:
|
|
|
359
429
|
backend_or_error(requested)
|
|
360
430
|
return doctor_backend(BACKENDS[requested], config)
|
|
361
431
|
|
|
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
|
|
432
|
+
for backend_id in candidate_backend_ids(config=config, allow_fallback=True):
|
|
433
|
+
check = doctor_backend(BACKENDS[backend_id], config)
|
|
434
|
+
if check.get("status") == "ok":
|
|
435
|
+
return check
|
|
374
436
|
|
|
375
437
|
return None
|
|
376
438
|
|
|
377
439
|
|
|
378
|
-
def
|
|
379
|
-
|
|
380
|
-
|
|
440
|
+
def candidate_backend_ids(
|
|
441
|
+
*,
|
|
442
|
+
config: dict[str, Any],
|
|
443
|
+
requested: str | None = None,
|
|
444
|
+
allow_fallback: bool = True,
|
|
445
|
+
) -> list[str]:
|
|
446
|
+
if requested:
|
|
447
|
+
backend_or_error(requested)
|
|
448
|
+
return [requested]
|
|
449
|
+
llm = config.get("llm") if isinstance(config.get("llm"), dict) else {}
|
|
450
|
+
configured = llm.get("backends") if isinstance(llm.get("backends"), dict) else {}
|
|
451
|
+
configured_ids = [backend_id for backend_id in configured if backend_id in BACKENDS]
|
|
452
|
+
if not configured_ids:
|
|
453
|
+
return []
|
|
454
|
+
from cli.aikit.decision_store import is_disabled
|
|
455
|
+
|
|
456
|
+
configured_ids = [backend_id for backend_id in configured_ids if not is_disabled("llms", backend_id)]
|
|
457
|
+
if not configured_ids:
|
|
458
|
+
return []
|
|
459
|
+
preference = llm_preference(config)
|
|
460
|
+
ordered = [backend_id for backend_id in preference["order"] if backend_id in configured_ids]
|
|
461
|
+
for backend_id in configured_ids:
|
|
462
|
+
if backend_id not in ordered:
|
|
463
|
+
ordered.append(backend_id)
|
|
464
|
+
return ordered if allow_fallback else ordered[:1]
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def invoke_agent_prompt(
|
|
468
|
+
prompt: str,
|
|
469
|
+
requested: str | None = None,
|
|
470
|
+
*,
|
|
471
|
+
public_name: str = "Agent DevKit",
|
|
472
|
+
allow_fallback: bool = True,
|
|
473
|
+
) -> dict[str, Any]:
|
|
474
|
+
config = load_config()
|
|
475
|
+
candidate_ids = candidate_backend_ids(config=config, requested=requested, allow_fallback=allow_fallback)
|
|
476
|
+
if not candidate_ids:
|
|
381
477
|
return {
|
|
382
478
|
"kind": "agent",
|
|
383
479
|
"status": "blocked",
|
|
384
480
|
"ok": False,
|
|
385
481
|
"requires_llm": True,
|
|
386
482
|
"llm_backend": requested,
|
|
483
|
+
"llm_backend_attempts": [],
|
|
484
|
+
"llm_fallback_enabled": allow_fallback,
|
|
387
485
|
"prompt_received": True,
|
|
388
486
|
"prompt_length": len(prompt),
|
|
389
487
|
"message": "agent requires a configured LLM backend for natural-language tasks.",
|
|
@@ -395,36 +493,107 @@ def invoke_agent_prompt(prompt: str, requested: str | None = None) -> dict[str,
|
|
|
395
493
|
"exit_code": 2,
|
|
396
494
|
}
|
|
397
495
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
496
|
+
attempts: list[dict[str, Any]] = []
|
|
497
|
+
last_error: str | None = None
|
|
498
|
+
for backend_id in candidate_ids:
|
|
499
|
+
backend = doctor_backend(BACKENDS[backend_id], config)
|
|
500
|
+
attempt = {"id": backend_id, "status": backend.get("status")}
|
|
501
|
+
attempts.append(attempt)
|
|
502
|
+
if backend.get("status") != "ok":
|
|
503
|
+
attempt["message"] = backend.get("message")
|
|
504
|
+
continue
|
|
505
|
+
try:
|
|
506
|
+
response = invoke_resolved_backend(backend, prompt, public_name=public_name)
|
|
507
|
+
except LlmPolicyError as exc:
|
|
508
|
+
attempt["status"] = "policy-blocked"
|
|
509
|
+
attempt["message"] = str(exc)
|
|
510
|
+
return failed_agent_payload(
|
|
511
|
+
prompt,
|
|
512
|
+
backend=backend,
|
|
513
|
+
attempts=attempts,
|
|
514
|
+
message=str(exc),
|
|
515
|
+
policy_error=True,
|
|
516
|
+
allow_fallback=allow_fallback,
|
|
517
|
+
)
|
|
518
|
+
except LlmInvocationError as exc:
|
|
519
|
+
attempt["status"] = "failed"
|
|
520
|
+
attempt["message"] = str(exc)
|
|
521
|
+
last_error = str(exc)
|
|
522
|
+
if requested or not allow_fallback:
|
|
523
|
+
break
|
|
524
|
+
continue
|
|
525
|
+
attempt["status"] = "ok"
|
|
401
526
|
return {
|
|
402
527
|
"kind": "agent",
|
|
403
|
-
"status": "
|
|
404
|
-
"ok":
|
|
528
|
+
"status": "ok",
|
|
529
|
+
"ok": True,
|
|
405
530
|
"requires_llm": True,
|
|
406
531
|
"llm_backend": backend["id"],
|
|
407
532
|
"llm_backend_status": backend["status"],
|
|
533
|
+
"llm_backend_attempts": attempts,
|
|
534
|
+
"llm_fallback_enabled": allow_fallback,
|
|
535
|
+
"llm_fallback_used": attempts[0]["id"] != backend["id"],
|
|
408
536
|
"prompt_received": True,
|
|
409
537
|
"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,
|
|
538
|
+
"response": response,
|
|
416
539
|
}
|
|
417
540
|
|
|
541
|
+
if last_error:
|
|
542
|
+
return failed_agent_payload(
|
|
543
|
+
prompt,
|
|
544
|
+
backend={"id": attempts[-1]["id"], "status": attempts[-1].get("status")},
|
|
545
|
+
attempts=attempts,
|
|
546
|
+
message=last_error,
|
|
547
|
+
policy_error=False,
|
|
548
|
+
allow_fallback=allow_fallback,
|
|
549
|
+
)
|
|
418
550
|
return {
|
|
419
551
|
"kind": "agent",
|
|
420
|
-
"status": "
|
|
421
|
-
"ok":
|
|
552
|
+
"status": "blocked",
|
|
553
|
+
"ok": False,
|
|
422
554
|
"requires_llm": True,
|
|
423
|
-
"llm_backend":
|
|
424
|
-
"
|
|
555
|
+
"llm_backend": requested,
|
|
556
|
+
"llm_backend_attempts": attempts,
|
|
557
|
+
"llm_fallback_enabled": allow_fallback,
|
|
425
558
|
"prompt_received": True,
|
|
426
559
|
"prompt_length": len(prompt),
|
|
427
|
-
"
|
|
560
|
+
"message": "agent did not find an available configured LLM backend.",
|
|
561
|
+
"next_steps": [
|
|
562
|
+
"Run `agent llm doctor` to verify configured backends.",
|
|
563
|
+
"Configure a backend with `agent llm configure <backend> --set-default`.",
|
|
564
|
+
"Adjust fallback order with `agent llm preference set --primary <backend>`.",
|
|
565
|
+
],
|
|
566
|
+
"exit_code": 2,
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def failed_agent_payload(
|
|
571
|
+
prompt: str,
|
|
572
|
+
*,
|
|
573
|
+
backend: dict[str, Any],
|
|
574
|
+
attempts: list[dict[str, Any]],
|
|
575
|
+
message: str,
|
|
576
|
+
policy_error: bool,
|
|
577
|
+
allow_fallback: bool,
|
|
578
|
+
) -> dict[str, Any]:
|
|
579
|
+
return {
|
|
580
|
+
"kind": "agent",
|
|
581
|
+
"status": "failed",
|
|
582
|
+
"ok": False,
|
|
583
|
+
"requires_llm": True,
|
|
584
|
+
"llm_backend": backend.get("id"),
|
|
585
|
+
"llm_backend_status": backend.get("status"),
|
|
586
|
+
"llm_backend_attempts": attempts,
|
|
587
|
+
"llm_fallback_enabled": allow_fallback,
|
|
588
|
+
"llm_policy_error": policy_error,
|
|
589
|
+
"prompt_received": True,
|
|
590
|
+
"prompt_length": len(prompt),
|
|
591
|
+
"message": message,
|
|
592
|
+
"next_steps": [
|
|
593
|
+
"Run `agent llm doctor` to verify the selected backend.",
|
|
594
|
+
"Use `agent run <agent> <capability>` when deterministic execution is enough.",
|
|
595
|
+
],
|
|
596
|
+
"exit_code": 1,
|
|
428
597
|
}
|
|
429
598
|
|
|
430
599
|
|
|
@@ -432,21 +601,33 @@ class LlmInvocationError(RuntimeError):
|
|
|
432
601
|
"""Raised for safe, user-facing LLM invocation errors."""
|
|
433
602
|
|
|
434
603
|
|
|
435
|
-
|
|
604
|
+
class LlmPolicyError(LlmInvocationError):
|
|
605
|
+
"""Raised when the backend rejects content for policy or safety reasons."""
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def invoke_resolved_backend(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
|
|
436
609
|
kind = backend.get("kind")
|
|
437
610
|
backend_id = backend.get("id")
|
|
438
611
|
if kind == "openai-compatible":
|
|
439
|
-
return invoke_openai_compatible(backend, prompt)
|
|
612
|
+
return invoke_openai_compatible(backend, prompt, public_name=public_name)
|
|
440
613
|
if kind == "anthropic":
|
|
441
|
-
return invoke_anthropic(backend, prompt)
|
|
614
|
+
return invoke_anthropic(backend, prompt, public_name=public_name)
|
|
442
615
|
if kind == "host-cli" and backend_id == "codex-cli":
|
|
443
|
-
return invoke_host_cli(
|
|
616
|
+
return invoke_host_cli(
|
|
617
|
+
[
|
|
618
|
+
str(backend.get("command") or "codex"),
|
|
619
|
+
"exec",
|
|
620
|
+
"--skip-git-repo-check",
|
|
621
|
+
"--ephemeral",
|
|
622
|
+
host_cli_prompt(prompt, name=public_name),
|
|
623
|
+
]
|
|
624
|
+
)
|
|
444
625
|
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])
|
|
626
|
+
return invoke_host_cli([str(backend.get("command") or "claude"), "--print", "--permission-mode", "plan", host_cli_prompt(prompt, name=public_name)])
|
|
446
627
|
raise LlmInvocationError(f"LLM backend is not invokable by agent yet: {backend_id}")
|
|
447
628
|
|
|
448
629
|
|
|
449
|
-
def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
|
|
630
|
+
def invoke_openai_compatible(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
|
|
450
631
|
base_url = str(backend.get("base_url") or "").rstrip("/")
|
|
451
632
|
model = str(backend.get("model") or "")
|
|
452
633
|
if not base_url or not model:
|
|
@@ -460,10 +641,7 @@ def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
|
|
|
460
641
|
"messages": [
|
|
461
642
|
{
|
|
462
643
|
"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
|
-
),
|
|
644
|
+
"content": identity_system_prompt(name=public_name),
|
|
467
645
|
},
|
|
468
646
|
{"role": "user", "content": prompt},
|
|
469
647
|
],
|
|
@@ -475,7 +653,7 @@ def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
|
|
|
475
653
|
raise LlmInvocationError("OpenAI-compatible backend returned an unexpected response shape.") from exc
|
|
476
654
|
|
|
477
655
|
|
|
478
|
-
def invoke_anthropic(backend: dict[str, Any], prompt: str) -> str:
|
|
656
|
+
def invoke_anthropic(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
|
|
479
657
|
api_key = api_key_from_ref(backend.get("api_key_ref"))
|
|
480
658
|
if not api_key:
|
|
481
659
|
raise LlmInvocationError("Anthropic backend is missing an API key environment variable.")
|
|
@@ -491,7 +669,7 @@ def invoke_anthropic(backend: dict[str, Any], prompt: str) -> str:
|
|
|
491
669
|
body = {
|
|
492
670
|
"model": model,
|
|
493
671
|
"max_tokens": 1024,
|
|
494
|
-
"system":
|
|
672
|
+
"system": identity_system_prompt(name=public_name),
|
|
495
673
|
"messages": [{"role": "user", "content": prompt}],
|
|
496
674
|
}
|
|
497
675
|
payload = post_json(f"{base_url}/messages", headers, body)
|
|
@@ -527,6 +705,13 @@ def post_json(url: str, headers: dict[str, str], body: dict[str, Any]) -> dict[s
|
|
|
527
705
|
with urllib.request.urlopen(request, timeout=DEFAULT_AGENT_TIMEOUT_SECONDS) as response: # noqa: S310 - user-configured backend URL.
|
|
528
706
|
raw = response.read().decode("utf-8")
|
|
529
707
|
except urllib.error.HTTPError as exc:
|
|
708
|
+
raw_error = ""
|
|
709
|
+
try:
|
|
710
|
+
raw_error = exc.read().decode("utf-8", errors="replace")
|
|
711
|
+
except OSError:
|
|
712
|
+
raw_error = ""
|
|
713
|
+
if is_policy_error(exc.code, raw_error):
|
|
714
|
+
raise LlmPolicyError(f"LLM backend policy error: {exc.code}") from exc
|
|
530
715
|
raise LlmInvocationError(f"LLM backend HTTP error: {exc.code}") from exc
|
|
531
716
|
except urllib.error.URLError as exc:
|
|
532
717
|
raise LlmInvocationError(f"LLM backend connection failed: {exc.reason}") from exc
|
|
@@ -536,9 +721,25 @@ def post_json(url: str, headers: dict[str, str], body: dict[str, Any]) -> dict[s
|
|
|
536
721
|
raise LlmInvocationError("LLM backend returned invalid JSON.") from exc
|
|
537
722
|
if not isinstance(payload, dict):
|
|
538
723
|
raise LlmInvocationError("LLM backend returned a non-object JSON payload.")
|
|
724
|
+
if payload_has_policy_error(payload):
|
|
725
|
+
raise LlmPolicyError("LLM backend returned a policy error.")
|
|
539
726
|
return payload
|
|
540
727
|
|
|
541
728
|
|
|
729
|
+
def is_policy_error(status_code: int, body: str) -> bool:
|
|
730
|
+
if status_code not in {400, 403}:
|
|
731
|
+
return False
|
|
732
|
+
return any(marker in body.lower() for marker in ("policy", "safety", "content_filter", "content filter", "not allowed"))
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def payload_has_policy_error(payload: dict[str, Any]) -> bool:
|
|
736
|
+
error = payload.get("error")
|
|
737
|
+
if not isinstance(error, dict):
|
|
738
|
+
return False
|
|
739
|
+
text = json.dumps(error, ensure_ascii=False).lower()
|
|
740
|
+
return any(marker in text for marker in ("policy", "safety", "content_filter", "content filter", "not allowed"))
|
|
741
|
+
|
|
742
|
+
|
|
542
743
|
def api_key_from_ref(value: Any) -> str | None:
|
|
543
744
|
ref = str(value or "")
|
|
544
745
|
if not ref.startswith("env:"):
|