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
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""Persistent agentic setup wizard state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from cli.aikit.app_home import app_path, ensure_app_home
|
|
13
|
+
from cli.aikit.decision_store import set_decision
|
|
14
|
+
from cli.aikit.memory import redact_secrets
|
|
15
|
+
from cli.aikit.sources import SourceRegistryError, add_source
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
YES_VALUES = {"s", "sim", "y", "yes", "true", "1", "ok"}
|
|
19
|
+
NO_VALUES = {"n", "nao", "não", "no", "false", "0"}
|
|
20
|
+
ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
21
|
+
SOURCE_ID_PATTERN = re.compile(r"[^a-z0-9._-]+")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WizardStateError(ValueError):
|
|
25
|
+
"""Raised for user-facing wizard state errors."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def wizard_home() -> Path:
|
|
29
|
+
ensure_app_home()
|
|
30
|
+
path = app_path("state", "wizards")
|
|
31
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
return path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def wizard_path(wizard_id: str) -> Path:
|
|
36
|
+
validate_wizard_id(wizard_id)
|
|
37
|
+
return wizard_home() / f"{wizard_id}.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_provider_wizard(
|
|
41
|
+
setup_wizard: dict[str, Any],
|
|
42
|
+
*,
|
|
43
|
+
execution_plan: dict[str, Any] | None = None,
|
|
44
|
+
route: dict[str, Any] | None = None,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Persist a provider setup wizard and return its public representation."""
|
|
47
|
+
now = now_iso()
|
|
48
|
+
wizard_id = f"wiz-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:8]}"
|
|
49
|
+
provider = str(setup_wizard.get("provider") or "provider")
|
|
50
|
+
safe_setup = sanitize_setup_wizard(setup_wizard, wizard_id=wizard_id)
|
|
51
|
+
first_question = safe_setup.get("next_question")
|
|
52
|
+
pending_questions: list[dict[str, Any]] = []
|
|
53
|
+
if isinstance(first_question, dict):
|
|
54
|
+
pending_questions.append(dict(first_question))
|
|
55
|
+
pending_questions.extend(dict(item) for item in safe_setup.get("questions") or [] if isinstance(item, dict))
|
|
56
|
+
status = "denied-by-user" if safe_setup.get("status") == "denied-by-user" else "waiting-for-user"
|
|
57
|
+
state = {
|
|
58
|
+
"kind": "provider-setup-wizard-state",
|
|
59
|
+
"schema_version": "ai-devkit.provider-wizard-state/v1",
|
|
60
|
+
"id": wizard_id,
|
|
61
|
+
"status": status,
|
|
62
|
+
"provider": provider,
|
|
63
|
+
"created_at": now,
|
|
64
|
+
"updated_at": now,
|
|
65
|
+
"setup_wizard": safe_setup,
|
|
66
|
+
"execution_plan": sanitize_execution_plan(execution_plan),
|
|
67
|
+
"route": sanitize_route(route),
|
|
68
|
+
"pending_questions": pending_questions,
|
|
69
|
+
"answered_questions": [],
|
|
70
|
+
"answers": {},
|
|
71
|
+
"source_result": None,
|
|
72
|
+
"stored_secret": False,
|
|
73
|
+
}
|
|
74
|
+
save_state(state)
|
|
75
|
+
return public_wizard(state)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def list_wizards(*, status: str | None = None) -> dict[str, Any]:
|
|
79
|
+
items = [public_wizard(load_state(path.stem)) for path in wizard_home().glob("*.json")]
|
|
80
|
+
if status:
|
|
81
|
+
items = [item for item in items if item.get("status") == status]
|
|
82
|
+
items.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
|
|
83
|
+
return {
|
|
84
|
+
"kind": "wizards",
|
|
85
|
+
"status": "ok",
|
|
86
|
+
"items": items,
|
|
87
|
+
"home": str(wizard_home()),
|
|
88
|
+
"stored_secret": False,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def show_wizard(wizard_id: str) -> dict[str, Any]:
|
|
93
|
+
return {
|
|
94
|
+
"kind": "wizard",
|
|
95
|
+
"status": "ok",
|
|
96
|
+
"wizard": public_wizard(load_state(wizard_id)),
|
|
97
|
+
"stored_secret": False,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cancel_wizard(wizard_id: str, *, reason: str | None = None) -> dict[str, Any]:
|
|
102
|
+
state = load_state(wizard_id)
|
|
103
|
+
state["status"] = "cancelled"
|
|
104
|
+
state["updated_at"] = now_iso()
|
|
105
|
+
state["cancel_reason"] = reason or "cancelled by user"
|
|
106
|
+
state["pending_questions"] = []
|
|
107
|
+
save_state(state)
|
|
108
|
+
return {
|
|
109
|
+
"kind": "wizard",
|
|
110
|
+
"status": "cancelled",
|
|
111
|
+
"wizard": public_wizard(state),
|
|
112
|
+
"stored_secret": False,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def answer_wizard(wizard_id: str, answer: str) -> dict[str, Any]:
|
|
117
|
+
state = load_state(wizard_id)
|
|
118
|
+
if state.get("status") in {"completed", "cancelled", "denied-by-user"}:
|
|
119
|
+
return {
|
|
120
|
+
"kind": "wizard",
|
|
121
|
+
"status": str(state.get("status")),
|
|
122
|
+
"wizard": public_wizard(state),
|
|
123
|
+
"stored_secret": False,
|
|
124
|
+
}
|
|
125
|
+
question = current_question(state)
|
|
126
|
+
if not question:
|
|
127
|
+
return complete_wizard(state)
|
|
128
|
+
|
|
129
|
+
parsed = parse_answer(question, answer)
|
|
130
|
+
if is_opt_in_question(state, question):
|
|
131
|
+
if not parsed:
|
|
132
|
+
provider = str(state.get("provider") or "")
|
|
133
|
+
set_decision("tools", provider, "denied_by_user", reason="agentic provider setup wizard opt-out")
|
|
134
|
+
state["status"] = "denied-by-user"
|
|
135
|
+
state["updated_at"] = now_iso()
|
|
136
|
+
state["pending_questions"] = []
|
|
137
|
+
append_answer(state, question, parsed)
|
|
138
|
+
save_state(state)
|
|
139
|
+
return {
|
|
140
|
+
"kind": "wizard",
|
|
141
|
+
"status": "denied-by-user",
|
|
142
|
+
"wizard": public_wizard(state),
|
|
143
|
+
"stored_secret": False,
|
|
144
|
+
}
|
|
145
|
+
pop_current_question(state)
|
|
146
|
+
append_answer(state, question, parsed)
|
|
147
|
+
state["status"] = "collecting-input"
|
|
148
|
+
state["updated_at"] = now_iso()
|
|
149
|
+
save_state(state)
|
|
150
|
+
return {
|
|
151
|
+
"kind": "wizard",
|
|
152
|
+
"status": "needs-input",
|
|
153
|
+
"wizard": public_wizard(state),
|
|
154
|
+
"next_question": current_question(state),
|
|
155
|
+
"stored_secret": False,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if is_skip_answer(question, parsed):
|
|
159
|
+
provider = str(state.get("provider") or "")
|
|
160
|
+
set_decision("tools", provider, "denied_by_user", reason="agentic provider setup wizard skipped")
|
|
161
|
+
state["status"] = "denied-by-user"
|
|
162
|
+
state["updated_at"] = now_iso()
|
|
163
|
+
state["pending_questions"] = []
|
|
164
|
+
append_answer(state, question, parsed)
|
|
165
|
+
save_state(state)
|
|
166
|
+
return {
|
|
167
|
+
"kind": "wizard",
|
|
168
|
+
"status": "denied-by-user",
|
|
169
|
+
"wizard": public_wizard(state),
|
|
170
|
+
"stored_secret": False,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
additions = dynamic_questions_for_answer(state, question, parsed)
|
|
174
|
+
pop_current_question(state)
|
|
175
|
+
if additions:
|
|
176
|
+
state["pending_questions"] = additions + list(state.get("pending_questions") or [])
|
|
177
|
+
append_answer(state, question, parsed)
|
|
178
|
+
state["updated_at"] = now_iso()
|
|
179
|
+
if current_question(state):
|
|
180
|
+
save_state(state)
|
|
181
|
+
return {
|
|
182
|
+
"kind": "wizard",
|
|
183
|
+
"status": "needs-input",
|
|
184
|
+
"wizard": public_wizard(state),
|
|
185
|
+
"next_question": current_question(state),
|
|
186
|
+
"stored_secret": False,
|
|
187
|
+
}
|
|
188
|
+
return complete_wizard(state)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def complete_wizard(state: dict[str, Any]) -> dict[str, Any]:
|
|
192
|
+
if state.get("source_result"):
|
|
193
|
+
state["status"] = "completed"
|
|
194
|
+
state["updated_at"] = now_iso()
|
|
195
|
+
save_state(state)
|
|
196
|
+
return {"kind": "wizard", "status": "completed", "wizard": public_wizard(state), "stored_secret": False}
|
|
197
|
+
try:
|
|
198
|
+
source_result = create_source_from_wizard(state)
|
|
199
|
+
except SourceRegistryError as exc:
|
|
200
|
+
state["status"] = "failed"
|
|
201
|
+
state["updated_at"] = now_iso()
|
|
202
|
+
state["error"] = str(exc)
|
|
203
|
+
save_state(state)
|
|
204
|
+
return {
|
|
205
|
+
"kind": "wizard",
|
|
206
|
+
"status": "failed",
|
|
207
|
+
"wizard": public_wizard(state),
|
|
208
|
+
"error": str(exc),
|
|
209
|
+
"stored_secret": False,
|
|
210
|
+
}
|
|
211
|
+
state["status"] = "completed"
|
|
212
|
+
state["updated_at"] = now_iso()
|
|
213
|
+
state["source_result"] = source_result
|
|
214
|
+
save_state(state)
|
|
215
|
+
return {
|
|
216
|
+
"kind": "wizard",
|
|
217
|
+
"status": "completed",
|
|
218
|
+
"wizard": public_wizard(state),
|
|
219
|
+
"source_result": source_result,
|
|
220
|
+
"resume_prompt": resume_prompt(state),
|
|
221
|
+
"stored_secret": False,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def create_source_from_wizard(state: dict[str, Any]) -> dict[str, Any]:
|
|
226
|
+
provider = str(state.get("provider") or "")
|
|
227
|
+
answers = answer_map(state)
|
|
228
|
+
source_id = str(answers.get("source_id") or suggested_source_id(state)).strip()
|
|
229
|
+
source_id = normalize_source_id(source_id or provider)
|
|
230
|
+
intent = str((state.get("setup_wizard") or {}).get("intent") or "").strip()
|
|
231
|
+
agent_id = str((state.get("setup_wizard") or {}).get("agent_id") or "").strip()
|
|
232
|
+
config_pairs: list[str] = []
|
|
233
|
+
env_refs: list[str] = []
|
|
234
|
+
env_files: list[str] = []
|
|
235
|
+
|
|
236
|
+
for record in state.get("answered_questions") or []:
|
|
237
|
+
question = record.get("question") if isinstance(record, dict) else None
|
|
238
|
+
if not isinstance(question, dict):
|
|
239
|
+
continue
|
|
240
|
+
value = record.get("answer")
|
|
241
|
+
if value in {None, ""}:
|
|
242
|
+
continue
|
|
243
|
+
if question.get("config_key") and not question.get("secret"):
|
|
244
|
+
config_pairs.append(f"{question['config_key']}={value}")
|
|
245
|
+
if question.get("env_ref_key"):
|
|
246
|
+
env_key = str(question["env_ref_key"])
|
|
247
|
+
env_value = str(value)
|
|
248
|
+
if not ENV_VAR_NAME_PATTERN.fullmatch(env_value):
|
|
249
|
+
raise SourceRegistryError("credential answers must be environment variable names, not raw secret values")
|
|
250
|
+
if redact_secrets(env_value) != env_value:
|
|
251
|
+
raise SourceRegistryError("credential answers must not contain raw secret values")
|
|
252
|
+
env_refs.append(f"{env_key}={env_value}")
|
|
253
|
+
if question.get("env_file"):
|
|
254
|
+
env_files.append(str(value))
|
|
255
|
+
|
|
256
|
+
default_for: list[str] = []
|
|
257
|
+
default_for_agent: list[str] = []
|
|
258
|
+
if answers.get("default_for_intent") is True and intent:
|
|
259
|
+
default_for.append(intent)
|
|
260
|
+
if agent_id:
|
|
261
|
+
default_for_agent.append(agent_id)
|
|
262
|
+
|
|
263
|
+
return add_source(
|
|
264
|
+
source_id,
|
|
265
|
+
provider=provider,
|
|
266
|
+
label=source_id,
|
|
267
|
+
config_pairs=config_pairs,
|
|
268
|
+
env_refs=env_refs,
|
|
269
|
+
env_files=env_files,
|
|
270
|
+
default_for=default_for,
|
|
271
|
+
default_for_agent=default_for_agent,
|
|
272
|
+
set_default=True,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def current_question(state: dict[str, Any]) -> dict[str, Any] | None:
|
|
277
|
+
questions = state.get("pending_questions") if isinstance(state.get("pending_questions"), list) else []
|
|
278
|
+
if not questions:
|
|
279
|
+
return None
|
|
280
|
+
question = questions[0]
|
|
281
|
+
return dict(question) if isinstance(question, dict) else None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def pop_current_question(state: dict[str, Any]) -> None:
|
|
285
|
+
questions = state.get("pending_questions") if isinstance(state.get("pending_questions"), list) else []
|
|
286
|
+
state["pending_questions"] = list(questions[1:])
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def append_answer(state: dict[str, Any], question: dict[str, Any], answer: Any) -> None:
|
|
290
|
+
state.setdefault("answered_questions", []).append(
|
|
291
|
+
{
|
|
292
|
+
"question": dict(question),
|
|
293
|
+
"answer": public_answer(question, answer),
|
|
294
|
+
"answered_at": now_iso(),
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
state.setdefault("answers", {})[str(question.get("id") or len(state["answered_questions"]))] = public_answer(question, answer)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def parse_answer(question: dict[str, Any], answer: str) -> Any:
|
|
301
|
+
raw = " ".join(str(answer or "").split())
|
|
302
|
+
if not raw and question.get("default") is not None:
|
|
303
|
+
return question.get("default")
|
|
304
|
+
if not raw and question.get("suggested_value"):
|
|
305
|
+
return question.get("suggested_value")
|
|
306
|
+
if question.get("env_ref_key"):
|
|
307
|
+
if not ENV_VAR_NAME_PATTERN.fullmatch(raw):
|
|
308
|
+
raise WizardStateError("informe o nome de uma variavel de ambiente, nao o valor bruto da credencial")
|
|
309
|
+
if redact_secrets(raw) != raw:
|
|
310
|
+
raise WizardStateError("informe uma referencia segura de credencial, nao o valor bruto do segredo")
|
|
311
|
+
return raw
|
|
312
|
+
if question.get("type") == "confirm":
|
|
313
|
+
lowered = raw.lower()
|
|
314
|
+
if lowered in YES_VALUES:
|
|
315
|
+
return True
|
|
316
|
+
if lowered in NO_VALUES:
|
|
317
|
+
return False
|
|
318
|
+
raise WizardStateError("responda sim ou nao")
|
|
319
|
+
if question.get("type") == "select":
|
|
320
|
+
options = [str(item) for item in question.get("options") or []]
|
|
321
|
+
if raw not in options:
|
|
322
|
+
raise WizardStateError(f"resposta invalida. opcoes: {', '.join(options)}")
|
|
323
|
+
return raw
|
|
324
|
+
if not raw and question.get("required"):
|
|
325
|
+
raise WizardStateError("resposta obrigatoria")
|
|
326
|
+
return raw
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def dynamic_questions_for_answer(state: dict[str, Any], question: dict[str, Any], answer: Any) -> list[dict[str, Any]]:
|
|
330
|
+
if question.get("type") != "select" or not str(question.get("id") or "").endswith("_auth"):
|
|
331
|
+
return []
|
|
332
|
+
provider = str(state.get("provider") or "provider")
|
|
333
|
+
setup = state.get("setup_wizard") if isinstance(state.get("setup_wizard"), dict) else {}
|
|
334
|
+
auth_methods = setup.get("auth_methods") if isinstance(setup.get("auth_methods"), list) else []
|
|
335
|
+
selected = str(answer)
|
|
336
|
+
if selected == "file":
|
|
337
|
+
return [
|
|
338
|
+
{
|
|
339
|
+
"id": f"{provider_slug(provider)}_credential_file",
|
|
340
|
+
"type": "text",
|
|
341
|
+
"text": "Qual e o caminho do arquivo local com a credencial?",
|
|
342
|
+
"env_file": True,
|
|
343
|
+
"required": True,
|
|
344
|
+
"secret": False,
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
if selected in {"env"} | {str(method.get("id")) for method in auth_methods if isinstance(method, dict)}:
|
|
348
|
+
secret_fields: list[str] = []
|
|
349
|
+
for method in auth_methods:
|
|
350
|
+
if not isinstance(method, dict):
|
|
351
|
+
continue
|
|
352
|
+
if selected != "env" and method.get("id") != selected:
|
|
353
|
+
continue
|
|
354
|
+
secret_fields.extend(str(item) for item in method.get("secret_fields") or [])
|
|
355
|
+
if selected == "env" and not secret_fields:
|
|
356
|
+
for option in setup.get("credential_options") or []:
|
|
357
|
+
if isinstance(option, dict) and option.get("env"):
|
|
358
|
+
secret_fields.append(str(option["env"]))
|
|
359
|
+
unique = []
|
|
360
|
+
for field in secret_fields:
|
|
361
|
+
if field and field not in unique:
|
|
362
|
+
unique.append(field)
|
|
363
|
+
return [
|
|
364
|
+
{
|
|
365
|
+
"id": f"{provider_slug(provider)}_{provider_slug(field)}_env_ref",
|
|
366
|
+
"type": "text",
|
|
367
|
+
"text": f"Qual variavel de ambiente contem {field}?",
|
|
368
|
+
"env_ref_key": field,
|
|
369
|
+
"required": True,
|
|
370
|
+
"secret": True,
|
|
371
|
+
"stores_secret": False,
|
|
372
|
+
"suggested_value": field,
|
|
373
|
+
}
|
|
374
|
+
for field in unique
|
|
375
|
+
]
|
|
376
|
+
return []
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def is_opt_in_question(state: dict[str, Any], question: dict[str, Any]) -> bool:
|
|
380
|
+
expected = ((state.get("setup_wizard") or {}).get("next_question") or {}).get("id") if isinstance(state.get("setup_wizard"), dict) else None
|
|
381
|
+
return bool(expected and question.get("id") == expected)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def is_skip_answer(question: dict[str, Any], answer: Any) -> bool:
|
|
385
|
+
return question.get("type") == "select" and str(answer) == "skip"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def answer_map(state: dict[str, Any]) -> dict[str, Any]:
|
|
389
|
+
result: dict[str, Any] = {}
|
|
390
|
+
for record in state.get("answered_questions") or []:
|
|
391
|
+
if not isinstance(record, dict):
|
|
392
|
+
continue
|
|
393
|
+
question = record.get("question")
|
|
394
|
+
if isinstance(question, dict):
|
|
395
|
+
result[str(question.get("id"))] = record.get("answer")
|
|
396
|
+
return result
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def public_answer(question: dict[str, Any], answer: Any) -> Any:
|
|
400
|
+
if question.get("env_ref_key"):
|
|
401
|
+
return str(answer)
|
|
402
|
+
if question.get("secret") and not question.get("env_ref_key"):
|
|
403
|
+
return "[REDACTED]"
|
|
404
|
+
return answer
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def public_wizard(state: dict[str, Any]) -> dict[str, Any]:
|
|
408
|
+
setup = state.get("setup_wizard") if isinstance(state.get("setup_wizard"), dict) else {}
|
|
409
|
+
question = current_question(state)
|
|
410
|
+
wizard = dict(setup)
|
|
411
|
+
wizard.update(
|
|
412
|
+
{
|
|
413
|
+
"wizard_id": state.get("id"),
|
|
414
|
+
"status": state.get("status"),
|
|
415
|
+
"provider": state.get("provider") or setup.get("provider"),
|
|
416
|
+
"state_path": str(wizard_path(str(state.get("id")))),
|
|
417
|
+
"created_at": state.get("created_at"),
|
|
418
|
+
"updated_at": state.get("updated_at"),
|
|
419
|
+
"answered_count": len(state.get("answered_questions") or []),
|
|
420
|
+
"pending_count": len(state.get("pending_questions") or []),
|
|
421
|
+
"next_question": question,
|
|
422
|
+
"resume_prompt": resume_prompt(state),
|
|
423
|
+
"stored_secret": False,
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
if state.get("source_result"):
|
|
427
|
+
wizard["source"] = (state["source_result"].get("source") if isinstance(state["source_result"], dict) else None)
|
|
428
|
+
if state.get("error"):
|
|
429
|
+
wizard["error"] = state.get("error")
|
|
430
|
+
return wizard
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def resume_prompt(state: dict[str, Any]) -> str | None:
|
|
434
|
+
setup = state.get("setup_wizard") if isinstance(state.get("setup_wizard"), dict) else {}
|
|
435
|
+
prompt = setup.get("resume_prompt")
|
|
436
|
+
return str(prompt) if prompt else None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def suggested_source_id(state: dict[str, Any]) -> str:
|
|
440
|
+
setup = state.get("setup_wizard") if isinstance(state.get("setup_wizard"), dict) else {}
|
|
441
|
+
return str(setup.get("suggested_source_id") or state.get("provider") or "source")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def sanitize_setup_wizard(setup_wizard: dict[str, Any], *, wizard_id: str) -> dict[str, Any]:
|
|
445
|
+
safe = json.loads(json.dumps(setup_wizard, ensure_ascii=False))
|
|
446
|
+
if safe.get("resume_prompt"):
|
|
447
|
+
safe["resume_prompt"] = redact_secrets(str(safe["resume_prompt"]))
|
|
448
|
+
safe["wizard_id"] = wizard_id
|
|
449
|
+
safe["state_path"] = str(wizard_path(wizard_id))
|
|
450
|
+
safe["stored_secret"] = False
|
|
451
|
+
return safe
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def sanitize_execution_plan(execution_plan: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
455
|
+
if not execution_plan:
|
|
456
|
+
return None
|
|
457
|
+
safe = json.loads(json.dumps(execution_plan, ensure_ascii=False))
|
|
458
|
+
if safe.get("prompt"):
|
|
459
|
+
safe["prompt"] = redact_secrets(str(safe["prompt"]))
|
|
460
|
+
return safe
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def sanitize_route(route: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
464
|
+
if not route:
|
|
465
|
+
return None
|
|
466
|
+
return json.loads(json.dumps(route, ensure_ascii=False))
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def save_state(state: dict[str, Any]) -> Path:
|
|
470
|
+
path = wizard_path(str(state["id"]))
|
|
471
|
+
path.write_text(json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
472
|
+
return path
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def load_state(wizard_id: str) -> dict[str, Any]:
|
|
476
|
+
path = wizard_path(wizard_id)
|
|
477
|
+
if not path.exists():
|
|
478
|
+
raise WizardStateError(f"wizard not found: {wizard_id}")
|
|
479
|
+
try:
|
|
480
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
481
|
+
except json.JSONDecodeError as exc:
|
|
482
|
+
raise WizardStateError(f"invalid wizard state: {wizard_id}") from exc
|
|
483
|
+
if not isinstance(data, dict):
|
|
484
|
+
raise WizardStateError(f"invalid wizard state: {wizard_id}")
|
|
485
|
+
return data
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def normalize_source_id(value: str) -> str:
|
|
489
|
+
normalized = SOURCE_ID_PATTERN.sub("-", value.lower()).strip("-")
|
|
490
|
+
return normalized or "source"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def validate_wizard_id(wizard_id: str) -> None:
|
|
494
|
+
if not re.fullmatch(r"wiz-[0-9]{14}-[a-f0-9]{8}", str(wizard_id or "")):
|
|
495
|
+
raise WizardStateError("invalid wizard id")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def provider_slug(provider: str) -> str:
|
|
499
|
+
return re.sub(r"[^a-z0-9]+", "_", provider.lower()).strip("_") or "provider"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def now_iso() -> str:
|
|
503
|
+
return datetime.now(timezone.utc).isoformat()
|