"""Task plugin interface. A task module drops into app/tasks/ and exposes a module-level `TASK = Task(...)`. The registry auto-discovers it; nav, sampling, running, scoring, and tracing are generic. Nothing else in the app needs to change. """ from dataclasses import dataclass, field from typing import Any, Callable @dataclass class Sample: input_id: str text: str @dataclass class Task: id: str title: str tagline: str system_prompt: str ui: dict # frontend hints: {"output": "label"|"config", "input_label": ..., "placeholder": ...} sample: Callable[[], Sample] lookup_truth: Callable[[str], dict | None] # input_id -> ground truth (None if unknown) parse_output: Callable[[str], Any] # final assistant text -> parsed result score: Callable[[dict, Any], dict[str, float]] # (truth, parsed) -> auto-scores present: Callable[[Any, dict | None], dict] # (parsed, truth) -> JSON payload for the UI tools: list[dict] = field(default_factory=list) # OpenAI tools format execute_tool: Callable[[str, dict], str] | None = None order: int = 100 # nav position; lower first backend: str | None = None # named backend (see config.backend); None = default def build_messages(self, text: str) -> list[dict]: return [ {"role": "system", "content": self.system_prompt}, {"role": "user", "content": text}, ] def meta(self) -> dict: return {"id": self.id, "title": self.title, "tagline": self.tagline, "ui": self.ui}