OpenRnv / environment.py
Vittal-M's picture
Add hackathon code
85d3923
"""Core Scheduling Optimisation Environment implementing the OpenEnv API contract.
Design principles
-----------------
* reset() always returns a valid Observation — never raises.
* step() clamps reward to [0.0, 1.0] unconditionally.
* Task-aware instance routing: conflict_classification and schedule_repair
are shown only infeasible instances; feasibility_check sees all 12.
* Per-step contextual feedback: the context string and info['grading_breakdown']
give the agent actionable signal on every step, enabling sample-efficient
multi-step improvement within a single episode.
"""
from __future__ import annotations
import copy
import json
from typing import Any
from graders.grader_classification import ConflictGrader
from graders.grader_detection import FeasibilityGrader
from graders.grader_fix import RepairGrader
from models import Action, Observation
# Grader singletons — one per task, reused across episodes.
_GRADERS: dict[str, Any] = {
"feasibility_check": FeasibilityGrader(),
"conflict_classification": ConflictGrader(),
"schedule_repair": RepairGrader(),
}
# ---------------------------------------------------------------------------
# Scheduling instance bank — 12 diverse instances.
#
# Each entry:
# instance – dict exposed to the agent (jobs + machines + proposed_schedule)
# is_feasible – bool, ground-truth for Task 1
# violation_type – str | None, ground-truth for Task 2
# optimal_schedule – dict, the repaired schedule for Task 3
# optimal_makespan – int, minimum achievable makespan
# description – one-line human-readable summary
# ---------------------------------------------------------------------------
INSTANCE_BANK: list[dict[str, Any]] = [
# ------------------------------------------------------------------ #
# 0 — resource_overload #
# J1[0,4) and J2[2,5) overlap on M1 (capacity=1). #
# Fix: sequence J2 after J1. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P01",
"jobs": [
{"id": "J1", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J3", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 2},
{"job_id": "J3", "machine_id": "M2", "start_time": 0},
]
},
},
"is_feasible": False,
"violation_type": "resource_overload",
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 4},
{"job_id": "J3", "machine_id": "M2", "start_time": 0},
]
},
"optimal_makespan": 7,
"description": "J1[0,4) and J2[2,5) overlap on M1 (capacity=1) → resource_overload.",
},
# ------------------------------------------------------------------ #
# 1 — deadline_violation #
# J1 starts late (t=5, dur=5), finishes at t=10 > deadline=8. #
# Fix: schedule J1 first so it finishes at t=5 ≤ 8. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P02",
"jobs": [
{"id": "J1", "duration": 5, "deadline": 8, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 5},
{"job_id": "J2", "machine_id": "M1", "start_time": 0},
]
},
},
"is_feasible": False,
"violation_type": "deadline_violation",
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 5},
]
},
"optimal_makespan": 8,
"description": "J1 starts at t=5 and finishes at t=10, violating deadline=8.",
},
# ------------------------------------------------------------------ #
# 2 — precedence_violation #
# J2 depends on J1 (J1 finishes t=8) but J2 starts at t=0. #
# Fix: start J1 first, then J2 after J1 completes. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P03",
"jobs": [
{"id": "J1", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 3, "deadline": 20, "dependencies": ["J1"], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 5},
{"job_id": "J2", "machine_id": "M2", "start_time": 0},
]
},
},
"is_feasible": False,
"violation_type": "precedence_violation",
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M2", "start_time": 3},
]
},
"optimal_makespan": 6,
"description": "J2 depends on J1; J2 starts at t=0 but J1 does not finish until t=8.",
},
# ------------------------------------------------------------------ #
# 3 — availability_conflict #
# M1 available [8,18]. J1 starts at t=5, before the window opens. #
# Fix: shift J1 to start at t=8 (first valid slot). #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P04",
"jobs": [
{"id": "J1", "duration": 4, "deadline": 24, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 3, "deadline": 24, "dependencies": [], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 8, "available_end": 18},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 5},
{"job_id": "J2", "machine_id": "M1", "start_time": 9},
]
},
},
"is_feasible": False,
"violation_type": "availability_conflict",
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 8},
{"job_id": "J2", "machine_id": "M1", "start_time": 12},
]
},
"optimal_makespan": 15,
"description": "J1 starts at t=5, before M1's available window [8,18] → availability_conflict.",
},
# ------------------------------------------------------------------ #
# 4 — capacity_exceeded #
# 3 jobs on M1 simultaneously; capacity=2 → load=3 > 2. #
# Fix: stagger one job to start after the first batch finishes. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P05",
"jobs": [
{"id": "J1", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J3", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 2, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 0},
{"job_id": "J3", "machine_id": "M1", "start_time": 0},
]
},
},
"is_feasible": False,
"violation_type": "capacity_exceeded",
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 0},
{"job_id": "J3", "machine_id": "M1", "start_time": 3},
]
},
"optimal_makespan": 6,
"description": "3 jobs start simultaneously on M1 (capacity=2); concurrent load=3 > 2.",
},
# ------------------------------------------------------------------ #
# 5 — resource_overload (variant) #
# J1[0,5) and J2[1,5) overlap on M1 (capacity=1). #
# Fix: run jobs sequentially. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P06",
"jobs": [
{"id": "J1", "duration": 5, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J3", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 1},
{"job_id": "J3", "machine_id": "M1", "start_time": 8},
]
},
},
"is_feasible": False,
"violation_type": "resource_overload",
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 5},
{"job_id": "J3", "machine_id": "M1", "start_time": 9},
]
},
"optimal_makespan": 11,
"description": "J1[0,5) and J2[1,5) overlap on M1 (capacity=1) → resource_overload.",
},
# ------------------------------------------------------------------ #
# 6 — deadline_violation (chain with avoidable idle time) #
# J1→J2→J3 chain. J1 starts at t=3 (wasted idle), making the chain #
# finish at t=15 > deadline=13. Fix: start J1 at t=0 → chain ends at #
# t=12 ≤ 13. NOTE: J3 duration is 3 (not 4) so the chain IS solvable. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P07",
"jobs": [
{"id": "J1", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 5, "deadline": 20, "dependencies": ["J1"], "resource_req": 1},
{"id": "J3", "duration": 3, "deadline": 13, "dependencies": ["J2"], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M3", "capacity": 1, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 3},
{"job_id": "J2", "machine_id": "M2", "start_time": 7},
{"job_id": "J3", "machine_id": "M3", "start_time": 12},
]
},
},
"is_feasible": False,
"violation_type": "deadline_violation",
# Optimal: eliminate idle prefix → J1 starts at t=0, chain finishes at t=12 ≤ 13
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M2", "start_time": 4},
{"job_id": "J3", "machine_id": "M3", "start_time": 9},
]
},
"optimal_makespan": 12,
"description": "J1 starts at t=3 (unnecessary idle); J3 finishes at t=15 > deadline=13.",
},
# ------------------------------------------------------------------ #
# 7 — precedence_violation (fan-in: two predecessors) #
# J3 depends on J1 and J2; J3 starts at t=2 but J2 finishes at t=4. #
# Fix: delay J3 start to t=4 (max predecessor finish time). #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P08",
"jobs": [
{"id": "J1", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J3", "duration": 2, "deadline": 20, "dependencies": ["J1", "J2"], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M3", "capacity": 1, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M2", "start_time": 0},
{"job_id": "J3", "machine_id": "M3", "start_time": 2},
]
},
},
"is_feasible": False,
"violation_type": "precedence_violation",
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M2", "start_time": 0},
{"job_id": "J3", "machine_id": "M3", "start_time": 4},
]
},
"optimal_makespan": 6,
"description": "J3 depends on J1 and J2; J3 starts at t=2 but J2 does not finish until t=4.",
},
# ------------------------------------------------------------------ #
# 8 — availability_conflict (maintenance window) #
# M1 available only [0,10]. J1 starts at t=9, runs [9,12) → exceeds #
# the window. Fix: schedule J1 before the window closes. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P09",
"jobs": [
{"id": "J1", "duration": 3, "deadline": 24, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 2, "deadline": 24, "dependencies": [], "resource_req": 1},
],
"machines": [
{
"id": "M1",
"capacity": 1,
"available_start": 0,
"available_end": 10,
"note": "M1 under maintenance t=[10,15]; use window [0,10] only.",
},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 9},
{"job_id": "J2", "machine_id": "M1", "start_time": 0},
]
},
},
"is_feasible": False,
"violation_type": "availability_conflict",
"optimal_schedule": {
"assignments": [
{"job_id": "J2", "machine_id": "M1", "start_time": 0},
{"job_id": "J1", "machine_id": "M1", "start_time": 2},
]
},
"optimal_makespan": 5,
"description": "J1 starts at t=9, extends into maintenance window [10,15] → availability_conflict.",
},
# ------------------------------------------------------------------ #
# 9 — capacity_exceeded (four jobs on capacity-3 machine) #
# Concurrent load at t=0 is 4 > capacity=3. #
# Fix: stagger the fourth job. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P10",
"jobs": [
{"id": "J1", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J3", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J4", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 3, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 0},
{"job_id": "J3", "machine_id": "M1", "start_time": 0},
{"job_id": "J4", "machine_id": "M1", "start_time": 0},
]
},
},
"is_feasible": False,
"violation_type": "capacity_exceeded",
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M1", "start_time": 0},
{"job_id": "J3", "machine_id": "M1", "start_time": 0},
{"job_id": "J4", "machine_id": "M1", "start_time": 2},
]
},
"optimal_makespan": 4,
"description": "4 jobs start simultaneously on M1 (capacity=3); concurrent load=4 > 3.",
},
# ------------------------------------------------------------------ #
# 10 — FEASIBLE: 3-job, 2-machine #
# All constraints satisfied in the proposed schedule. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P11",
"jobs": [
{"id": "J1", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1},
{"id": "J3", "duration": 5, "deadline": 20, "dependencies": ["J1"], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M2", "start_time": 0},
{"job_id": "J3", "machine_id": "M2", "start_time": 4},
]
},
},
"is_feasible": True,
"violation_type": None,
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M2", "start_time": 0},
{"job_id": "J3", "machine_id": "M2", "start_time": 4},
]
},
"optimal_makespan": 9,
"description": "Fully feasible 3-job, 2-machine schedule — all constraints satisfied.",
},
# ------------------------------------------------------------------ #
# 11 — FEASIBLE: 5-job, 3-machine with fan-in precedence #
# All constraints satisfied in the proposed schedule. #
# ------------------------------------------------------------------ #
{
"instance": {
"problem_id": "P12",
"jobs": [
{"id": "J1", "duration": 3, "deadline": 30, "dependencies": [], "resource_req": 1},
{"id": "J2", "duration": 2, "deadline": 30, "dependencies": [], "resource_req": 1},
{"id": "J3", "duration": 4, "deadline": 30, "dependencies": [], "resource_req": 1},
{"id": "J4", "duration": 3, "deadline": 30, "dependencies": ["J1", "J2"], "resource_req": 1},
{"id": "J5", "duration": 2, "deadline": 30, "dependencies": ["J3"], "resource_req": 1},
],
"machines": [
{"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24},
{"id": "M3", "capacity": 1, "available_start": 0, "available_end": 24},
],
"proposed_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M2", "start_time": 0},
{"job_id": "J3", "machine_id": "M3", "start_time": 0},
{"job_id": "J4", "machine_id": "M1", "start_time": 3},
{"job_id": "J5", "machine_id": "M3", "start_time": 4},
]
},
},
"is_feasible": True,
"violation_type": None,
"optimal_schedule": {
"assignments": [
{"job_id": "J1", "machine_id": "M1", "start_time": 0},
{"job_id": "J2", "machine_id": "M2", "start_time": 0},
{"job_id": "J3", "machine_id": "M3", "start_time": 0},
{"job_id": "J4", "machine_id": "M1", "start_time": 3},
{"job_id": "J5", "machine_id": "M3", "start_time": 4},
]
},
"optimal_makespan": 6,
"description": "Fully feasible 5-job, 3-machine schedule with fan-in precedence — all constraints satisfied.",
},
]
# ---------------------------------------------------------------------------
# Task-specific instance pools (built once after INSTANCE_BANK is defined).
# This ensures task-appropriate instances are shown per task:
# feasibility_check → all 12 (mix of feasible and infeasible)
# conflict_classification → 10 infeasible only (feasible has no violation)
# schedule_repair → 10 infeasible with known optimal repairs
# ---------------------------------------------------------------------------
_TASK_POOLS: dict[str, list[dict[str, Any]]] = {
"feasibility_check": INSTANCE_BANK,
"conflict_classification": [e for e in INSTANCE_BANK if not e["is_feasible"]],
"schedule_repair": [
e for e in INSTANCE_BANK if not e["is_feasible"] and e.get("optimal_schedule")
],
}
class SchedulingOptEnv:
"""OpenEnv-compatible scheduling optimisation environment.
Public API (OpenEnv contract)
-----------------------------
reset(task_id: str) → Observation
step(action: Action) → (Observation, float, bool, dict)
state() → dict
"""
def __init__(self) -> None:
self._task_id: str = "feasibility_check"
self._step: int = 0
self._max_steps: int = 3
# Per-task episode counters for round-robin cycling within each pool
self._task_counters: dict[str, int] = {}
# The instance used in the current episode (set by reset)
self._current_instance: dict[str, Any] = {}
self._done: bool = True
self._history: list[dict[str, Any]] = []
self._cumulative_reward: float = 0.0
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def reset(self, task_id: str = "feasibility_check") -> Observation:
"""Start a new episode.
Selects the next instance from the task-appropriate pool in round-robin
order so that repeated resets present diverse scheduling problems.
Always succeeds — never raises an exception.
"""
self._task_id = task_id
self._step = 0
self._done = False
self._history = []
self._cumulative_reward = 0.0
step_limits: dict[str, int] = {
"feasibility_check": 3,
"conflict_classification": 5,
"schedule_repair": 8,
}
self._max_steps = step_limits.get(task_id, 3)
# Task-aware round-robin instance selection
pool = _TASK_POOLS.get(task_id, INSTANCE_BANK)
idx = self._task_counters.get(task_id, 0) % len(pool)
self._current_instance = pool[idx]
self._task_counters[task_id] = idx + 1
return Observation(
schedule_instance=json.dumps(self._current_instance["instance"], indent=2),
task_id=task_id,
context=self._build_context(task_id, step=0, last_reward=None),
step_number=0,
)
def step(self, action: Action) -> tuple[Observation, float, bool, dict[str, Any]]:
"""Process one agent action.
Returns (observation, reward, done, info).
Reward is always clamped to [0.0, 1.0].
"""
if self._done:
return (
Observation(
schedule_instance="{}",
task_id=self._task_id,
context="Episode is over. Call /reset to start a new episode.",
step_number=self._step,
),
0.0,
True,
{"error": "episode_already_done"},
)
self._step += 1
grader = _GRADERS.get(self._task_id, _GRADERS["feasibility_check"])
reward: float = grader.grade(action, self._current_instance)
reward = max(0.0, min(1.0, float(reward))) # hard clamp — invariant
self._cumulative_reward += reward
# Capture grading breakdown for rich info dict
breakdown: dict[str, Any] = getattr(grader, "last_breakdown", {})
# Record step history (truncate long responses for storage efficiency)
self._history.append({
"step": self._step,
"action": action.response[:300],
"reward": round(reward, 4),
})
# Termination: max steps exhausted or near-perfect reward (≥0.95)
done = self._step >= self._max_steps or reward >= 0.95
self._done = done
# Build next observation
if done:
best = max(h["reward"] for h in self._history)
ctx = (
"Episode complete — constraint satisfied."
if reward >= 0.95
else f"Max steps reached. Best reward this episode: {best:.2f}."
)
obs = Observation(
schedule_instance="{}",
task_id=self._task_id,
context=ctx,
step_number=self._step,
)
else:
obs = Observation(
schedule_instance=json.dumps(
self._current_instance["instance"], indent=2
),
task_id=self._task_id,
context=self._build_context(
self._task_id, step=self._step, last_reward=reward
),
step_number=self._step,
)
info: dict[str, Any] = {
"step_reward": round(reward, 4),
"cumulative_reward": round(self._cumulative_reward, 4),
"steps_remaining": max(0, self._max_steps - self._step),
"instance_description": self._current_instance.get("description", ""),
"grading_breakdown": breakdown,
}
return obs, round(reward, 4), done, info
def state(self) -> dict[str, Any]:
"""Return a snapshot of the full internal environment state."""
return {
"task_id": self._task_id,
"step": self._step,
"max_steps": self._max_steps,
"done": self._done,
"cumulative_reward": round(self._cumulative_reward, 4),
"history": copy.deepcopy(self._history),
"current_instance_id": (
self._current_instance.get("instance", {}).get("problem_id", "")
),
"current_instance_feasible": self._current_instance.get("is_feasible"),
"task_counters": dict(self._task_counters),
"instance_pool_sizes": {k: len(v) for k, v in _TASK_POOLS.items()},
}
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
@staticmethod
def _build_context(
task_id: str, step: int, last_reward: float | None
) -> str:
"""Build a context string that adapts to the current step and last reward.
On the first step (step=0) a clear task description is returned.
On retry steps (step>0, last_reward<0.95) an informative hint is appended
to guide the agent toward a better answer.
"""
base_contexts: dict[str, str] = {
"feasibility_check": (
"Examine the proposed_schedule against all four constraint categories "
"(machine capacity, job deadlines, precedence dependencies, machine "
"availability windows). Reply with exactly 'feasible' if every constraint "
"is satisfied, or 'infeasible' if any constraint is violated."
),
"conflict_classification": (
"The proposed_schedule is infeasible. Identify the PRIMARY constraint "
"violation and reply with exactly one of: resource_overload, "
"deadline_violation, precedence_violation, availability_conflict, "
"capacity_exceeded."
),
"schedule_repair": (
"The proposed_schedule is infeasible. Return ONLY a JSON object with key "
'"assignments": a list of {"job_id": str, "machine_id": str, '
'"start_time": int} dicts that resolves ALL violations (capacity, '
"deadlines, precedence, availability) and minimises total makespan."
),
}
ctx = base_contexts.get(task_id, "Analyse the scheduling instance.")
# Add retry hint when the agent is wrong but still has steps remaining
if step > 0 and last_reward is not None and last_reward < 0.95:
hints: dict[str, str] = {
"feasibility_check": (
" ← Previous answer was incorrect. "
"Re-examine all four constraint types carefully."
),
"conflict_classification": (
" ← Previous classification was wrong. "
"Check whether jobs share a machine simultaneously (resource/capacity), "
"miss their deadlines, violate ordering, or run outside availability windows."
),
"schedule_repair": (
" ← Previous repair had remaining violations. "
"Ensure no two jobs overlap on a capacity-1 machine, every job "
"finishes before its deadline, precedence order is respected, and "
"all jobs run within machine availability windows."
),
}
ctx += hints.get(task_id, "")
return ctx
@staticmethod
def get_instance_bank() -> list[dict[str, Any]]:
"""Return the full instance bank (all 12 entries)."""
return INSTANCE_BANK