From 726e512603ac445411be92415a3083e826bd9888 Mon Sep 17 00:00:00 2001 From: sladro Date: Thu, 2 Apr 2026 11:10:28 +0800 Subject: [PATCH] feat: bootstrap artifact loop engine package --- engine/__init__.py | 27 +++++++++++++ engine/models.py | 84 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + tests/__init__.py | 1 + tests/test_task_loader.py | 60 ++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+) create mode 100644 engine/__init__.py create mode 100644 engine/models.py create mode 100644 tests/__init__.py create mode 100644 tests/test_task_loader.py diff --git a/engine/__init__.py b/engine/__init__.py new file mode 100644 index 0000000..b77f6d6 --- /dev/null +++ b/engine/__init__.py @@ -0,0 +1,27 @@ +from .models import ( + ArtifactsConfig, + BudgetConfig, + ConstraintConfig, + LoggingConfig, + MutationConfig, + ObjectiveConfig, + PolicyConfig, + RunnerConfig, + ScorerConfig, + ScorerParseConfig, + TaskConfig, +) + +__all__ = [ + "ArtifactsConfig", + "BudgetConfig", + "ConstraintConfig", + "LoggingConfig", + "MutationConfig", + "ObjectiveConfig", + "PolicyConfig", + "RunnerConfig", + "ScorerConfig", + "ScorerParseConfig", + "TaskConfig", +] diff --git a/engine/models.py b/engine/models.py new file mode 100644 index 0000000..40ab1f6 --- /dev/null +++ b/engine/models.py @@ -0,0 +1,84 @@ +from dataclasses import dataclass, field +from typing import Literal + + +@dataclass(frozen=True) +class ArtifactsConfig: + include: list[str] = field(default_factory=list) + exclude: list[str] = field(default_factory=list) + max_files_per_iteration: int = 0 + + +@dataclass(frozen=True) +class MutationConfig: + mode: str = "" + allowed_file_types: list[str] = field(default_factory=list) + max_changed_lines: int = 0 + + +@dataclass(frozen=True) +class RunnerConfig: + command: str = "" + cwd: str = "." + timeout_seconds: int = 0 + + +@dataclass(frozen=True) +class ScorerParseConfig: + format: str = "" + score_field: str = "" + metrics_field: str = "" + + +@dataclass(frozen=True) +class ScorerConfig: + type: str = "" + command: str = "" + parse: ScorerParseConfig = field(default_factory=ScorerParseConfig) + + +@dataclass(frozen=True) +class ObjectiveConfig: + primary_metric: str = "" + direction: Literal["maximize", "minimize"] = "maximize" + + +@dataclass(frozen=True) +class ConstraintConfig: + metric: str = "" + op: str = "" + value: int | float = 0 + + +@dataclass(frozen=True) +class PolicyConfig: + keep_if: str = "" + tie_breakers: list[str] = field(default_factory=list) + on_failure: str = "" + + +@dataclass(frozen=True) +class BudgetConfig: + max_iterations: int = 0 + max_failures: int = 0 + + +@dataclass(frozen=True) +class LoggingConfig: + results_file: str = "" + candidate_dir: str = "" + + +@dataclass(frozen=True) +class TaskConfig: + id: str = "" + description: str = "" + artifacts: ArtifactsConfig = field(default_factory=ArtifactsConfig) + mutation: MutationConfig = field(default_factory=MutationConfig) + runner: RunnerConfig = field(default_factory=RunnerConfig) + scorer: ScorerConfig = field(default_factory=ScorerConfig) + objective: ObjectiveConfig = field(default_factory=ObjectiveConfig) + constraints: list[ConstraintConfig] = field(default_factory=list) + policy: PolicyConfig = field(default_factory=PolicyConfig) + budget: BudgetConfig = field(default_factory=BudgetConfig) + logging: LoggingConfig = field(default_factory=LoggingConfig) diff --git a/pyproject.toml b/pyproject.toml index 94ae329..3236b1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "pandas>=2.3.3", "pyarrow>=21.0.0", "requests>=2.32.0", + "PyYAML>=6.0.2", "rustbpe>=0.1.0", "tiktoken>=0.11.0", "torch==2.9.1", diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_task_loader.py b/tests/test_task_loader.py new file mode 100644 index 0000000..1aa6756 --- /dev/null +++ b/tests/test_task_loader.py @@ -0,0 +1,60 @@ +from pathlib import Path +import tempfile +import unittest + +from engine.task_loader import load_task + + +class TaskLoaderSmokeTest(unittest.TestCase): + def test_loads_minimal_task(self) -> None: + task_yaml = """ +id: demo +description: Demo task +artifacts: + include: + - tasks/demo/sample.txt + exclude: [] + max_files_per_iteration: 1 +mutation: + mode: direct_edit + allowed_file_types: [".txt"] + max_changed_lines: 10 +runner: + command: "python -c \\\"print('run')\\\"" + cwd: "." + timeout_seconds: 10 +scorer: + type: command + command: "python -c \\\"import json; print(json.dumps({'score': 1, 'metrics': {'violation_count': 0}}))\\\"" + parse: + format: json + score_field: "score" + metrics_field: "metrics" +objective: + primary_metric: score + direction: maximize +constraints: + - metric: violation_count + op: "<=" + value: 0 +policy: + keep_if: better_primary + tie_breakers: [] + on_failure: discard +budget: + max_iterations: 3 + max_failures: 1 +logging: + results_file: work/results.jsonl + candidate_dir: work/candidates +""" + with tempfile.TemporaryDirectory() as tmp: + task_path = Path(tmp) / "task.yaml" + task_path.write_text(task_yaml, encoding="utf-8") + task = load_task(task_path) + self.assertEqual(task.id, "demo") + self.assertEqual(task.objective.direction, "maximize") + + +if __name__ == "__main__": + unittest.main()