Download src/sr_harness/core/context.py from YuMeow/SRHarness: direct link, hf CLI and curl.
- Browser
- Download file 19.2 kB
-
https://huggingface.co/spaces/YuMeow/SRHarness/resolve/main/src/sr_harness/core/context.py
- Command line
-
hf download hf://spaces/YuMeow/SRHarness/src/sr_harness/core/context.py
-
curl -L -o context.py https://huggingface.co/spaces/YuMeow/SRHarness/resolve/main/src/sr_harness/core/context.py
19.2 kB
| """Shared runtime context for agents, evaluators, and tools.""" | |
| from __future__ import annotations | |
| import argparse | |
| import threading | |
| from typing import TYPE_CHECKING, Any | |
| import numpy as np | |
| if TYPE_CHECKING: | |
| from ..evaluator import DefaultEvaluator | |
| from ..runtime.workspace import Workspace | |
| _DEFAULT_EVALUATOR = object() | |
| class AgentContext: | |
| """Authoritative runtime context shared across agents, evaluators, and tools. | |
| Holds the active dataset arrays (``data``), variable and axis metadata, | |
| selected ``target`` and feature names, the active | |
| :class:`~sr_harness.evaluator.DefaultEvaluator`, the conversation | |
| :class:`~sr_harness.runtime.Workspace`, and cached training/validation views. | |
| Args: | |
| args: Runtime configuration namespace shared by agents, evaluators, and tools. | |
| data: Mapping of variable and axis names to NumPy arrays. | |
| target: Name of the active dependent variable, if selected. | |
| variable_descriptions: Human-readable descriptions keyed by variable or axis name. | |
| variable_axes: Mapping from each variable name to its tuple of axis names. | |
| variable_structures: Mapping from edge-aligned variable names to their relation name. | |
| relation_names: Set of variable names representing graph or hypergraph edge tables. | |
| num_nodes: Total node count when graph or hypergraph relations are present. | |
| evaluator: Evaluator instance used for splitting, parameter fitting, and scoring. | |
| workspace: Optional managed workspace accessible to workspace-aware tools. | |
| """ | |
| def __init__(self, *, args: argparse.Namespace | None = None, data: dict[str, Any] | None = None, target: str | None = None, variable_descriptions: dict[str, str] | None = None, variable_axes: dict[str, tuple[str, ...]] | None = None, variable_structures: dict[str, str] | None = None, relation_names: set[str] | None = None, num_nodes: int | None = None, evaluator: DefaultEvaluator | object = _DEFAULT_EVALUATOR, workspace: Workspace | None = None) -> None: | |
| """Initialize an agent runtime context. | |
| Args: | |
| args: Runtime configuration namespace. | |
| data: Mapping of variable and axis names to NumPy arrays. | |
| target: Name of the active target variable. | |
| variable_descriptions: Descriptions keyed by variable or axis name. | |
| variable_axes: Axis tuples keyed by variable name. | |
| variable_structures: Relation variable names keyed by edge-aligned variable name. | |
| relation_names: Names of graph or hypergraph relation variables. | |
| num_nodes: Node count for relational datasets. | |
| evaluator: Active formula evaluator instance. | |
| workspace: Optional managed workspace instance. | |
| """ | |
| if args is not None and not isinstance(args, argparse.Namespace): | |
| raise TypeError("args must be an argparse.Namespace") | |
| self.args = args if args is not None else argparse.Namespace() | |
| defaults = { | |
| "validation_fraction": 0.0, | |
| "split_by": "random", | |
| "split_random_state": 42, | |
| "split_ood_variable": None, | |
| "data_revision": 0, | |
| } | |
| for name, value in defaults.items(): | |
| if not hasattr(self.args, name): | |
| setattr(self.args, name, value) | |
| self.workspace = workspace | |
| if any(not isinstance(name, str) or not name for name in (data or {})): | |
| raise TypeError("data keys must be non-empty strings") | |
| self.data = {name: np.asarray(value) for name, value in (data or {}).items()} | |
| self.target = target | |
| if variable_descriptions is None: | |
| self.variable_descriptions = {name: "" for name in self.data} | |
| else: | |
| self.variable_descriptions = dict(variable_descriptions) | |
| if variable_axes is None: | |
| self.variable_axes = {name: () for name in self.data} | |
| else: | |
| self.variable_axes = {name: tuple(axes) for name, axes in variable_axes.items()} | |
| self.variable_structures = dict(variable_structures or {}) | |
| self.relation_names = set(relation_names or self.variable_structures.values()) | |
| self.num_nodes = num_nodes | |
| from ..evaluator import DefaultEvaluator, GraphEvaluator | |
| if evaluator is _DEFAULT_EVALUATOR: | |
| evaluator = GraphEvaluator() if self.relation_names else DefaultEvaluator() | |
| if not isinstance(evaluator, DefaultEvaluator): | |
| raise TypeError("evaluator must be a DefaultEvaluator instance") | |
| self.evaluator: DefaultEvaluator = evaluator | |
| self._split_cache: dict[str, AgentContext] | None = None | |
| self._lock = threading.RLock() | |
| self._validate(allow_incomplete=not self.data or self.target is None) | |
| def _validate(self, *, allow_incomplete: bool = False) -> None: | |
| names = set(self.data) | |
| if not allow_incomplete and self.target not in names: | |
| raise ValueError("target must name an entry in data") | |
| if set(self.variable_descriptions) != names: | |
| raise ValueError("variable_descriptions keys must equal data keys") | |
| if any(not isinstance(value, str) for value in self.variable_descriptions.values()): | |
| raise TypeError("variable descriptions must be strings") | |
| if any( | |
| not isinstance(axis, str) or not axis | |
| for dimensions in self.variable_axes.values() | |
| for axis in dimensions | |
| ): | |
| raise TypeError("variable_axes values must contain non-empty axis names") | |
| non_axes = set(self.variable_axes) | |
| axes = {axis for dimensions in self.variable_axes.values() for axis in dimensions} | |
| if non_axes & axes or non_axes | axes != names: | |
| raise ValueError("variable_axes keys and values must partition data keys") | |
| for axis in axes: | |
| if self.data[axis].ndim != 1: | |
| raise ValueError(f"axis variable {axis!r} must be one-dimensional") | |
| if (self.num_nodes is None) != (not self.relation_names): | |
| raise ValueError("relation_names and num_nodes must appear together") | |
| if self.num_nodes is not None and ( | |
| isinstance(self.num_nodes, bool) | |
| or not isinstance(self.num_nodes, int) | |
| or self.num_nodes < 1 | |
| ): | |
| raise ValueError("num_nodes must be a positive integer") | |
| for relation_name in self.relation_names: | |
| if relation_name not in names: | |
| raise ValueError("relation_names must reference entries in data") | |
| relation = self.data[relation_name] | |
| if relation.ndim != 2 or relation.shape[1] not in {2, 3}: | |
| raise ValueError(f"relation variable {relation_name!r} must have shape (E, 2) or (H, 3)") | |
| if relation.dtype.kind not in "iu": | |
| raise ValueError(f"relation variable {relation_name!r} must contain integer endpoints") | |
| if np.any(relation < 0) or np.any(relation >= self.num_nodes): | |
| raise ValueError(f"relation variable {relation_name!r} endpoints must be in [0, {self.num_nodes})") | |
| for variable, structure in self.variable_structures.items(): | |
| if variable not in names or structure not in names: | |
| raise ValueError("variable_structures must reference entries in data") | |
| if variable == structure: | |
| raise ValueError("a structured variable cannot reference itself") | |
| if structure not in self.relation_names: | |
| raise ValueError("variable_structures values must name relation variables") | |
| relation = self.data[structure] | |
| if relation.ndim != 2 or relation.shape[1] not in {2, 3}: | |
| raise ValueError(f"structure variable {structure!r} must have shape (E, 2) or (H, 3)") | |
| if self.data[variable].ndim == 0 or self.data[variable].shape[-1] != relation.shape[0]: | |
| raise ValueError(f"structured variable {variable!r} must end in the relation dimension") | |
| if relation.dtype.kind not in "iu": | |
| raise ValueError(f"structure variable {structure!r} must contain integer endpoints") | |
| if np.any(relation < 0) or np.any(relation >= self.num_nodes): | |
| raise ValueError(f"structure variable {structure!r} endpoints must be in [0, {self.num_nodes})") | |
| def invalidate_splits(self) -> None: | |
| """Clear cached training and validation context splits so they are recomputed on next access.""" | |
| with self._lock: | |
| self._split_cache = None | |
| def _sync_builtin_evaluator(self) -> None: | |
| from ..evaluator import DefaultEvaluator, GraphEvaluator | |
| if type(self.evaluator) not in {DefaultEvaluator, GraphEvaluator}: | |
| return | |
| self.evaluator = GraphEvaluator() if self.relation_names else DefaultEvaluator() | |
| def _splits(self) -> dict[str, AgentContext]: | |
| with self._lock: | |
| if self.target is None: | |
| raise ValueError("target must be configured before splitting AgentContext") | |
| if self._split_cache is None: | |
| splits = self.evaluator.split(self) | |
| if set(splits) != {"train", "validation"}: | |
| raise ValueError("DefaultEvaluator.split() must return train and validation contexts") | |
| if any(not isinstance(split, AgentContext) for split in splits.values()): | |
| raise TypeError("DefaultEvaluator.split() values must be AgentContext instances") | |
| self._split_cache = splits | |
| return self._split_cache | |
| def with_data(self, data: dict[str, np.ndarray]) -> AgentContext: | |
| """Create a lightweight view over ``data`` that shares runtime arguments and metadata with this context. | |
| Args: | |
| data: Replacement mapping of variable and axis arrays. | |
| Returns: | |
| AgentContext: New context view bound to ``data``. | |
| """ | |
| return AgentContext( | |
| args=self.args, data=data, target=self.target, | |
| variable_descriptions={name: self.variable_descriptions[name] for name in data}, | |
| variable_axes={name: axes for name, axes in self.variable_axes.items() if name in data}, | |
| variable_structures={name: structure for name, structure in self.variable_structures.items() if name in data and structure in data}, | |
| relation_names=self.relation_names & set(data), | |
| num_nodes=self.num_nodes, evaluator=self.evaluator, workspace=self.workspace, | |
| ) | |
| def train_split(self) -> AgentContext: | |
| """Return the cached training split context, calling ``evaluator.split(self)`` on first access. | |
| Returns: | |
| AgentContext: Context view containing the training split arrays. | |
| """ | |
| return self._splits()["train"] | |
| def validation_split(self) -> AgentContext: | |
| """Return the cached validation split context, calling ``evaluator.split(self)`` on first access. | |
| Returns: | |
| AgentContext: Context view containing the validation split arrays. | |
| """ | |
| return self._splits()["validation"] | |
| def axis_names(self) -> tuple[str, ...]: | |
| """Return all coordinate axis names referenced by variables in first-appearance order. | |
| Returns: | |
| tuple[str, ...]: Ordered axis names. | |
| """ | |
| return tuple(dict.fromkeys(axis for axes in self.variable_axes.values() for axis in axes)) | |
| def variable_names(self) -> tuple[str, ...]: | |
| """Return all non-axis variable names in manifest order. | |
| Returns: | |
| tuple[str, ...]: Ordered variable names. | |
| """ | |
| return tuple(self.variable_axes) | |
| def feature_names(self) -> tuple[str, ...]: | |
| """Return selected input feature names (excluding axes and the active target). | |
| Returns: | |
| tuple[str, ...]: Active feature variable names. | |
| """ | |
| return tuple(name for name in self.variable_axes if name != self.target) | |
| def commit_context_data(self, loaded: dict[str, Any]) -> dict[str, Any]: | |
| """Replace the live dataset with a validated ``load_context_data()`` bundle and increment ``data_revision``. | |
| Args: | |
| loaded: Validated dataset dictionary returned by :func:`load_context_data`. | |
| Returns: | |
| dict[str, Any]: Summary containing the new ``revision``, ``variables``, | |
| ``axes``, ``relation_names``, and ``num_nodes``. | |
| """ | |
| values = {str(name): np.asarray(value) for name, value in loaded["data"].items()} | |
| self.data = values | |
| self.variable_descriptions = dict(loaded["variable_descriptions"]) | |
| self.variable_axes = { | |
| name: tuple(axes) for name, axes in loaded["variable_axes"].items() | |
| } | |
| self.variable_structures = dict(loaded["variable_structures"]) | |
| self.relation_names = set(loaded["relation_names"]) | |
| self.num_nodes = loaded["num_nodes"] | |
| self._sync_builtin_evaluator() | |
| self.target = self.target if self.target in values else None | |
| self.invalidate_splits() | |
| self.args.data_revision += 1 | |
| self._validate(allow_incomplete=True) | |
| return {"revision": self.args.data_revision, "variables": list(values)} | |
| def commit_data(self, data: dict[str, Any], *, target: str, features: list[str] | None = None, variable_descriptions: dict[str, str] | None = None) -> dict[str, Any]: | |
| """Commit aligned 1D/tabular arrays directly into the live context and increment ``data_revision``. | |
| Args: | |
| data: Mapping of column names to NumPy-compatible arrays. | |
| target: Name of the target variable to predict. | |
| features: Names of input feature variables to retain. | |
| variable_descriptions: Optional human-readable descriptions by column name. | |
| Returns: | |
| dict[str, Any]: Summary containing the new ``revision``, ``target``, | |
| ``features``, ``columns``, and ``rows``. | |
| """ | |
| selected = list(dict.fromkeys([*(features or []), target])) | |
| if target not in data: | |
| raise ValueError(f"target column does not exist: {target}") | |
| arrays = {name: np.asarray(data[name]) for name in selected} | |
| with self._lock: | |
| self.data = arrays | |
| self.target = target | |
| self.variable_descriptions = {name: str((variable_descriptions or {}).get(name, "")) for name in arrays} | |
| self.variable_axes = {name: () for name in arrays} | |
| self.variable_structures = {} | |
| self.relation_names = set() | |
| self.num_nodes = None | |
| self._sync_builtin_evaluator() | |
| self.invalidate_splits() | |
| self.args.data_revision += 1 | |
| self._validate() | |
| return {"revision": self.args.data_revision, "target": target, "features": list(self.feature_names()), "columns": list(arrays), "rows": len(arrays[target])} | |
| def add_features(self, features: dict[str, Any], *, descriptions: dict[str, str] | None = None) -> dict[str, Any]: | |
| """Append new sample-aligned feature arrays to the active dataset while keeping the current target. | |
| Args: | |
| features: Mapping of new feature names to aligned arrays. | |
| descriptions: Optional descriptions for the newly added features. | |
| Returns: | |
| dict[str, Any]: Updated commit summary after merging the new features. | |
| Raises: | |
| ValueError: If no target variable is currently configured or feature shapes do not align. | |
| """ | |
| if self.target is None: | |
| raise ValueError("target must be configured before adding features") | |
| descriptions = self.variable_descriptions | { | |
| name: (descriptions or {}).get(name, "") for name in features | |
| } | |
| return self.commit_data(self.data | features, target=self.target, features=[*self.feature_names(), *features], variable_descriptions=descriptions) | |
| def update_selection(self, *, target: str, features: list[str], variable_descriptions: dict[str, str] | None = None) -> dict[str, Any]: | |
| """Update the active target, selected input features, and variable descriptions. | |
| Args: | |
| target: Name of the target variable. | |
| features: List of input feature variable names. | |
| variable_descriptions: Optional updated descriptions keyed by variable or axis name. | |
| Returns: | |
| dict[str, Any]: Summary with the incremented ``revision``, ``target``, and ``features``. | |
| """ | |
| keep_variables = set(features) | {target} | |
| keep_axes = {axis for name, axes in self.variable_axes.items() if name in keep_variables for axis in axes} | |
| keep = keep_variables | keep_axes | |
| self.data = {name: value for name, value in self.data.items() if name in keep} | |
| self.target = target | |
| descriptions = self.variable_descriptions | dict(variable_descriptions or {}) | |
| self.variable_descriptions = {name: descriptions.get(name, "") for name in self.data} | |
| self.variable_axes = {name: axes for name, axes in self.variable_axes.items() if name in keep_variables} | |
| self.variable_structures = {name: structure for name, structure in self.variable_structures.items() if name in self.data and structure in self.data} | |
| self.relation_names &= set(self.data) | |
| if not self.relation_names: | |
| self.num_nodes = None | |
| self._sync_builtin_evaluator() | |
| self.invalidate_splits() | |
| self.args.data_revision += 1 | |
| self._validate() | |
| return {"revision": self.args.data_revision, "target": target, "features": list(self.feature_names()), "selection_changed": True} | |
| def schema(self) -> dict[str, Any]: | |
| """Return a JSON-serializable summary of variables, shapes, dtypes, axes, and roles in the context. | |
| Returns: | |
| dict[str, Any]: Schema dictionary suitable for WebUI previews and diagnostics. | |
| """ | |
| axes = set(self.axis_names()) | |
| return { | |
| "revision": self.args.data_revision, "num_nodes": self.num_nodes, | |
| "target": self.target, "features": list(self.feature_names()), "columns": list(self.data), | |
| "rows": len(self.data[self.target]) if self.target in self.data else 0, | |
| "variable_descriptions": dict(self.variable_descriptions), | |
| "variables": {name: {"shape": list(value.shape), "dtype": str(value.dtype), "axes": list(self.variable_axes.get(name, ())), "description": self.variable_descriptions[name], **({"kind": "relation"} if name in self.relation_names else {})} for name, value in self.data.items() if name not in axes}, | |
| "axes": {name: {"size": len(self.data[name]), "dtype": str(self.data[name].dtype), "description": self.variable_descriptions[name]} for name in axes}, | |
| } | |
| def __getstate__(self) -> dict[str, Any]: | |
| state = self.__dict__.copy() | |
| state.pop("_lock", None) | |
| return state | |
| def __setstate__(self, state: dict[str, Any]) -> None: | |
| self.__dict__.update(state) | |
| self._lock = threading.RLock() | |