Download src/sr_harness/core/context_data.py from YuMeow/SRHarness: direct link, hf CLI and curl.
- Browser
- Download file 22.2 kB
-
https://huggingface.co/spaces/YuMeow/SRHarness/resolve/main/src/sr_harness/core/context_data.py
- Command line
-
hf download hf://spaces/YuMeow/SRHarness/src/sr_harness/core/context_data.py
-
curl -L -o context_data.py https://huggingface.co/spaces/YuMeow/SRHarness/resolve/main/src/sr_harness/core/context_data.py
22.2 kB
| """Manifest-backed NumPy data stored in a workspace directory.""" | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from pathlib import Path | |
| import tempfile | |
| from typing import TYPE_CHECKING, Any | |
| import numpy as np | |
| if TYPE_CHECKING: | |
| from ..runtime.workspace import Workspace | |
| class ContextManifestError(ValueError): | |
| """Raised when a context-data manifest cannot be validated.""" | |
| class _ContextDataReader: | |
| """Private implementation shared by the public loading functions.""" | |
| MANIFEST_NAME = "manifest.json" | |
| REQUIRED_ROOT_FIELDS = {"variables", "axes"} | |
| OPTIONAL_ROOT_FIELDS = {"num_nodes"} | |
| REQUIRED_VARIABLE_FIELDS = {"file", "description", "axes"} | |
| OPTIONAL_VARIABLE_FIELDS = {"kind", "structure"} | |
| AXIS_SOURCE_FIELDS = {"values", "file", "size"} | |
| AXIS_FIELDS = {"description", *AXIS_SOURCE_FIELDS} | |
| def __init__(self, directory: str | Path, *, workspace: Workspace | None = None): | |
| self.directory = Path(directory).resolve() | |
| self.workspace = workspace | |
| def inspect(self) -> dict[str, Any]: | |
| """Validate the store and return diagnostics without raising. | |
| Returns: | |
| A serializable report containing errors, warnings, and array summaries. | |
| """ | |
| errors: list[str] = [] | |
| warnings: list[str] = [] | |
| manifest = self._read_manifest(errors) | |
| loaded = ( | |
| self._validate_manifest(manifest, errors, warnings) | |
| if manifest is not None | |
| else None | |
| ) | |
| report: dict[str, Any] = { | |
| "valid": not errors, | |
| "path": str(self.directory), | |
| "errors": errors, | |
| "warnings": warnings, | |
| "num_nodes": loaded["num_nodes"] if loaded is not None else None, | |
| "variables": {}, | |
| "axes": {}, | |
| } | |
| if loaded is not None: | |
| report["variables"] = { | |
| name: { | |
| "file": manifest["variables"][name]["file"], | |
| "shape": list(value.shape), | |
| "dtype": str(value.dtype), | |
| "axes": list(loaded["variable_axes"][name]), | |
| **( | |
| {"structure": loaded["variable_structures"][name]} | |
| if name in loaded["variable_structures"] else {} | |
| ), | |
| **({"kind": "relation"} if name in loaded["relation_names"] else {}), | |
| "description": loaded["variable_descriptions"][name], | |
| } | |
| for name, value in loaded["data"].items() | |
| if name in manifest["variables"] | |
| } | |
| report["axes"] = { | |
| name: { | |
| "size": len(loaded["data"][name]), | |
| "dtype": str(loaded["data"][name].dtype), | |
| "storage": axis["storage"], | |
| "description": loaded["variable_descriptions"][name], | |
| } | |
| for name, axis in loaded["axis_metadata"].items() | |
| } | |
| return report | |
| def load(self) -> dict[str, Any]: | |
| """Validate and load all variables and axes. | |
| Returns: | |
| An AgentContext-ready mapping containing arrays and metadata. | |
| Raises: | |
| ContextManifestError: If the manifest or referenced arrays are invalid. | |
| """ | |
| errors: list[str] = [] | |
| warnings: list[str] = [] | |
| manifest = self._read_manifest(errors) | |
| loaded = ( | |
| self._validate_manifest(manifest, errors, warnings) | |
| if manifest is not None else None | |
| ) | |
| if errors or loaded is None: | |
| raise ContextManifestError("Invalid context.data:\n- " + "\n- ".join(errors)) | |
| return loaded | |
| def _read_manifest(self, errors: list[str]) -> dict[str, Any] | None: | |
| manifest_path = self.directory / self.MANIFEST_NAME | |
| if not self.directory.is_dir(): | |
| errors.append(f"directory does not exist: {self.directory}") | |
| return None | |
| if not manifest_path.is_file(): | |
| errors.append(f"missing {self.MANIFEST_NAME}") | |
| return None | |
| def reject_duplicates(pairs): | |
| result = {} | |
| for key, value in pairs: | |
| if key in result: | |
| raise ContextManifestError(f"duplicate JSON key: {key}") | |
| result[key] = value | |
| return result | |
| try: | |
| opener = self.workspace.safe_open if self.workspace is not None else open | |
| with opener(manifest_path, "r", encoding="utf-8") as f: | |
| manifest = json.load(f, object_pairs_hook=reject_duplicates) | |
| except (ContextManifestError, json.JSONDecodeError, OSError) as exc: | |
| errors.append(f"cannot read {self.MANIFEST_NAME}: {exc}") | |
| return None | |
| if not isinstance(manifest, dict): | |
| errors.append("manifest root must be a JSON object") | |
| return None | |
| return manifest | |
| def _validate_manifest( | |
| self, | |
| manifest: dict[str, Any], | |
| errors: list[str], | |
| warnings: list[str], | |
| ) -> dict[str, Any] | None: | |
| self._check_fields( | |
| "manifest", | |
| manifest, | |
| self.REQUIRED_ROOT_FIELDS, | |
| errors, | |
| optional=self.OPTIONAL_ROOT_FIELDS, | |
| ) | |
| variables = manifest.get("variables") | |
| axes = manifest.get("axes") | |
| num_nodes = manifest.get("num_nodes") | |
| if "num_nodes" in manifest and ( | |
| isinstance(num_nodes, bool) or not isinstance(num_nodes, int) or num_nodes < 1 | |
| ): | |
| errors.append("manifest.num_nodes must be a positive integer") | |
| num_nodes = None | |
| if not isinstance(variables, dict) or not variables: | |
| errors.append("manifest.variables must be a non-empty object") | |
| variables = {} | |
| if not isinstance(axes, dict): | |
| errors.append("manifest.axes must be an object") | |
| axes = {} | |
| loaded_axes: dict[str, dict[str, Any]] = {} | |
| referenced_files = {self.MANIFEST_NAME} | |
| for name, spec in axes.items(): | |
| location = f"axes.{name}" | |
| if not self._valid_name(name): | |
| errors.append(f"{location}: name must be non-empty and contain no path separators") | |
| continue | |
| if not isinstance(spec, dict): | |
| errors.append(f"{location} must be an object") | |
| continue | |
| if "description" not in spec: | |
| errors.append(f"{location} is missing fields: ['description']") | |
| if extra := sorted(set(spec) - self.AXIS_FIELDS): | |
| errors.append(f"{location} contains unsupported fields: {extra}") | |
| description = self._description(location, spec, errors) | |
| sources = self.AXIS_SOURCE_FIELDS & set(spec) | |
| if len(sources) != 1: | |
| errors.append(f"{location} must contain exactly one of values, file, or size") | |
| continue | |
| source = next(iter(sources)) | |
| try: | |
| if source == "values": | |
| if not isinstance(spec["values"], list) or not spec["values"]: | |
| raise ValueError("values must be a non-empty JSON array") | |
| if any(isinstance(value, (list, dict)) for value in spec["values"]): | |
| raise ValueError("values must contain scalar JSON values") | |
| values = np.asarray(spec["values"]) | |
| elif source == "size": | |
| size = spec["size"] | |
| if isinstance(size, bool) or not isinstance(size, int) or size < 1: | |
| raise ValueError("size must be a positive integer") | |
| values = np.arange(size) | |
| else: | |
| expected = f"{name}.npy" | |
| if spec["file"] != expected: | |
| raise ValueError(f"file must be {expected!r}") | |
| referenced_files.add(expected) | |
| values = self._load_array(expected) | |
| if values.ndim != 1: | |
| raise ValueError(f"axis array must be one-dimensional, got shape {values.shape}") | |
| if values.ndim != 1: | |
| raise ValueError("values must be one-dimensional") | |
| loaded_axes[name] = {"values": values, "description": description, "storage": source} | |
| except (OSError, ValueError) as exc: | |
| errors.append(f"{location}: {exc}") | |
| loaded_values: dict[str, np.ndarray] = {} | |
| variable_axes: dict[str, tuple[str, ...]] = {} | |
| variable_structures: dict[str, str] = {} | |
| relation_names: set[str] = set() | |
| descriptions: dict[str, str] = {} | |
| used_axes: set[str] = set() | |
| for name, spec in variables.items(): | |
| location = f"variables.{name}" | |
| if not self._valid_name(name): | |
| errors.append(f"{location}: name must be non-empty and contain no path separators") | |
| continue | |
| if not isinstance(spec, dict): | |
| errors.append(f"{location} must be an object") | |
| continue | |
| self._check_fields( | |
| location, | |
| spec, | |
| self.REQUIRED_VARIABLE_FIELDS, | |
| errors, | |
| optional=self.OPTIONAL_VARIABLE_FIELDS, | |
| ) | |
| description = self._description(location, spec, errors) | |
| structure = spec.get("structure") | |
| if structure is not None and not self._valid_name(structure): | |
| errors.append(f"{location}.structure must name an A or T variable") | |
| structure = None | |
| kind = spec.get("kind") | |
| if kind not in {None, "relation"}: | |
| errors.append(f"{location}.kind must be 'relation' when provided") | |
| kind = None | |
| expected = f"{name}.npy" | |
| if spec.get("file") != expected: | |
| errors.append(f"{location}.file must be {expected!r}") | |
| continue | |
| declared_axes = spec.get("axes") | |
| if not isinstance(declared_axes, list) or any( | |
| not isinstance(axis, str) or not axis for axis in declared_axes | |
| ): | |
| errors.append(f"{location}.axes must be an array of non-empty axis names") | |
| continue | |
| missing_axes = [axis for axis in declared_axes if axis not in axes] | |
| if missing_axes: | |
| errors.append(f"{location}.axes references missing axes: {missing_axes}") | |
| continue | |
| referenced_files.add(expected) | |
| try: | |
| value = self._load_array(expected) | |
| except (OSError, ValueError) as exc: | |
| errors.append(f"{location}: {exc}") | |
| continue | |
| if value.ndim != len(declared_axes): | |
| errors.append( | |
| f"{location}: array has {value.ndim} dimensions but declares " | |
| f"{len(declared_axes)} axes" | |
| ) | |
| continue | |
| mismatches = [ | |
| f"{axis}={len(loaded_axes[axis]['values'])} (data={value.shape[index]})" | |
| for index, axis in enumerate(declared_axes) | |
| if axis in loaded_axes and len(loaded_axes[axis]["values"]) != value.shape[index] | |
| ] | |
| if mismatches: | |
| errors.append(f"{location}: axis length mismatch: {', '.join(mismatches)}") | |
| continue | |
| loaded_values[name] = value | |
| variable_axes[name] = tuple(declared_axes) | |
| if structure is not None: | |
| variable_structures[name] = structure | |
| if kind == "relation": | |
| relation_names.add(name) | |
| descriptions[name] = description | |
| used_axes.update(declared_axes) | |
| unused_axes = sorted(set(axes) - used_axes) | |
| if unused_axes: | |
| errors.append(f"manifest.axes contains unreferenced axes: {unused_axes}") | |
| if bool(relation_names) != (num_nodes is not None): | |
| errors.append("manifest.num_nodes and relation metadata must appear together") | |
| for relation_name in relation_names: | |
| location = f"variables.{relation_name}" | |
| relation = loaded_values.get(relation_name) | |
| if relation is None: | |
| continue | |
| if relation.ndim != 2 or relation.shape[1] not in {2, 3}: | |
| errors.append( | |
| f"{location}: relation must have shape (E, 2) or (H, 3), got {relation.shape}" | |
| ) | |
| continue | |
| if relation.dtype.kind not in "iu": | |
| errors.append(f"{location}: relation endpoints must use an integer dtype") | |
| elif num_nodes is not None and (np.any(relation < 0) or np.any(relation >= num_nodes)): | |
| errors.append(f"{location}: relation endpoints must be in [0, {num_nodes})") | |
| for variable, structure in variable_structures.items(): | |
| location = f"variables.{variable}.structure" | |
| if variable == structure: | |
| errors.append(f"{location}: a structured variable cannot reference itself") | |
| continue | |
| if structure not in loaded_values: | |
| errors.append(f"{location} references missing variable {structure!r}") | |
| continue | |
| if structure not in relation_names: | |
| errors.append(f"{location} must reference a variable with kind 'relation'") | |
| continue | |
| relation = loaded_values[structure] | |
| if relation.ndim != 2 or relation.shape[1] not in {2, 3}: | |
| errors.append( | |
| f"{location}: {structure!r} must have shape (E, 2) or (H, 3), " | |
| f"got {relation.shape}" | |
| ) | |
| continue | |
| dependent = loaded_values.get(variable) | |
| if dependent is not None and ( | |
| dependent.ndim == 0 or dependent.shape[-1] != relation.shape[0] | |
| ): | |
| errors.append( | |
| f"{location}: {variable!r} must end in a dimension of " | |
| f"length {relation.shape[0]}" | |
| ) | |
| if relation.dtype.kind not in "iu": | |
| errors.append(f"{location}: relation endpoints must use an integer dtype") | |
| elif num_nodes is not None and (np.any(relation < 0) or np.any(relation >= num_nodes)): | |
| errors.append(f"{location}: relation endpoints must be in [0, {num_nodes})") | |
| actual_npy = {path.name for path in self.directory.glob("*.npy") if path.is_file()} | |
| untracked = sorted(actual_npy - referenced_files) | |
| if untracked: | |
| warnings.append(f"unreferenced NPY files: {untracked}") | |
| if errors: | |
| return None | |
| data = {**{name: axis["values"] for name, axis in loaded_axes.items()}, **loaded_values} | |
| all_descriptions = {name: axis["description"] for name, axis in loaded_axes.items()} | descriptions | |
| return { | |
| "data": data, | |
| "variable_descriptions": all_descriptions, | |
| "variable_axes": variable_axes, | |
| "variable_structures": variable_structures, | |
| "relation_names": relation_names, | |
| "num_nodes": num_nodes, | |
| "axis_metadata": loaded_axes, | |
| } | |
| def _load_array(self, filename: str) -> np.ndarray: | |
| path = self.directory / filename | |
| if not path.is_file() or path.parent != self.directory: | |
| raise ValueError(f"missing array file: {filename}") | |
| opener = self.workspace.safe_open if self.workspace is not None else open | |
| with opener(path, "rb") as f: | |
| value = np.load(f, allow_pickle=False) | |
| if not isinstance(value, np.ndarray): | |
| raise ValueError(f"{filename} does not contain one NPY array") | |
| if value.dtype.hasobject: | |
| raise ValueError(f"{filename} uses object dtype, which is not allowed") | |
| return value | |
| def _valid_name(name: Any) -> bool: | |
| return ( | |
| isinstance(name, str) | |
| and bool(name.strip()) | |
| and name not in {".", ".."} | |
| and "/" not in name | |
| and "\\" not in name | |
| and "\0" not in name | |
| ) | |
| def _description(location: str, spec: dict[str, Any], errors: list[str]) -> str: | |
| description = spec.get("description") | |
| if not isinstance(description, str): | |
| errors.append(f"{location}.description must be a string") | |
| return "" | |
| return description.strip() | |
| def _check_fields( | |
| location: str, | |
| value: dict[str, Any], | |
| expected: set[str], | |
| errors: list[str], | |
| *, | |
| optional: set[str] | None = None, | |
| ) -> None: | |
| optional = optional or set() | |
| missing = sorted(expected - set(value)) | |
| extra = sorted(set(value) - expected - optional) | |
| if missing: | |
| errors.append(f"{location} is missing fields: {missing}") | |
| if extra: | |
| errors.append(f"{location} contains unsupported fields: {extra}") | |
| def load_context_data(directory: str | Path, *, workspace: Workspace | None = None) -> dict[str, Any]: | |
| """Validate and load one manifest-backed ``context.data`` directory. | |
| Args: | |
| directory: Directory containing ``manifest.json`` and array files. | |
| workspace: Workspace enforcing safe reads, when available. | |
| Returns: | |
| An AgentContext-ready mapping containing arrays and metadata. | |
| """ | |
| return _ContextDataReader(directory, workspace=workspace).load() | |
| def inspect_context_data(directory: str | Path, *, workspace: Workspace | None = None) -> dict[str, Any]: | |
| """Return validation diagnostics without raising for manifest errors. | |
| Args: | |
| directory: Directory containing ``manifest.json`` and array files. | |
| workspace: Workspace enforcing safe reads, when available. | |
| Returns: | |
| A serializable validation report. | |
| """ | |
| return _ContextDataReader(directory, workspace=workspace).inspect() | |
| def update_context_data_descriptions( | |
| directory: str | Path, | |
| descriptions: dict[str, str], | |
| *, | |
| workspace: Workspace | None = None, | |
| ) -> dict[str, Any]: | |
| """Update variable and axis descriptions in ``manifest.json``. | |
| The complete store is validated before the edit. An atomic replacement is | |
| used when the directory permits creating entries. If the directory is | |
| read-only but ``manifest.json`` itself is writable, the existing file is | |
| updated in place, matching normal POSIX file-permission semantics. Because | |
| only existing string-valued description fields are changed, structural | |
| validity is preserved. The returned mapping matches :func:`load_context_data`. | |
| """ | |
| directory = Path(directory).resolve() | |
| manifest_path = directory / _ContextDataReader.MANIFEST_NAME | |
| loaded = load_context_data(directory, workspace=workspace) | |
| opener = workspace.safe_open if workspace is not None else open | |
| if not isinstance(descriptions, dict) or any( | |
| not isinstance(name, str) or not isinstance(value, str) | |
| for name, value in descriptions.items() | |
| ): | |
| raise ValueError("descriptions must map variable names to text") | |
| unknown = set(descriptions) - set(loaded["data"]) | |
| if unknown: | |
| raise ValueError(f"unknown context.data variables: {sorted(unknown)}") | |
| with opener(manifest_path, "r", encoding="utf-8") as f: | |
| manifest = json.load(f) | |
| for section in ("variables", "axes"): | |
| for name, spec in manifest[section].items(): | |
| if name in descriptions: | |
| spec["description"] = descriptions[name].strip() | |
| serialized = json.dumps(manifest, ensure_ascii=False, indent=2) + "\n" | |
| if workspace is not None: | |
| relative_manifest = str(manifest_path.relative_to(workspace.path)) | |
| temporary_path = None | |
| try: | |
| with tempfile.NamedTemporaryFile( | |
| mode="w", | |
| encoding="utf-8", | |
| dir=workspace.path, | |
| prefix=".manifest-", | |
| suffix=".json.tmp", | |
| delete=False, | |
| ) as stream: | |
| temporary_path = Path(stream.name) | |
| stream.write(serialized) | |
| stream.flush() | |
| os.fsync(stream.fileno()) | |
| workspace.replace_file(temporary_path, relative_manifest) | |
| temporary_path = None | |
| finally: | |
| if temporary_path is not None: | |
| temporary_path.unlink(missing_ok=True) | |
| updated = dict(loaded) | |
| updated["variable_descriptions"] = loaded["variable_descriptions"] | { | |
| name: value.strip() for name, value in descriptions.items() | |
| } | |
| return updated | |
| temporary_path = None | |
| try: | |
| try: | |
| with tempfile.NamedTemporaryFile( | |
| mode="w", | |
| encoding="utf-8", | |
| dir=directory, | |
| prefix=".manifest-", | |
| suffix=".json.tmp", | |
| delete=False, | |
| ) as stream: | |
| temporary_path = Path(stream.name) | |
| stream.write(serialized) | |
| stream.flush() | |
| os.fsync(stream.fileno()) | |
| os.replace(temporary_path, manifest_path) | |
| temporary_path = None | |
| except PermissionError: | |
| if temporary_path is not None: | |
| temporary_path.unlink(missing_ok=True) | |
| with manifest_path.open("w", encoding="utf-8") as stream: | |
| stream.write(serialized) | |
| stream.flush() | |
| os.fsync(stream.fileno()) | |
| finally: | |
| if temporary_path is not None: | |
| temporary_path.unlink(missing_ok=True) | |
| updated = dict(loaded) | |
| updated["variable_descriptions"] = loaded["variable_descriptions"] | { | |
| name: value.strip() for name, value in descriptions.items() | |
| } | |
| return updated | |