kaax-mcp 0.1.10 → 0.1.11
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/dist/index.js +1 -1
- package/dist/python.d.ts +52 -0
- package/dist/python.js +152 -0
- package/dist/python.js.map +1 -0
- package/dist/tools.js +237 -0
- package/dist/tools.js.map +1 -1
- package/dist/training.d.ts +114 -0
- package/dist/training.js +426 -0
- package/dist/training.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import { buildResources } from "./resources.js";
|
|
|
30
30
|
import { buildPrompts } from "./prompts.js";
|
|
31
31
|
import { runInit } from "./init.js";
|
|
32
32
|
const SERVER_NAME = "kaax-mcp";
|
|
33
|
-
const SERVER_VERSION = "0.1.
|
|
33
|
+
const SERVER_VERSION = "0.1.11";
|
|
34
34
|
function bail(msg) {
|
|
35
35
|
// stderr only — MCP clients pipe stdout for protocol traffic.
|
|
36
36
|
process.stderr.write(`[${SERVER_NAME}] ${msg}\n`);
|
package/dist/python.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python + ultralytics bridge — detect what the user already has so YOLO
|
|
3
|
+
* training can be driven locally. Same philosophy as gdal.ts: we never
|
|
4
|
+
* bundle multi-hundred-MB native deps, we probe for them on PATH and
|
|
5
|
+
* give per-OS install hints when missing.
|
|
6
|
+
*
|
|
7
|
+
* What we look for:
|
|
8
|
+
* - python3 / python (in that order — POSIX prefers `python3`, Windows
|
|
9
|
+
* often only has `python` on PATH).
|
|
10
|
+
* - pip (via `python -m pip --version`).
|
|
11
|
+
* - ultralytics package (via `python -c "import ultralytics"`).
|
|
12
|
+
* - CUDA availability through PyTorch (the user might have ultralytics
|
|
13
|
+
* on CPU only — training a YOLOv8/11 on CPU is 30-100× slower, so
|
|
14
|
+
* it's worth surfacing).
|
|
15
|
+
*
|
|
16
|
+
* Security:
|
|
17
|
+
* - execFile only (no shell). User-controlled paths never enter argv.
|
|
18
|
+
* - 5-15 s timeouts on every probe.
|
|
19
|
+
* - We treat `python` invocations the same as any other PATH binary;
|
|
20
|
+
* the user's local PATH is the trust boundary.
|
|
21
|
+
*/
|
|
22
|
+
export interface ToolPresence {
|
|
23
|
+
available: boolean;
|
|
24
|
+
command?: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CudaInfo {
|
|
28
|
+
available: boolean;
|
|
29
|
+
deviceName?: string;
|
|
30
|
+
deviceCount?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface PythonCheckResult {
|
|
33
|
+
/** True iff python + ultralytics are both reachable (the minimum needed to train). */
|
|
34
|
+
ready: boolean;
|
|
35
|
+
python: ToolPresence;
|
|
36
|
+
pip: ToolPresence;
|
|
37
|
+
ultralytics: ToolPresence;
|
|
38
|
+
/** Undefined if we can't determine (ultralytics or torch missing). */
|
|
39
|
+
cuda?: CudaInfo;
|
|
40
|
+
/** Per-OS install instructions per dependency. */
|
|
41
|
+
installHints: {
|
|
42
|
+
python: string;
|
|
43
|
+
ultralytics: string;
|
|
44
|
+
cuda: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Probe the user's PATH for a working Python + ultralytics setup.
|
|
49
|
+
* Order: python3 → python (Windows fallback). Each subsequent probe
|
|
50
|
+
* uses the python we found; we don't mix interpreters.
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkTrainingEnvironment(): Promise<PythonCheckResult>;
|
package/dist/python.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python + ultralytics bridge — detect what the user already has so YOLO
|
|
3
|
+
* training can be driven locally. Same philosophy as gdal.ts: we never
|
|
4
|
+
* bundle multi-hundred-MB native deps, we probe for them on PATH and
|
|
5
|
+
* give per-OS install hints when missing.
|
|
6
|
+
*
|
|
7
|
+
* What we look for:
|
|
8
|
+
* - python3 / python (in that order — POSIX prefers `python3`, Windows
|
|
9
|
+
* often only has `python` on PATH).
|
|
10
|
+
* - pip (via `python -m pip --version`).
|
|
11
|
+
* - ultralytics package (via `python -c "import ultralytics"`).
|
|
12
|
+
* - CUDA availability through PyTorch (the user might have ultralytics
|
|
13
|
+
* on CPU only — training a YOLOv8/11 on CPU is 30-100× slower, so
|
|
14
|
+
* it's worth surfacing).
|
|
15
|
+
*
|
|
16
|
+
* Security:
|
|
17
|
+
* - execFile only (no shell). User-controlled paths never enter argv.
|
|
18
|
+
* - 5-15 s timeouts on every probe.
|
|
19
|
+
* - We treat `python` invocations the same as any other PATH binary;
|
|
20
|
+
* the user's local PATH is the trust boundary.
|
|
21
|
+
*/
|
|
22
|
+
import { execFile } from "child_process";
|
|
23
|
+
import { promisify } from "util";
|
|
24
|
+
const execFileP = promisify(execFile);
|
|
25
|
+
const TIMEOUTS = {
|
|
26
|
+
version: 5_000,
|
|
27
|
+
import: 15_000, // importing ultralytics is slow first time (it pulls in torch)
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Probe the user's PATH for a working Python + ultralytics setup.
|
|
31
|
+
* Order: python3 → python (Windows fallback). Each subsequent probe
|
|
32
|
+
* uses the python we found; we don't mix interpreters.
|
|
33
|
+
*/
|
|
34
|
+
export async function checkTrainingEnvironment() {
|
|
35
|
+
const result = {
|
|
36
|
+
ready: false,
|
|
37
|
+
python: { available: false },
|
|
38
|
+
pip: { available: false },
|
|
39
|
+
ultralytics: { available: false },
|
|
40
|
+
installHints: {
|
|
41
|
+
python: pythonInstallHint(),
|
|
42
|
+
ultralytics: "Once you have Python on PATH:\n python -m pip install ultralytics\n(adds torch + everything else needed for YOLOv8 / v11 training.)",
|
|
43
|
+
cuda: "For GPU acceleration: install CUDA 11.8+ and the matching PyTorch from\n https://pytorch.org/get-started/locally/\nIf you skip this, training falls back to CPU — works, but 30-100× slower on real datasets.",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
// 1. Find a Python. Order:
|
|
47
|
+
// - python3: POSIX standard
|
|
48
|
+
// - python : Linux fallback, also Windows when properly installed
|
|
49
|
+
// - py : Windows Python Launcher (`py.exe`), the only command
|
|
50
|
+
// that works on a fresh Windows install with Python from
|
|
51
|
+
// python.org if 'Add to PATH' was unchecked.
|
|
52
|
+
for (const cmd of ["python3", "python", "py"]) {
|
|
53
|
+
try {
|
|
54
|
+
const { stdout, stderr } = await execFileP(cmd, ["--version"], {
|
|
55
|
+
timeout: TIMEOUTS.version,
|
|
56
|
+
});
|
|
57
|
+
// Python prints to stderr on some versions, stdout on others. On
|
|
58
|
+
// Windows, an un-installed `python` resolves to the Microsoft
|
|
59
|
+
// Store app-execution stub, which exits non-zero — that throws
|
|
60
|
+
// and falls through to the next candidate.
|
|
61
|
+
const line = (stdout || stderr).trim().split("\n")[0];
|
|
62
|
+
if (line.toLowerCase().startsWith("python")) {
|
|
63
|
+
result.python = { available: true, command: cmd, version: line };
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* try next */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!result.python.available)
|
|
72
|
+
return result;
|
|
73
|
+
// 2. pip via `python -m pip`. Robust to setups where `pip` itself isn't
|
|
74
|
+
// on PATH but is invokable as a module.
|
|
75
|
+
try {
|
|
76
|
+
const { stdout } = await execFileP(result.python.command, ["-m", "pip", "--version"], { timeout: TIMEOUTS.version });
|
|
77
|
+
result.pip = {
|
|
78
|
+
available: true,
|
|
79
|
+
command: `${result.python.command} -m pip`,
|
|
80
|
+
version: stdout.trim().split("\n")[0],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* pip missing — keep going, we still want to report what we have */
|
|
85
|
+
}
|
|
86
|
+
// 3. ultralytics — single-shot import. First run is slow (~10 s on a
|
|
87
|
+
// cold install) because it pulls torch into the namespace.
|
|
88
|
+
try {
|
|
89
|
+
const { stdout } = await execFileP(result.python.command, [
|
|
90
|
+
"-c",
|
|
91
|
+
"import ultralytics, sys; sys.stdout.write(ultralytics.__version__)",
|
|
92
|
+
], { timeout: TIMEOUTS.import });
|
|
93
|
+
result.ultralytics = {
|
|
94
|
+
available: true,
|
|
95
|
+
version: stdout.trim(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* not installed */
|
|
100
|
+
}
|
|
101
|
+
if (result.ultralytics.available) {
|
|
102
|
+
// 4. CUDA via torch. We don't probe torch separately — it's a
|
|
103
|
+
// transitive dep of ultralytics, so if ultralytics imports OK
|
|
104
|
+
// torch is necessarily there.
|
|
105
|
+
try {
|
|
106
|
+
const probe = [
|
|
107
|
+
"import torch, sys",
|
|
108
|
+
"ok = torch.cuda.is_available()",
|
|
109
|
+
"name = torch.cuda.get_device_name(0) if ok else ''",
|
|
110
|
+
"count = torch.cuda.device_count() if ok else 0",
|
|
111
|
+
"sys.stdout.write(str(ok) + chr(10) + name + chr(10) + str(count))",
|
|
112
|
+
].join("; ");
|
|
113
|
+
const { stdout } = await execFileP(result.python.command, ["-c", probe], { timeout: TIMEOUTS.import });
|
|
114
|
+
const lines = stdout.split("\n");
|
|
115
|
+
const ok = lines[0]?.trim() === "True";
|
|
116
|
+
result.cuda = {
|
|
117
|
+
available: ok,
|
|
118
|
+
deviceName: ok ? lines[1]?.trim() || undefined : undefined,
|
|
119
|
+
deviceCount: ok ? Number(lines[2]?.trim() ?? "0") : 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* torch query failed; leave cuda undefined */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
result.ready = result.python.available && result.ultralytics.available;
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
function pythonInstallHint() {
|
|
130
|
+
const os = process.platform;
|
|
131
|
+
if (os === "win32") {
|
|
132
|
+
return [
|
|
133
|
+
"Windows install: download Python 3.10+ from https://www.python.org/downloads/",
|
|
134
|
+
"Tick 'Add python.exe to PATH' on the first install screen.",
|
|
135
|
+
"After install, open a new PowerShell so the PATH refreshes, then re-run kaax_check_training_environment.",
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
if (os === "darwin") {
|
|
139
|
+
return [
|
|
140
|
+
"macOS install: with Homebrew (https://brew.sh/):",
|
|
141
|
+
" brew install python",
|
|
142
|
+
"Restart your shell after install.",
|
|
143
|
+
].join("\n");
|
|
144
|
+
}
|
|
145
|
+
return [
|
|
146
|
+
"Linux install:",
|
|
147
|
+
" Debian / Ubuntu: sudo apt install python3 python3-pip python3-venv",
|
|
148
|
+
" Fedora: sudo dnf install python3 python3-pip",
|
|
149
|
+
" Arch: sudo pacman -S python python-pip",
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=python.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"python.js","sourceRoot":"","sources":["../src/python.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.js
CHANGED
|
@@ -17,6 +17,8 @@ import { addIdField, bufferLayer, clipByMask, differenceLayers, extractFeaturesI
|
|
|
17
17
|
import { clusterPointsByRow, detectRowOrientation, generateRowLines, } from "./gis-rows.js";
|
|
18
18
|
import { defaultLayerName, exportLayersToExcel, generateMarkdownSummary, } from "./gis-reports.js";
|
|
19
19
|
import { checkGdal, generateGeoPdf } from "./gdal.js";
|
|
20
|
+
import { checkTrainingEnvironment } from "./python.js";
|
|
21
|
+
import { getTrainingStatus, listTrainings, startTraining, validateDataset, } from "./training.js";
|
|
20
22
|
import { existsSync } from "fs";
|
|
21
23
|
import { mkdir } from "fs/promises";
|
|
22
24
|
import { homedir } from "os";
|
|
@@ -411,6 +413,48 @@ const SummaryReportArgs = z.object({
|
|
|
411
413
|
title: z.string().optional(),
|
|
412
414
|
});
|
|
413
415
|
const CheckGdalArgs = z.object({});
|
|
416
|
+
const CheckTrainingEnvArgs = z.object({});
|
|
417
|
+
const ValidateDatasetArgs = z.object({
|
|
418
|
+
path: z
|
|
419
|
+
.string()
|
|
420
|
+
.describe("Path to data.yaml OR to a directory that contains it. The tool will resolve the train/val image and label subdirectories from the YAML."),
|
|
421
|
+
});
|
|
422
|
+
const StartTrainingArgs = z.object({
|
|
423
|
+
dataYaml: z
|
|
424
|
+
.string()
|
|
425
|
+
.describe("Path to the dataset's data.yaml (resolve with kaax_validate_training_dataset first)."),
|
|
426
|
+
pythonCommand: z
|
|
427
|
+
.string()
|
|
428
|
+
.optional()
|
|
429
|
+
.describe("Python command to use. Defaults to whatever kaax_check_training_environment found."),
|
|
430
|
+
model: z
|
|
431
|
+
.string()
|
|
432
|
+
.optional()
|
|
433
|
+
.describe("Base weights — `yolo11n.pt` (default), `yolo11s.pt`, `yolo11m.pt`, etc."),
|
|
434
|
+
epochs: z.number().int().min(1).max(2000).optional().describe("Default 100."),
|
|
435
|
+
imgsz: z.number().int().min(64).optional().describe("Image size, default 640."),
|
|
436
|
+
batch: z
|
|
437
|
+
.number()
|
|
438
|
+
.int()
|
|
439
|
+
.optional()
|
|
440
|
+
.describe("Batch size. Default = auto (ultralytics' -1)."),
|
|
441
|
+
project: z
|
|
442
|
+
.string()
|
|
443
|
+
.optional()
|
|
444
|
+
.describe("YOLO output project dir. Default 'runs/detect'."),
|
|
445
|
+
name: z
|
|
446
|
+
.string()
|
|
447
|
+
.optional()
|
|
448
|
+
.describe("Run name under the project dir. Default 'kaax-mcp'."),
|
|
449
|
+
extraYoloArgs: z
|
|
450
|
+
.array(z.string())
|
|
451
|
+
.optional()
|
|
452
|
+
.describe("Optional extra args forwarded verbatim to the YOLO Python API. Refused if they contain shell metacharacters."),
|
|
453
|
+
});
|
|
454
|
+
const CheckTrainingStatusArgs = z.object({
|
|
455
|
+
trainingId: z.string().min(8).describe("ID returned by kaax_start_yolo_training"),
|
|
456
|
+
});
|
|
457
|
+
const ListTrainingsArgs = z.object({});
|
|
414
458
|
const GenerateGeoPdfArgs = z.object({
|
|
415
459
|
layers: z
|
|
416
460
|
.array(z.object({
|
|
@@ -1586,6 +1630,199 @@ export function buildTools() {
|
|
|
1586
1630
|
}
|
|
1587
1631
|
},
|
|
1588
1632
|
},
|
|
1633
|
+
// ── Local YOLO training — environment detection ──────────────────
|
|
1634
|
+
{
|
|
1635
|
+
name: "kaax_check_training_environment",
|
|
1636
|
+
description: "[Free] Probe the user's machine for the Python + ultralytics + CUDA stack needed to train a YOLO model locally. Reports what's present (Python version, pip, ultralytics version, GPU name if available) and provides per-OS install commands for whatever is missing. Call this BEFORE any training tool so the user knows what to install. The training itself runs entirely on the user's machine — Kaax's server never sees the dataset.",
|
|
1637
|
+
inputSchema: zodToJsonSchema(CheckTrainingEnvArgs),
|
|
1638
|
+
async handler() {
|
|
1639
|
+
try {
|
|
1640
|
+
const r = await checkTrainingEnvironment();
|
|
1641
|
+
const lines = [];
|
|
1642
|
+
if (r.ready) {
|
|
1643
|
+
lines.push("✅ Ready to train YOLO locally.");
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
lines.push("⚠️ Training environment is not complete yet.");
|
|
1647
|
+
}
|
|
1648
|
+
lines.push("");
|
|
1649
|
+
lines.push(` • Python: ${r.python.available ? `${r.python.version} (${r.python.command})` : "❌ not on PATH"}`);
|
|
1650
|
+
lines.push(` • pip: ${r.pip.available ? r.pip.version : "❌ not invokable"}`);
|
|
1651
|
+
lines.push(` • ultralytics: ${r.ultralytics.available ? r.ultralytics.version : "❌ not installed"}`);
|
|
1652
|
+
if (r.cuda) {
|
|
1653
|
+
lines.push(` • CUDA / GPU: ${r.cuda.available
|
|
1654
|
+
? `✅ ${r.cuda.deviceName ?? "(unnamed GPU)"} · ${r.cuda.deviceCount ?? 1} device(s)`
|
|
1655
|
+
: "❌ unavailable — training will run on CPU (30-100× slower)"}`);
|
|
1656
|
+
}
|
|
1657
|
+
else if (r.ultralytics.available) {
|
|
1658
|
+
lines.push(` • CUDA / GPU: ⚠️ couldn't query torch`);
|
|
1659
|
+
}
|
|
1660
|
+
lines.push("");
|
|
1661
|
+
// Actionable install hints when something's missing.
|
|
1662
|
+
if (!r.python.available) {
|
|
1663
|
+
lines.push("📦 To install Python:");
|
|
1664
|
+
lines.push(r.installHints.python.split("\n").map((s) => ` ${s}`).join("\n"));
|
|
1665
|
+
lines.push("");
|
|
1666
|
+
}
|
|
1667
|
+
if (r.python.available && !r.ultralytics.available) {
|
|
1668
|
+
lines.push("📦 To install ultralytics (YOLO):");
|
|
1669
|
+
lines.push(r.installHints.ultralytics.split("\n").map((s) => ` ${s}`).join("\n"));
|
|
1670
|
+
lines.push("");
|
|
1671
|
+
}
|
|
1672
|
+
if (r.ready && r.cuda && !r.cuda.available) {
|
|
1673
|
+
lines.push("💡 Optional — to enable GPU training:");
|
|
1674
|
+
lines.push(r.installHints.cuda.split("\n").map((s) => ` ${s}`).join("\n"));
|
|
1675
|
+
lines.push("");
|
|
1676
|
+
}
|
|
1677
|
+
if (r.ready) {
|
|
1678
|
+
lines.push("Next: kaax_validate_training_dataset (Phase 2B) once it ships — for now, this tool tells you the env is ready and you can run `yolo train …` by hand.");
|
|
1679
|
+
}
|
|
1680
|
+
return text(...lines);
|
|
1681
|
+
}
|
|
1682
|
+
catch (err) {
|
|
1683
|
+
return errorText(err);
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
name: "kaax_validate_training_dataset",
|
|
1689
|
+
description: "[Free] Sanity-check a local YOLO dataset before training. Resolves data.yaml + train/val image and label dirs, counts files, lists class names. Reports issues (missing dirs, no val set, missing names) so the user fixes them BEFORE wasting GPU hours on a broken dataset.",
|
|
1690
|
+
inputSchema: zodToJsonSchema(ValidateDatasetArgs),
|
|
1691
|
+
async handler(args) {
|
|
1692
|
+
try {
|
|
1693
|
+
const { path } = ValidateDatasetArgs.parse(args);
|
|
1694
|
+
const r = await validateDataset(path);
|
|
1695
|
+
const lines = [];
|
|
1696
|
+
if (r.ok) {
|
|
1697
|
+
lines.push("✅ Dataset looks ready to train.");
|
|
1698
|
+
}
|
|
1699
|
+
else {
|
|
1700
|
+
lines.push("⚠️ Dataset has issues — fix them before training.");
|
|
1701
|
+
}
|
|
1702
|
+
lines.push("");
|
|
1703
|
+
if (r.dataYamlPath)
|
|
1704
|
+
lines.push(` data.yaml: ${r.dataYamlPath}`);
|
|
1705
|
+
if (r.classes && r.classes.length > 0) {
|
|
1706
|
+
lines.push(` Classes (${r.classes.length}): ${r.classes.slice(0, 10).join(", ")}${r.classes.length > 10 ? ", …" : ""}`);
|
|
1707
|
+
}
|
|
1708
|
+
if (r.imagesTrain)
|
|
1709
|
+
lines.push(` Train images: ${r.imagesTrain.fileCount} files in ${r.imagesTrain.path}`);
|
|
1710
|
+
if (r.labelsTrain)
|
|
1711
|
+
lines.push(` Train labels: ${r.labelsTrain.fileCount} files in ${r.labelsTrain.path}`);
|
|
1712
|
+
if (r.imagesVal)
|
|
1713
|
+
lines.push(` Val images: ${r.imagesVal.fileCount} files in ${r.imagesVal.path}`);
|
|
1714
|
+
if (r.labelsVal)
|
|
1715
|
+
lines.push(` Val labels: ${r.labelsVal.fileCount} files in ${r.labelsVal.path}`);
|
|
1716
|
+
if (r.issues.length > 0) {
|
|
1717
|
+
lines.push("");
|
|
1718
|
+
lines.push("Issues:");
|
|
1719
|
+
for (const i of r.issues) {
|
|
1720
|
+
lines.push(` ${i.level === "error" ? "🔴" : "🟡"} ${i.message}`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return text(...lines);
|
|
1724
|
+
}
|
|
1725
|
+
catch (err) {
|
|
1726
|
+
return errorText(err);
|
|
1727
|
+
}
|
|
1728
|
+
},
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
name: "kaax_start_yolo_training",
|
|
1732
|
+
description: "[Free] Kick off YOLO training **locally** as a detached background process. Returns a trainingId immediately; the training continues even if the MCP exits. Poll kaax_check_training_status with the trainingId to watch progress. The dataset never leaves the user's machine — Kaax's server is not involved. Requires Python + ultralytics on PATH (see kaax_check_training_environment).",
|
|
1733
|
+
inputSchema: zodToJsonSchema(StartTrainingArgs),
|
|
1734
|
+
async handler(args) {
|
|
1735
|
+
try {
|
|
1736
|
+
const parsed = StartTrainingArgs.parse(args);
|
|
1737
|
+
let python = parsed.pythonCommand;
|
|
1738
|
+
if (!python) {
|
|
1739
|
+
const env = await checkTrainingEnvironment();
|
|
1740
|
+
if (!env.ready || !env.python.command) {
|
|
1741
|
+
return text("❌ Cannot start training — Python or ultralytics is missing. Run kaax_check_training_environment for install instructions.");
|
|
1742
|
+
}
|
|
1743
|
+
python = env.python.command;
|
|
1744
|
+
}
|
|
1745
|
+
const meta = await startTraining({
|
|
1746
|
+
pythonCommand: python,
|
|
1747
|
+
dataYaml: parsed.dataYaml,
|
|
1748
|
+
model: parsed.model,
|
|
1749
|
+
epochs: parsed.epochs,
|
|
1750
|
+
imgsz: parsed.imgsz,
|
|
1751
|
+
batch: parsed.batch,
|
|
1752
|
+
project: parsed.project,
|
|
1753
|
+
name: parsed.name,
|
|
1754
|
+
extraYoloArgs: parsed.extraYoloArgs,
|
|
1755
|
+
});
|
|
1756
|
+
return text("🚀 Training started in the background.", "", ` Training id : ${meta.id}`, ` Process id : ${meta.pid}`, ` Model : ${meta.model}`, ` Epochs : ${meta.epochs}`, ` Image size : ${meta.imgsz}`, ` Batch : ${meta.batch}`, ` Logs : ~/.kaax/training/${meta.id}/stdout.log`, "", "Use kaax_check_training_status(trainingId) to poll progress.", "Heads up: on CPU this can take many hours per epoch — go grab coffee.");
|
|
1757
|
+
}
|
|
1758
|
+
catch (err) {
|
|
1759
|
+
return errorText(err);
|
|
1760
|
+
}
|
|
1761
|
+
},
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
name: "kaax_check_training_status",
|
|
1765
|
+
description: "[Free] Poll the state of a training run started with kaax_start_yolo_training. Reports whether the process is still alive, how many epochs have completed (from the log), and the path to `best.pt` once training finishes. Safe to call repeatedly.",
|
|
1766
|
+
inputSchema: zodToJsonSchema(CheckTrainingStatusArgs),
|
|
1767
|
+
async handler(args) {
|
|
1768
|
+
try {
|
|
1769
|
+
const { trainingId } = CheckTrainingStatusArgs.parse(args);
|
|
1770
|
+
const s = await getTrainingStatus(trainingId);
|
|
1771
|
+
const lines = [];
|
|
1772
|
+
const m = s.meta;
|
|
1773
|
+
lines.push(`Training ${m.id} — status: ${m.status}`);
|
|
1774
|
+
lines.push(` Started: ${m.startedAt}`);
|
|
1775
|
+
if (m.endedAt)
|
|
1776
|
+
lines.push(` Ended: ${m.endedAt}`);
|
|
1777
|
+
lines.push(` Model: ${m.model} · epochs: ${m.epochs} · imgsz: ${m.imgsz}`);
|
|
1778
|
+
if (s.epochsCompleted !== undefined) {
|
|
1779
|
+
lines.push(` Epochs completed: ${s.epochsCompleted}/${m.epochs}`);
|
|
1780
|
+
}
|
|
1781
|
+
if (s.bestPt) {
|
|
1782
|
+
lines.push(` best.pt: ${s.bestPt}`);
|
|
1783
|
+
}
|
|
1784
|
+
if (m.exitCode !== undefined && m.exitCode !== null) {
|
|
1785
|
+
lines.push(` Exit code: ${m.exitCode}`);
|
|
1786
|
+
}
|
|
1787
|
+
lines.push("");
|
|
1788
|
+
lines.push("Recent log:");
|
|
1789
|
+
for (const line of s.logTail.slice(-12)) {
|
|
1790
|
+
lines.push(` ${line}`);
|
|
1791
|
+
}
|
|
1792
|
+
if (m.status === "completed") {
|
|
1793
|
+
lines.push("");
|
|
1794
|
+
lines.push("✅ Training done. Next: upload the model to Kaax with kaax_upload_model_to_kaax (Phase 2C — coming).");
|
|
1795
|
+
}
|
|
1796
|
+
return text(...lines);
|
|
1797
|
+
}
|
|
1798
|
+
catch (err) {
|
|
1799
|
+
return errorText(err);
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
},
|
|
1803
|
+
{
|
|
1804
|
+
name: "kaax_list_trainings",
|
|
1805
|
+
description: "[Free] List every YOLO training run kept under ~/.kaax/training. Newest first. Useful when the user has lost track of a trainingId.",
|
|
1806
|
+
inputSchema: zodToJsonSchema(ListTrainingsArgs),
|
|
1807
|
+
async handler() {
|
|
1808
|
+
try {
|
|
1809
|
+
const all = await listTrainings();
|
|
1810
|
+
if (all.length === 0) {
|
|
1811
|
+
return text("No training runs found under ~/.kaax/training. Start one with kaax_start_yolo_training.");
|
|
1812
|
+
}
|
|
1813
|
+
const lines = [`Found ${all.length} training run(s):`, ""];
|
|
1814
|
+
for (const m of all.slice(0, 20)) {
|
|
1815
|
+
lines.push(` • ${m.id} ${m.status.padEnd(10)} ${m.startedAt} ${m.model} epochs=${m.epochs}`);
|
|
1816
|
+
}
|
|
1817
|
+
if (all.length > 20)
|
|
1818
|
+
lines.push(` … and ${all.length - 20} more`);
|
|
1819
|
+
return text(...lines);
|
|
1820
|
+
}
|
|
1821
|
+
catch (err) {
|
|
1822
|
+
return errorText(err);
|
|
1823
|
+
}
|
|
1824
|
+
},
|
|
1825
|
+
},
|
|
1589
1826
|
{
|
|
1590
1827
|
name: "kaax_cross_validate_analyses",
|
|
1591
1828
|
description: "[Pro] High-level orchestrator for the canonical 'lines of A inside zones of B' cross-validation. Given two analysisIds (Pro because it reads analyses): downloads both, locates the SHP layers inside each report ZIP, runs extract_features_in_polygons, writes <outputDir>/<idA>_inside.zip + _outside.zip, and reports stats. One tool call does what would otherwise need 5.",
|