agent-devkit 0.1.5 → 0.1.6
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 +2 -2
- package/package.json +1 -1
- package/runtime/README.md +74 -5
- package/runtime/agents/README.md +8 -0
- package/runtime/agents/execution-reviewer/AGENTS.md +8 -0
- package/runtime/agents/execution-reviewer/README.md +13 -0
- package/runtime/agents/execution-reviewer/agent.yaml +25 -0
- package/runtime/agents/execution-reviewer/capabilities/review-agent-result/capability.yaml +15 -0
- package/runtime/agents/execution-reviewer/capabilities/review-agent-result/decision-rules.md +6 -0
- package/runtime/agents/execution-reviewer/capabilities/review-agent-result/workflow.md +7 -0
- package/runtime/agents/execution-reviewer/capabilities/review-final-output/capability.yaml +15 -0
- package/runtime/agents/execution-reviewer/capabilities/review-final-output/decision-rules.md +6 -0
- package/runtime/agents/execution-reviewer/capabilities/review-final-output/workflow.md +7 -0
- package/runtime/agents/execution-reviewer/capabilities/review-plan/capability.yaml +15 -0
- package/runtime/agents/execution-reviewer/capabilities/review-plan/decision-rules.md +6 -0
- package/runtime/agents/execution-reviewer/capabilities/review-plan/workflow.md +7 -0
- package/runtime/agents/execution-reviewer/infra/README.md +4 -0
- package/runtime/agents/execution-reviewer/knowledge/context.md +6 -0
- package/runtime/agents/execution-reviewer/knowledge/system.md +18 -0
- package/runtime/agents/execution-reviewer/templates/README.md +4 -0
- package/runtime/agents/local-llm-operator/AGENTS.md +8 -0
- package/runtime/agents/local-llm-operator/README.md +12 -0
- package/runtime/agents/local-llm-operator/agent.yaml +26 -0
- package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/capability.yaml +15 -0
- package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/decision-rules.md +6 -0
- package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/workflow.md +7 -0
- package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/capability.yaml +15 -0
- package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/decision-rules.md +5 -0
- package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/workflow.md +6 -0
- package/runtime/agents/local-llm-operator/capabilities/select-local-worker/capability.yaml +15 -0
- package/runtime/agents/local-llm-operator/capabilities/select-local-worker/decision-rules.md +6 -0
- package/runtime/agents/local-llm-operator/capabilities/select-local-worker/workflow.md +7 -0
- package/runtime/agents/local-llm-operator/infra/README.md +4 -0
- package/runtime/agents/local-llm-operator/knowledge/context.md +6 -0
- package/runtime/agents/local-llm-operator/knowledge/system.md +18 -0
- package/runtime/agents/local-llm-operator/templates/README.md +4 -0
- package/runtime/agents/provider-configurator/AGENTS.md +8 -0
- package/runtime/agents/provider-configurator/README.md +5 -0
- package/runtime/agents/provider-configurator/agent.yaml +25 -0
- package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/capability.yaml +15 -0
- package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/decision-rules.md +6 -0
- package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/workflow.md +7 -0
- package/runtime/agents/provider-configurator/capabilities/configure-provider-source/capability.yaml +15 -0
- package/runtime/agents/provider-configurator/capabilities/configure-provider-source/decision-rules.md +6 -0
- package/runtime/agents/provider-configurator/capabilities/configure-provider-source/workflow.md +8 -0
- package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/capability.yaml +15 -0
- package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/decision-rules.md +6 -0
- package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/workflow.md +7 -0
- package/runtime/agents/provider-configurator/infra/README.md +5 -0
- package/runtime/agents/provider-configurator/knowledge/context.md +8 -0
- package/runtime/agents/provider-configurator/knowledge/system.md +19 -0
- package/runtime/agents/provider-configurator/templates/README.md +4 -0
- package/runtime/agents/task-orchestrator/AGENTS.md +8 -0
- package/runtime/agents/task-orchestrator/README.md +7 -0
- package/runtime/agents/task-orchestrator/agent.yaml +31 -0
- package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/capability.yaml +15 -0
- package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/decision-rules.md +6 -0
- package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/workflow.md +7 -0
- package/runtime/agents/task-orchestrator/capabilities/plan-task/capability.yaml +15 -0
- package/runtime/agents/task-orchestrator/capabilities/plan-task/decision-rules.md +6 -0
- package/runtime/agents/task-orchestrator/capabilities/plan-task/workflow.md +8 -0
- package/runtime/agents/task-orchestrator/capabilities/select-specialists/capability.yaml +15 -0
- package/runtime/agents/task-orchestrator/capabilities/select-specialists/decision-rules.md +6 -0
- package/runtime/agents/task-orchestrator/capabilities/select-specialists/workflow.md +7 -0
- package/runtime/agents/task-orchestrator/infra/README.md +5 -0
- package/runtime/agents/task-orchestrator/knowledge/context.md +9 -0
- package/runtime/agents/task-orchestrator/knowledge/system.md +20 -0
- package/runtime/agents/task-orchestrator/templates/README.md +4 -0
- package/runtime/cli/README.md +76 -11
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/agent_executor.py +66 -0
- package/runtime/cli/aikit/agent_registry.py +80 -0
- package/runtime/cli/aikit/app_home.py +87 -2
- package/runtime/cli/aikit/audit.py +44 -0
- package/runtime/cli/aikit/control_router.py +298 -0
- package/runtime/cli/aikit/decision_store.py +17 -0
- package/runtime/cli/aikit/diagnostics.py +2 -1
- package/runtime/cli/aikit/execution_reviewer.py +174 -0
- package/runtime/cli/aikit/install.py +4 -3
- package/runtime/cli/aikit/local_llm_operator.py +112 -0
- package/runtime/cli/aikit/lock.py +4 -1
- package/runtime/cli/aikit/main.py +355 -80
- package/runtime/cli/aikit/model_router.py +13 -4
- package/runtime/cli/aikit/orchestrator.py +535 -0
- package/runtime/cli/aikit/sessions.py +37 -0
- package/runtime/cli/aikit/sources.py +6 -2
- package/runtime/cli/aikit/wizard_state.py +503 -0
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import shutil
|
|
6
7
|
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
CANONICAL_APP_HOME_ENV = "AGENT_DEVKIT_HOME"
|
|
9
12
|
APP_HOME_ENV = "AI_DEVKIT_CONFIG_HOME"
|
|
10
13
|
LEGACY_APP_HOME_ENV = "AIKIT_CONFIG_HOME"
|
|
11
|
-
DEFAULT_APP_HOME_NAME = ".
|
|
14
|
+
DEFAULT_APP_HOME_NAME = ".agent-devkit"
|
|
15
|
+
LEGACY_DEFAULT_APP_HOME_NAME = ".ai-devkit"
|
|
12
16
|
|
|
13
17
|
APP_DIRS = (
|
|
14
18
|
"bin",
|
|
@@ -27,12 +31,93 @@ APP_DIRS = (
|
|
|
27
31
|
|
|
28
32
|
def app_home() -> Path:
|
|
29
33
|
"""Return the configured Agent DevKit home directory."""
|
|
30
|
-
raw = os.environ.get(APP_HOME_ENV) or os.environ.get(LEGACY_APP_HOME_ENV)
|
|
34
|
+
raw = os.environ.get(CANONICAL_APP_HOME_ENV) or os.environ.get(APP_HOME_ENV) or os.environ.get(LEGACY_APP_HOME_ENV)
|
|
31
35
|
if raw:
|
|
32
36
|
return Path(raw).expanduser().resolve()
|
|
37
|
+
canonical = canonical_default_app_home()
|
|
38
|
+
legacy = legacy_default_app_home()
|
|
39
|
+
if canonical.exists():
|
|
40
|
+
return canonical
|
|
41
|
+
if legacy.exists():
|
|
42
|
+
return legacy
|
|
43
|
+
return canonical
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def canonical_default_app_home() -> Path:
|
|
33
47
|
return (Path.home() / DEFAULT_APP_HOME_NAME).resolve()
|
|
34
48
|
|
|
35
49
|
|
|
50
|
+
def legacy_default_app_home() -> Path:
|
|
51
|
+
return (Path.home() / LEGACY_DEFAULT_APP_HOME_NAME).resolve()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def app_home_status() -> dict[str, Any]:
|
|
55
|
+
explicit_env = active_home_env()
|
|
56
|
+
canonical = canonical_default_app_home()
|
|
57
|
+
legacy = legacy_default_app_home()
|
|
58
|
+
home = app_home()
|
|
59
|
+
status = "canonical"
|
|
60
|
+
if explicit_env:
|
|
61
|
+
status = "env"
|
|
62
|
+
elif home == legacy:
|
|
63
|
+
status = "legacy-default-detected"
|
|
64
|
+
elif not canonical.exists() and not legacy.exists():
|
|
65
|
+
status = "canonical-default"
|
|
66
|
+
return {
|
|
67
|
+
"kind": "app-home-status",
|
|
68
|
+
"status": status,
|
|
69
|
+
"home": str(home),
|
|
70
|
+
"canonical_home": str(canonical),
|
|
71
|
+
"legacy_home": str(legacy),
|
|
72
|
+
"active_env": explicit_env,
|
|
73
|
+
"canonical_exists": canonical.exists(),
|
|
74
|
+
"legacy_exists": legacy.exists(),
|
|
75
|
+
"migration_available": not explicit_env and legacy.exists() and not canonical.exists(),
|
|
76
|
+
"migration_command": "agent config migrate-home",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def active_home_env() -> dict[str, str] | None:
|
|
81
|
+
for name in (CANONICAL_APP_HOME_ENV, APP_HOME_ENV, LEGACY_APP_HOME_ENV):
|
|
82
|
+
value = os.environ.get(name)
|
|
83
|
+
if value:
|
|
84
|
+
return {"name": name, "value": str(Path(value).expanduser().resolve())}
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def migrate_default_home(*, dry_run: bool = False) -> dict[str, Any]:
|
|
89
|
+
source = legacy_default_app_home()
|
|
90
|
+
destination = canonical_default_app_home()
|
|
91
|
+
payload: dict[str, Any] = {
|
|
92
|
+
"kind": "home-migration",
|
|
93
|
+
"source": str(source),
|
|
94
|
+
"destination": str(destination),
|
|
95
|
+
"dry_run": dry_run,
|
|
96
|
+
"executed": False,
|
|
97
|
+
}
|
|
98
|
+
if destination.exists():
|
|
99
|
+
return {**payload, "status": "not-needed", "message": "Canonical Agent DevKit home already exists."}
|
|
100
|
+
if not source.exists():
|
|
101
|
+
return {**payload, "status": "not-needed", "message": "Legacy AI DevKit home was not found."}
|
|
102
|
+
if dry_run:
|
|
103
|
+
return {**payload, "status": "planned", "message": "Legacy home would be migrated to canonical Agent DevKit home."}
|
|
104
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
try:
|
|
106
|
+
source.rename(destination)
|
|
107
|
+
method = "rename"
|
|
108
|
+
except OSError:
|
|
109
|
+
shutil.copytree(source, destination)
|
|
110
|
+
shutil.rmtree(source)
|
|
111
|
+
method = "copytree"
|
|
112
|
+
return {
|
|
113
|
+
**payload,
|
|
114
|
+
"status": "migrated",
|
|
115
|
+
"executed": True,
|
|
116
|
+
"method": method,
|
|
117
|
+
"message": "Legacy home migrated to canonical Agent DevKit home.",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
36
121
|
def app_path(*parts: str) -> Path:
|
|
37
122
|
return app_home().joinpath(*parts)
|
|
38
123
|
|
|
@@ -60,6 +60,7 @@ def record_audit(
|
|
|
60
60
|
"prompt_normalized": normalize_prompt(prompt) if prompt else None,
|
|
61
61
|
"session": extract_session(safe_result),
|
|
62
62
|
"route": safe_result.get("route"),
|
|
63
|
+
"orchestration": extract_orchestration(safe_result),
|
|
63
64
|
"agent": extract_agent(safe_result, args),
|
|
64
65
|
"providers": extract_providers(safe_result),
|
|
65
66
|
"sources": extract_sources(safe_result),
|
|
@@ -282,6 +283,43 @@ def extract_external_actions(result: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
282
283
|
return actions
|
|
283
284
|
|
|
284
285
|
|
|
286
|
+
def extract_orchestration(result: dict[str, Any]) -> dict[str, Any] | None:
|
|
287
|
+
plan = result.get("execution_plan")
|
|
288
|
+
if not isinstance(plan, dict):
|
|
289
|
+
return None
|
|
290
|
+
return {
|
|
291
|
+
"kind": plan.get("kind"),
|
|
292
|
+
"status": plan.get("status"),
|
|
293
|
+
"coordinator_agent": (plan.get("coordinator_agent") or {}).get("id") if isinstance(plan.get("coordinator_agent"), dict) else None,
|
|
294
|
+
"domain_agent": (plan.get("domain_agent") or {}).get("id") if isinstance(plan.get("domain_agent"), dict) else None,
|
|
295
|
+
"specialist_tasks": [
|
|
296
|
+
{
|
|
297
|
+
"agent_id": task.get("agent_id"),
|
|
298
|
+
"capability_id": task.get("capability_id"),
|
|
299
|
+
"status": task.get("status"),
|
|
300
|
+
}
|
|
301
|
+
for task in plan.get("specialist_tasks") or []
|
|
302
|
+
if isinstance(task, dict)
|
|
303
|
+
],
|
|
304
|
+
"configuration_tasks": [
|
|
305
|
+
{
|
|
306
|
+
"agent_id": task.get("agent_id"),
|
|
307
|
+
"provider": task.get("provider"),
|
|
308
|
+
"status": task.get("status"),
|
|
309
|
+
}
|
|
310
|
+
for task in plan.get("configuration_tasks") or []
|
|
311
|
+
if isinstance(task, dict)
|
|
312
|
+
],
|
|
313
|
+
"review_task": {
|
|
314
|
+
"agent_id": (plan.get("review_task") or {}).get("agent_id"),
|
|
315
|
+
"status": (plan.get("review_task") or {}).get("status"),
|
|
316
|
+
}
|
|
317
|
+
if isinstance(plan.get("review_task"), dict)
|
|
318
|
+
else None,
|
|
319
|
+
"trace": result.get("orchestration_trace") or plan.get("trace") or [],
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
285
323
|
def summarize_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
286
324
|
return {
|
|
287
325
|
"kind": result.get("kind"),
|
|
@@ -326,6 +364,12 @@ def render_audit_md(entry: dict[str, Any]) -> str:
|
|
|
326
364
|
json.dumps(entry.get("providers") or {}, ensure_ascii=False, indent=2, sort_keys=True),
|
|
327
365
|
"```",
|
|
328
366
|
"",
|
|
367
|
+
"## Orchestration",
|
|
368
|
+
"",
|
|
369
|
+
"```json",
|
|
370
|
+
json.dumps(entry.get("orchestration") or {}, ensure_ascii=False, indent=2, sort_keys=True),
|
|
371
|
+
"```",
|
|
372
|
+
"",
|
|
329
373
|
"## Permissions",
|
|
330
374
|
"",
|
|
331
375
|
"```json",
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Natural-language control routing for tools, integrations, skills and LLMs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from cli.aikit.decision_store import forget_decision, list_decisions, reset_decisions, set_decision
|
|
10
|
+
from cli.aikit.llm import BACKENDS
|
|
11
|
+
from cli.aikit.providers import load_providers
|
|
12
|
+
from cli.aikit.toolchain import load_toolchain
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CATEGORY_ALIASES: dict[str, tuple[str, ...]] = {
|
|
16
|
+
"tools": ("tool", "tools", "ferramenta", "ferramentas", "cli", "clis", "toolchain"),
|
|
17
|
+
"integrations": ("integration", "integrations", "integracao", "integracoes", "integração", "integrações", "provider", "providers"),
|
|
18
|
+
"skills": ("skill", "skills", "habilidade", "habilidades"),
|
|
19
|
+
"llms": ("llm", "llms", "modelo", "modelos", "backend", "backends"),
|
|
20
|
+
}
|
|
21
|
+
LIST_MARKERS = ("mostre", "mostrar", "liste", "listar", "ver", "exiba", "exibir")
|
|
22
|
+
ENABLE_MARKERS = ("reative", "ative", "habilite", "enable")
|
|
23
|
+
DISABLE_MARKERS = ("desative", "desabilite", "disable", "bloqueie", "bloquear")
|
|
24
|
+
FORGET_MARKERS = ("esqueca", "esqueça", "remova", "remover", "limpe", "limpar", "delete", "apague", "apagar")
|
|
25
|
+
DECISION_MARKERS = ("decisao", "decisão", "decisoes", "decisões", "decision", "decisions", "bloqueio")
|
|
26
|
+
TOKEN_PATTERN = re.compile(r"[a-z0-9._:-]+")
|
|
27
|
+
SKILL_ROW_PATTERN = re.compile(r"^\|\s*`([^`]+)`\s*\|")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def dispatch_natural_control_prompt(root: Path, normalized_prompt: str) -> dict[str, Any] | None:
|
|
31
|
+
action = detect_action(normalized_prompt)
|
|
32
|
+
category = detect_category(normalized_prompt)
|
|
33
|
+
if action == "list":
|
|
34
|
+
return list_payload(root, category=category)
|
|
35
|
+
if action == "reset" and category:
|
|
36
|
+
result = reset_decisions(category)
|
|
37
|
+
return control_payload(
|
|
38
|
+
status="ok",
|
|
39
|
+
response=f"Decisoes de {category} foram resetadas.",
|
|
40
|
+
result={"action": "reset", "category": category, "decision": result},
|
|
41
|
+
)
|
|
42
|
+
if action not in {"enable", "disable", "forget"}:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
index = build_control_index(root)
|
|
46
|
+
matches = resolve_target(index, normalized_prompt, category=category)
|
|
47
|
+
if not matches:
|
|
48
|
+
return needs_input_unknown(root, normalized_prompt, category=category)
|
|
49
|
+
if len(matches) > 1:
|
|
50
|
+
return needs_input_ambiguous(matches, normalized_prompt, category=category)
|
|
51
|
+
|
|
52
|
+
target = matches[0]
|
|
53
|
+
if action == "forget":
|
|
54
|
+
decision = forget_decision(str(target["category"]), str(target["id"]))
|
|
55
|
+
return control_payload(
|
|
56
|
+
status="ok",
|
|
57
|
+
response=f"Decisao local sobre {target['id']} removida.",
|
|
58
|
+
result={"action": "forget", "category": target["category"], "id": target["id"], "removed": decision.get("removed"), "decision": decision},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
state = "enabled" if action == "enable" else "disabled_by_user"
|
|
62
|
+
decision = set_decision(str(target["category"]), str(target["id"]), state, reason=f"natural prompt: {action}")
|
|
63
|
+
return control_payload(
|
|
64
|
+
status="ok",
|
|
65
|
+
response=f"{target['id']} foi {'ativado' if state == 'enabled' else 'desativado'} para {target['category']}.",
|
|
66
|
+
result={"action": action, "category": target["category"], "id": target["id"], "state": state, "decision": decision.get("item")},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def plan_natural_control_prompt(root: Path, normalized_prompt: str) -> dict[str, Any] | None:
|
|
71
|
+
action = detect_action(normalized_prompt)
|
|
72
|
+
category = detect_category(normalized_prompt)
|
|
73
|
+
if action == "list":
|
|
74
|
+
payload = list_payload(root, category=category)
|
|
75
|
+
payload["dry_run"] = True
|
|
76
|
+
return payload
|
|
77
|
+
if action == "reset" and category:
|
|
78
|
+
return control_payload(
|
|
79
|
+
status="planned",
|
|
80
|
+
response=f"Dry-run: decisoes de {category} seriam resetadas.",
|
|
81
|
+
result={"action": "reset", "category": category},
|
|
82
|
+
) | {"dry_run": True, "ok": True}
|
|
83
|
+
if action not in {"enable", "disable", "forget"}:
|
|
84
|
+
return None
|
|
85
|
+
index = build_control_index(root)
|
|
86
|
+
matches = resolve_target(index, normalized_prompt, category=category)
|
|
87
|
+
if not matches:
|
|
88
|
+
payload = needs_input_unknown(root, normalized_prompt, category=category)
|
|
89
|
+
payload["dry_run"] = True
|
|
90
|
+
return payload
|
|
91
|
+
if len(matches) > 1:
|
|
92
|
+
payload = needs_input_ambiguous(matches, normalized_prompt, category=category)
|
|
93
|
+
payload["dry_run"] = True
|
|
94
|
+
return payload
|
|
95
|
+
target = matches[0]
|
|
96
|
+
state = "enabled" if action == "enable" else "disabled_by_user"
|
|
97
|
+
return control_payload(
|
|
98
|
+
status="planned",
|
|
99
|
+
response=f"Dry-run: {target['id']} seria {'ativado' if action == 'enable' else 'desativado' if action == 'disable' else 'removido das decisoes'} para {target['category']}.",
|
|
100
|
+
result={"action": action, "category": target["category"], "id": target["id"], "state": state if action != "forget" else None},
|
|
101
|
+
) | {"dry_run": True, "ok": True}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_control_index(root: Path) -> dict[str, list[dict[str, Any]]]:
|
|
105
|
+
return {
|
|
106
|
+
"tools": tool_items(root),
|
|
107
|
+
"integrations": integration_items(root),
|
|
108
|
+
"skills": skill_items(root),
|
|
109
|
+
"llms": llm_items(),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def tool_items(root: Path) -> list[dict[str, Any]]:
|
|
114
|
+
return [
|
|
115
|
+
item("tools", tool_id, label=str(spec.get("label") or tool_id), source="toolchain", aliases=aliases_for(tool_id, spec.get("label")))
|
|
116
|
+
for tool_id, spec in load_toolchain(root).items()
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def integration_items(root: Path) -> list[dict[str, Any]]:
|
|
121
|
+
items = []
|
|
122
|
+
for provider in load_providers(root):
|
|
123
|
+
provider_id = str(provider.get("id") or "")
|
|
124
|
+
if provider_id:
|
|
125
|
+
items.append(item("integrations", provider_id, label=str(provider.get("name") or provider_id), source="providers", aliases=aliases_for(provider_id, provider.get("name"))))
|
|
126
|
+
return items
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def skill_items(root: Path) -> list[dict[str, Any]]:
|
|
130
|
+
catalog = root / "vendor" / "skills" / "CATALOG.md"
|
|
131
|
+
if not catalog.exists():
|
|
132
|
+
return []
|
|
133
|
+
items = []
|
|
134
|
+
for line in catalog.read_text(encoding="utf-8").splitlines():
|
|
135
|
+
match = SKILL_ROW_PATTERN.match(line)
|
|
136
|
+
if not match:
|
|
137
|
+
continue
|
|
138
|
+
skill_id = match.group(1).strip()
|
|
139
|
+
if skill_id and skill_id.lower() != "skill":
|
|
140
|
+
items.append(item("skills", skill_id, label=skill_id, source="vendor-skills", aliases=aliases_for(skill_id)))
|
|
141
|
+
return items
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def llm_items() -> list[dict[str, Any]]:
|
|
145
|
+
return [
|
|
146
|
+
item("llms", backend_id, label=backend.display_name, source="llm-backends", aliases=aliases_for(backend_id, backend.display_name))
|
|
147
|
+
for backend_id, backend in BACKENDS.items()
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def item(category: str, item_id: str, *, label: str, source: str, aliases: set[str]) -> dict[str, Any]:
|
|
152
|
+
return {"category": category, "id": item_id, "label": label, "source": source, "aliases": sorted(aliases)}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def aliases_for(item_id: str, label: Any = None) -> set[str]:
|
|
156
|
+
values = {normalize(item_id)}
|
|
157
|
+
if label:
|
|
158
|
+
values.add(normalize(str(label)))
|
|
159
|
+
values.add(normalize(item_id.replace("-", " ")))
|
|
160
|
+
values.add(normalize(item_id.replace("_", " ")))
|
|
161
|
+
for token in re.split(r"[-_\s]+", item_id):
|
|
162
|
+
normalized = normalize(token)
|
|
163
|
+
if normalized and normalized not in {"cli", "mcp", "api", "tool", "skill", "plugin"}:
|
|
164
|
+
values.add(normalized)
|
|
165
|
+
if item_id.endswith("-cli"):
|
|
166
|
+
values.add(normalize(item_id.removesuffix("-cli")))
|
|
167
|
+
if item_id.endswith("-mcp"):
|
|
168
|
+
values.add(normalize(item_id.removesuffix("-mcp")))
|
|
169
|
+
return {value for value in values if value}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def detect_action(prompt: str) -> str | None:
|
|
173
|
+
if contains_any(prompt, LIST_MARKERS):
|
|
174
|
+
return "list"
|
|
175
|
+
if contains_any(prompt, FORGET_MARKERS) and contains_any(prompt, DECISION_MARKERS) and detect_category(prompt):
|
|
176
|
+
return "reset"
|
|
177
|
+
if contains_any(prompt, FORGET_MARKERS) and contains_any(prompt, DECISION_MARKERS):
|
|
178
|
+
return "forget"
|
|
179
|
+
if contains_any(prompt, FORGET_MARKERS) and detect_category(prompt):
|
|
180
|
+
return "reset"
|
|
181
|
+
if contains_any(prompt, DISABLE_MARKERS):
|
|
182
|
+
return "disable"
|
|
183
|
+
if contains_any(prompt, ENABLE_MARKERS):
|
|
184
|
+
return "enable"
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def detect_category(prompt: str) -> str | None:
|
|
189
|
+
for category, aliases in CATEGORY_ALIASES.items():
|
|
190
|
+
if contains_any(prompt, aliases):
|
|
191
|
+
return category
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def list_payload(root: Path, *, category: str | None) -> dict[str, Any]:
|
|
196
|
+
index = build_control_index(root)
|
|
197
|
+
if category:
|
|
198
|
+
result = {
|
|
199
|
+
"kind": "control-index",
|
|
200
|
+
"status": "ok",
|
|
201
|
+
"category": category,
|
|
202
|
+
"items": public_items(index.get(category, [])),
|
|
203
|
+
"decisions": list_decisions(category),
|
|
204
|
+
}
|
|
205
|
+
return control_payload(status="ok", response=f"Estes sao os itens catalogados para {category}.", result=result)
|
|
206
|
+
result = {
|
|
207
|
+
"kind": "control-index",
|
|
208
|
+
"status": "ok",
|
|
209
|
+
"categories": {key: public_items(value) for key, value in index.items()},
|
|
210
|
+
"decisions": list_decisions(),
|
|
211
|
+
}
|
|
212
|
+
return control_payload(status="ok", response="Estes sao os recursos controlaveis catalogados.", result=result)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def resolve_target(index: dict[str, list[dict[str, Any]]], prompt: str, *, category: str | None) -> list[dict[str, Any]]:
|
|
216
|
+
candidates = index.get(category, []) if category else [item for items in index.values() for item in items]
|
|
217
|
+
prompt_tokens = set(TOKEN_PATTERN.findall(prompt))
|
|
218
|
+
matches = []
|
|
219
|
+
for candidate in candidates:
|
|
220
|
+
aliases = set(candidate.get("aliases") or [])
|
|
221
|
+
if any(alias_matches_prompt(alias, prompt, prompt_tokens) for alias in aliases):
|
|
222
|
+
matches.append(candidate)
|
|
223
|
+
continue
|
|
224
|
+
candidate_tokens = set(TOKEN_PATTERN.findall(normalize(str(candidate.get("id") or ""))))
|
|
225
|
+
if candidate_tokens and candidate_tokens <= prompt_tokens:
|
|
226
|
+
matches.append(candidate)
|
|
227
|
+
return sorted(unique_matches(matches), key=lambda item: (str(item["category"]), str(item["id"])))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def unique_matches(matches: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
231
|
+
seen: set[tuple[str, str]] = set()
|
|
232
|
+
unique = []
|
|
233
|
+
for match in matches:
|
|
234
|
+
key = (str(match.get("category")), str(match.get("id")))
|
|
235
|
+
if key not in seen:
|
|
236
|
+
seen.add(key)
|
|
237
|
+
unique.append(match)
|
|
238
|
+
return unique
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def alias_matches_prompt(alias: str, prompt: str, prompt_tokens: set[str]) -> bool:
|
|
242
|
+
if not alias:
|
|
243
|
+
return False
|
|
244
|
+
alias_tokens = set(TOKEN_PATTERN.findall(alias))
|
|
245
|
+
if len(alias_tokens) == 1:
|
|
246
|
+
return alias in prompt_tokens
|
|
247
|
+
return alias in prompt
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def needs_input_unknown(root: Path, prompt: str, *, category: str | None) -> dict[str, Any]:
|
|
251
|
+
index = build_control_index(root)
|
|
252
|
+
categories = [category] if category else sorted(index)
|
|
253
|
+
return {
|
|
254
|
+
"kind": "agent",
|
|
255
|
+
"status": "needs-input",
|
|
256
|
+
"ok": False,
|
|
257
|
+
"mode": "control-center-route",
|
|
258
|
+
"requires_llm": False,
|
|
259
|
+
"unknown_control_target": True,
|
|
260
|
+
"category": category,
|
|
261
|
+
"prompt": prompt,
|
|
262
|
+
"available_categories": categories,
|
|
263
|
+
"response": "Nao encontrei esse recurso nos catalogos locais controlaveis.",
|
|
264
|
+
"result": {"categories": {key: public_items(index.get(key, [])) for key in categories}},
|
|
265
|
+
"exit_code": 2,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def needs_input_ambiguous(matches: list[dict[str, Any]], prompt: str, *, category: str | None) -> dict[str, Any]:
|
|
270
|
+
return {
|
|
271
|
+
"kind": "agent",
|
|
272
|
+
"status": "needs-input",
|
|
273
|
+
"ok": False,
|
|
274
|
+
"mode": "control-center-route",
|
|
275
|
+
"requires_llm": False,
|
|
276
|
+
"ambiguous_control_target": True,
|
|
277
|
+
"category": category,
|
|
278
|
+
"prompt": prompt,
|
|
279
|
+
"matches": public_items(matches),
|
|
280
|
+
"response": "Encontrei mais de um recurso compativel. Informe a categoria ou id exato.",
|
|
281
|
+
"exit_code": 2,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def control_payload(*, status: str, response: str, result: dict[str, Any]) -> dict[str, Any]:
|
|
286
|
+
return {"kind": "agent", "status": status, "ok": status == "ok", "mode": "control-center-route", "requires_llm": False, "response": response, "result": result}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def public_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
290
|
+
return [{"category": item.get("category"), "id": item.get("id"), "label": item.get("label"), "source": item.get("source")} for item in items]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def contains_any(prompt: str, markers: tuple[str, ...]) -> bool:
|
|
294
|
+
return any(normalize(marker) in prompt for marker in markers)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def normalize(value: str) -> str:
|
|
298
|
+
return " ".join(value.lower().split())
|
|
@@ -122,6 +122,23 @@ def reset_decisions(category: str | None = None) -> dict[str, Any]:
|
|
|
122
122
|
return {"kind": "decisions-reset", "status": "reset", "category": category, "path": str(path)}
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def forget_decision(category: str, item_id: str) -> dict[str, Any]:
|
|
126
|
+
validate_category(category)
|
|
127
|
+
validate_item_id(item_id)
|
|
128
|
+
data = load_decisions()
|
|
129
|
+
key = decision_key(category, item_id)
|
|
130
|
+
removed = data.get("items", {}).pop(key, None)
|
|
131
|
+
path = save_decisions(data)
|
|
132
|
+
return {
|
|
133
|
+
"kind": "decision-forget",
|
|
134
|
+
"status": "forgotten" if isinstance(removed, dict) else "not-found",
|
|
135
|
+
"path": str(path),
|
|
136
|
+
"category": category,
|
|
137
|
+
"id": item_id,
|
|
138
|
+
"removed": public_decision(removed) if isinstance(removed, dict) else None,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
125
142
|
def is_disabled(category: str, item_id: str) -> bool:
|
|
126
143
|
decision = get_decision(category, item_id)
|
|
127
144
|
return bool(decision and decision.get("state") in {"disabled_by_user", "denied_by_user"})
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from cli.aikit.app_home import APP_DIRS, app_home
|
|
10
|
+
from cli.aikit.app_home import APP_DIRS, app_home, app_home_status
|
|
11
11
|
from cli.aikit.llm import config_path as llm_config_path
|
|
12
12
|
from cli.aikit.llm import doctor_backends
|
|
13
13
|
from cli.aikit.lock import lock_path, lock_status, read_lock
|
|
@@ -44,6 +44,7 @@ def runtime_diagnostics(
|
|
|
44
44
|
"status": status,
|
|
45
45
|
"root": str(root),
|
|
46
46
|
"app_home": str(app_home()),
|
|
47
|
+
"app_home_status": app_home_status(),
|
|
47
48
|
"app_dirs": {
|
|
48
49
|
name: {
|
|
49
50
|
"path": str(app_home() / name),
|