SRHarness / src /sr_harness /core /context.py
system's picture
system HF Staff
Deploy SRHarness v1.0.8 (f138545a336c)
8956871 verified
Raw History Blame Contribute Delete
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,
)
@property
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"]
@property
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()