agent-devkit 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ """Ollama local LLM discovery and model management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+ import urllib.error
10
+ import urllib.request
11
+ from typing import Any
12
+
13
+
14
+ OLLAMA_TIMEOUT_SECONDS = 120
15
+ DEFAULT_BASE_URL = "http://localhost:11434"
16
+
17
+
18
+ def ollama_status(*, base_url: str = DEFAULT_BASE_URL) -> dict[str, Any]:
19
+ binary = shutil.which("ollama")
20
+ version = command_output(["ollama", "--version"]) if binary else None
21
+ daemon = daemon_status(base_url) if binary else {"status": "unknown", "message": "Ollama binary is not installed."}
22
+ models = list_local_models(binary_available=bool(binary))
23
+ status = "ok" if binary else "missing"
24
+ return {
25
+ "kind": "ollama-status",
26
+ "status": status,
27
+ "binary": binary,
28
+ "version": first_line(version),
29
+ "base_url": base_url,
30
+ "daemon": daemon,
31
+ "models": models,
32
+ "model_count": len(models),
33
+ "install_plan": install_plan() if not binary else None,
34
+ }
35
+
36
+
37
+ def ollama_models() -> dict[str, Any]:
38
+ binary = shutil.which("ollama")
39
+ items = list_local_models(binary_available=bool(binary))
40
+ return {
41
+ "kind": "ollama-models",
42
+ "status": "ok" if binary else "missing",
43
+ "binary": binary,
44
+ "items": items,
45
+ "recommended": recommended_models(installed={item["name"] for item in items}),
46
+ }
47
+
48
+
49
+ def ollama_pull(model: str | None, *, yes: bool = False, dry_run: bool = False) -> dict[str, Any]:
50
+ if not model:
51
+ raise ValueError("ollama pull requires a model name")
52
+ binary = shutil.which("ollama")
53
+ command = ["ollama", "pull", model]
54
+ if dry_run or not yes:
55
+ return {
56
+ "kind": "ollama-pull",
57
+ "status": "planned" if dry_run else "needs-confirmation",
58
+ "ok": bool(dry_run),
59
+ "model": model,
60
+ "binary": binary,
61
+ "command": command,
62
+ "dry_run": dry_run,
63
+ "yes": yes,
64
+ "message": "Use --yes to pull the model." if not dry_run and not yes else "Dry-run only.",
65
+ }
66
+ if not binary:
67
+ return {
68
+ "kind": "ollama-pull",
69
+ "status": "missing",
70
+ "ok": False,
71
+ "model": model,
72
+ "command": command,
73
+ "message": "Ollama binary not found in PATH.",
74
+ "install_plan": install_plan(),
75
+ "exit_code": 2,
76
+ }
77
+ process = run_command(command, timeout=OLLAMA_TIMEOUT_SECONDS * 5)
78
+ return {
79
+ "kind": "ollama-pull",
80
+ "status": "ok" if process.returncode == 0 else "failed",
81
+ "ok": process.returncode == 0,
82
+ "model": model,
83
+ "binary": binary,
84
+ "command": command,
85
+ "exit_code": process.returncode,
86
+ "stdout": safe_tail(process.stdout),
87
+ "stderr": safe_tail(process.stderr),
88
+ }
89
+
90
+
91
+ def ollama_update(*, yes: bool = False, dry_run: bool = False) -> dict[str, Any]:
92
+ command = update_command()
93
+ if dry_run or not yes:
94
+ return {
95
+ "kind": "ollama-update",
96
+ "status": "planned" if dry_run else "needs-confirmation",
97
+ "ok": bool(dry_run),
98
+ "command": command,
99
+ "dry_run": dry_run,
100
+ "yes": yes,
101
+ "message": "Use --yes to run the update command." if not dry_run and not yes else "Dry-run only.",
102
+ }
103
+ process = run_command(command, timeout=OLLAMA_TIMEOUT_SECONDS * 5, shell=True)
104
+ return {
105
+ "kind": "ollama-update",
106
+ "status": "ok" if process.returncode == 0 else "failed",
107
+ "ok": process.returncode == 0,
108
+ "command": command,
109
+ "exit_code": process.returncode,
110
+ "stdout": safe_tail(process.stdout),
111
+ "stderr": safe_tail(process.stderr),
112
+ }
113
+
114
+
115
+ def list_local_models(*, binary_available: bool) -> list[dict[str, Any]]:
116
+ if not binary_available:
117
+ return []
118
+ output = command_output(["ollama", "list"])
119
+ if not output:
120
+ return []
121
+ return parse_ollama_list(output)
122
+
123
+
124
+ def parse_ollama_list(output: str) -> list[dict[str, Any]]:
125
+ items: list[dict[str, Any]] = []
126
+ for line in output.splitlines():
127
+ stripped = line.strip()
128
+ if not stripped or stripped.lower().startswith("name "):
129
+ continue
130
+ parts = stripped.split()
131
+ if not parts:
132
+ continue
133
+ items.append(
134
+ {
135
+ "name": parts[0],
136
+ "id": parts[1] if len(parts) > 1 else None,
137
+ "size": parts[2] if len(parts) > 2 else None,
138
+ "modified": " ".join(parts[3:]) if len(parts) > 3 else None,
139
+ }
140
+ )
141
+ return items
142
+
143
+
144
+ def recommended_models(*, installed: set[str]) -> list[dict[str, Any]]:
145
+ catalog = [
146
+ ("qwen2.5-coder", "coding", "operational code reading and generation"),
147
+ ("deepseek-coder", "coding", "code analysis and mechanical refactors"),
148
+ ("deepseek-r1", "reasoning", "local reasoning drafts with mandatory review"),
149
+ ("llama3.2", "general", "general local summaries and classification"),
150
+ ("mistral", "general", "lightweight operational summaries"),
151
+ ("gemma", "classification", "small extraction and classification tasks"),
152
+ ]
153
+ return [
154
+ {
155
+ "name": name,
156
+ "family": family,
157
+ "recommended_for": recommended_for,
158
+ "installed": any(item == name or item.startswith(f"{name}:") for item in installed),
159
+ }
160
+ for name, family, recommended_for in catalog
161
+ ]
162
+
163
+
164
+ def daemon_status(base_url: str) -> dict[str, Any]:
165
+ url = base_url.rstrip("/") + "/api/tags"
166
+ try:
167
+ with urllib.request.urlopen(url, timeout=2) as response: # noqa: S310 - local configurable daemon URL.
168
+ raw = response.read().decode("utf-8", errors="replace")
169
+ except urllib.error.URLError as exc:
170
+ return {"status": "unavailable", "message": str(exc.reason)}
171
+ try:
172
+ payload = json.loads(raw)
173
+ except json.JSONDecodeError:
174
+ return {"status": "unknown", "message": "Ollama daemon returned non-JSON response."}
175
+ return {"status": "ok", "models": len(payload.get("models") or []) if isinstance(payload, dict) else None}
176
+
177
+
178
+ def install_plan() -> dict[str, Any]:
179
+ system = platform.system().lower()
180
+ if system == "darwin":
181
+ command = "brew install ollama"
182
+ elif system.startswith("win"):
183
+ command = "winget install Ollama.Ollama"
184
+ else:
185
+ command = "curl -fsSL https://ollama.com/install.sh | sh"
186
+ return {"platform": platform_key(), "command": command, "requires_opt_in": True}
187
+
188
+
189
+ def update_command() -> str:
190
+ system = platform.system().lower()
191
+ if system == "darwin":
192
+ return "brew upgrade ollama"
193
+ if system.startswith("win"):
194
+ return "winget upgrade Ollama.Ollama"
195
+ return "curl -fsSL https://ollama.com/install.sh | sh"
196
+
197
+
198
+ def platform_key() -> str:
199
+ system = platform.system().lower()
200
+ if system == "darwin":
201
+ return "darwin"
202
+ if system.startswith("win"):
203
+ return "windows"
204
+ return "linux"
205
+
206
+
207
+ def command_output(command: list[str]) -> str | None:
208
+ try:
209
+ process = run_command(command, timeout=10)
210
+ except OSError:
211
+ return None
212
+ if process.returncode != 0:
213
+ return None
214
+ return process.stdout.strip() or process.stderr.strip()
215
+
216
+
217
+ def run_command(command: list[str] | str, *, timeout: int, shell: bool = False) -> subprocess.CompletedProcess[str]:
218
+ return subprocess.run(
219
+ command,
220
+ shell=shell,
221
+ check=False,
222
+ text=True,
223
+ stdout=subprocess.PIPE,
224
+ stderr=subprocess.PIPE,
225
+ timeout=timeout,
226
+ )
227
+
228
+
229
+ def safe_tail(value: str | None, *, limit: int = 4000) -> str:
230
+ text = value or ""
231
+ return text[-limit:]
232
+
233
+
234
+ def first_line(value: str | None) -> str | None:
235
+ if not value:
236
+ return None
237
+ return value.splitlines()[0]
@@ -0,0 +1,19 @@
1
+ """Backward-compatible provider wizard wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from cli.aikit.configuration_orchestrator import provider_setup_wizard
9
+
10
+
11
+ def missing_source_wizard(prompt: str, route: dict[str, Any], *, root: Path | None = None) -> dict[str, Any]:
12
+ provider = str(route.get("provider") or "")
13
+ return provider_setup_wizard(
14
+ root or Path(__file__).resolve().parents[2],
15
+ provider,
16
+ prompt=prompt,
17
+ route=route,
18
+ reason="No reusable source is configured for this routed prompt.",
19
+ )
@@ -0,0 +1,40 @@
1
+ """Mandatory review gate decisions before completing agentic work."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+
9
+ REVIEW_REQUIRED_PATTERN = re.compile(
10
+ r"(?i)\b(c[oó]digo|software|documento|spec|especifica|requisit|plano|automac|pr|pull request|deploy|infra|sql|banco|seguran)\b"
11
+ )
12
+
13
+
14
+ def build_review_gate(prompt: str, *, route: dict[str, Any] | None = None, model_plan: dict[str, Any] | None = None) -> dict[str, Any]:
15
+ required = bool(REVIEW_REQUIRED_PATTERN.search(prompt))
16
+ if route:
17
+ required = True
18
+ if model_plan and (model_plan.get("local_llm_selected") or model_plan.get("local_llm_recommended")):
19
+ required = True
20
+ return {
21
+ "kind": "review-gate",
22
+ "required": required,
23
+ "status": "pending" if required else "not-required",
24
+ "preferred_reviewers": ["claude-code", "codex-cli"],
25
+ "reason": (
26
+ "Deliverable or delegated local-LLM work requires coordinator review before completion."
27
+ if required
28
+ else "Prompt does not require a formal review gate."
29
+ ),
30
+ "route": route,
31
+ }
32
+
33
+
34
+ def mark_reviewed(payload: dict[str, Any], *, reviewer: str | None = None, notes: str | None = None) -> dict[str, Any]:
35
+ gate = dict(payload)
36
+ if gate.get("required"):
37
+ gate["status"] = "reviewed"
38
+ gate["reviewer"] = reviewer or "coordinator"
39
+ gate["notes"] = notes or "Reviewed by the active coordinator before final response."
40
+ return gate