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.
Files changed (58) hide show
  1. autoforge/__init__.py +3 -0
  2. autoforge/agents/__init__.py +0 -0
  3. autoforge/agents/loop.py +168 -0
  4. autoforge/agents/providers.py +272 -0
  5. autoforge/api.py +330 -0
  6. autoforge/cli/__init__.py +0 -0
  7. autoforge/cli/agent.py +102 -0
  8. autoforge/cli/data.py +42 -0
  9. autoforge/cli/hpo.py +98 -0
  10. autoforge/cli/main.py +112 -0
  11. autoforge/cli/models.py +172 -0
  12. autoforge/cli/output.py +40 -0
  13. autoforge/cli/packs.py +185 -0
  14. autoforge/cli/quickstart.py +61 -0
  15. autoforge/cli/runs.py +233 -0
  16. autoforge/config.py +31 -0
  17. autoforge/data/__init__.py +0 -0
  18. autoforge/data/profile.py +99 -0
  19. autoforge/data/registry.py +47 -0
  20. autoforge/engine/__init__.py +0 -0
  21. autoforge/engine/context.py +125 -0
  22. autoforge/engine/diagnose.py +134 -0
  23. autoforge/engine/hpo.py +355 -0
  24. autoforge/engine/runner.py +380 -0
  25. autoforge/engine/store.py +207 -0
  26. autoforge/engine/validate.py +172 -0
  27. autoforge/engine/worker.py +68 -0
  28. autoforge/errors.py +17 -0
  29. autoforge/examples/__init__.py +0 -0
  30. autoforge/examples/churn.csv +601 -0
  31. autoforge/gui/__init__.py +0 -0
  32. autoforge/gui/app.py +243 -0
  33. autoforge/gui/static/app.js +546 -0
  34. autoforge/gui/static/index.html +26 -0
  35. autoforge/gui/static/style.css +83 -0
  36. autoforge/hardware/__init__.py +0 -0
  37. autoforge/hardware/detect.py +161 -0
  38. autoforge/packs/__init__.py +0 -0
  39. autoforge/packs/authoring.py +203 -0
  40. autoforge/packs/errors.py +42 -0
  41. autoforge/packs/index.py +149 -0
  42. autoforge/packs/installer.py +241 -0
  43. autoforge/packs/manifest.py +192 -0
  44. autoforge/policy/__init__.py +0 -0
  45. autoforge/policy/privacy.py +68 -0
  46. autoforge/registry/__init__.py +0 -0
  47. autoforge/registry/models.py +350 -0
  48. autoforge/schemas/__init__.py +0 -0
  49. autoforge/schemas/core.py +137 -0
  50. autoforge/serve/__init__.py +0 -0
  51. autoforge/serve/mcp_server.py +293 -0
  52. autoforge/serve/model_mcp.py +98 -0
  53. autoforge/serve/rest.py +87 -0
  54. autoforge_trainer-1.0.2.dist-info/METADATA +216 -0
  55. autoforge_trainer-1.0.2.dist-info/RECORD +58 -0
  56. autoforge_trainer-1.0.2.dist-info/WHEEL +4 -0
  57. autoforge_trainer-1.0.2.dist-info/entry_points.txt +2 -0
  58. autoforge_trainer-1.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,172 @@
1
+ """Deterministic plan checks (no AI involved). A wrong AI plan stops here."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import csv
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+ from pydantic import BaseModel, Field, ValidationError
11
+
12
+ from autoforge.errors import AutoForgeError
13
+ from autoforge.hardware.detect import detect_hardware
14
+ from autoforge.packs import installer
15
+ from autoforge.packs.errors import PackError
16
+ from autoforge.schemas.core import HardwareProfile, TrainingPlan
17
+
18
+
19
+ class PlanInvalid(AutoForgeError):
20
+ code = "PLAN_INVALID"
21
+ hint = "Fix every item in details.problems, then run `autoforge plan validate` again."
22
+
23
+
24
+ class PlanCheck(BaseModel):
25
+ ok: bool
26
+ problems: list[str] = Field(default_factory=list)
27
+ warnings: list[str] = Field(default_factory=list)
28
+ device: str | None = None
29
+ entrypoint: str | None = Field(default=None, description="absolute 'file.py:function'")
30
+ hyperparameters: dict = Field(default_factory=dict, description="recipe defaults merged with the plan")
31
+ pack_version: str | None = None
32
+ pack_source: str | None = None
33
+ pack_root: str | None = None
34
+ predictor: str | None = Field(default=None, description="predictor module, relative to the pack root")
35
+ exporters: dict[str, str] = Field(default_factory=dict)
36
+
37
+
38
+ def load_plan_file(path: Path) -> TrainingPlan:
39
+ """Load a YAML/JSON plan; relative paths are resolved against the plan file's directory."""
40
+ path = Path(path)
41
+ try:
42
+ raw = yaml.safe_load(path.read_text(encoding="utf-8"))
43
+ except FileNotFoundError:
44
+ raise PlanInvalid(f"plan file not found: {path}")
45
+ except yaml.YAMLError as e:
46
+ raise PlanInvalid(f"plan file is not valid YAML/JSON: {e}")
47
+ return parse_plan(raw, path.resolve().parent)
48
+
49
+
50
+ def parse_plan(raw: object, base: Path) -> TrainingPlan:
51
+ """Validate a plan dict; relative paths are resolved against `base`."""
52
+ try:
53
+ plan = TrainingPlan.model_validate(raw)
54
+ except ValidationError as e:
55
+ problems = [f"{'.'.join(map(str, err['loc']))}: {err['msg']}" for err in e.errors()]
56
+ raise PlanInvalid("plan does not match the TrainingPlan schema", details={"problems": problems})
57
+ plan.data_path = str((base / plan.data_path).resolve())
58
+ if plan.custom_code_path:
59
+ file, _, func = plan.custom_code_path.partition(":")
60
+ plan.custom_code_path = str((base / file).resolve()) + (f":{func}" if func else "")
61
+ return plan
62
+
63
+
64
+ def _function_defined(file: Path, func: str) -> bool:
65
+ try:
66
+ tree = ast.parse(file.read_text(encoding="utf-8"))
67
+ except (OSError, SyntaxError):
68
+ return False
69
+ return any(isinstance(n, ast.FunctionDef) and n.name == func for n in tree.body)
70
+
71
+
72
+ def _csv_columns(path: Path) -> list[str] | None:
73
+ try:
74
+ with path.open(newline="", encoding="utf-8") as f:
75
+ return next(csv.reader(f), [])
76
+ except (OSError, UnicodeDecodeError):
77
+ return None
78
+
79
+
80
+ def check_plan(plan: TrainingPlan, hardware: HardwareProfile | None = None) -> PlanCheck:
81
+ hw = hardware or detect_hardware()
82
+ problems: list[str] = []
83
+ warnings: list[str] = []
84
+ out = PlanCheck(ok=False)
85
+ has_cuda = hw.best_device == "cuda"
86
+
87
+ # data
88
+ data = Path(plan.data_path)
89
+ if not data.exists():
90
+ problems.append(f"data_path not found: {plan.data_path}")
91
+ elif data.suffix.lower() == ".csv" and plan.task.target:
92
+ cols = _csv_columns(data)
93
+ if cols is not None and plan.task.target not in cols:
94
+ problems.append(f"target column '{plan.task.target}' not in CSV columns {cols}")
95
+
96
+ # method: exactly one of pack+recipe or custom code
97
+ required_device = "any"
98
+ if plan.custom_code_path and plan.pack:
99
+ problems.append("set either pack+recipe or custom_code_path, not both")
100
+ elif plan.custom_code_path:
101
+ file, _, func = plan.custom_code_path.partition(":")
102
+ func = func or "train"
103
+ if not Path(file).is_file():
104
+ problems.append(f"custom code not found: {file}")
105
+ elif not _function_defined(Path(file), func):
106
+ problems.append(f"function '{func}' not defined at top level of {file}")
107
+ else:
108
+ out.entrypoint = f"{Path(file).resolve()}:{func}"
109
+ out.hyperparameters = dict(plan.hyperparameters)
110
+ warnings.append("custom code runs with full permissions; it is versioned in the run's git workspace")
111
+ elif plan.pack:
112
+ try:
113
+ rec, pack = installer.get_installed(plan.pack)
114
+ except PackError as e:
115
+ problems.append(str(e))
116
+ else:
117
+ out.pack_version, out.pack_source = rec.version, rec.source
118
+ recipe = pack.recipe(plan.recipe or "")
119
+ if recipe is None:
120
+ problems.append(
121
+ f"recipe '{plan.recipe}' not in pack '{plan.pack}'; available: {[r.id for r in pack.recipes]}"
122
+ )
123
+ else:
124
+ if plan.task.task_type not in recipe.task_types:
125
+ problems.append(
126
+ f"recipe '{recipe.id}' does not support task '{plan.task.task_type}' "
127
+ f"(supports {recipe.task_types})"
128
+ )
129
+ req = recipe.requires
130
+ required_device = req.device
131
+ if req.device == "cuda" and not has_cuda:
132
+ problems.append(f"recipe '{recipe.id}' needs an NVIDIA GPU with CUDA; none usable")
133
+ if req.min_vram_gb and has_cuda and hw.max_vram_gb < req.min_vram_gb:
134
+ problems.append(f"recipe needs {req.min_vram_gb} GB VRAM; largest GPU has {hw.max_vram_gb} GB")
135
+ if req.min_ram_gb and hw.ram_available_gb < req.min_ram_gb:
136
+ warnings.append(f"recipe suggests {req.min_ram_gb} GB free RAM; {hw.ram_available_gb} GB available")
137
+ unknown = set(plan.hyperparameters) - set(recipe.defaults) - set(recipe.search_space)
138
+ if unknown:
139
+ warnings.append(f"hyperparameters not declared by the recipe: {sorted(unknown)}")
140
+ file, func = recipe.entrypoint.rsplit(":", 1)
141
+ out.entrypoint = f"{(pack.root / file).resolve()}:{func}"
142
+ out.pack_root = str(pack.root)
143
+ out.predictor, out.exporters = recipe.predictor, dict(recipe.exporters)
144
+ out.hyperparameters = recipe.defaults | plan.hyperparameters
145
+ missing = installer.missing_dependencies(pack)
146
+ if missing:
147
+ problems.append(f"missing Python packages: {missing} (pip install them)")
148
+ else:
149
+ problems.append("plan needs pack+recipe or custom_code_path")
150
+
151
+ # device
152
+ if plan.device == "cuda" and not has_cuda:
153
+ problems.append("plan asks for device 'cuda' but no usable NVIDIA GPU/CUDA was found")
154
+ if plan.device == "cuda" and required_device == "cpu":
155
+ problems.append("recipe is CPU-only but plan asks for 'cuda'")
156
+ if required_device in ("cpu", "cuda"):
157
+ out.device = required_device
158
+ elif plan.device != "auto":
159
+ out.device = plan.device
160
+ else:
161
+ out.device = hw.best_device
162
+
163
+ out.problems, out.warnings = problems, warnings
164
+ out.ok = not problems
165
+ return out
166
+
167
+
168
+ def require_valid(plan: TrainingPlan, hardware: HardwareProfile | None = None) -> PlanCheck:
169
+ check = check_plan(plan, hardware)
170
+ if not check.ok:
171
+ raise PlanInvalid(f"{len(check.problems)} problem(s) in plan", details=check.model_dump())
172
+ return check
@@ -0,0 +1,68 @@
1
+ """Worker process: `python -m autoforge.engine.worker <run_dir>`.
2
+
3
+ Loads the trainer entrypoint, runs it with a TrainContext and records the
4
+ outcome in state.json. Runs in its own process so a crash or kill never
5
+ takes down the caller.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import sys
12
+ import traceback
13
+ from pathlib import Path
14
+
15
+ from autoforge.engine.context import RunStopped, TrainContext
16
+ from autoforge.engine.store import RunState, RunStore, now_iso
17
+ from autoforge.schemas.core import RunStatus
18
+
19
+
20
+ def load_entrypoint(entrypoint: str):
21
+ file, func = entrypoint.rsplit(":", 1)
22
+ path = Path(file)
23
+ spec = importlib.util.spec_from_file_location(f"autoforge_trainer_{path.stem}", path)
24
+ module = importlib.util.module_from_spec(spec)
25
+ sys.path.insert(0, str(path.parent)) # allow trainers to import sibling modules
26
+ spec.loader.exec_module(module)
27
+ return getattr(module, func)
28
+
29
+
30
+ def run(run_dir: Path) -> int:
31
+ store = RunStore(run_dir)
32
+ rec = store.record()
33
+ state = RunState(status=RunStatus.running, started_at=now_iso())
34
+ store.write_state(state)
35
+ ctx = TrainContext(
36
+ run_dir=store.dir,
37
+ data_path=rec.plan.data_path,
38
+ target=rec.plan.task.target,
39
+ task_type=rec.plan.task.task_type,
40
+ hyperparameters=rec.plan.hyperparameters,
41
+ device=rec.device,
42
+ seed=rec.plan.seed,
43
+ resume_from=rec.resume_from,
44
+ )
45
+ try:
46
+ fn = load_entrypoint(rec.entrypoint)
47
+ result = fn(ctx) or {}
48
+ if not isinstance(result, dict):
49
+ raise TypeError(f"trainer must return a dict, got {type(result).__name__}")
50
+ state.metrics = {k: float(v) for k, v in (result.get("metrics") or {}).items()}
51
+ state.model_path = result.get("model_path")
52
+ for name, value in state.metrics.items():
53
+ ctx.log_metric(name, value, step=None)
54
+ state.status = RunStatus.succeeded
55
+ except RunStopped:
56
+ state.status = RunStatus.stopped
57
+ except BaseException as e: # noqa: BLE001 - record every failure, including KeyboardInterrupt
58
+ traceback.print_exc()
59
+ state.status = RunStatus.failed
60
+ state.error = f"{type(e).__name__}: {e}"
61
+ state.finished_at = now_iso()
62
+ store.write_state(state)
63
+ print(f"[autoforge] run {store.run_id} finished: {state.status.value}", flush=True)
64
+ return 0 if state.status is RunStatus.succeeded else 1
65
+
66
+
67
+ if __name__ == "__main__":
68
+ sys.exit(run(Path(sys.argv[1])))
autoforge/errors.py ADDED
@@ -0,0 +1,17 @@
1
+ """Base error: every AutoForge error carries a stable code, an optional hint
2
+ and optional structured details, so AI clients can branch on them."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+
9
+ class AutoForgeError(Exception):
10
+ code = "AUTOFORGE_ERROR"
11
+ hint: str | None = None
12
+
13
+ def __init__(self, message: str, hint: str | None = None, details: Any = None):
14
+ super().__init__(message)
15
+ if hint is not None:
16
+ self.hint = hint
17
+ self.details = details
File without changes