Spaces:
Running
Running
Download src/profiles/behavior.py from ThomasHeisig/MHRN-Space: direct link, hf CLI and curl.
- Browser
- Download file 6.45 kB
-
https://huggingface.co/spaces/ThomasHeisig/MHRN-Space/resolve/main/src/profiles/behavior.py
- Command line
-
hf download hf://spaces/ThomasHeisig/MHRN-Space/src/profiles/behavior.py
-
curl -L -o behavior.py https://huggingface.co/spaces/ThomasHeisig/MHRN-Space/resolve/main/src/profiles/behavior.py
6.45 kB
| """Operational, non-psychological behavior profile for controlled tasks.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import os | |
| import tempfile | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Any | |
| from src.embodiment.models import ActionCommand | |
| BEHAVIOR_SCHEMA_VERSION = 1 | |
| _DIMENSIONS = ( | |
| "exploration", | |
| "novelty_response", | |
| "persistence", | |
| "strategy_switching", | |
| "cost_weight", | |
| ) | |
| def _digest(value: dict[str, Any]) -> str: | |
| unsigned = json.loads(json.dumps(value, ensure_ascii=True)) | |
| unsigned.pop("integrity_digest", None) | |
| return hashlib.sha256( | |
| json.dumps( | |
| unsigned, sort_keys=True, separators=(",", ":"), ensure_ascii=True | |
| ).encode("utf-8") | |
| ).hexdigest() | |
| def _bounded(value: float) -> float: | |
| return max(0.0, min(1.0, float(value))) | |
| class BehaviorProfile: | |
| """Three-level profile with deterministic, slowly changing disposition.""" | |
| profile_id: str | |
| initial: dict[str, float] = field(default_factory=dict) | |
| situational: dict[str, float] = field(default_factory=dict) | |
| disposition: dict[str, float] = field(default_factory=dict) | |
| update_rate: float = 0.05 | |
| update_log: list[dict[str, Any]] = field(default_factory=list[dict[str, Any]]) | |
| max_update_log: int = 256 | |
| schema_version: int = BEHAVIOR_SCHEMA_VERSION | |
| def __post_init__(self) -> None: | |
| defaults = { | |
| "exploration": 0.2, | |
| "novelty_response": 0.5, | |
| "persistence": 0.5, | |
| "strategy_switching": 0.3, | |
| "cost_weight": 0.5, | |
| } | |
| for dimension in _DIMENSIONS: | |
| self.initial[dimension] = _bounded( | |
| self.initial.get(dimension, defaults[dimension]) | |
| ) | |
| self.situational[dimension] = _bounded(self.situational.get(dimension, 0.0)) | |
| self.disposition[dimension] = _bounded( | |
| self.disposition.get(dimension, self.initial[dimension]) | |
| ) | |
| if not 0.0 < self.update_rate <= 1.0: | |
| raise ValueError("update_rate must be between 0 and 1") | |
| if self.max_update_log <= 0: | |
| raise ValueError("max_update_log must be positive") | |
| self.update_log = self.update_log[-self.max_update_log :] | |
| def value(self, dimension: str) -> float: | |
| if dimension not in _DIMENSIONS: | |
| raise KeyError(dimension) | |
| return _bounded(self.disposition[dimension] + self.situational[dimension]) | |
| def select_action( | |
| self, candidates: tuple[ActionCommand, ...], *, tick: int | |
| ) -> ActionCommand | None: | |
| """Select a candidate using profile state and simulation tick only.""" | |
| if not candidates: | |
| return None | |
| if len(candidates) == 1: | |
| return candidates[0] | |
| exploration = self.value("exploration") | |
| bucket = (tick * 1103515245 + 12345) % 1000 | |
| if bucket < int(exploration * 1000): | |
| index = tick % len(candidates) | |
| return candidates[index] | |
| return candidates[0] | |
| def update(self, *, success: bool, tick: int, source: str = "task_outcome") -> None: | |
| """Apply the disclosed bounded update rule using simulation time.""" | |
| target = 0.0 if success else 1.0 | |
| old = dict(self.disposition) | |
| for dimension in _DIMENSIONS: | |
| if dimension == "persistence": | |
| target = 0.0 if success else 1.0 | |
| elif dimension == "strategy_switching": | |
| target = 1.0 if not success else 0.0 | |
| else: | |
| target = self.initial[dimension] | |
| self.disposition[dimension] = _bounded( | |
| self.disposition[dimension] | |
| + self.update_rate * (target - self.disposition[dimension]) | |
| ) | |
| self.situational["persistence"] = _bounded(0.2 if success else 0.6) | |
| self.situational["strategy_switching"] = _bounded(0.2 if not success else 0.0) | |
| self.update_log.append( | |
| { | |
| "tick": tick, | |
| "source": source, | |
| "rule": "bounded_ema_to_outcome_target_v1", | |
| "success": success, | |
| "before": old, | |
| "after": dict(self.disposition), | |
| } | |
| ) | |
| self.update_log = self.update_log[-self.max_update_log :] | |
| def state_dict(self) -> dict[str, Any]: | |
| state: dict[str, Any] = { | |
| "schema_version": self.schema_version, | |
| "owner": "profiles.behavior", | |
| "profile_id": self.profile_id, | |
| "initial": dict(self.initial), | |
| "situational": dict(self.situational), | |
| "disposition": dict(self.disposition), | |
| "update_rate": self.update_rate, | |
| "update_log": list(self.update_log), | |
| "max_update_log": self.max_update_log, | |
| } | |
| state["integrity_digest"] = _digest(state) | |
| return state | |
| def save(self, path: Path) -> Path: | |
| payload = json.dumps( | |
| self.state_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=True | |
| ).encode("utf-8") | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent)) | |
| try: | |
| with os.fdopen(fd, "wb") as stream: | |
| stream.write(payload) | |
| stream.flush() | |
| os.fsync(stream.fileno()) | |
| os.replace(temporary, path) | |
| finally: | |
| if os.path.exists(temporary): | |
| os.unlink(temporary) | |
| return path | |
| def load(cls, path: Path) -> "BehaviorProfile": | |
| state = json.loads(path.read_text(encoding="utf-8")) | |
| if ( | |
| not isinstance(state, dict) | |
| or state.get("schema_version") != BEHAVIOR_SCHEMA_VERSION | |
| ): | |
| raise ValueError("unsupported behavior profile schema") | |
| if state.get("owner") != "profiles.behavior" or state.get( | |
| "integrity_digest" | |
| ) != _digest(state): | |
| raise ValueError("behavior profile integrity check failed") | |
| return cls( | |
| str(state["profile_id"]), | |
| dict(state["initial"]), | |
| dict(state["situational"]), | |
| dict(state["disposition"]), | |
| float(state["update_rate"]), | |
| list(state["update_log"]), | |
| int(state.get("max_update_log", 256)), | |
| ) | |