yaml-edit / src /yaml_edit /ml /predictor.py
erayack's picture
feat: release experimental YAML edit model under MIT
8d20d7d verified
Raw History Blame Contribute Delete
3.22 kB
"""Default-off learned predictor that can only compile core-valid copied actions."""
from dataclasses import dataclass
import torch
from yaml_edit.engine import preview, propose
from .checkpoint import ArtifactConfig
from .features import MlInputError, prepare_input
from .model import CandidateScorer, collate_features
_DISPOSITIONS = ("supported", "needs_clarification", "unsupported")
@dataclass(slots=True)
class LearnedPredictor:
"""Callable evaluator-compatible wrapper around a loaded candidate scorer."""
config: ArtifactConfig
model: CandidateScorer
device: str = "cpu"
def __post_init__(self) -> None:
"""Require a coherent checkpoint and explicit device without fallback."""
self.config.validate()
self.model.validate_architecture(
self.config.feature_dimension, self.config.hidden_dimension
)
if self.device not in {"cpu", "mps"}:
raise ValueError("Device must be explicitly 'cpu' or 'mps'.")
if self.device == "mps" and not torch.backends.mps.is_available():
raise ValueError("MPS was requested but is not available.")
self.model.to(torch.device(self.device))
self.model.eval()
def __call__(self, source: str, instruction: str) -> dict[str, object]:
"""Predict from source/instruction only, abstaining if no valid action exists."""
try:
prepared = prepare_input(source, instruction)
except MlInputError:
return {"disposition": "unsupported", "proposal": None}
batch = collate_features(
[list(prepared.vector)],
[[list(action.features) for action in prepared.actions]],
)
device = torch.device(self.device)
with torch.no_grad():
disposition_logits, action_logits = self.model(
batch.vectors.to(device),
batch.actions.to(device),
batch.action_mask.to(device),
)
disposition_probabilities = torch.softmax(disposition_logits[0], dim=0)
disposition_index = int(torch.argmax(disposition_probabilities).item())
supported_confidence = float(disposition_probabilities[0].item())
if disposition_index == 0 and supported_confidence < self.config.disposition_threshold:
disposition_index = int(torch.argmax(disposition_probabilities[1:]).item()) + 1
if disposition_index != 0:
return {"disposition": _DISPOSITIONS[disposition_index], "proposal": None}
if not prepared.actions:
return {"disposition": "unsupported", "proposal": None}
with torch.no_grad():
action_probabilities = torch.softmax(action_logits[0, : len(prepared.actions)], dim=0)
action_index = int(torch.argmax(action_probabilities).item())
if float(action_probabilities[action_index].item()) < self.config.action_threshold:
return {"disposition": "unsupported", "proposal": None}
action = prepared.actions[action_index]
proposal = propose(source, action.candidate.id, action.value)
preview(source, proposal)
return {"disposition": "supported", "proposal": proposal.to_dict()}