autoforge-trainer 1.0.2__py3-none-any.whl
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.
- autoforge/__init__.py +3 -0
- autoforge/agents/__init__.py +0 -0
- autoforge/agents/loop.py +168 -0
- autoforge/agents/providers.py +272 -0
- autoforge/api.py +330 -0
- autoforge/cli/__init__.py +0 -0
- autoforge/cli/agent.py +102 -0
- autoforge/cli/data.py +42 -0
- autoforge/cli/hpo.py +98 -0
- autoforge/cli/main.py +112 -0
- autoforge/cli/models.py +172 -0
- autoforge/cli/output.py +40 -0
- autoforge/cli/packs.py +185 -0
- autoforge/cli/quickstart.py +61 -0
- autoforge/cli/runs.py +233 -0
- autoforge/config.py +31 -0
- autoforge/data/__init__.py +0 -0
- autoforge/data/profile.py +99 -0
- autoforge/data/registry.py +47 -0
- autoforge/engine/__init__.py +0 -0
- autoforge/engine/context.py +125 -0
- autoforge/engine/diagnose.py +134 -0
- autoforge/engine/hpo.py +355 -0
- autoforge/engine/runner.py +380 -0
- autoforge/engine/store.py +207 -0
- autoforge/engine/validate.py +172 -0
- autoforge/engine/worker.py +68 -0
- autoforge/errors.py +17 -0
- autoforge/examples/__init__.py +0 -0
- autoforge/examples/churn.csv +601 -0
- autoforge/gui/__init__.py +0 -0
- autoforge/gui/app.py +243 -0
- autoforge/gui/static/app.js +546 -0
- autoforge/gui/static/index.html +26 -0
- autoforge/gui/static/style.css +83 -0
- autoforge/hardware/__init__.py +0 -0
- autoforge/hardware/detect.py +161 -0
- autoforge/packs/__init__.py +0 -0
- autoforge/packs/authoring.py +203 -0
- autoforge/packs/errors.py +42 -0
- autoforge/packs/index.py +149 -0
- autoforge/packs/installer.py +241 -0
- autoforge/packs/manifest.py +192 -0
- autoforge/policy/__init__.py +0 -0
- autoforge/policy/privacy.py +68 -0
- autoforge/registry/__init__.py +0 -0
- autoforge/registry/models.py +350 -0
- autoforge/schemas/__init__.py +0 -0
- autoforge/schemas/core.py +137 -0
- autoforge/serve/__init__.py +0 -0
- autoforge/serve/mcp_server.py +293 -0
- autoforge/serve/model_mcp.py +98 -0
- autoforge/serve/rest.py +87 -0
- autoforge_trainer-1.0.2.dist-info/METADATA +216 -0
- autoforge_trainer-1.0.2.dist-info/RECORD +58 -0
- autoforge_trainer-1.0.2.dist-info/WHEEL +4 -0
- autoforge_trainer-1.0.2.dist-info/entry_points.txt +2 -0
- autoforge_trainer-1.0.2.dist-info/licenses/LICENSE +21 -0
autoforge/cli/models.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""`autoforge model ...`, `autoforge serve` and `autoforge serve-mcp`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from autoforge.cli.output import JsonOpt, emit, errors_as_envelope, fail
|
|
12
|
+
from autoforge.cli.runs import ActorOpt, _actor
|
|
13
|
+
|
|
14
|
+
model_app = typer.Typer(help="Registered models: register, inspect, predict, export.", no_args_is_help=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@model_app.command("register")
|
|
18
|
+
def register(
|
|
19
|
+
run_id: str,
|
|
20
|
+
name: str = typer.Option(..., "--name", "-n", help="Model name (lowercase, digits, '-', '_')."),
|
|
21
|
+
description: str = typer.Option("", "--description", "-d"),
|
|
22
|
+
actor: str = ActorOpt,
|
|
23
|
+
as_json: bool = JsonOpt,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Turn a succeeded run into the next version of a model."""
|
|
26
|
+
from autoforge import api
|
|
27
|
+
|
|
28
|
+
with errors_as_envelope(as_json):
|
|
29
|
+
card = api.model_register(run_id, name, _actor(actor), description)
|
|
30
|
+
note = "" if card["servable"] else "\n(not servable: the recipe has no predictor)"
|
|
31
|
+
emit(as_json, card, f"Registered {card['name']}:v{card['version']} from {run_id}{note}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@model_app.command("list")
|
|
35
|
+
def list_cmd(as_json: bool = JsonOpt) -> None:
|
|
36
|
+
"""Registered models (latest version of each)."""
|
|
37
|
+
from autoforge import api
|
|
38
|
+
|
|
39
|
+
rows = api.model_list()
|
|
40
|
+
human = "\n".join(
|
|
41
|
+
f"{r['model']:<28} {r['task']:<24} " + ", ".join(f"{k}={v:.4g}" for k, v in r["metrics"].items())
|
|
42
|
+
for r in rows) or "(no models)"
|
|
43
|
+
emit(as_json, rows, human)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@model_app.command("info")
|
|
47
|
+
def info(model: str, as_json: bool = JsonOpt) -> None:
|
|
48
|
+
"""Model card: features, labels, metrics, source run."""
|
|
49
|
+
from autoforge import api
|
|
50
|
+
|
|
51
|
+
with errors_as_envelope(as_json):
|
|
52
|
+
emit(as_json, api.model_info(model))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@model_app.command("predict")
|
|
56
|
+
def predict(
|
|
57
|
+
model: str,
|
|
58
|
+
rows_json: str = typer.Option(None, "--rows", help='JSON list of objects, e.g. \'[{"age": 30}]\''),
|
|
59
|
+
rows_file: Path = typer.Option(None, "--file", "-f", help="JSON file, or CSV with a header row."),
|
|
60
|
+
as_json: bool = JsonOpt,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Predict with a registered model."""
|
|
63
|
+
import csv
|
|
64
|
+
|
|
65
|
+
from autoforge import api
|
|
66
|
+
|
|
67
|
+
if rows_file:
|
|
68
|
+
if rows_file.suffix.lower() == ".csv":
|
|
69
|
+
with rows_file.open(newline="", encoding="utf-8") as f:
|
|
70
|
+
rows = [{k: v for k, v in r.items()} for r in csv.DictReader(f)]
|
|
71
|
+
else:
|
|
72
|
+
rows = json.loads(rows_file.read_text(encoding="utf-8"))
|
|
73
|
+
elif rows_json:
|
|
74
|
+
rows = json.loads(rows_json)
|
|
75
|
+
else:
|
|
76
|
+
fail(as_json, "BAD_REQUEST", "give --rows or --file")
|
|
77
|
+
with errors_as_envelope(as_json):
|
|
78
|
+
out = api.model_predict(model, rows)
|
|
79
|
+
emit(as_json, out)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@model_app.command("export")
|
|
83
|
+
def export(
|
|
84
|
+
model: str,
|
|
85
|
+
fmt: str = typer.Option("bundle", "--format", help="bundle (zip) or a format the model supports, e.g. onnx."),
|
|
86
|
+
out: Path = typer.Option(None, "--out", "-o"),
|
|
87
|
+
as_json: bool = JsonOpt,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Export a model as a portable bundle or another format (e.g. ONNX)."""
|
|
90
|
+
from autoforge import api
|
|
91
|
+
|
|
92
|
+
with errors_as_envelope(as_json):
|
|
93
|
+
res = api.model_export(model, fmt, str(out) if out else None)
|
|
94
|
+
emit(as_json, res, f"Exported {res['model']} ({fmt}) → {res['path']} sha256 {res['sha256'][:16]}…")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@model_app.command("import")
|
|
98
|
+
def import_cmd(bundle: Path, name: str = typer.Option(None, "--name"), actor: str = ActorOpt,
|
|
99
|
+
as_json: bool = JsonOpt) -> None:
|
|
100
|
+
"""Import a bundle exported elsewhere as a new version."""
|
|
101
|
+
from autoforge.registry import models as reg
|
|
102
|
+
|
|
103
|
+
with errors_as_envelope(as_json):
|
|
104
|
+
card = reg.import_bundle(bundle, name, _actor(actor))
|
|
105
|
+
emit(as_json, card.model_dump(mode="json"), f"Imported {card.ref}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@model_app.command("remove")
|
|
109
|
+
def remove(model: str, as_json: bool = JsonOpt) -> None:
|
|
110
|
+
"""Delete a model version (name:vN) or all versions (name)."""
|
|
111
|
+
from autoforge.registry import models as reg
|
|
112
|
+
|
|
113
|
+
with errors_as_envelope(as_json):
|
|
114
|
+
gone = reg.remove(model)
|
|
115
|
+
emit(as_json, gone, "Removed " + ", ".join(gone))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def serve(
|
|
119
|
+
model: list[str] = typer.Option(None, "--model", "-m", help="Only serve these model names (repeatable)."),
|
|
120
|
+
host: str = typer.Option("127.0.0.1", "--host", help="Use 0.0.0.0 to accept connections from other machines."),
|
|
121
|
+
port: int = typer.Option(8000, "--port", "-p"),
|
|
122
|
+
api_key_env: str = typer.Option(None, "--api-key-env", help="Env var holding an API key clients must send."),
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Serve registered models over HTTP (REST)."""
|
|
125
|
+
import uvicorn
|
|
126
|
+
|
|
127
|
+
from autoforge.serve.rest import create_app
|
|
128
|
+
|
|
129
|
+
key = None
|
|
130
|
+
if api_key_env:
|
|
131
|
+
key = os.environ.get(api_key_env)
|
|
132
|
+
if not key:
|
|
133
|
+
fail(False, "BAD_REQUEST", f"{api_key_env} is not set")
|
|
134
|
+
if host not in ("127.0.0.1", "localhost") and not key:
|
|
135
|
+
typer.echo(typer.style("warning: serving on a network interface without an API key", fg="yellow"), err=True)
|
|
136
|
+
typer.echo(f"Serving {'all models' if not model else ', '.join(model)} on http://{host}:{port} (docs: docs/DEPLOY.md)")
|
|
137
|
+
uvicorn.run(create_app(model or None, key), host=host, port=port, log_level="info")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def serve_mcp(
|
|
141
|
+
model: list[str] = typer.Option(None, "--model", "-m", help="Only expose these model names (repeatable)."),
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Expose registered models as MCP tools (predict_<name>) over stdio — prediction only, no training."""
|
|
144
|
+
from autoforge.serve.model_mcp import main
|
|
145
|
+
|
|
146
|
+
main(model or None)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def gui(
|
|
150
|
+
port: int = typer.Option(8800, "--port", "-p"),
|
|
151
|
+
host: str = typer.Option("127.0.0.1", "--host",
|
|
152
|
+
help="Bind address. Use 0.0.0.0 only inside Docker with -p 127.0.0.1:8800:8800."),
|
|
153
|
+
open_browser: bool = typer.Option(True, "--open/--no-open", help="Open the browser automatically."),
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Open the web interface (local only: 127.0.0.1, protected by a one-time token)."""
|
|
156
|
+
import secrets
|
|
157
|
+
import threading
|
|
158
|
+
import webbrowser
|
|
159
|
+
|
|
160
|
+
import uvicorn
|
|
161
|
+
|
|
162
|
+
from autoforge.gui.app import create_app
|
|
163
|
+
|
|
164
|
+
token = secrets.token_urlsafe(24)
|
|
165
|
+
url = f"http://127.0.0.1:{port}/"
|
|
166
|
+
typer.echo(f"AutoForge GUI: {url} (Ctrl+C to stop)")
|
|
167
|
+
if host not in ("127.0.0.1", "localhost"):
|
|
168
|
+
typer.echo(typer.style(f"warning: binding {host}; the GUI still only answers requests addressed to "
|
|
169
|
+
"localhost/127.0.0.1, so publish the port only on 127.0.0.1", fg="yellow"), err=True)
|
|
170
|
+
if open_browser:
|
|
171
|
+
threading.Timer(1.0, lambda: webbrowser.open(url)).start()
|
|
172
|
+
uvicorn.run(create_app(token), host=host, port=port, log_level="warning")
|
autoforge/cli/output.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Shared output helpers: every command emits the Envelope shape with --json."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Any, Iterator
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from autoforge.errors import AutoForgeError
|
|
12
|
+
from autoforge.schemas.core import Envelope, ErrorInfo
|
|
13
|
+
|
|
14
|
+
JsonOpt = typer.Option(False, "--json", help="Machine-readable JSON output.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def emit(as_json: bool, data: Any = None, human: str | None = None) -> None:
|
|
18
|
+
if as_json:
|
|
19
|
+
typer.echo(Envelope(ok=True, data=data).model_dump_json(indent=2))
|
|
20
|
+
else:
|
|
21
|
+
typer.echo(human if human is not None else json.dumps(data, indent=2, ensure_ascii=False))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fail(as_json: bool, code: str, message: str, hint: str | None = None, details: Any = None) -> None:
|
|
25
|
+
if as_json:
|
|
26
|
+
err = ErrorInfo(code=code, message=message, hint=hint)
|
|
27
|
+
env = Envelope(ok=False, error=err, data=details)
|
|
28
|
+
typer.echo(env.model_dump_json(indent=2))
|
|
29
|
+
else:
|
|
30
|
+
typer.echo(f"Error [{code}]: {message}" + (f"\nHint: {hint}" if hint else ""), err=True)
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@contextmanager
|
|
35
|
+
def errors_as_envelope(as_json: bool) -> Iterator[None]:
|
|
36
|
+
"""Turn any AutoForgeError into an error envelope carrying its code and details."""
|
|
37
|
+
try:
|
|
38
|
+
yield
|
|
39
|
+
except AutoForgeError as e:
|
|
40
|
+
fail(as_json, e.code, str(e), e.hint, e.details)
|
autoforge/cli/packs.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""`autoforge pack ...` and `autoforge index ...` commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from autoforge.cli.output import JsonOpt, emit, errors_as_envelope, fail
|
|
10
|
+
from autoforge.packs import installer
|
|
11
|
+
from autoforge.packs.index import PackIndex, dump_index, entry_from_pack, load_index, parse_index, search
|
|
12
|
+
from autoforge.packs.manifest import collect_problems, load_pack
|
|
13
|
+
|
|
14
|
+
pack_app = typer.Typer(help="Install, inspect and search method packs.", no_args_is_help=True)
|
|
15
|
+
index_app = typer.Typer(help="Maintain a pack index (index.json).", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pack_app.command("validate")
|
|
19
|
+
def validate(path: Path, as_json: bool = JsonOpt) -> None:
|
|
20
|
+
"""Check a pack directory without executing any of its code."""
|
|
21
|
+
pack, problems = collect_problems(path)
|
|
22
|
+
if pack is None:
|
|
23
|
+
fail(as_json, "PACK_INVALID", f"{len(problems)} problem(s)", None, {"problems": problems})
|
|
24
|
+
emit(as_json, {"name": pack.manifest.name, "version": pack.manifest.version,
|
|
25
|
+
"recipes": [r.id for r in pack.recipes]},
|
|
26
|
+
f"OK: {pack.manifest.name} {pack.manifest.version} ({len(pack.recipes)} recipes)")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pack_app.command("install")
|
|
30
|
+
def install(
|
|
31
|
+
source: str = typer.Argument(help="index name, ./local/path, or github:owner/repo[/subdir][@ref]"),
|
|
32
|
+
force: bool = typer.Option(False, "--force", help="Reinstall if already installed."),
|
|
33
|
+
as_json: bool = JsonOpt,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Install a pack and pin its commit in the lock file."""
|
|
36
|
+
with errors_as_envelope(as_json):
|
|
37
|
+
rec = installer.install(source, force=force)
|
|
38
|
+
emit(as_json, rec.model_dump(), f"Installed {rec.name} {rec.version} from {rec.source}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pack_app.command("remove")
|
|
42
|
+
def remove(name: str, as_json: bool = JsonOpt) -> None:
|
|
43
|
+
"""Uninstall a pack."""
|
|
44
|
+
with errors_as_envelope(as_json):
|
|
45
|
+
rec = installer.remove(name)
|
|
46
|
+
emit(as_json, rec.model_dump(), f"Removed {rec.name}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pack_app.command("list")
|
|
50
|
+
def list_installed(as_json: bool = JsonOpt) -> None:
|
|
51
|
+
"""List installed packs."""
|
|
52
|
+
lock = installer.read_lock()
|
|
53
|
+
rows = [r.model_dump() for r in lock.values()]
|
|
54
|
+
human = "\n".join(f"{r.name:<28} {r.version:<10} {r.source}" for r in lock.values()) or "(no packs installed)"
|
|
55
|
+
emit(as_json, rows, human)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pack_app.command("info")
|
|
59
|
+
def info(
|
|
60
|
+
name: str,
|
|
61
|
+
knowledge: bool = typer.Option(True, "--knowledge/--no-knowledge", help="Include KNOWLEDGE.md text."),
|
|
62
|
+
as_json: bool = JsonOpt,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Show an installed pack's manifest, recipes, missing deps and KNOWLEDGE.md (for AI reading)."""
|
|
65
|
+
with errors_as_envelope(as_json):
|
|
66
|
+
rec, pack = installer.get_installed(name)
|
|
67
|
+
data = {
|
|
68
|
+
"installed": rec.model_dump(),
|
|
69
|
+
"manifest": pack.manifest.model_dump(),
|
|
70
|
+
"recipes": [r.model_dump() for r in pack.recipes],
|
|
71
|
+
"missing_dependencies": installer.missing_dependencies(pack),
|
|
72
|
+
}
|
|
73
|
+
if knowledge:
|
|
74
|
+
data["knowledge"] = pack.knowledge()
|
|
75
|
+
emit(as_json, data)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pack_app.command("search")
|
|
79
|
+
def search_cmd(
|
|
80
|
+
query: str = typer.Argument(None, help="Free-text keywords."),
|
|
81
|
+
task: str = typer.Option(None, "--task", help="Exact task type, e.g. tabular-classification."),
|
|
82
|
+
fit: bool = typer.Option(False, "--fit", help="Only recipes this machine's hardware can run."),
|
|
83
|
+
offline: bool = typer.Option(False, "--offline", help="Use the cached index only."),
|
|
84
|
+
as_json: bool = JsonOpt,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Search the pack index."""
|
|
87
|
+
hw = None
|
|
88
|
+
if fit:
|
|
89
|
+
from autoforge.hardware.detect import detect_hardware
|
|
90
|
+
|
|
91
|
+
hw = detect_hardware()
|
|
92
|
+
with errors_as_envelope(as_json):
|
|
93
|
+
index, notes = load_index(refresh=not offline)
|
|
94
|
+
hits = search(index, task=task, query=query, hardware=hw)
|
|
95
|
+
lock = installer.read_lock()
|
|
96
|
+
rows = [
|
|
97
|
+
h.model_dump(mode="json") | {"installed": h.entry.name in lock, "install_with": h.entry.name}
|
|
98
|
+
for h in hits
|
|
99
|
+
]
|
|
100
|
+
human = "\n".join(
|
|
101
|
+
f"{h.entry.name:<28} {h.entry.version:<8} {', '.join(h.runnable_recipes):<30} {h.entry.summary}"
|
|
102
|
+
for h in hits
|
|
103
|
+
) or "(no matches)"
|
|
104
|
+
if as_json:
|
|
105
|
+
emit(True, {"hits": rows, "notes": notes})
|
|
106
|
+
else:
|
|
107
|
+
emit(False, human="\n".join([*(f"note: {n}" for n in notes), human]))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@index_app.command("entry")
|
|
111
|
+
def index_entry(
|
|
112
|
+
path: Path,
|
|
113
|
+
source: str = typer.Option(..., help="github:owner/repo[/subdir] where the pack lives"),
|
|
114
|
+
commit: str = typer.Option(..., help="40-char commit SHA to pin"),
|
|
115
|
+
tag: list[str] = typer.Option([], "--tag", help="Search tag; repeatable."),
|
|
116
|
+
as_json: bool = JsonOpt,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Print the index.json entry for a local pack checkout."""
|
|
119
|
+
with errors_as_envelope(as_json):
|
|
120
|
+
pack = load_pack(path)
|
|
121
|
+
entry = entry_from_pack(pack, source, commit, tag)
|
|
122
|
+
emit(as_json, entry.model_dump(mode="json"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@index_app.command("add")
|
|
126
|
+
def index_add(
|
|
127
|
+
index_file: Path,
|
|
128
|
+
path: Path,
|
|
129
|
+
source: str = typer.Option(...),
|
|
130
|
+
commit: str = typer.Option(...),
|
|
131
|
+
tag: list[str] = typer.Option([], "--tag"),
|
|
132
|
+
as_json: bool = JsonOpt,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Add or replace a pack's entry in an index.json file."""
|
|
135
|
+
with errors_as_envelope(as_json):
|
|
136
|
+
pack = load_pack(path)
|
|
137
|
+
index = parse_index(index_file.read_text(encoding="utf-8")) if index_file.exists() else PackIndex()
|
|
138
|
+
entry = entry_from_pack(pack, source, commit, tag)
|
|
139
|
+
index.packs = sorted([p for p in index.packs if p.name != entry.name] + [entry], key=lambda p: p.name)
|
|
140
|
+
index_file.write_text(dump_index(index), encoding="utf-8")
|
|
141
|
+
emit(as_json, entry.model_dump(mode="json"), f"Wrote {entry.name} {entry.version} to {index_file}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@index_app.command("validate")
|
|
145
|
+
def index_validate(index_file: Path, as_json: bool = JsonOpt) -> None:
|
|
146
|
+
"""Validate an index.json file (structure, SHAs, duplicate names)."""
|
|
147
|
+
with errors_as_envelope(as_json):
|
|
148
|
+
index = parse_index(index_file.read_text(encoding="utf-8"))
|
|
149
|
+
emit(as_json, {"packs": len(index.packs)}, f"OK: {len(index.packs)} pack(s)")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pack_app.command("new")
|
|
153
|
+
def new(
|
|
154
|
+
name: str,
|
|
155
|
+
task: str = typer.Option(..., "--task", help="Task type, e.g. text-classification."),
|
|
156
|
+
dest: Path = typer.Option(Path("."), "--dest", help="Parent directory."),
|
|
157
|
+
as_json: bool = JsonOpt,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Create a valid pack skeleton (pack.yaml, KNOWLEDGE.md, recipe, trainer, smoke test)."""
|
|
160
|
+
from autoforge.packs.authoring import scaffold
|
|
161
|
+
|
|
162
|
+
with errors_as_envelope(as_json):
|
|
163
|
+
res = scaffold(name, task, dest)
|
|
164
|
+
emit(as_json, res, f"Created {res['path']}\nNext: edit trainers/train.py and KNOWLEDGE.md, then "
|
|
165
|
+
f"`autoforge pack test {res['path']}`")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pack_app.command("test")
|
|
169
|
+
def test(
|
|
170
|
+
target: str = typer.Argument(help="Installed pack name or a pack directory."),
|
|
171
|
+
timeout: float = typer.Option(600, "--timeout"),
|
|
172
|
+
as_json: bool = JsonOpt,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Run a pack's smoke tests (tests/) with pytest."""
|
|
175
|
+
from autoforge.packs.authoring import test_pack
|
|
176
|
+
|
|
177
|
+
path = target
|
|
178
|
+
if not Path(target).is_dir():
|
|
179
|
+
with errors_as_envelope(as_json):
|
|
180
|
+
path = installer.get_installed(target)[0].path
|
|
181
|
+
with errors_as_envelope(as_json):
|
|
182
|
+
res = test_pack(path, timeout)
|
|
183
|
+
if not res["ok"]:
|
|
184
|
+
fail(as_json, "PACK_TEST_FAILED", res["summary"] or "tests failed", "read output_tail", res)
|
|
185
|
+
emit(as_json, res, f"OK: {res['summary']} ({res['seconds']} s)")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""`autoforge quickstart`: the whole loop in one command, on bundled example data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from importlib import resources
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from autoforge.cli.output import JsonOpt, emit, errors_as_envelope
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def quickstart(
|
|
14
|
+
recipe: str = typer.Option("random_forest", "--recipe", help="logistic_regression or random_forest"),
|
|
15
|
+
as_json: bool = JsonOpt,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Hardware check → install a pack → train → register → predict, on a bundled churn dataset (~1 min)."""
|
|
18
|
+
from autoforge import api
|
|
19
|
+
from autoforge.errors import AutoForgeError
|
|
20
|
+
from autoforge.packs import installer
|
|
21
|
+
from autoforge.schemas.core import BrainKind
|
|
22
|
+
|
|
23
|
+
start = time.monotonic()
|
|
24
|
+
log = (lambda *a: None) if as_json else typer.echo
|
|
25
|
+
data = str(resources.files("autoforge.examples") / "churn.csv")
|
|
26
|
+
with errors_as_envelope(as_json):
|
|
27
|
+
hw = api.hardware()
|
|
28
|
+
log(f"1/5 hardware: {hw['cpu']['model']}, device {hw['best_device']}")
|
|
29
|
+
if "tabular-classic" not in installer.read_lock():
|
|
30
|
+
try:
|
|
31
|
+
rec = api.pack_install("tabular-classic")
|
|
32
|
+
except AutoForgeError: # offline or index unreachable: fall back to the GitHub source
|
|
33
|
+
rec = api.pack_install("github:Honguan/autoforge/packs/tabular-classic@main")
|
|
34
|
+
log(f"2/5 installed pack tabular-classic {rec['version']}")
|
|
35
|
+
else:
|
|
36
|
+
log("2/5 pack tabular-classic already installed")
|
|
37
|
+
plan = {"task": {"task_type": "tabular-classification", "input_modality": "tabular", "output_modality": "label",
|
|
38
|
+
"target": "churned", "metric": "f1_macro"},
|
|
39
|
+
"data_path": data, "pack": "tabular-classic", "recipe": recipe,
|
|
40
|
+
"hyperparameters": {"class_weight": "balanced"}}
|
|
41
|
+
run = api.run_start(plan, "human", BrainKind.human)
|
|
42
|
+
log(f"3/5 training {run['run_id']} …")
|
|
43
|
+
view = api.run_wait(run["run_id"], 120)
|
|
44
|
+
if view["status"] != "succeeded":
|
|
45
|
+
raise AutoForgeError(f"training {view['status']}: {view.get('error')}",
|
|
46
|
+
hint=f"autoforge run logs {run['run_id']}")
|
|
47
|
+
log(" " + ", ".join(f"{k}={v:.3f}" for k, v in view["metrics"].items()))
|
|
48
|
+
card = api.model_register(run["run_id"], "quickstart-churn", "human", "Quickstart churn model")
|
|
49
|
+
log(f"4/5 registered model {card['name']}:v{card['version']}")
|
|
50
|
+
rows = [{"tenure_months": 2, "monthly_fee": 110, "contract": "month-to-month", "support_calls": 5},
|
|
51
|
+
{"tenure_months": 60, "monthly_fee": 25, "contract": "two-year", "support_calls": 0}]
|
|
52
|
+
pred = api.model_predict(card["name"], rows)
|
|
53
|
+
for r, p in zip(rows, pred["predictions"]):
|
|
54
|
+
log(f"5/5 predict {r} → churn={p['prediction']} (p={p['probabilities'].get('1', 0):.2f})")
|
|
55
|
+
seconds = round(time.monotonic() - start, 1)
|
|
56
|
+
emit(as_json, {"seconds": seconds, "run_id": run["run_id"], "model": f"{card['name']}:v{card['version']}",
|
|
57
|
+
"metrics": view["metrics"], "predictions": pred["predictions"]},
|
|
58
|
+
f"\nDone in {seconds} s. Next:\n"
|
|
59
|
+
f" autoforge gui # web interface\n"
|
|
60
|
+
f" autoforge serve # REST API for the model\n"
|
|
61
|
+
f" claude mcp add autoforge -- autoforge mcp # let Claude drive AutoForge")
|