agent-devkit 0.3.0 → 0.3.2
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 +25 -11
- package/package.json +1 -1
- package/runtime/README.md +20 -10
- package/runtime/cli/README.md +14 -6
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/aliases.py +181 -0
- package/runtime/cli/aikit/app_home.py +1 -0
- package/runtime/cli/aikit/cli_dispatch.py +5 -1
- package/runtime/cli/aikit/cli_parser.py +3 -2
- package/runtime/cli/aikit/embedded_mini_brain.py +351 -0
- package/runtime/cli/aikit/human_output.py +32 -0
- package/runtime/cli/aikit/interactive_wizard.py +181 -13
- package/runtime/cli/aikit/llm.py +28 -2
- package/runtime/cli/aikit/local_llm.py +19 -4
- package/runtime/cli/aikit/local_llm_operator.py +15 -5
- package/runtime/cli/aikit/mini_brain.py +56 -44
- package/runtime/cli/aikit/model_router.py +42 -9
- package/runtime/cli/aikit/natural_prompt_runtime.py +86 -2
- package/runtime/cli/aikit/onboarding.py +3 -3
- package/runtime/cli/aikit/personality.py +3 -0
- package/runtime/cli/aikit/review_gate.py +14 -2
- package/runtime/models/qwen2.5-0.5b-instruct/manifest.json +30 -0
- package/runtime/scripts/release-catalog-snapshot.json +1 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Embedded mini-brain runtime backed by an on-demand GGUF model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from cli.aikit.app_home import app_path, ensure_app_home
|
|
16
|
+
from cli.aikit.identity import identity_system_prompt
|
|
17
|
+
from cli.aikit.runtime_paths import ROOT
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
EMBEDDED_BACKEND_ID = "embedded-mini-brain"
|
|
21
|
+
EMBEDDED_MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
|
|
22
|
+
EMBEDDED_MODEL_NAME = "qwen2.5-0.5b-instruct"
|
|
23
|
+
EMBEDDED_MODEL_SOURCE = "https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q2_k.gguf"
|
|
24
|
+
EMBEDDED_MODEL_SIZE_BYTES = 415182688
|
|
25
|
+
EMBEDDED_MODEL_PATH = app_path("models", EMBEDDED_MODEL_NAME)
|
|
26
|
+
EMBEDDED_MANIFEST_PATH = ROOT / "models" / EMBEDDED_MODEL_NAME / "manifest.json"
|
|
27
|
+
EMBEDDED_MODEL_FILE = EMBEDDED_MODEL_PATH / "qwen2.5-0.5b-instruct-q2_k.gguf"
|
|
28
|
+
EMBEDDED_MODEL_SHA256 = "9ee36184e616dfc76df4f5dd66f908dbde6979524ae36e6cefb67f532f798cb8"
|
|
29
|
+
EMBEDDED_RUNTIME = "llama-cpp-python"
|
|
30
|
+
EMBEDDED_RUNTIME_REQUIREMENT = "llama-cpp-python>=0.3.9"
|
|
31
|
+
EMBEDDED_MAX_RESPONSE_CHARS = 2000
|
|
32
|
+
DEFAULT_MAX_TOKENS = 220
|
|
33
|
+
DEFAULT_CONTEXT_TOKENS = 2048
|
|
34
|
+
SMOKE_RESPONSE_ENV = "AGENT_DEVKIT_EMBEDDED_SMOKE_RESPONSE"
|
|
35
|
+
SOURCE_ENV = "AGENT_DEVKIT_EMBEDDED_MODEL_SOURCE"
|
|
36
|
+
SKIP_DEP_INSTALL_ENV = "AGENT_DEVKIT_EMBEDDED_SKIP_DEP_INSTALL"
|
|
37
|
+
|
|
38
|
+
_LLAMA_CACHE: Any | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def embedded_mini_brain_status() -> dict[str, Any]:
|
|
42
|
+
manifest_exists = EMBEDDED_MANIFEST_PATH.exists()
|
|
43
|
+
model_exists = EMBEDDED_MODEL_FILE.exists()
|
|
44
|
+
model_sha256 = sha256_file(EMBEDDED_MODEL_FILE) if model_exists else None
|
|
45
|
+
smoke_mode = bool(os.environ.get(SMOKE_RESPONSE_ENV))
|
|
46
|
+
model_file_valid = smoke_mode or (model_sha256 == EMBEDDED_MODEL_SHA256 if model_exists else False)
|
|
47
|
+
dependency = llama_cpp_dependency_status()
|
|
48
|
+
available = model_file_valid and dependency["status"] == "ok"
|
|
49
|
+
if available:
|
|
50
|
+
status = "ok"
|
|
51
|
+
elif not model_exists:
|
|
52
|
+
status = "not-installed"
|
|
53
|
+
elif not model_file_valid:
|
|
54
|
+
status = "invalid-model"
|
|
55
|
+
elif dependency["status"] != "ok":
|
|
56
|
+
status = "dependency-missing"
|
|
57
|
+
else:
|
|
58
|
+
status = "missing"
|
|
59
|
+
return {
|
|
60
|
+
"kind": "embedded-mini-brain",
|
|
61
|
+
"id": EMBEDDED_BACKEND_ID,
|
|
62
|
+
"status": status,
|
|
63
|
+
"available": available,
|
|
64
|
+
"configured": model_file_valid,
|
|
65
|
+
"provider": EMBEDDED_BACKEND_ID,
|
|
66
|
+
"runtime": EMBEDDED_RUNTIME,
|
|
67
|
+
"runtime_requirement": EMBEDDED_RUNTIME_REQUIREMENT,
|
|
68
|
+
"model": EMBEDDED_MODEL_ID,
|
|
69
|
+
"hf_model": EMBEDDED_MODEL_ID,
|
|
70
|
+
"model_name": EMBEDDED_MODEL_NAME,
|
|
71
|
+
"model_path": str(EMBEDDED_MODEL_PATH),
|
|
72
|
+
"model_file": str(EMBEDDED_MODEL_FILE),
|
|
73
|
+
"model_file_present": model_exists,
|
|
74
|
+
"model_file_valid": model_file_valid,
|
|
75
|
+
"model_file_sha256": model_sha256,
|
|
76
|
+
"smoke_mode": smoke_mode,
|
|
77
|
+
"model_size_bytes": EMBEDDED_MODEL_SIZE_BYTES,
|
|
78
|
+
"download_url": model_source(),
|
|
79
|
+
"sha256": EMBEDDED_MODEL_SHA256,
|
|
80
|
+
"manifest_path": str(EMBEDDED_MANIFEST_PATH),
|
|
81
|
+
"manifest_present": manifest_exists,
|
|
82
|
+
"dependency": dependency,
|
|
83
|
+
"auth": "none",
|
|
84
|
+
"stored_secret": False,
|
|
85
|
+
"install_command": "agent setup mini-brain --yes",
|
|
86
|
+
"message": (
|
|
87
|
+
"Embedded Qwen2.5 mini-brain is available for real local inference."
|
|
88
|
+
if available
|
|
89
|
+
else "Embedded mini-brain model is not installed or llama_cpp runtime is missing."
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def invoke_embedded_mini_brain(prompt: str, *, public_name: str = "Agent DevKit") -> str:
|
|
95
|
+
status = embedded_mini_brain_status()
|
|
96
|
+
if not status["available"]:
|
|
97
|
+
raise EmbeddedMiniBrainError(status["message"])
|
|
98
|
+
smoke_response = os.environ.get(SMOKE_RESPONSE_ENV)
|
|
99
|
+
if smoke_response:
|
|
100
|
+
return f"{public_name}: {smoke_response}"[:EMBEDDED_MAX_RESPONSE_CHARS]
|
|
101
|
+
llama = load_llama()
|
|
102
|
+
payload = llama.create_chat_completion(
|
|
103
|
+
messages=[
|
|
104
|
+
{
|
|
105
|
+
"role": "system",
|
|
106
|
+
"content": embedded_system_prompt(public_name),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"role": "user",
|
|
110
|
+
"content": prompt,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
max_tokens=int(os.environ.get("AGENT_DEVKIT_EMBEDDED_MAX_TOKENS", str(DEFAULT_MAX_TOKENS))),
|
|
114
|
+
temperature=float(os.environ.get("AGENT_DEVKIT_EMBEDDED_TEMPERATURE", "0.2")),
|
|
115
|
+
top_p=float(os.environ.get("AGENT_DEVKIT_EMBEDDED_TOP_P", "0.9")),
|
|
116
|
+
repeat_penalty=float(os.environ.get("AGENT_DEVKIT_EMBEDDED_REPEAT_PENALTY", "1.08")),
|
|
117
|
+
)
|
|
118
|
+
try:
|
|
119
|
+
content = str(payload["choices"][0]["message"]["content"]).strip()
|
|
120
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
121
|
+
raise EmbeddedMiniBrainError("Embedded mini-brain returned an unexpected response shape.") from exc
|
|
122
|
+
if not content:
|
|
123
|
+
raise EmbeddedMiniBrainError("Embedded mini-brain returned an empty response.")
|
|
124
|
+
return content[:EMBEDDED_MAX_RESPONSE_CHARS]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def embedded_backend_doctor() -> dict[str, Any]:
|
|
128
|
+
status = embedded_mini_brain_status()
|
|
129
|
+
return {
|
|
130
|
+
"id": EMBEDDED_BACKEND_ID,
|
|
131
|
+
"display_name": "Embedded mini-brain",
|
|
132
|
+
"kind": "embedded-local",
|
|
133
|
+
"status": status["status"],
|
|
134
|
+
"configured": status["configured"],
|
|
135
|
+
"model": EMBEDDED_MODEL_ID,
|
|
136
|
+
"model_file": status["model_file"],
|
|
137
|
+
"runtime": EMBEDDED_RUNTIME,
|
|
138
|
+
"auth_status": "none",
|
|
139
|
+
"message": status["message"],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def embedded_backend_config() -> dict[str, Any]:
|
|
144
|
+
return {
|
|
145
|
+
"kind": "embedded-local",
|
|
146
|
+
"auth": "none",
|
|
147
|
+
"model": EMBEDDED_MODEL_ID,
|
|
148
|
+
"runtime": EMBEDDED_RUNTIME,
|
|
149
|
+
"model_file": str(EMBEDDED_MODEL_FILE),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def setup_embedded_mini_brain(*, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
|
|
154
|
+
before = embedded_mini_brain_status()
|
|
155
|
+
plan = embedded_install_plan()
|
|
156
|
+
if dry_run or not yes:
|
|
157
|
+
needs_confirmation = not dry_run and not yes
|
|
158
|
+
return {
|
|
159
|
+
"kind": "embedded-mini-brain-install",
|
|
160
|
+
"status": "planned" if dry_run else "needs-confirmation",
|
|
161
|
+
"ok": bool(dry_run),
|
|
162
|
+
"exit_code": 2 if needs_confirmation else 0,
|
|
163
|
+
"dry_run": dry_run,
|
|
164
|
+
"yes": yes,
|
|
165
|
+
"before": before,
|
|
166
|
+
"after": before,
|
|
167
|
+
"plan": plan,
|
|
168
|
+
"message": "Use --yes to download the embedded mini-brain model and install its local runtime.",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ensure_app_home()
|
|
172
|
+
EMBEDDED_MODEL_PATH.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
download_result = ensure_model_file()
|
|
174
|
+
dependency_result = ensure_llama_cpp_dependency()
|
|
175
|
+
after = embedded_mini_brain_status()
|
|
176
|
+
ok = after.get("available") is True
|
|
177
|
+
return {
|
|
178
|
+
"kind": "embedded-mini-brain-install",
|
|
179
|
+
"status": "ok" if ok else "failed",
|
|
180
|
+
"ok": ok,
|
|
181
|
+
"exit_code": 0 if ok else 1,
|
|
182
|
+
"dry_run": False,
|
|
183
|
+
"yes": True,
|
|
184
|
+
"before": before,
|
|
185
|
+
"after": after,
|
|
186
|
+
"plan": plan,
|
|
187
|
+
"download": download_result,
|
|
188
|
+
"dependency_install": dependency_result,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def embedded_install_plan() -> dict[str, Any]:
|
|
193
|
+
return {
|
|
194
|
+
"provider": EMBEDDED_BACKEND_ID,
|
|
195
|
+
"model": EMBEDDED_MODEL_ID,
|
|
196
|
+
"model_name": EMBEDDED_MODEL_NAME,
|
|
197
|
+
"download_url": model_source(),
|
|
198
|
+
"size_bytes": EMBEDDED_MODEL_SIZE_BYTES,
|
|
199
|
+
"sha256": EMBEDDED_MODEL_SHA256,
|
|
200
|
+
"destination": str(EMBEDDED_MODEL_FILE),
|
|
201
|
+
"runtime_requirement": EMBEDDED_RUNTIME_REQUIREMENT,
|
|
202
|
+
"writes": [
|
|
203
|
+
str(EMBEDDED_MODEL_FILE),
|
|
204
|
+
str(app_path("python")),
|
|
205
|
+
],
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def embedded_system_prompt(public_name: str) -> str:
|
|
210
|
+
return "\n".join(
|
|
211
|
+
[
|
|
212
|
+
identity_system_prompt(name=public_name),
|
|
213
|
+
"Voce e o mini cerebro local embarcado do Agent DevKit.",
|
|
214
|
+
"Responda em portugues claro quando o usuario escrever em portugues.",
|
|
215
|
+
"Voce pode conversar, orientar onboarding/setup, explicar capacidades e preparar tarefas simples.",
|
|
216
|
+
"Nao finja ser Claude, Codex, OpenAI ou Ollama.",
|
|
217
|
+
"Nao aprove escrita externa, operacoes destrutivas, decisoes finais de seguranca ou revisoes finais.",
|
|
218
|
+
"Quando a tarefa exigir alto julgamento, diga que pode acionar Claude, Codex, Ollama ou APIs se configurados.",
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def load_llama() -> Any:
|
|
224
|
+
global _LLAMA_CACHE
|
|
225
|
+
if _LLAMA_CACHE is not None:
|
|
226
|
+
return _LLAMA_CACHE
|
|
227
|
+
try:
|
|
228
|
+
from llama_cpp import Llama # type: ignore
|
|
229
|
+
except ImportError as exc:
|
|
230
|
+
raise EmbeddedMiniBrainError("llama-cpp-python is required for embedded mini-brain inference.") from exc
|
|
231
|
+
if not EMBEDDED_MODEL_FILE.exists():
|
|
232
|
+
raise EmbeddedMiniBrainError(f"Embedded model file not found: {EMBEDDED_MODEL_FILE}")
|
|
233
|
+
if sha256_file(EMBEDDED_MODEL_FILE) != EMBEDDED_MODEL_SHA256:
|
|
234
|
+
raise EmbeddedMiniBrainError(f"Embedded model file failed SHA-256 validation: {EMBEDDED_MODEL_FILE}")
|
|
235
|
+
_LLAMA_CACHE = Llama(
|
|
236
|
+
model_path=str(EMBEDDED_MODEL_FILE),
|
|
237
|
+
n_ctx=int(os.environ.get("AGENT_DEVKIT_EMBEDDED_N_CTX", str(DEFAULT_CONTEXT_TOKENS))),
|
|
238
|
+
n_threads=int(os.environ.get("AGENT_DEVKIT_EMBEDDED_THREADS", str(max(1, min(4, os.cpu_count() or 1))))),
|
|
239
|
+
verbose=os.environ.get("AGENT_DEVKIT_EMBEDDED_VERBOSE") == "1",
|
|
240
|
+
)
|
|
241
|
+
return _LLAMA_CACHE
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def llama_cpp_dependency_status() -> dict[str, Any]:
|
|
245
|
+
if os.environ.get(SMOKE_RESPONSE_ENV):
|
|
246
|
+
return {
|
|
247
|
+
"status": "ok",
|
|
248
|
+
"module": "llama_cpp",
|
|
249
|
+
"package": "llama-cpp-python",
|
|
250
|
+
"mode": "smoke",
|
|
251
|
+
}
|
|
252
|
+
try:
|
|
253
|
+
import llama_cpp # type: ignore
|
|
254
|
+
except ImportError:
|
|
255
|
+
return {
|
|
256
|
+
"status": "missing",
|
|
257
|
+
"module": "llama_cpp",
|
|
258
|
+
"package": "llama-cpp-python",
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
"status": "ok",
|
|
262
|
+
"module": "llama_cpp",
|
|
263
|
+
"package": "llama-cpp-python",
|
|
264
|
+
"version": getattr(llama_cpp, "__version__", None),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def ensure_model_file() -> dict[str, Any]:
|
|
269
|
+
if os.environ.get(SMOKE_RESPONSE_ENV):
|
|
270
|
+
return {
|
|
271
|
+
"status": "skipped",
|
|
272
|
+
"ok": True,
|
|
273
|
+
"model_file": str(EMBEDDED_MODEL_FILE),
|
|
274
|
+
"reason": "smoke-mode",
|
|
275
|
+
}
|
|
276
|
+
if EMBEDDED_MODEL_FILE.exists() and sha256_file(EMBEDDED_MODEL_FILE) == EMBEDDED_MODEL_SHA256:
|
|
277
|
+
return {
|
|
278
|
+
"status": "already-installed",
|
|
279
|
+
"ok": True,
|
|
280
|
+
"model_file": str(EMBEDDED_MODEL_FILE),
|
|
281
|
+
"sha256": EMBEDDED_MODEL_SHA256,
|
|
282
|
+
}
|
|
283
|
+
partial = EMBEDDED_MODEL_FILE.with_suffix(EMBEDDED_MODEL_FILE.suffix + ".part")
|
|
284
|
+
source = model_source()
|
|
285
|
+
try:
|
|
286
|
+
if Path(source).expanduser().exists():
|
|
287
|
+
shutil.copyfile(Path(source).expanduser(), partial)
|
|
288
|
+
else:
|
|
289
|
+
with urllib.request.urlopen(source, timeout=120) as response, partial.open("wb") as target:
|
|
290
|
+
shutil.copyfileobj(response, target)
|
|
291
|
+
except (OSError, urllib.error.URLError) as exc:
|
|
292
|
+
return {
|
|
293
|
+
"status": "failed",
|
|
294
|
+
"ok": False,
|
|
295
|
+
"model_file": str(EMBEDDED_MODEL_FILE),
|
|
296
|
+
"source": source,
|
|
297
|
+
"message": str(exc),
|
|
298
|
+
}
|
|
299
|
+
actual_sha = sha256_file(partial)
|
|
300
|
+
if actual_sha != EMBEDDED_MODEL_SHA256:
|
|
301
|
+
return {
|
|
302
|
+
"status": "failed",
|
|
303
|
+
"ok": False,
|
|
304
|
+
"model_file": str(EMBEDDED_MODEL_FILE),
|
|
305
|
+
"source": source,
|
|
306
|
+
"sha256": actual_sha,
|
|
307
|
+
"expected_sha256": EMBEDDED_MODEL_SHA256,
|
|
308
|
+
"message": "Downloaded embedded model failed SHA-256 validation.",
|
|
309
|
+
}
|
|
310
|
+
partial.replace(EMBEDDED_MODEL_FILE)
|
|
311
|
+
return {
|
|
312
|
+
"status": "downloaded",
|
|
313
|
+
"ok": True,
|
|
314
|
+
"model_file": str(EMBEDDED_MODEL_FILE),
|
|
315
|
+
"source": source,
|
|
316
|
+
"sha256": EMBEDDED_MODEL_SHA256,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def ensure_llama_cpp_dependency() -> dict[str, Any]:
|
|
321
|
+
current = llama_cpp_dependency_status()
|
|
322
|
+
if current.get("status") == "ok":
|
|
323
|
+
return {"status": "already-installed", "ok": True, "dependency": current}
|
|
324
|
+
if os.environ.get(SKIP_DEP_INSTALL_ENV) == "1":
|
|
325
|
+
return {"status": "skipped", "ok": True, "dependency": current, "reason": "disabled-by-env"}
|
|
326
|
+
command = [sys.executable, "-m", "pip", "install", EMBEDDED_RUNTIME_REQUIREMENT]
|
|
327
|
+
process = subprocess.run(command, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=900)
|
|
328
|
+
return {
|
|
329
|
+
"status": "installed" if process.returncode == 0 else "failed",
|
|
330
|
+
"ok": process.returncode == 0,
|
|
331
|
+
"command": command,
|
|
332
|
+
"exit_code": process.returncode,
|
|
333
|
+
"stdout": process.stdout[-4000:],
|
|
334
|
+
"stderr": process.stderr[-4000:],
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def model_source() -> str:
|
|
339
|
+
return os.environ.get(SOURCE_ENV) or EMBEDDED_MODEL_SOURCE
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def sha256_file(path: Path) -> str:
|
|
343
|
+
hash_obj = hashlib.sha256()
|
|
344
|
+
with path.open("rb") as file:
|
|
345
|
+
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
|
346
|
+
hash_obj.update(chunk)
|
|
347
|
+
return hash_obj.hexdigest()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class EmbeddedMiniBrainError(RuntimeError):
|
|
351
|
+
"""Raised when embedded local inference cannot run."""
|
|
@@ -115,6 +115,8 @@ def print_human(result: dict[str, Any]) -> None:
|
|
|
115
115
|
print_aliases(result)
|
|
116
116
|
elif kind == "alias":
|
|
117
117
|
print_alias(result)
|
|
118
|
+
elif kind == "alias-path":
|
|
119
|
+
print_alias_path(result)
|
|
118
120
|
elif kind == "sessions":
|
|
119
121
|
print_sessions(result)
|
|
120
122
|
elif kind == "session":
|
|
@@ -630,6 +632,9 @@ def print_architecture(result: dict[str, Any]) -> None:
|
|
|
630
632
|
def print_agent_response(result: dict[str, Any]) -> None:
|
|
631
633
|
if result.get("status") == "ok":
|
|
632
634
|
print(result.get("response", ""))
|
|
635
|
+
alias = result.get("alias") if isinstance(result.get("alias"), dict) else None
|
|
636
|
+
if alias:
|
|
637
|
+
print_alias_path_hint(alias.get("path_status") if isinstance(alias.get("path_status"), dict) else None)
|
|
633
638
|
return
|
|
634
639
|
print(result.get("message") or result.get("response") or "Agent execution did not complete.")
|
|
635
640
|
question = result.get("next_question") or ((result.get("setup_wizard") or {}).get("next_question") if isinstance(result.get("setup_wizard"), dict) else None)
|
|
@@ -815,6 +820,12 @@ def print_personality(result: dict[str, Any]) -> None:
|
|
|
815
820
|
print("Setup questions:")
|
|
816
821
|
for question in result["questions"]:
|
|
817
822
|
print(f"- {question}")
|
|
823
|
+
alias = result.get("alias") if isinstance(result.get("alias"), dict) else None
|
|
824
|
+
if alias:
|
|
825
|
+
print(f"Alias: {alias.get('name') or alias.get('suggested_name') or '-'} ({alias.get('status')})")
|
|
826
|
+
if alias.get("path"):
|
|
827
|
+
print(f"Alias path: {alias['path']}")
|
|
828
|
+
print_alias_path_hint(alias.get("path_status") if isinstance(alias.get("path_status"), dict) else None)
|
|
818
829
|
|
|
819
830
|
|
|
820
831
|
def print_aliases(result: dict[str, Any]) -> None:
|
|
@@ -824,6 +835,7 @@ def print_aliases(result: dict[str, Any]) -> None:
|
|
|
824
835
|
return
|
|
825
836
|
for item in result["items"]:
|
|
826
837
|
print(f"- {item['name']}: {item['path']}")
|
|
838
|
+
print_alias_path_hint(item.get("path_status") if isinstance(item.get("path_status"), dict) else None, indent=" ")
|
|
827
839
|
|
|
828
840
|
|
|
829
841
|
def print_alias(result: dict[str, Any]) -> None:
|
|
@@ -835,6 +847,26 @@ def print_alias(result: dict[str, Any]) -> None:
|
|
|
835
847
|
for path in result["removed_paths"]:
|
|
836
848
|
print(f"- {path}")
|
|
837
849
|
print(f"Config: {result['config_path']}")
|
|
850
|
+
print_alias_path_hint(result.get("path_status") if isinstance(result.get("path_status"), dict) else None)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def print_alias_path(result: dict[str, Any]) -> None:
|
|
854
|
+
print(f"Alias PATH: {result.get('status')}")
|
|
855
|
+
print(f"Bin: {result.get('bin_dir') or '-'}")
|
|
856
|
+
if result.get("profile"):
|
|
857
|
+
print(f"Profile: {result['profile']}")
|
|
858
|
+
if result.get("message"):
|
|
859
|
+
print(result["message"])
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def print_alias_path_hint(path_status: dict[str, Any] | None, *, indent: str = "") -> None:
|
|
863
|
+
if not path_status or not path_status.get("setup_required"):
|
|
864
|
+
return
|
|
865
|
+
setup = path_status.get("setup") if isinstance(path_status.get("setup"), dict) else {}
|
|
866
|
+
print(f"{indent}PATH: {path_status.get('bin_dir')} is not active in this shell.")
|
|
867
|
+
command = setup.get("command")
|
|
868
|
+
if command:
|
|
869
|
+
print(f"{indent}Run: {command}")
|
|
838
870
|
|
|
839
871
|
|
|
840
872
|
def print_sessions(result: dict[str, Any]) -> None:
|
|
@@ -2,19 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from cli.aikit.core.requests import AgentPromptRequest
|
|
9
10
|
from cli.aikit.core.runtime import run_agent_prompt
|
|
11
|
+
from cli.aikit.aliases import setup_alias_path
|
|
10
12
|
from cli.aikit.llm import BACKENDS, configure_backend
|
|
11
|
-
from cli.aikit.mini_brain import DEFAULT_OLLAMA_MODEL
|
|
13
|
+
from cli.aikit.mini_brain import DEFAULT_OLLAMA_MODEL
|
|
12
14
|
from cli.aikit.ollama import ollama_status
|
|
13
15
|
from cli.aikit.onboarding import onboarding_status
|
|
14
16
|
from cli.aikit.personality import load_personality, update_personality
|
|
15
17
|
from cli.aikit.runtime_paths import ROOT
|
|
16
18
|
from cli.aikit.wizard_state import WizardStateError, answer_wizard, cancel_wizard, show_wizard
|
|
17
19
|
|
|
20
|
+
ONBOARDING_MODE_OPTIONS = [
|
|
21
|
+
{
|
|
22
|
+
"id": "minimal",
|
|
23
|
+
"number": "1",
|
|
24
|
+
"label": "minimo",
|
|
25
|
+
"description": "identidade, mini-cerebro local embarcado e memoria",
|
|
26
|
+
"recommended": True,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "complete",
|
|
30
|
+
"number": "2",
|
|
31
|
+
"label": "completo",
|
|
32
|
+
"description": "minimo + toolchain, sources, notificacoes, knowledge e memorias",
|
|
33
|
+
"recommended": False,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "skip",
|
|
37
|
+
"number": "3",
|
|
38
|
+
"label": "pular",
|
|
39
|
+
"description": "",
|
|
40
|
+
"recommended": False,
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
18
44
|
|
|
19
45
|
def maybe_run_interactive_wizard(result: dict[str, Any]) -> dict[str, Any]:
|
|
20
46
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
@@ -104,11 +130,9 @@ def run_interactive_onboarding(result: dict[str, Any]) -> dict[str, Any]:
|
|
|
104
130
|
print("\nOllama nao foi encontrado.")
|
|
105
131
|
if command:
|
|
106
132
|
print(f"Instalacao sugerida: {command}")
|
|
107
|
-
print("
|
|
108
|
-
elif ask_yes_no(f"Deseja
|
|
109
|
-
|
|
110
|
-
setup = setup_mini_brain(yes=True, set_default=set_default)
|
|
111
|
-
print(setup.get("message") or f"Mini cerebro: {setup.get('status')}")
|
|
133
|
+
print("O mini cerebro embarcado ja funciona; instale Ollama apenas se quiser workers locais adicionais.")
|
|
134
|
+
elif ask_yes_no(f"Deseja instalar o modelo Ollama opcional {DEFAULT_OLLAMA_MODEL} para workers locais?", default=False):
|
|
135
|
+
print("Rode: agent local-llm install " + DEFAULT_OLLAMA_MODEL + " --yes")
|
|
112
136
|
|
|
113
137
|
fresh = onboarding_status(ROOT)
|
|
114
138
|
toolchain = fresh.get("toolchain") if isinstance(fresh.get("toolchain"), dict) else {}
|
|
@@ -128,18 +152,148 @@ def run_interactive_onboarding(result: dict[str, Any]) -> dict[str, Any]:
|
|
|
128
152
|
|
|
129
153
|
|
|
130
154
|
def choose_onboarding_mode() -> str:
|
|
155
|
+
selected = choose_onboarding_mode_with_arrows()
|
|
156
|
+
if selected:
|
|
157
|
+
return selected
|
|
158
|
+
print("\nModos de onboarding:")
|
|
159
|
+
for option in ONBOARDING_MODE_OPTIONS:
|
|
160
|
+
print(format_onboarding_option(option, selected=False, include_selector=False))
|
|
161
|
+
answer = ask_text("Escolha o modo:").strip().lower()
|
|
162
|
+
return parse_onboarding_mode_answer(answer)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def choose_onboarding_mode_with_arrows() -> str | None:
|
|
166
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty() or os.environ.get("TERM") == "dumb":
|
|
167
|
+
return None
|
|
168
|
+
try:
|
|
169
|
+
return read_onboarding_mode_selection()
|
|
170
|
+
except KeyboardInterrupt:
|
|
171
|
+
print()
|
|
172
|
+
return "skip"
|
|
173
|
+
except (OSError, ValueError):
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def read_onboarding_mode_selection() -> str:
|
|
178
|
+
selected_index = 0
|
|
179
|
+
typed_answer = ""
|
|
131
180
|
print("\nModos de onboarding:")
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
181
|
+
render_onboarding_options(selected_index)
|
|
182
|
+
print_onboarding_prompt(typed_answer)
|
|
183
|
+
while True:
|
|
184
|
+
key = read_key()
|
|
185
|
+
if key in {"\x03", "\x04"}:
|
|
186
|
+
raise KeyboardInterrupt
|
|
187
|
+
if key in {"\r", "\n"}:
|
|
188
|
+
if typed_answer:
|
|
189
|
+
parsed = parse_onboarding_mode_answer(typed_answer.strip().lower(), default="")
|
|
190
|
+
if parsed:
|
|
191
|
+
return parsed
|
|
192
|
+
typed_answer = ""
|
|
193
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
194
|
+
continue
|
|
195
|
+
return str(ONBOARDING_MODE_OPTIONS[selected_index]["id"])
|
|
196
|
+
if key in {"\x1b[A", "k"}:
|
|
197
|
+
typed_answer = ""
|
|
198
|
+
selected_index = (selected_index - 1) % len(ONBOARDING_MODE_OPTIONS)
|
|
199
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
200
|
+
continue
|
|
201
|
+
if key in {"\x1b[B", "j"}:
|
|
202
|
+
typed_answer = ""
|
|
203
|
+
selected_index = (selected_index + 1) % len(ONBOARDING_MODE_OPTIONS)
|
|
204
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
205
|
+
continue
|
|
206
|
+
if key in {"\x7f", "\b"}:
|
|
207
|
+
typed_answer = typed_answer[:-1]
|
|
208
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
209
|
+
continue
|
|
210
|
+
parsed = parse_onboarding_mode_answer(key.strip().lower(), default="")
|
|
211
|
+
if parsed:
|
|
212
|
+
print()
|
|
213
|
+
return parsed
|
|
214
|
+
if key.isprintable():
|
|
215
|
+
typed_answer += key
|
|
216
|
+
rerender_onboarding_options(selected_index, typed_answer)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def render_onboarding_options(selected_index: int) -> None:
|
|
220
|
+
for index, option in enumerate(ONBOARDING_MODE_OPTIONS):
|
|
221
|
+
print(format_onboarding_option(option, selected=index == selected_index, include_selector=True))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def rerender_onboarding_options(selected_index: int, typed_answer: str) -> None:
|
|
225
|
+
lines_to_move = len(ONBOARDING_MODE_OPTIONS) + 1
|
|
226
|
+
sys.stdout.write(f"\x1b[{lines_to_move}A")
|
|
227
|
+
for index, option in enumerate(ONBOARDING_MODE_OPTIONS):
|
|
228
|
+
sys.stdout.write("\x1b[2K")
|
|
229
|
+
sys.stdout.write(format_onboarding_option(option, selected=index == selected_index, include_selector=True) + "\n")
|
|
230
|
+
sys.stdout.write("\x1b[2K")
|
|
231
|
+
sys.stdout.write(onboarding_prompt_line(typed_answer) + "\n")
|
|
232
|
+
sys.stdout.flush()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def format_onboarding_option(option: dict[str, Any], *, selected: bool, include_selector: bool) -> str:
|
|
236
|
+
selector = "> " if selected else " "
|
|
237
|
+
prefix = selector if include_selector else ""
|
|
238
|
+
description = f": {option['description']}" if option.get("description") else ""
|
|
239
|
+
recommended = " (Recomendado)" if option.get("recommended") else ""
|
|
240
|
+
return f"{prefix}{option['number']}. {option['label']}{description}{recommended}"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def parse_onboarding_mode_answer(answer: str, *, default: str = "minimal") -> str:
|
|
244
|
+
if not answer:
|
|
245
|
+
return default
|
|
136
246
|
if answer in {"1", "minimo", "mínimo", "minimal"}:
|
|
137
247
|
return "minimal"
|
|
138
248
|
if answer in {"2", "completo", "complete", "full"}:
|
|
139
249
|
return "complete"
|
|
140
250
|
if answer in {"3", "pular", "skip", "cancelar", "cancel"}:
|
|
141
251
|
return "skip"
|
|
142
|
-
return
|
|
252
|
+
return default
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def print_onboarding_prompt(typed_answer: str) -> None:
|
|
256
|
+
print(onboarding_prompt_line(typed_answer))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def onboarding_prompt_line(typed_answer: str) -> str:
|
|
260
|
+
suffix = f" {typed_answer}" if typed_answer else ""
|
|
261
|
+
return f"Escolha o modo:{suffix}"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def read_key() -> str:
|
|
265
|
+
if os.name == "nt":
|
|
266
|
+
import msvcrt
|
|
267
|
+
|
|
268
|
+
char = msvcrt.getwch()
|
|
269
|
+
if char in {"\x00", "\xe0"}:
|
|
270
|
+
code = msvcrt.getwch()
|
|
271
|
+
if code == "H":
|
|
272
|
+
return "\x1b[A"
|
|
273
|
+
if code == "P":
|
|
274
|
+
return "\x1b[B"
|
|
275
|
+
return char
|
|
276
|
+
|
|
277
|
+
import termios
|
|
278
|
+
import tty
|
|
279
|
+
import select
|
|
280
|
+
|
|
281
|
+
fd = sys.stdin.fileno()
|
|
282
|
+
old_settings = termios.tcgetattr(fd)
|
|
283
|
+
try:
|
|
284
|
+
tty.setraw(fd)
|
|
285
|
+
char = sys.stdin.read(1)
|
|
286
|
+
if char == "\x1b":
|
|
287
|
+
sequence = ""
|
|
288
|
+
for _ in range(2):
|
|
289
|
+
ready, _, _ = select.select([sys.stdin], [], [], 0.01)
|
|
290
|
+
if not ready:
|
|
291
|
+
break
|
|
292
|
+
sequence += sys.stdin.read(1)
|
|
293
|
+
char += sequence
|
|
294
|
+
return char
|
|
295
|
+
finally:
|
|
296
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
143
297
|
|
|
144
298
|
|
|
145
299
|
def configure_personality_interactively(agent: dict[str, Any]) -> None:
|
|
@@ -151,17 +305,31 @@ def configure_personality_interactively(agent: dict[str, Any]) -> None:
|
|
|
151
305
|
language = ask_text("Idioma padrao das respostas?", default=str(agent.get("language") or "pt-BR"))
|
|
152
306
|
tone = ask_text("Tom das respostas?", default=current_tone)
|
|
153
307
|
detail_level = ask_text("Nivel de detalhe?", default=current_detail)
|
|
154
|
-
update_personality(
|
|
308
|
+
payload = update_personality(
|
|
155
309
|
agent_name=agent_name,
|
|
156
310
|
user_name=user_name,
|
|
157
311
|
language=language,
|
|
158
312
|
tone=tone,
|
|
159
313
|
detail_level=detail_level,
|
|
160
314
|
)
|
|
315
|
+
alias = payload.get("alias") if isinstance(payload.get("alias"), dict) else None
|
|
316
|
+
if not alias:
|
|
317
|
+
return
|
|
318
|
+
if alias.get("status") == "added":
|
|
319
|
+
print(f"Comando local criado: {alias.get('name')}")
|
|
320
|
+
elif alias.get("message"):
|
|
321
|
+
print(f"Alias nao configurado automaticamente: {alias['message']}")
|
|
322
|
+
path_status = alias.get("path_status") if isinstance(alias.get("path_status"), dict) else {}
|
|
323
|
+
if path_status.get("setup_required"):
|
|
324
|
+
bin_dir = path_status.get("bin_dir")
|
|
325
|
+
print(f"O comando foi criado em {bin_dir}, mas essa pasta ainda nao esta no PATH.")
|
|
326
|
+
if ask_yes_no("Deseja habilitar aliases do Agent DevKit no shell para proximas sessoes?", default=True):
|
|
327
|
+
setup = setup_alias_path(yes=True)
|
|
328
|
+
print(setup.get("message") or "PATH de aliases atualizado.")
|
|
161
329
|
|
|
162
330
|
|
|
163
331
|
def configure_llm_interactively() -> None:
|
|
164
|
-
print("\nNenhum backend LLM coordenador utilizavel foi detectado.")
|
|
332
|
+
print("\nNenhum backend LLM coordenador externo utilizavel foi detectado.")
|
|
165
333
|
print("Opcoes: claude-code, codex-cli, ollama, openai, anthropic, openrouter, pular")
|
|
166
334
|
choice = ask_text("Qual backend deseja configurar primeiro?", default="pular").strip().lower()
|
|
167
335
|
if choice in {"", "pular", "skip", "cancelar", "cancel"}:
|