agent-devkit 0.1.0 → 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 +42 -2
- package/package.json +1 -1
- package/runtime/README.md +119 -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 +111 -10
- 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/configuration_orchestrator.py +291 -0
- package/runtime/cli/aikit/control_router.py +298 -0
- package/runtime/cli/aikit/decision_store.py +175 -0
- package/runtime/cli/aikit/diagnostics.py +2 -1
- package/runtime/cli/aikit/execution_reviewer.py +174 -0
- package/runtime/cli/aikit/fallback.py +48 -2
- package/runtime/cli/aikit/install.py +4 -3
- package/runtime/cli/aikit/llm.py +9 -0
- package/runtime/cli/aikit/local_llm_operator.py +112 -0
- package/runtime/cli/aikit/lock.py +4 -1
- package/runtime/cli/aikit/main.py +604 -21
- package/runtime/cli/aikit/model_router.py +79 -0
- package/runtime/cli/aikit/ollama.py +237 -0
- package/runtime/cli/aikit/orchestrator.py +535 -0
- package/runtime/cli/aikit/provider_wizard.py +19 -0
- package/runtime/cli/aikit/review_gate.py +40 -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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Execution helpers for multi-agent plans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RunCapability = Callable[[dict[str, Any], str, list[str]], dict[str, Any]]
|
|
9
|
+
LoadAgent = Callable[[str], dict[str, Any]]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execute_plan_tasks(
|
|
13
|
+
plan: dict[str, Any],
|
|
14
|
+
*,
|
|
15
|
+
load_agent: LoadAgent,
|
|
16
|
+
run_capability: RunCapability,
|
|
17
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
18
|
+
executed: list[dict[str, Any]] = []
|
|
19
|
+
blocked: list[dict[str, Any]] = []
|
|
20
|
+
for task in plan.get("specialist_tasks") or []:
|
|
21
|
+
if not task.get("primary"):
|
|
22
|
+
continue
|
|
23
|
+
if not task.get("executable"):
|
|
24
|
+
blocked.append(blocked_task(task, "Capability has no executable runner."))
|
|
25
|
+
continue
|
|
26
|
+
if str(task.get("write_policy") or "") not in {"read_only", "dry_run"}:
|
|
27
|
+
blocked.append(blocked_task(task, "Capability requires explicit confirmation for external write effects."))
|
|
28
|
+
continue
|
|
29
|
+
try:
|
|
30
|
+
agent = load_agent(str(task["agent_id"]))
|
|
31
|
+
result = run_capability(agent, str(task["capability_id"]), list(task.get("args") or []))
|
|
32
|
+
except Exception as exc: # pragma: no cover - converted to user-facing payload.
|
|
33
|
+
blocked.append(blocked_task(task, str(exc)))
|
|
34
|
+
continue
|
|
35
|
+
item = public_task_result(task, result)
|
|
36
|
+
if result.get("ok"):
|
|
37
|
+
executed.append(item)
|
|
38
|
+
else:
|
|
39
|
+
blocked.append(item)
|
|
40
|
+
return executed, blocked
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def public_task_result(task: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"id": task.get("id"),
|
|
46
|
+
"agent_id": task.get("agent_id"),
|
|
47
|
+
"capability_id": task.get("capability_id"),
|
|
48
|
+
"status": result.get("status"),
|
|
49
|
+
"ok": bool(result.get("ok")),
|
|
50
|
+
"provider": task.get("provider"),
|
|
51
|
+
"write_policy": task.get("write_policy"),
|
|
52
|
+
"result": result,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def blocked_task(task: dict[str, Any], reason: str) -> dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"id": task.get("id"),
|
|
59
|
+
"agent_id": task.get("agent_id"),
|
|
60
|
+
"capability_id": task.get("capability_id"),
|
|
61
|
+
"status": "blocked",
|
|
62
|
+
"ok": False,
|
|
63
|
+
"provider": task.get("provider"),
|
|
64
|
+
"write_policy": task.get("write_policy"),
|
|
65
|
+
"reason": reason,
|
|
66
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Agent and capability registry helpers for orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_agent_registry(root: Path) -> dict[str, Any]:
|
|
12
|
+
agents_dir = root / "agents"
|
|
13
|
+
agents: dict[str, dict[str, Any]] = {}
|
|
14
|
+
capabilities: dict[str, dict[str, Any]] = {}
|
|
15
|
+
if not agents_dir.exists():
|
|
16
|
+
return {"agents": agents, "capabilities": capabilities}
|
|
17
|
+
for agent_path in sorted(path for path in agents_dir.iterdir() if path.is_dir()):
|
|
18
|
+
manifest_path = agent_path / "agent.yaml"
|
|
19
|
+
if not manifest_path.exists():
|
|
20
|
+
continue
|
|
21
|
+
agent = read_yaml(manifest_path)
|
|
22
|
+
agent_id = str(agent.get("id") or agent_path.name)
|
|
23
|
+
agent["id"] = agent_id
|
|
24
|
+
agent["path"] = str(agent_path.relative_to(root))
|
|
25
|
+
agent["capabilities_index"] = {}
|
|
26
|
+
for capability_path in sorted((agent_path / "capabilities").glob("*/capability.yaml")):
|
|
27
|
+
capability = read_yaml(capability_path)
|
|
28
|
+
capability_id = str(capability.get("id") or capability_path.parent.name)
|
|
29
|
+
short_id = capability_id.rsplit(".", 1)[-1]
|
|
30
|
+
public_capability = {
|
|
31
|
+
"id": capability_id,
|
|
32
|
+
"short_id": short_id,
|
|
33
|
+
"agent_id": agent_id,
|
|
34
|
+
"name": capability.get("name") or short_id,
|
|
35
|
+
"purpose": compact(str(capability.get("purpose") or "")),
|
|
36
|
+
"write_policy": capability.get("write_policy"),
|
|
37
|
+
"inputs": capability.get("inputs") or {},
|
|
38
|
+
"outputs": capability.get("outputs") or {},
|
|
39
|
+
"integration": capability.get("integration") or {},
|
|
40
|
+
"requires": capability.get("requires") or {},
|
|
41
|
+
"has_runner": bool(((capability.get("entrypoint") or {}).get("runner"))),
|
|
42
|
+
"path": str(capability_path.parent.relative_to(root)),
|
|
43
|
+
}
|
|
44
|
+
agent["capabilities_index"][short_id] = public_capability
|
|
45
|
+
capabilities[f"{agent_id}/{short_id}"] = public_capability
|
|
46
|
+
agents[agent_id] = public_agent(agent)
|
|
47
|
+
return {"agents": agents, "capabilities": capabilities}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def public_agent(agent: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
surface = agent.get("agent_surface") if isinstance(agent.get("agent_surface"), dict) else {}
|
|
52
|
+
return {
|
|
53
|
+
"id": agent.get("id"),
|
|
54
|
+
"name": agent.get("name") or agent.get("id"),
|
|
55
|
+
"kind": agent.get("kind"),
|
|
56
|
+
"status": agent.get("status"),
|
|
57
|
+
"purpose": compact(str(agent.get("purpose") or "")),
|
|
58
|
+
"depends_on_agents": list(agent.get("depends_on_agents") or []),
|
|
59
|
+
"orchestrated_agents": list(surface.get("orchestrated_agents") or []),
|
|
60
|
+
"capabilities": list(agent.get("capabilities") or []),
|
|
61
|
+
"capabilities_index": dict(agent.get("capabilities_index") or {}),
|
|
62
|
+
"write_policy": agent.get("write_policy"),
|
|
63
|
+
"path": agent.get("path"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_capability(registry: dict[str, Any], agent_id: str, capability_id: str) -> dict[str, Any] | None:
|
|
68
|
+
return (registry.get("capabilities") or {}).get(f"{agent_id}/{capability_id}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def read_yaml(path: Path) -> dict[str, Any]:
|
|
72
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
73
|
+
return data if isinstance(data, dict) else {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def compact(value: str, *, limit: int = 240) -> str:
|
|
77
|
+
text = " ".join(value.split())
|
|
78
|
+
if len(text) <= limit:
|
|
79
|
+
return text
|
|
80
|
+
return text[: limit - 3].rstrip() + "..."
|
|
@@ -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,291 @@
|
|
|
1
|
+
"""Global configuration orchestration for missing providers and sources."""
|
|
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 is_disabled
|
|
10
|
+
from cli.aikit.providers import ProviderRegistryError, load_providers, provider_or_error
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PROJECT_PATTERN = re.compile(r"(?i)\bprojeto\s+([A-Za-z0-9._ -]{2,80}?)(?:\s+no\b|\s+na\b|\s+do\b|\s+da\b|$)")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def provider_setup_wizard(
|
|
17
|
+
root: Path,
|
|
18
|
+
provider_id: str,
|
|
19
|
+
*,
|
|
20
|
+
prompt: str | None = None,
|
|
21
|
+
route: dict[str, Any] | None = None,
|
|
22
|
+
agent_id: str | None = None,
|
|
23
|
+
capability_id: str | None = None,
|
|
24
|
+
reason: str | None = None,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
provider = load_provider(root, provider_id)
|
|
27
|
+
provider_name = str(provider.get("name") or provider_id)
|
|
28
|
+
intent = str((route or {}).get("intent") or capability_id or "provider-configuration")
|
|
29
|
+
suggested_project = infer_project(prompt or "")
|
|
30
|
+
source_context = bool(route and route.get("intent"))
|
|
31
|
+
suggested_source_id = suggest_source_id(provider_id, suggested_project)
|
|
32
|
+
disabled = is_disabled("tools", provider_id) or is_disabled("integrations", provider_id)
|
|
33
|
+
questions = build_questions(provider, suggested_project=suggested_project, suggested_source_id=suggested_source_id, include_source=source_context)
|
|
34
|
+
next_question = disabled_notice(provider_id, provider_name) if disabled else opt_in_question(provider_id, provider_name)
|
|
35
|
+
return {
|
|
36
|
+
"kind": "provider-setup-wizard",
|
|
37
|
+
"status": "denied-by-user" if disabled else "waiting-for-user",
|
|
38
|
+
"provider": provider_id,
|
|
39
|
+
"provider_name": provider_name,
|
|
40
|
+
"provider_kind": provider.get("kind"),
|
|
41
|
+
"intent": intent,
|
|
42
|
+
"agent_id": agent_id or (route or {}).get("agent_id"),
|
|
43
|
+
"capability_id": capability_id or (route or {}).get("capability_id"),
|
|
44
|
+
"reason": reason,
|
|
45
|
+
"resume_prompt": prompt,
|
|
46
|
+
"suggested_source_id": suggested_source_id if source_context else None,
|
|
47
|
+
"suggested_config": suggested_config(provider_id, suggested_project),
|
|
48
|
+
"next_question": next_question,
|
|
49
|
+
"questions": questions,
|
|
50
|
+
"credential_options": credential_options(provider),
|
|
51
|
+
"config_fields": public_config_fields(provider),
|
|
52
|
+
"auth_methods": public_auth_methods(provider),
|
|
53
|
+
"fallbacks": list(provider.get("fallbacks", []) or []),
|
|
54
|
+
"stores_secret": False,
|
|
55
|
+
"owner_agent": "provider-configurator",
|
|
56
|
+
"message": (
|
|
57
|
+
f"{provider_name} esta desativado. O agente seguira sem essa ferramenta ate voce reativar."
|
|
58
|
+
if disabled
|
|
59
|
+
else f"Para continuar, o agente precisa configurar ou autorizar {provider_name}."
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def provider_wizard_from_requirement(
|
|
65
|
+
root: Path,
|
|
66
|
+
provider_id: str,
|
|
67
|
+
*,
|
|
68
|
+
agent_id: str | None = None,
|
|
69
|
+
capability_id: str | None = None,
|
|
70
|
+
reason: str | None = None,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
return provider_setup_wizard(
|
|
73
|
+
root,
|
|
74
|
+
provider_id,
|
|
75
|
+
agent_id=agent_id,
|
|
76
|
+
capability_id=capability_id,
|
|
77
|
+
reason=reason or "Capability provider requirement is not configured.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_provider(root: Path, provider_id: str) -> dict[str, Any]:
|
|
82
|
+
try:
|
|
83
|
+
return provider_or_error(load_providers(root), provider_id)
|
|
84
|
+
except ProviderRegistryError:
|
|
85
|
+
return {
|
|
86
|
+
"id": provider_id,
|
|
87
|
+
"name": provider_id,
|
|
88
|
+
"kind": "unknown",
|
|
89
|
+
"config_fields": [],
|
|
90
|
+
"auth_methods": [],
|
|
91
|
+
"fallbacks": [],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_questions(
|
|
96
|
+
provider: dict[str, Any],
|
|
97
|
+
*,
|
|
98
|
+
suggested_project: str | None,
|
|
99
|
+
suggested_source_id: str,
|
|
100
|
+
include_source: bool,
|
|
101
|
+
) -> list[dict[str, Any]]:
|
|
102
|
+
questions: list[dict[str, Any]] = []
|
|
103
|
+
for field in provider.get("config_fields", []) or []:
|
|
104
|
+
if not isinstance(field, dict):
|
|
105
|
+
continue
|
|
106
|
+
name = str(field.get("name") or "").strip()
|
|
107
|
+
if not name:
|
|
108
|
+
continue
|
|
109
|
+
suggested_value = suggested_value_for_field(provider, name, suggested_project)
|
|
110
|
+
questions.append(
|
|
111
|
+
{
|
|
112
|
+
"id": field_question_id(provider["id"], name),
|
|
113
|
+
"type": "text",
|
|
114
|
+
"text": question_text(provider, name, suggested_value=suggested_value),
|
|
115
|
+
"config_key": name,
|
|
116
|
+
"env": name,
|
|
117
|
+
"required": bool(field.get("required")),
|
|
118
|
+
"secret": bool(field.get("secret")),
|
|
119
|
+
"default": field.get("default"),
|
|
120
|
+
"suggested_value": suggested_value,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
auth_methods = public_auth_methods(provider)
|
|
124
|
+
if auth_methods:
|
|
125
|
+
questions.append(
|
|
126
|
+
{
|
|
127
|
+
"id": f"{provider_slug(str(provider['id']))}_auth",
|
|
128
|
+
"type": "select",
|
|
129
|
+
"text": "Como deseja autenticar?",
|
|
130
|
+
"options": [method["id"] for method in auth_methods] + ["env", "file", "skip"],
|
|
131
|
+
"auth_methods": auth_methods,
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
if include_source:
|
|
135
|
+
questions.append(
|
|
136
|
+
{
|
|
137
|
+
"id": "source_id",
|
|
138
|
+
"type": "text",
|
|
139
|
+
"text": "Qual nome local deseja dar para esta fonte?",
|
|
140
|
+
"suggested_value": suggested_source_id,
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
questions.append(
|
|
144
|
+
{
|
|
145
|
+
"id": "default_for_intent",
|
|
146
|
+
"type": "confirm",
|
|
147
|
+
"text": "Posso usar esta fonte como padrao para este tipo de pedido?",
|
|
148
|
+
"default": True,
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
if not questions:
|
|
152
|
+
questions.append(
|
|
153
|
+
{
|
|
154
|
+
"id": f"{provider_slug(str(provider['id']))}_confirm",
|
|
155
|
+
"type": "confirm",
|
|
156
|
+
"text": f"Posso tentar usar {provider.get('name') or provider['id']} com a cadeia de credenciais nativa?",
|
|
157
|
+
"default": False,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
return questions
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def opt_in_question(provider_id: str, provider_name: str) -> dict[str, Any]:
|
|
164
|
+
return {
|
|
165
|
+
"id": f"{provider_slug(provider_id)}_opt_in",
|
|
166
|
+
"type": "confirm",
|
|
167
|
+
"text": f"Posso configurar ou autorizar {provider_name} para atender este pedido?",
|
|
168
|
+
"default": False,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def disabled_notice(provider_id: str, provider_name: str) -> dict[str, Any]:
|
|
173
|
+
return {
|
|
174
|
+
"id": f"{provider_slug(provider_id)}_disabled",
|
|
175
|
+
"type": "notice",
|
|
176
|
+
"text": f"{provider_name} esta desativado por decisao do usuario. Reative para usar esta integracao.",
|
|
177
|
+
"default": False,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def public_config_fields(provider: dict[str, Any]) -> list[dict[str, Any]]:
|
|
182
|
+
fields = []
|
|
183
|
+
for field in provider.get("config_fields", []) or []:
|
|
184
|
+
if not isinstance(field, dict):
|
|
185
|
+
continue
|
|
186
|
+
fields.append(
|
|
187
|
+
{
|
|
188
|
+
"name": field.get("name"),
|
|
189
|
+
"required": bool(field.get("required")),
|
|
190
|
+
"secret": bool(field.get("secret")),
|
|
191
|
+
"default": field.get("default"),
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
return fields
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def public_auth_methods(provider: dict[str, Any]) -> list[dict[str, Any]]:
|
|
198
|
+
methods = []
|
|
199
|
+
for method in provider.get("auth_methods", []) or []:
|
|
200
|
+
if not isinstance(method, dict):
|
|
201
|
+
continue
|
|
202
|
+
methods.append(
|
|
203
|
+
{
|
|
204
|
+
"id": method.get("id"),
|
|
205
|
+
"label": method.get("label") or method.get("id"),
|
|
206
|
+
"native": bool(method.get("native")),
|
|
207
|
+
"config_fields": list(method.get("config_fields", []) or []),
|
|
208
|
+
"secret_fields": list(method.get("secret_fields", []) or []),
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
return methods
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def credential_options(provider: dict[str, Any]) -> list[dict[str, Any]]:
|
|
215
|
+
options = []
|
|
216
|
+
for method in public_auth_methods(provider):
|
|
217
|
+
for secret_field in method.get("secret_fields") or []:
|
|
218
|
+
options.append(
|
|
219
|
+
{
|
|
220
|
+
"id": f"env:{secret_field}",
|
|
221
|
+
"label": f"Variavel de ambiente {secret_field}",
|
|
222
|
+
"env": secret_field,
|
|
223
|
+
"auth_method": method.get("id"),
|
|
224
|
+
"stores_secret": False,
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
options.extend(
|
|
228
|
+
[
|
|
229
|
+
{"id": "file", "label": "Arquivo local de credencial", "stores_secret": False},
|
|
230
|
+
{"id": "native", "label": "Cadeia nativa ou CLI ja autenticada", "stores_secret": False},
|
|
231
|
+
{"id": "skip", "label": "Ignorar esta ferramenta", "stores_secret": False},
|
|
232
|
+
]
|
|
233
|
+
)
|
|
234
|
+
seen: set[str] = set()
|
|
235
|
+
unique = []
|
|
236
|
+
for option in options:
|
|
237
|
+
option_id = str(option.get("id"))
|
|
238
|
+
if option_id not in seen:
|
|
239
|
+
seen.add(option_id)
|
|
240
|
+
unique.append(option)
|
|
241
|
+
return unique
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def suggested_config(provider_id: str, suggested_project: str | None) -> dict[str, Any]:
|
|
245
|
+
config: dict[str, Any] = {}
|
|
246
|
+
if provider_id == "azure-devops" and suggested_project:
|
|
247
|
+
config["project"] = suggested_project
|
|
248
|
+
config["AZURE_DEVOPS_PROJECT"] = suggested_project
|
|
249
|
+
return config
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def suggested_value_for_field(provider: dict[str, Any], name: str, suggested_project: str | None) -> str | None:
|
|
253
|
+
if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_PROJECT" and suggested_project:
|
|
254
|
+
return suggested_project
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def question_text(provider: dict[str, Any], name: str, *, suggested_value: str | None) -> str:
|
|
259
|
+
if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_ORG":
|
|
260
|
+
return "Qual e a organizacao do Azure DevOps?"
|
|
261
|
+
if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_PROJECT":
|
|
262
|
+
if suggested_value:
|
|
263
|
+
return f'O projeto e "{suggested_value}"? Se nao, informe o nome correto.'
|
|
264
|
+
return "Qual e o nome do projeto no Azure DevOps?"
|
|
265
|
+
label = name.replace("_", " ").lower()
|
|
266
|
+
return f"Informe {label} para {provider.get('name') or provider.get('id')}."
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def field_question_id(provider_id: str, name: str) -> str:
|
|
270
|
+
return f"{provider_slug(provider_id)}_{provider_slug(name)}"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def infer_project(prompt: str) -> str | None:
|
|
274
|
+
match = PROJECT_PATTERN.search(prompt)
|
|
275
|
+
if not match:
|
|
276
|
+
return None
|
|
277
|
+
project = " ".join(match.group(1).strip(" .,_-").split())
|
|
278
|
+
return project or None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def suggest_source_id(provider: str, project: str | None) -> str:
|
|
282
|
+
parts = [provider]
|
|
283
|
+
if project:
|
|
284
|
+
parts.append(project)
|
|
285
|
+
raw = "-".join(parts)
|
|
286
|
+
normalized = re.sub(r"[^a-z0-9._-]+", "-", raw.lower()).strip("-")
|
|
287
|
+
return normalized or provider_slug(provider)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def provider_slug(provider: str) -> str:
|
|
291
|
+
return re.sub(r"[^a-z0-9]+", "_", provider.lower()).strip("_") or "provider"
|