InfoClassifier / tasks /registry.py
UTKARSH MISHRA
Initial commit: InfoClassifier project
e5e36db
Raw History Blame Contribute Delete
5.21 kB
"""
tasks/registry.py
=================
Deterministic task registry for InformationIntegrityEnv.
Loads pre-built scenario datasets from tasks/{difficulty}/scenarios.json.
NO external API calls — all scenarios are static and reproducible.
Usage
-----
from tasks.registry import TASKS
task = TASKS["easy"]
scenario = task.get_scenario(0) # by index
scenario = task.get_scenario("easy_001") # by id
all_sc = task.get_all_scenarios()
# Random selection (seeded for reproducibility)
import random
rng = random.Random(42)
scenario = task.sample(rng)
"""
from __future__ import annotations
import json
import os
import random
from typing import Dict, List, Optional
# ---------------------------------------------------------------------------
# Path resolution
# ---------------------------------------------------------------------------
_TASKS_DIR = os.path.dirname(os.path.abspath(__file__))
def _load_scenarios(difficulty: str) -> List[dict]:
path = os.path.join(_TASKS_DIR, difficulty, "scenarios.json")
if not os.path.exists(path):
raise FileNotFoundError(
f"Scenario dataset not found: {path}\n"
f"Run data_collector.py to build datasets, or check tasks/{difficulty}/scenarios.json"
)
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
if not isinstance(data, list) or not data:
raise ValueError(f"scenarios.json for '{difficulty}' must be a non-empty JSON array")
return data
# ---------------------------------------------------------------------------
# Task class
# ---------------------------------------------------------------------------
class Task:
"""
Wraps one difficulty-level dataset.
Parameters
----------
difficulty : str
One of 'easy', 'medium', 'hard'.
"""
def __init__(self, difficulty: str) -> None:
self.difficulty = difficulty
self._scenarios: List[dict] = _load_scenarios(difficulty)
self._index_by_id: Dict[str, dict] = {s["id"]: s for s in self._scenarios}
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def get_scenario(self, scenario_id: str | int) -> dict:
"""
Return a scenario by index (int) or scenario id (str).
Returns a fresh copy so the env cannot mutate the dataset.
"""
if isinstance(scenario_id, int):
if scenario_id < 0 or scenario_id >= len(self._scenarios):
raise IndexError(
f"Scenario index {scenario_id} out of range "
f"(dataset has {len(self._scenarios)} scenarios)"
)
return dict(self._scenarios[scenario_id])
# String id lookup
if scenario_id not in self._index_by_id:
raise KeyError(
f"Scenario id '{scenario_id}' not found in {self.difficulty} dataset. "
f"Available ids: {list(self._index_by_id.keys())}"
)
return dict(self._index_by_id[scenario_id])
def get_all_scenarios(self) -> List[dict]:
"""Return all scenarios as a list of dicts (copies)."""
return [dict(s) for s in self._scenarios]
def sample(self, rng: Optional[random.Random] = None) -> dict:
"""Return a uniformly random scenario (optionally seeded)."""
rng = rng or random.Random()
return dict(rng.choice(self._scenarios))
def __len__(self) -> int:
return len(self._scenarios)
def __repr__(self) -> str:
return f"Task(difficulty={self.difficulty!r}, n_scenarios={len(self)})"
# ---------------------------------------------------------------------------
# Lazy-loaded registry singleton
# ---------------------------------------------------------------------------
class _TaskRegistry:
"""
Lazy-loading registry. Datasets are only read from disk on first access,
so importing this module does not cause IO at import time.
"""
_cache: Dict[str, Task] = {}
_SUPPORTED = ("easy", "medium", "hard")
def __getitem__(self, difficulty: str) -> Task:
key = difficulty.lower().strip()
if key not in self._SUPPORTED:
raise KeyError(
f"Unknown difficulty '{difficulty}'. "
f"Supported values: {list(self._SUPPORTED)}"
)
if key not in self._cache:
self._cache[key] = Task(key)
return self._cache[key]
def __contains__(self, difficulty: str) -> bool:
return difficulty.lower().strip() in self._SUPPORTED
def keys(self):
return self._SUPPORTED
def items(self):
return [(d, self[d]) for d in self._SUPPORTED]
def reload(self) -> None:
"""Force reload all datasets from disk (useful for tests)."""
self._cache.clear()
def __repr__(self) -> str:
loaded = list(self._cache.keys())
return f"TaskRegistry(supported={list(self._SUPPORTED)}, loaded={loaded})"
# Singleton — import and use directly: from tasks.registry import TASKS
TASKS = _TaskRegistry()