Merge branch 'main' into chathu01
Browse files- agents/base_agent.py +16 -10
- agents/command.py +21 -5
- curriculum.py +3 -3
- env/dynamics.py +20 -4
- env/environment.py +4 -1
- env/models.py +1 -0
- env/multiagent.py +50 -8
- env/reward.py +0 -4
- pyproject.toml +10 -0
- requirements.txt +12 -0
- server.py +54 -109
- tasks/easy_task.py +5 -0
- tasks/grader.py +48 -67
- tasks/hard_task.py +5 -0
- tasks/medium_task.py +5 -0
- test_integration.py +278 -0
- train_colab.py +5 -8
- trainer.py +30 -13
agents/base_agent.py
CHANGED
|
@@ -31,19 +31,25 @@ class BaseAgent(ABC):
|
|
| 31 |
def propose(self) -> List[Proposal]:
|
| 32 |
"""Return one Proposal per available sensor this agent controls."""
|
| 33 |
|
| 34 |
-
def update(self, reward: float, conflict_history: List
|
|
|
|
|
|
|
|
|
|
| 35 |
self.episode_reward += reward
|
| 36 |
for record in conflict_history:
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
else:
|
| 42 |
-
|
| 43 |
-
if record.agents_involved and record.agents_involved[0] == self.agent_id:
|
| 44 |
-
self.wins[ctype] = self.wins.get(ctype, 0) + 1
|
| 45 |
-
else:
|
| 46 |
-
self.losses[ctype] = self.losses.get(ctype, 0) + 1
|
| 47 |
self.conflict_history.extend(conflict_history)
|
| 48 |
self.conflict_history = self.conflict_history[-10:]
|
| 49 |
|
|
|
|
| 31 |
def propose(self) -> List[Proposal]:
|
| 32 |
"""Return one Proposal per available sensor this agent controls."""
|
| 33 |
|
| 34 |
+
def update(self, reward: float, conflict_history: List) -> None:
|
| 35 |
+
"""Accept ConflictRecord objects from interaction/conflict.py.
|
| 36 |
+
Uses involved_agents (not agents_involved) and has no resolution field.
|
| 37 |
+
"""
|
| 38 |
self.episode_reward += reward
|
| 39 |
for record in conflict_history:
|
| 40 |
+
# Support both interaction.conflict.ConflictRecord (dataclass) and
|
| 41 |
+
# env.models.ConflictRecord (Pydantic) by reading whichever field exists.
|
| 42 |
+
involved = (
|
| 43 |
+
getattr(record, "involved_agents", None)
|
| 44 |
+
or getattr(record, "agents_involved", [])
|
| 45 |
+
)
|
| 46 |
+
ctype = str(getattr(record, "conflict_type", "unknown"))
|
| 47 |
+
if self.agent_id in involved:
|
| 48 |
+
# Heuristic: first listed agent is the "winner" of the conflict
|
| 49 |
+
if involved and involved[0] == self.agent_id:
|
| 50 |
+
self.wins[ctype] = self.wins.get(ctype, 0) + 1
|
| 51 |
else:
|
| 52 |
+
self.losses[ctype] = self.losses.get(ctype, 0) + 1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
self.conflict_history.extend(conflict_history)
|
| 54 |
self.conflict_history = self.conflict_history[-10:]
|
| 55 |
|
agents/command.py
CHANGED
|
@@ -108,21 +108,37 @@ class CommandAgent(BaseAgent):
|
|
| 108 |
})
|
| 109 |
return proposals
|
| 110 |
|
| 111 |
-
def issue_override(self, conflict
|
| 112 |
-
"""Called by resolver when conflict persists. Returns a binding override proposal.
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
target = next((t for t in self._active_targets() if t.id == target_id), None)
|
| 115 |
if target is None:
|
| 116 |
return None
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
used_sensors = {p.sensor_id for p in self._all_proposals}
|
| 119 |
free_sensor = next(
|
| 120 |
(s for s in self._available_sensors() if s.id not in used_sensors), None
|
| 121 |
)
|
| 122 |
|
| 123 |
if free_sensor is None:
|
| 124 |
-
# Pick the proposal from the first involved agent to re-assign
|
| 125 |
-
involved = conflict.agents_involved
|
| 126 |
override_prop = next(
|
| 127 |
(p for p in self._all_proposals
|
| 128 |
if p.agent_id in involved and p.target_id == target_id),
|
|
|
|
| 108 |
})
|
| 109 |
return proposals
|
| 110 |
|
| 111 |
+
def issue_override(self, conflict) -> Optional[Proposal]:
|
| 112 |
+
"""Called by resolver when conflict persists. Returns a binding override proposal.
|
| 113 |
+
Works with interaction.conflict.ConflictRecord (involved_targets/involved_agents)
|
| 114 |
+
and env.models.ConflictRecord (target_id/agents_involved).
|
| 115 |
+
"""
|
| 116 |
+
# Resolve target_id from whichever schema is present
|
| 117 |
+
involved_targets = getattr(conflict, "involved_targets", None)
|
| 118 |
+
target_id = (
|
| 119 |
+
involved_targets[0]
|
| 120 |
+
if involved_targets
|
| 121 |
+
else getattr(conflict, "target_id", None)
|
| 122 |
+
)
|
| 123 |
+
if target_id is None:
|
| 124 |
+
return None
|
| 125 |
+
|
| 126 |
target = next((t for t in self._active_targets() if t.id == target_id), None)
|
| 127 |
if target is None:
|
| 128 |
return None
|
| 129 |
|
| 130 |
+
# Resolve involved agents from whichever schema is present
|
| 131 |
+
involved = (
|
| 132 |
+
getattr(conflict, "involved_agents", None)
|
| 133 |
+
or getattr(conflict, "agents_involved", [])
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
used_sensors = {p.sensor_id for p in self._all_proposals}
|
| 137 |
free_sensor = next(
|
| 138 |
(s for s in self._available_sensors() if s.id not in used_sensors), None
|
| 139 |
)
|
| 140 |
|
| 141 |
if free_sensor is None:
|
|
|
|
|
|
|
| 142 |
override_prop = next(
|
| 143 |
(p for p in self._all_proposals
|
| 144 |
if p.agent_id in involved and p.target_id == target_id),
|
curriculum.py
CHANGED
|
@@ -110,6 +110,7 @@ class CurriculumEngine:
|
|
| 110 |
Called after each episode with the episode's coordination and efficiency scores.
|
| 111 |
Returns {"phase": int, "difficulty": float, "action": str}.
|
| 112 |
"""
|
|
|
|
| 113 |
self._score_history.append(coordination_score)
|
| 114 |
self._eff_history.append(efficiency_score)
|
| 115 |
|
|
@@ -181,10 +182,9 @@ class CurriculumEngine:
|
|
| 181 |
# ── Episode lifecycle ────────────────────────────────────────────
|
| 182 |
def reset_episode(self) -> None:
|
| 183 |
"""
|
| 184 |
-
|
| 185 |
-
|
| 186 |
"""
|
| 187 |
-
self.episode_count += 1
|
| 188 |
if self.should_trigger_self_play():
|
| 189 |
self._self_play_triggers_count += 1
|
| 190 |
logger.info(
|
|
|
|
| 110 |
Called after each episode with the episode's coordination and efficiency scores.
|
| 111 |
Returns {"phase": int, "difficulty": float, "action": str}.
|
| 112 |
"""
|
| 113 |
+
self.episode_count += 1
|
| 114 |
self._score_history.append(coordination_score)
|
| 115 |
self._eff_history.append(efficiency_score)
|
| 116 |
|
|
|
|
| 182 |
# ── Episode lifecycle ────────────────────────────────────────────
|
| 183 |
def reset_episode(self) -> None:
|
| 184 |
"""
|
| 185 |
+
Called at the START of each episode to check for self-play triggers.
|
| 186 |
+
Does NOT increment episode_count — that is done by update() at episode end.
|
| 187 |
"""
|
|
|
|
| 188 |
if self.should_trigger_self_play():
|
| 189 |
self._self_play_triggers_count += 1
|
| 190 |
logger.info(
|
env/dynamics.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import List
|
|
| 4 |
from env.models import Sensor, Target
|
| 5 |
|
| 6 |
SENSOR_TYPES = ["satellite", "drone", "radar"]
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
def initialize_sensors(seed: int = 42) -> List[Sensor]:
|
|
@@ -34,7 +35,12 @@ def spawn_targets(
|
|
| 34 |
rng = random.Random((seed * 6364136223846793005 + step) & 0xFFFFFFFFFFFFFFFF)
|
| 35 |
count = rng.randint(2, 4)
|
| 36 |
targets = [
|
| 37 |
-
Target(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
for i in range(count)
|
| 39 |
]
|
| 40 |
|
|
@@ -47,12 +53,17 @@ def spawn_targets(
|
|
| 47 |
if injected:
|
| 48 |
# Elevate existing target to P3
|
| 49 |
targets = [
|
| 50 |
-
Target(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
for t in targets
|
| 52 |
]
|
| 53 |
else:
|
| 54 |
# Prepend new P3 target in the overlap zone
|
| 55 |
-
targets = [Target(id=overlap_id, priority=3, active=True)] + targets
|
| 56 |
|
| 57 |
return targets
|
| 58 |
|
|
@@ -69,7 +80,12 @@ def spawn_targets_stochastic(step: int, seed: int = 42, density_factor: float =
|
|
| 69 |
p *= rng.random()
|
| 70 |
count = max(1, k - 1)
|
| 71 |
return [
|
| 72 |
-
Target(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
for i in range(count)
|
| 74 |
]
|
| 75 |
|
|
|
|
| 4 |
from env.models import Sensor, Target
|
| 5 |
|
| 6 |
SENSOR_TYPES = ["satellite", "drone", "radar"]
|
| 7 |
+
TARGET_TYPES = ["strategic", "kinetic", "airspace"]
|
| 8 |
|
| 9 |
|
| 10 |
def initialize_sensors(seed: int = 42) -> List[Sensor]:
|
|
|
|
| 35 |
rng = random.Random((seed * 6364136223846793005 + step) & 0xFFFFFFFFFFFFFFFF)
|
| 36 |
count = rng.randint(2, 4)
|
| 37 |
targets = [
|
| 38 |
+
Target(
|
| 39 |
+
id=f"T{step}_{i+1}",
|
| 40 |
+
priority=rng.randint(1, 3),
|
| 41 |
+
active=True,
|
| 42 |
+
type=rng.choice(TARGET_TYPES),
|
| 43 |
+
)
|
| 44 |
for i in range(count)
|
| 45 |
]
|
| 46 |
|
|
|
|
| 53 |
if injected:
|
| 54 |
# Elevate existing target to P3
|
| 55 |
targets = [
|
| 56 |
+
Target(
|
| 57 |
+
id=t.id,
|
| 58 |
+
priority=3 if t.id == overlap_id else t.priority,
|
| 59 |
+
active=t.active,
|
| 60 |
+
type=t.type,
|
| 61 |
+
)
|
| 62 |
for t in targets
|
| 63 |
]
|
| 64 |
else:
|
| 65 |
# Prepend new P3 target in the overlap zone
|
| 66 |
+
targets = [Target(id=overlap_id, priority=3, active=True, type=rng.choice(TARGET_TYPES))] + targets
|
| 67 |
|
| 68 |
return targets
|
| 69 |
|
|
|
|
| 80 |
p *= rng.random()
|
| 81 |
count = max(1, k - 1)
|
| 82 |
return [
|
| 83 |
+
Target(
|
| 84 |
+
id=f"T{step}_{i+1}",
|
| 85 |
+
priority=rng.randint(1, 3),
|
| 86 |
+
active=True,
|
| 87 |
+
type=rng.choice(TARGET_TYPES),
|
| 88 |
+
)
|
| 89 |
for i in range(count)
|
| 90 |
]
|
| 91 |
|
env/environment.py
CHANGED
|
@@ -126,6 +126,9 @@ PRIORITY_REWARD = {3: 2.0, 2: 1.0, 1: 0.5}
|
|
| 126 |
MISSED_HIGH_PENALTY = -2.0
|
| 127 |
IDLE_PENALTY = -2.0
|
| 128 |
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
class AryaXEnv:
|
| 131 |
def __init__(self, max_steps: int = 20, seed: int = 42, density_factor: float = 2.5,
|
|
@@ -222,7 +225,7 @@ class AryaXEnv:
|
|
| 222 |
for agent_type in AGENT_TYPES:
|
| 223 |
agent_sensors = [s for s in self.sensors if s.type == agent_type or agent_type == "command"]
|
| 224 |
masked_targets = apply_mask(self.targets, agent_type, agent_sensors)
|
| 225 |
-
rng = random.Random(self.seed + self.current_step +
|
| 226 |
noisy_targets = add_observation_noise(masked_targets, agent_type, rng)
|
| 227 |
# Apply schema drift to noisy targets
|
| 228 |
drifted = [Target(id=t.id, priority=priority_map.get(t.priority, t.priority), active=t.active)
|
|
|
|
| 126 |
MISSED_HIGH_PENALTY = -2.0
|
| 127 |
IDLE_PENALTY = -2.0
|
| 128 |
|
| 129 |
+
# Stable per-agent salts — avoids hash() which is randomised per Python process
|
| 130 |
+
_AGENT_SALT = {"satellite": 1001, "drone": 2002, "radar": 3003, "command": 4004}
|
| 131 |
+
|
| 132 |
|
| 133 |
class AryaXEnv:
|
| 134 |
def __init__(self, max_steps: int = 20, seed: int = 42, density_factor: float = 2.5,
|
|
|
|
| 225 |
for agent_type in AGENT_TYPES:
|
| 226 |
agent_sensors = [s for s in self.sensors if s.type == agent_type or agent_type == "command"]
|
| 227 |
masked_targets = apply_mask(self.targets, agent_type, agent_sensors)
|
| 228 |
+
rng = random.Random(self.seed + self.current_step + _AGENT_SALT.get(agent_type, 0))
|
| 229 |
noisy_targets = add_observation_noise(masked_targets, agent_type, rng)
|
| 230 |
# Apply schema drift to noisy targets
|
| 231 |
drifted = [Target(id=t.id, priority=priority_map.get(t.priority, t.priority), active=t.active)
|
env/models.py
CHANGED
|
@@ -13,6 +13,7 @@ class Target(BaseModel):
|
|
| 13 |
id: str
|
| 14 |
priority: int # 1=low, 2=medium, 3=high
|
| 15 |
active: bool
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
class Observation(BaseModel):
|
|
|
|
| 13 |
id: str
|
| 14 |
priority: int # 1=low, 2=medium, 3=high
|
| 15 |
active: bool
|
| 16 |
+
type: str = "strategic" # "strategic" | "kinetic" | "airspace"
|
| 17 |
|
| 18 |
|
| 19 |
class Observation(BaseModel):
|
env/multiagent.py
CHANGED
|
@@ -8,7 +8,7 @@ import random
|
|
| 8 |
from typing import Dict, List, Optional, Tuple, Any
|
| 9 |
from dataclasses import dataclass
|
| 10 |
from env.models import Sensor, Target, Action
|
| 11 |
-
from env.dynamics import initialize_sensors, spawn_targets
|
| 12 |
from interaction import NegotiationLayer
|
| 13 |
from interaction.reward import RewardEngine
|
| 14 |
|
|
@@ -53,11 +53,17 @@ def _cmd_override(tied: list[dict]) -> dict:
|
|
| 53 |
# ── Multi-agent environment ───────────────────────────────────────────────────
|
| 54 |
class AryaXEnv:
|
| 55 |
def __init__(self, max_steps: int = 10, seed: int = 42,
|
| 56 |
-
density_factor: float = 1.0, failure_prob: float = 0.0
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
self.
|
| 60 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
self.sensors: List[Sensor] = []
|
| 62 |
self.targets: List[Target] = []
|
| 63 |
self.current_step: int = 0
|
|
@@ -68,9 +74,31 @@ class AryaXEnv:
|
|
| 68 |
self.reward_engine = RewardEngine()
|
| 69 |
self._agent_rewards: Dict[str, float] = {a: 0.0 for a in AGENT_TYPES}
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
def reset(self) -> Dict[str, AgentObservation]:
|
| 72 |
self.sensors = initialize_sensors(self.seed)
|
| 73 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
self.current_step = 0
|
| 75 |
self._assignments = []
|
| 76 |
self._missed = []
|
|
@@ -174,9 +202,23 @@ class AryaXEnv:
|
|
| 174 |
)
|
| 175 |
|
| 176 |
self.current_step += 1
|
| 177 |
-
self.targets =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
for s in self.sensors:
|
| 179 |
s.available = True
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
done = self.current_step >= self.max_steps
|
| 182 |
conflict_rate = self.negotiation.get_conflict_rate()
|
|
|
|
| 8 |
from typing import Dict, List, Optional, Tuple, Any
|
| 9 |
from dataclasses import dataclass
|
| 10 |
from env.models import Sensor, Target, Action
|
| 11 |
+
from env.dynamics import initialize_sensors, spawn_targets, spawn_targets_stochastic, apply_correlated_failures
|
| 12 |
from interaction import NegotiationLayer
|
| 13 |
from interaction.reward import RewardEngine
|
| 14 |
|
|
|
|
| 53 |
# ── Multi-agent environment ───────────────────────────────────────────────────
|
| 54 |
class AryaXEnv:
|
| 55 |
def __init__(self, max_steps: int = 10, seed: int = 42,
|
| 56 |
+
density_factor: float = 1.0, failure_prob: float = 0.0,
|
| 57 |
+
conflict_injection: bool = False,
|
| 58 |
+
min_targets: int = 0, max_targets: int = 0):
|
| 59 |
+
self.max_steps = max_steps
|
| 60 |
+
self.seed = seed
|
| 61 |
+
self.density_factor = density_factor
|
| 62 |
+
self.failure_prob = failure_prob
|
| 63 |
+
self.conflict_injection = conflict_injection
|
| 64 |
+
# When non-zero, clamp spawned target count to [min_targets, max_targets]
|
| 65 |
+
self.min_targets = min_targets
|
| 66 |
+
self.max_targets = max_targets
|
| 67 |
self.sensors: List[Sensor] = []
|
| 68 |
self.targets: List[Target] = []
|
| 69 |
self.current_step: int = 0
|
|
|
|
| 74 |
self.reward_engine = RewardEngine()
|
| 75 |
self._agent_rewards: Dict[str, float] = {a: 0.0 for a in AGENT_TYPES}
|
| 76 |
|
| 77 |
+
def _clamp_targets(self, targets: List[Target]) -> List[Target]:
|
| 78 |
+
"""Enforce min/max target count from curriculum targets_per_step_range."""
|
| 79 |
+
if self.max_targets > 0 and len(targets) > self.max_targets:
|
| 80 |
+
targets = targets[:self.max_targets]
|
| 81 |
+
if self.min_targets > 0 and len(targets) < self.min_targets:
|
| 82 |
+
# Pad with copies of the highest-priority target
|
| 83 |
+
base = targets[-1] if targets else Target(id="T_pad_1", priority=1, active=True)
|
| 84 |
+
while len(targets) < self.min_targets:
|
| 85 |
+
pad = Target(
|
| 86 |
+
id=f"{base.id}_pad{len(targets)}",
|
| 87 |
+
priority=base.priority,
|
| 88 |
+
active=True,
|
| 89 |
+
type=base.type,
|
| 90 |
+
)
|
| 91 |
+
targets.append(pad)
|
| 92 |
+
return targets
|
| 93 |
+
|
| 94 |
def reset(self) -> Dict[str, AgentObservation]:
|
| 95 |
self.sensors = initialize_sensors(self.seed)
|
| 96 |
+
if self.failure_prob > 0.0:
|
| 97 |
+
rng = random.Random(self.seed)
|
| 98 |
+
self.sensors = apply_correlated_failures(self.sensors, rng.randint(0, 9999), self.failure_prob)
|
| 99 |
+
self.targets = self._clamp_targets(
|
| 100 |
+
spawn_targets_stochastic(step=0, seed=self.seed, density_factor=self.density_factor)
|
| 101 |
+
)
|
| 102 |
self.current_step = 0
|
| 103 |
self._assignments = []
|
| 104 |
self._missed = []
|
|
|
|
| 202 |
)
|
| 203 |
|
| 204 |
self.current_step += 1
|
| 205 |
+
self.targets = self._clamp_targets(spawn_targets_stochastic(
|
| 206 |
+
step=self.current_step, seed=self.seed, density_factor=self.density_factor
|
| 207 |
+
))
|
| 208 |
+
if self.conflict_injection:
|
| 209 |
+
# Guarantee at least one P3 target to exercise the conflict pipeline
|
| 210 |
+
from env.dynamics import spawn_targets as _spawn_det
|
| 211 |
+
injected = _spawn_det(step=self.current_step, seed=self.seed, conflict_injection=True)
|
| 212 |
+
p3_ids = {t.id for t in self.targets if t.priority == 3}
|
| 213 |
+
for t in injected:
|
| 214 |
+
if t.priority == 3 and t.id not in p3_ids:
|
| 215 |
+
self.targets.append(t)
|
| 216 |
+
break
|
| 217 |
for s in self.sensors:
|
| 218 |
s.available = True
|
| 219 |
+
if self.failure_prob > 0.0:
|
| 220 |
+
rng = random.Random(self.seed + self.current_step)
|
| 221 |
+
self.sensors = apply_correlated_failures(self.sensors, rng.randint(0, 9999), self.failure_prob)
|
| 222 |
|
| 223 |
done = self.current_step >= self.max_steps
|
| 224 |
conflict_rate = self.negotiation.get_conflict_rate()
|
env/reward.py
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
NOTE: This file is deprecated.
|
| 3 |
-
Reward logic is implemented directly in environment.py (step_batch).
|
| 4 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pyproject.toml
CHANGED
|
@@ -19,6 +19,16 @@ dependencies = [
|
|
| 19 |
"python-dotenv>=1.0.1",
|
| 20 |
]
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
[project.scripts]
|
| 23 |
server = "server.app:main"
|
| 24 |
|
|
|
|
| 19 |
"python-dotenv>=1.0.1",
|
| 20 |
]
|
| 21 |
|
| 22 |
+
[project.optional-dependencies]
|
| 23 |
+
training = [
|
| 24 |
+
"trl>=0.8.0",
|
| 25 |
+
"transformers>=4.40.0",
|
| 26 |
+
"peft>=0.10.0",
|
| 27 |
+
"torch",
|
| 28 |
+
"accelerate",
|
| 29 |
+
"bitsandbytes",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
[project.scripts]
|
| 33 |
server = "server.app:main"
|
| 34 |
|
requirements.txt
CHANGED
|
@@ -29,6 +29,18 @@ ffmpy==1.0.0
|
|
| 29 |
filelock==3.25.2
|
| 30 |
Flask==3.0.3
|
| 31 |
Flask-Cors==4.0.1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
fsspec==2026.3.0
|
| 33 |
gradio==6.11.0
|
| 34 |
gradio_client==2.4.0
|
|
|
|
| 29 |
filelock==3.25.2
|
| 30 |
Flask==3.0.3
|
| 31 |
Flask-Cors==4.0.1
|
| 32 |
+
|
| 33 |
+
# ── Optional: training stack (GRPO/TRL) ──────────────────────────────────────
|
| 34 |
+
# These are NOT installed by default. Install manually to enable trainer.py:
|
| 35 |
+
# pip install trl>=0.8.0 transformers>=4.40.0 peft>=0.10.0 torch accelerate bitsandbytes
|
| 36 |
+
# On Colab with GPU: also install unsloth (see train_colab.py)
|
| 37 |
+
# trl
|
| 38 |
+
# transformers
|
| 39 |
+
# peft
|
| 40 |
+
# torch
|
| 41 |
+
# accelerate
|
| 42 |
+
# bitsandbytes
|
| 43 |
+
# unsloth
|
| 44 |
fsspec==2026.3.0
|
| 45 |
gradio==6.11.0
|
| 46 |
gradio_client==2.4.0
|
server.py
CHANGED
|
@@ -12,8 +12,6 @@ from flask import Flask, request, jsonify, render_template
|
|
| 12 |
from env import SentinelEnv
|
| 13 |
from env.models import Action
|
| 14 |
from env.multiagent import AryaXEnv, Proposal, AGENT_TYPES
|
| 15 |
-
from interaction import NegotiationLayer
|
| 16 |
-
from interaction.reward import RewardEngine
|
| 17 |
|
| 18 |
app = Flask(__name__)
|
| 19 |
|
|
@@ -23,10 +21,8 @@ obs = None
|
|
| 23 |
_target_positions: dict = {}
|
| 24 |
|
| 25 |
# ── Multi-agent env ───────────────────────────────────────────────────────────
|
| 26 |
-
mx_env
|
| 27 |
-
mx_obs
|
| 28 |
-
_negotiation = NegotiationLayer()
|
| 29 |
-
_reward_eng = RewardEngine()
|
| 30 |
|
| 31 |
# ── LLM client (optional) ─────────────────────────────────────────────────────
|
| 32 |
_llm_client = None
|
|
@@ -198,26 +194,32 @@ def _build_multi_prompt(agent_id: str, agent_obs) -> str:
|
|
| 198 |
f" - id={t['id']} priority={t['priority']} active={t['active']}"
|
| 199 |
for t in agent_obs.targets if t["active"]
|
| 200 |
)
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
Timestep: {agent_obs.timestep}
|
| 206 |
-
Your agent type: {agent_id}
|
| 207 |
|
| 208 |
-
|
| 209 |
{sensors}
|
| 210 |
|
| 211 |
Active Threats:
|
| 212 |
{targets}
|
| 213 |
|
| 214 |
-
Respond ONLY with a JSON array:
|
| 215 |
-
[{{"sensor_id": "
|
| 216 |
"""
|
| 217 |
|
| 218 |
|
| 219 |
-
def _greedy_proposals(agent_id: str, agent_obs, used_sensors: set) -> list[Proposal]:
|
| 220 |
"""Greedy proposals for one agent — only claim sensors matching agent type."""
|
|
|
|
|
|
|
| 221 |
my_sensors = [
|
| 222 |
s for s in agent_obs.sensors
|
| 223 |
if s["available"] and s["id"] not in used_sensors
|
|
@@ -227,7 +229,7 @@ def _greedy_proposals(agent_id: str, agent_obs, used_sensors: set) -> list[Propo
|
|
| 227 |
[t for t in agent_obs.targets if t["active"]],
|
| 228 |
key=lambda t: -t["priority"]
|
| 229 |
)
|
| 230 |
-
proposals
|
| 231 |
for sensor in my_sensors:
|
| 232 |
for target in targets:
|
| 233 |
if target["id"] not in used_targets:
|
|
@@ -282,8 +284,10 @@ def _lora_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
|
|
| 282 |
|
| 283 |
def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
|
| 284 |
"""Build proposals from all agents. Returns (proposals, source)."""
|
|
|
|
| 285 |
proposals: list[Proposal] = []
|
| 286 |
used_sensors: set = set()
|
|
|
|
| 287 |
|
| 288 |
if _has_adapters:
|
| 289 |
return _lora_multi_proposals(agent_obs_map)
|
|
@@ -293,6 +297,9 @@ def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
|
|
| 293 |
for agent_id in AGENT_TYPES:
|
| 294 |
agent_obs = agent_obs_map[agent_id]
|
| 295 |
prompt = _build_multi_prompt(agent_id, agent_obs)
|
|
|
|
|
|
|
|
|
|
| 296 |
response = _llm_client.chat.completions.create(
|
| 297 |
model=MODEL_NAME,
|
| 298 |
messages=[{"role": "user", "content": prompt}],
|
|
@@ -300,27 +307,38 @@ def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
|
|
| 300 |
temperature=0.0
|
| 301 |
)
|
| 302 |
raw = response.choices[0].message.content.strip()
|
|
|
|
| 303 |
start, end = raw.find("["), raw.rfind("]") + 1
|
|
|
|
|
|
|
|
|
|
| 304 |
items = json.loads(raw[start:end])
|
| 305 |
-
valid_sensors = {
|
|
|
|
|
|
|
|
|
|
| 306 |
valid_targets = {t["id"] for t in agent_obs.targets if t["active"]}
|
| 307 |
for item in items:
|
| 308 |
sid, tid = item.get("sensor_id"), item.get("target_id")
|
| 309 |
-
if sid in valid_sensors and tid in valid_targets:
|
| 310 |
proposals.append(Proposal(agent_id=agent_id, sensor_id=sid, target_id=tid))
|
| 311 |
used_sensors.add(sid)
|
| 312 |
if proposals:
|
|
|
|
| 313 |
return proposals, "llm"
|
|
|
|
| 314 |
except Exception as e:
|
| 315 |
-
print(f"[LLM multi] Error: {e}. Falling back to greedy.")
|
| 316 |
|
| 317 |
-
# Greedy fallback —
|
| 318 |
used_sensors = set()
|
|
|
|
| 319 |
for agent_id in AGENT_TYPES:
|
| 320 |
agent_obs = agent_obs_map[agent_id]
|
| 321 |
-
for p in _greedy_proposals(agent_id, agent_obs, used_sensors):
|
| 322 |
proposals.append(p)
|
| 323 |
used_sensors.add(p.sensor_id)
|
|
|
|
| 324 |
|
| 325 |
return proposals, "greedy"
|
| 326 |
|
|
@@ -449,8 +467,6 @@ def reset_multi():
|
|
| 449 |
mx_env.seed = seed
|
| 450 |
mx_env.max_steps = max_steps
|
| 451 |
mx_obs = mx_env.reset()
|
| 452 |
-
_negotiation.reset()
|
| 453 |
-
_reward_eng.reset()
|
| 454 |
return jsonify({
|
| 455 |
"seed": seed,
|
| 456 |
"max_steps": max_steps,
|
|
@@ -479,113 +495,40 @@ def step_multi():
|
|
| 479 |
if p.get("agent_id") and p.get("sensor_id") and p.get("target_id")
|
| 480 |
]
|
| 481 |
|
| 482 |
-
#
|
| 483 |
-
sat_obs = mx_obs.get("satellite") or next(iter(mx_obs.values()))
|
| 484 |
-
sensors_dict = sat_obs.sensors
|
| 485 |
-
targets_dict = sat_obs.targets
|
| 486 |
-
proposals_raw = [
|
| 487 |
-
{"agent_id": p.agent_id, "sensor_id": p.sensor_id,
|
| 488 |
-
"target_id": p.target_id, "capability_score": 0.5}
|
| 489 |
-
for p in proposals
|
| 490 |
-
]
|
| 491 |
-
assigned_sids = {p.sensor_id for p in proposals}
|
| 492 |
-
idle_sensors = [s["id"] for s in sensors_dict if s["id"] not in assigned_sids]
|
| 493 |
-
world_state = {
|
| 494 |
-
"sensors": sensors_dict,
|
| 495 |
-
"targets": targets_dict,
|
| 496 |
-
"idle_sensors": idle_sensors,
|
| 497 |
-
"step": sat_obs.timestep,
|
| 498 |
-
"proposals": proposals_raw,
|
| 499 |
-
}
|
| 500 |
-
|
| 501 |
-
# Run NegotiationLayer (interaction/ package)
|
| 502 |
-
neg_result = _negotiation.negotiate(
|
| 503 |
-
proposals_raw, world_state, lambda tied: tied[0]
|
| 504 |
-
)
|
| 505 |
-
|
| 506 |
-
# Step env with resolved proposals
|
| 507 |
mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals)
|
| 508 |
|
| 509 |
-
#
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
)
|
| 513 |
-
|
| 514 |
-
# Build conflict list from neg_result
|
| 515 |
-
conflicts = [
|
| 516 |
-
{
|
| 517 |
-
"type": str(c.conflict_type),
|
| 518 |
-
"agents": c.involved_agents,
|
| 519 |
-
"sensor_id": c.involved_sensors[0] if c.involved_sensors else None,
|
| 520 |
-
"target_id": c.involved_targets[0] if c.involved_targets else None,
|
| 521 |
-
}
|
| 522 |
-
for c in neg_result.conflicts_detected
|
| 523 |
-
]
|
| 524 |
-
conflict_rate = _negotiation.get_conflict_rate()
|
| 525 |
|
| 526 |
return jsonify({
|
| 527 |
"observations": {k: v.to_dict() for k, v in mx_obs.items()},
|
| 528 |
"step_rewards": step_rewards,
|
| 529 |
-
"per_agent_rewards":
|
| 530 |
"done": done,
|
| 531 |
"info": info,
|
| 532 |
"conflict_rate": round(conflict_rate, 4),
|
| 533 |
"conflicts": conflicts,
|
| 534 |
-
"override_invoked":
|
| 535 |
})
|
| 536 |
|
| 537 |
|
| 538 |
@app.post("/auto_multi")
|
| 539 |
def auto_multi():
|
| 540 |
global mx_obs
|
|
|
|
| 541 |
if mx_obs is None:
|
| 542 |
return jsonify({"error": "Call /reset_multi first"}), 400
|
| 543 |
|
| 544 |
proposals, source = _get_multi_proposals(mx_obs)
|
| 545 |
|
| 546 |
-
#
|
| 547 |
-
sat_obs = mx_obs.get("satellite") or next(iter(mx_obs.values()))
|
| 548 |
-
sensors_dict = sat_obs.sensors
|
| 549 |
-
targets_dict = sat_obs.targets
|
| 550 |
-
proposals_raw = [
|
| 551 |
-
{"agent_id": p.agent_id, "sensor_id": p.sensor_id,
|
| 552 |
-
"target_id": p.target_id, "capability_score": 0.5}
|
| 553 |
-
for p in proposals
|
| 554 |
-
]
|
| 555 |
-
assigned_sids = {p.sensor_id for p in proposals}
|
| 556 |
-
idle_sensors = [s["id"] for s in sensors_dict if s["id"] not in assigned_sids]
|
| 557 |
-
world_state = {
|
| 558 |
-
"sensors": sensors_dict,
|
| 559 |
-
"targets": targets_dict,
|
| 560 |
-
"idle_sensors": idle_sensors,
|
| 561 |
-
"step": sat_obs.timestep,
|
| 562 |
-
"proposals": proposals_raw,
|
| 563 |
-
}
|
| 564 |
-
|
| 565 |
-
# Run NegotiationLayer (interaction/ package)
|
| 566 |
-
neg_result = _negotiation.negotiate(
|
| 567 |
-
proposals_raw, world_state, lambda tied: tied[0]
|
| 568 |
-
)
|
| 569 |
-
|
| 570 |
-
# Step env with resolved proposals (neg_result.final_assignments already validated)
|
| 571 |
mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals)
|
| 572 |
|
| 573 |
-
#
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
)
|
| 577 |
-
|
| 578 |
-
# Build conflict list from neg_result
|
| 579 |
-
conflicts = [
|
| 580 |
-
{
|
| 581 |
-
"type": str(c.conflict_type),
|
| 582 |
-
"agents": c.involved_agents,
|
| 583 |
-
"sensor_id": c.involved_sensors[0] if c.involved_sensors else None,
|
| 584 |
-
"target_id": c.involved_targets[0] if c.involved_targets else None,
|
| 585 |
-
}
|
| 586 |
-
for c in neg_result.conflicts_detected
|
| 587 |
-
]
|
| 588 |
-
conflict_rate = _negotiation.get_conflict_rate()
|
| 589 |
|
| 590 |
return jsonify({
|
| 591 |
"proposals": [{"agent_id": p.agent_id, "sensor_id": p.sensor_id,
|
|
@@ -593,12 +536,12 @@ def auto_multi():
|
|
| 593 |
"agent": source,
|
| 594 |
"observations": {k: v.to_dict() for k, v in mx_obs.items()},
|
| 595 |
"step_rewards": step_rewards,
|
| 596 |
-
"per_agent_rewards":
|
| 597 |
"done": done,
|
| 598 |
"info": info,
|
| 599 |
"conflict_rate": round(conflict_rate, 4),
|
| 600 |
"conflicts": conflicts,
|
| 601 |
-
"override_invoked":
|
| 602 |
})
|
| 603 |
|
| 604 |
|
|
@@ -610,5 +553,7 @@ def ui():
|
|
| 610 |
|
| 611 |
|
| 612 |
if __name__ == "__main__":
|
|
|
|
|
|
|
| 613 |
port = int(os.environ.get("PORT", 7860))
|
| 614 |
-
app.run(host="0.0.0.0", port=port, debug=
|
|
|
|
| 12 |
from env import SentinelEnv
|
| 13 |
from env.models import Action
|
| 14 |
from env.multiagent import AryaXEnv, Proposal, AGENT_TYPES
|
|
|
|
|
|
|
| 15 |
|
| 16 |
app = Flask(__name__)
|
| 17 |
|
|
|
|
| 21 |
_target_positions: dict = {}
|
| 22 |
|
| 23 |
# ── Multi-agent env ───────────────────────────────────────────────────────────
|
| 24 |
+
mx_env = AryaXEnv(max_steps=10, seed=42)
|
| 25 |
+
mx_obs = None # Dict[str, AgentObservation] | None
|
|
|
|
|
|
|
| 26 |
|
| 27 |
# ── LLM client (optional) ─────────────────────────────────────────────────────
|
| 28 |
_llm_client = None
|
|
|
|
| 194 |
f" - id={t['id']} priority={t['priority']} active={t['active']}"
|
| 195 |
for t in agent_obs.targets if t["active"]
|
| 196 |
)
|
| 197 |
+
if not my_sensors:
|
| 198 |
+
return ""
|
| 199 |
+
sensor_ids = [s['id'] for s in my_sensors]
|
| 200 |
+
target_ids = [t['id'] for t in agent_obs.targets if t['active']]
|
| 201 |
+
return f"""You are the {agent_id} agent. Assign ONLY your sensors to threats.
|
| 202 |
+
Your sensors: {sensor_ids}
|
| 203 |
+
Active threat IDs: {target_ids}
|
| 204 |
+
Priority 3=HIGH, 2=MED, 1=LOW. Cover HIGH first. Each sensor to a DIFFERENT target.
|
| 205 |
|
| 206 |
Timestep: {agent_obs.timestep}
|
|
|
|
| 207 |
|
| 208 |
+
Your Sensors:
|
| 209 |
{sensors}
|
| 210 |
|
| 211 |
Active Threats:
|
| 212 |
{targets}
|
| 213 |
|
| 214 |
+
Respond ONLY with a JSON array using ONLY the sensor IDs and target IDs listed above:
|
| 215 |
+
[{{"sensor_id": "{sensor_ids[0]}", "target_id": "{target_ids[0] if target_ids else 'T0_1'}"}}]
|
| 216 |
"""
|
| 217 |
|
| 218 |
|
| 219 |
+
def _greedy_proposals(agent_id: str, agent_obs, used_sensors: set, used_targets: set = None) -> list[Proposal]:
|
| 220 |
"""Greedy proposals for one agent — only claim sensors matching agent type."""
|
| 221 |
+
if used_targets is None:
|
| 222 |
+
used_targets = set()
|
| 223 |
my_sensors = [
|
| 224 |
s for s in agent_obs.sensors
|
| 225 |
if s["available"] and s["id"] not in used_sensors
|
|
|
|
| 229 |
[t for t in agent_obs.targets if t["active"]],
|
| 230 |
key=lambda t: -t["priority"]
|
| 231 |
)
|
| 232 |
+
proposals = []
|
| 233 |
for sensor in my_sensors:
|
| 234 |
for target in targets:
|
| 235 |
if target["id"] not in used_targets:
|
|
|
|
| 284 |
|
| 285 |
def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
|
| 286 |
"""Build proposals from all agents. Returns (proposals, source)."""
|
| 287 |
+
import sys
|
| 288 |
proposals: list[Proposal] = []
|
| 289 |
used_sensors: set = set()
|
| 290 |
+
print(f"[_get_multi_proposals] called, llm_client={_llm_client is not None}", flush=True)
|
| 291 |
|
| 292 |
if _has_adapters:
|
| 293 |
return _lora_multi_proposals(agent_obs_map)
|
|
|
|
| 297 |
for agent_id in AGENT_TYPES:
|
| 298 |
agent_obs = agent_obs_map[agent_id]
|
| 299 |
prompt = _build_multi_prompt(agent_id, agent_obs)
|
| 300 |
+
if not prompt:
|
| 301 |
+
continue
|
| 302 |
+
print(f"[LLM multi] calling LLM for {agent_id}", flush=True)
|
| 303 |
response = _llm_client.chat.completions.create(
|
| 304 |
model=MODEL_NAME,
|
| 305 |
messages=[{"role": "user", "content": prompt}],
|
|
|
|
| 307 |
temperature=0.0
|
| 308 |
)
|
| 309 |
raw = response.choices[0].message.content.strip()
|
| 310 |
+
print(f"[LLM multi] {agent_id} response: {raw[:200]}", flush=True)
|
| 311 |
start, end = raw.find("["), raw.rfind("]") + 1
|
| 312 |
+
if start == -1 or end == 0:
|
| 313 |
+
print(f"[LLM multi] {agent_id} no JSON array found", flush=True)
|
| 314 |
+
continue
|
| 315 |
items = json.loads(raw[start:end])
|
| 316 |
+
valid_sensors = {
|
| 317 |
+
s["id"] for s in agent_obs.sensors
|
| 318 |
+
if s["available"] and (agent_id == "command" or s["type"] == agent_id)
|
| 319 |
+
}
|
| 320 |
valid_targets = {t["id"] for t in agent_obs.targets if t["active"]}
|
| 321 |
for item in items:
|
| 322 |
sid, tid = item.get("sensor_id"), item.get("target_id")
|
| 323 |
+
if sid in valid_sensors and tid in valid_targets and sid not in used_sensors:
|
| 324 |
proposals.append(Proposal(agent_id=agent_id, sensor_id=sid, target_id=tid))
|
| 325 |
used_sensors.add(sid)
|
| 326 |
if proposals:
|
| 327 |
+
print(f"[LLM multi] Generated {len(proposals)} proposals", flush=True)
|
| 328 |
return proposals, "llm"
|
| 329 |
+
print("[LLM multi] No valid proposals, falling back to greedy", flush=True)
|
| 330 |
except Exception as e:
|
| 331 |
+
print(f"[LLM multi] Error: {e}. Falling back to greedy.", flush=True)
|
| 332 |
|
| 333 |
+
# Greedy fallback — partition both sensors and targets across agents
|
| 334 |
used_sensors = set()
|
| 335 |
+
used_targets: set = set()
|
| 336 |
for agent_id in AGENT_TYPES:
|
| 337 |
agent_obs = agent_obs_map[agent_id]
|
| 338 |
+
for p in _greedy_proposals(agent_id, agent_obs, used_sensors, used_targets):
|
| 339 |
proposals.append(p)
|
| 340 |
used_sensors.add(p.sensor_id)
|
| 341 |
+
used_targets.add(p.target_id)
|
| 342 |
|
| 343 |
return proposals, "greedy"
|
| 344 |
|
|
|
|
| 467 |
mx_env.seed = seed
|
| 468 |
mx_env.max_steps = max_steps
|
| 469 |
mx_obs = mx_env.reset()
|
|
|
|
|
|
|
| 470 |
return jsonify({
|
| 471 |
"seed": seed,
|
| 472 |
"max_steps": max_steps,
|
|
|
|
| 495 |
if p.get("agent_id") and p.get("sensor_id") and p.get("target_id")
|
| 496 |
]
|
| 497 |
|
| 498 |
+
# Step env with proposals — env runs negotiation internally
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals)
|
| 500 |
|
| 501 |
+
# Extract conflict info from env's internal negotiation layer
|
| 502 |
+
conflict_rate = mx_env.negotiation.get_conflict_rate()
|
| 503 |
+
conflicts = info.get("conflicts", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
|
| 505 |
return jsonify({
|
| 506 |
"observations": {k: v.to_dict() for k, v in mx_obs.items()},
|
| 507 |
"step_rewards": step_rewards,
|
| 508 |
+
"per_agent_rewards": dict(mx_env._agent_rewards),
|
| 509 |
"done": done,
|
| 510 |
"info": info,
|
| 511 |
"conflict_rate": round(conflict_rate, 4),
|
| 512 |
"conflicts": conflicts,
|
| 513 |
+
"override_invoked": False,
|
| 514 |
})
|
| 515 |
|
| 516 |
|
| 517 |
@app.post("/auto_multi")
|
| 518 |
def auto_multi():
|
| 519 |
global mx_obs
|
| 520 |
+
print(f"[auto_multi] called, mx_obs is None: {mx_obs is None}", flush=True)
|
| 521 |
if mx_obs is None:
|
| 522 |
return jsonify({"error": "Call /reset_multi first"}), 400
|
| 523 |
|
| 524 |
proposals, source = _get_multi_proposals(mx_obs)
|
| 525 |
|
| 526 |
+
# Step env with proposals — env runs negotiation internally
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals)
|
| 528 |
|
| 529 |
+
# Extract conflict info from env's internal negotiation layer
|
| 530 |
+
conflict_rate = mx_env.negotiation.get_conflict_rate()
|
| 531 |
+
conflicts = info.get("conflicts", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
return jsonify({
|
| 534 |
"proposals": [{"agent_id": p.agent_id, "sensor_id": p.sensor_id,
|
|
|
|
| 536 |
"agent": source,
|
| 537 |
"observations": {k: v.to_dict() for k, v in mx_obs.items()},
|
| 538 |
"step_rewards": step_rewards,
|
| 539 |
+
"per_agent_rewards": dict(mx_env._agent_rewards),
|
| 540 |
"done": done,
|
| 541 |
"info": info,
|
| 542 |
"conflict_rate": round(conflict_rate, 4),
|
| 543 |
"conflicts": conflicts,
|
| 544 |
+
"override_invoked": False,
|
| 545 |
})
|
| 546 |
|
| 547 |
|
|
|
|
| 553 |
|
| 554 |
|
| 555 |
if __name__ == "__main__":
|
| 556 |
+
import logging
|
| 557 |
+
logging.basicConfig(level=logging.INFO)
|
| 558 |
port = int(os.environ.get("PORT", 7860))
|
| 559 |
+
app.run(host="0.0.0.0", port=port, debug=True, use_reloader=False)
|
tasks/easy_task.py
CHANGED
|
@@ -28,3 +28,8 @@ def get_easy_task():
|
|
| 28 |
def get_easy_env():
|
| 29 |
from env import SentinelEnv
|
| 30 |
return SentinelEnv(max_steps=EASY_TASK_CONFIG["max_steps"], seed=42, config=EASY_TASK_CONFIG)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
def get_easy_env():
|
| 29 |
from env import SentinelEnv
|
| 30 |
return SentinelEnv(max_steps=EASY_TASK_CONFIG["max_steps"], seed=42, config=EASY_TASK_CONFIG)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def get_easy_multi_env():
|
| 34 |
+
from env.multiagent import AryaXEnv
|
| 35 |
+
return AryaXEnv(max_steps=EASY_TASK_CONFIG["max_steps"], seed=42, density_factor=1.5)
|
tasks/grader.py
CHANGED
|
@@ -127,100 +127,79 @@ def run_deterministic_eval(
|
|
| 127 |
"""
|
| 128 |
try:
|
| 129 |
from env.environment import SentinelEnv
|
| 130 |
-
from
|
| 131 |
-
from tasks.
|
| 132 |
-
from tasks.
|
|
|
|
| 133 |
except ImportError as exc:
|
| 134 |
raise ImportError(f"env/ or tasks/ package not found: {exc}") from exc
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
results: dict = {}
|
| 143 |
all_scores: list[float] = []
|
| 144 |
all_conflict_rates: list[float] = []
|
| 145 |
|
| 146 |
for task_name, get_env_fn, seed in TASKS:
|
| 147 |
-
env: SentinelEnv = get_env_fn()
|
| 148 |
-
obs = env.reset()
|
| 149 |
-
|
| 150 |
if mode == "multi":
|
| 151 |
-
# Multi-agent mode — uses
|
| 152 |
try:
|
| 153 |
from interaction import NegotiationLayer
|
| 154 |
from interaction.reward import RewardEngine
|
| 155 |
except ImportError as exc:
|
| 156 |
raise ImportError(f"interaction/ package not found: {exc}") from exc
|
| 157 |
|
|
|
|
|
|
|
| 158 |
nl = NegotiationLayer()
|
| 159 |
reward_eng = RewardEngine()
|
| 160 |
-
agent_ids = ["
|
| 161 |
total_rewards: dict[str, float] = {aid: 0.0 for aid in agent_ids}
|
| 162 |
done = False
|
| 163 |
|
| 164 |
rng = __import__("random").Random(seed)
|
| 165 |
|
| 166 |
while not done:
|
| 167 |
-
sensors_list = [
|
| 168 |
-
{"id": s.id, "type": s.type, "range": s.range, "available": s.available}
|
| 169 |
-
for s in obs.sensors
|
| 170 |
-
]
|
| 171 |
-
targets_list = [
|
| 172 |
-
{"id": t.id, "priority": t.priority, "active": t.active, "type": "strategic"}
|
| 173 |
-
for t in obs.targets
|
| 174 |
-
]
|
| 175 |
-
|
| 176 |
# Greedy proposals per agent
|
|
|
|
| 177 |
proposals = []
|
| 178 |
-
available = [s for s in sensors_list if s.get("available", True)]
|
| 179 |
-
active_t = sorted(
|
| 180 |
-
[t for t in targets_list if t.get("active", True)],
|
| 181 |
-
key=lambda t: -t.get("priority", 0),
|
| 182 |
-
)
|
| 183 |
used_sensors: set[str] = set()
|
| 184 |
used_targets: set[str] = set()
|
| 185 |
-
for
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
if
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
def _cmd_fn(tied):
|
| 212 |
-
return max(tied, key=lambda p: p.get("capability_score", 0.0))
|
| 213 |
-
|
| 214 |
-
neg_result = nl.negotiate(proposals, world_state, _cmd_fn)
|
| 215 |
-
env_actions = [
|
| 216 |
-
{"sensor_id": a["sensor_id"], "target_id": a["target_id"]}
|
| 217 |
-
for a in neg_result.final_assignments
|
| 218 |
-
]
|
| 219 |
-
obs, _, done, _ = env.step_batch(env_actions)
|
| 220 |
-
|
| 221 |
-
step_r = reward_eng.compute_step_reward(
|
| 222 |
-
neg_result.final_assignments, neg_result, world_state, agent_ids
|
| 223 |
-
)
|
| 224 |
for aid, r in step_r.items():
|
| 225 |
total_rewards[aid] += r
|
| 226 |
|
|
@@ -228,7 +207,7 @@ def run_deterministic_eval(
|
|
| 228 |
per_agent_rewards=total_rewards,
|
| 229 |
step_count=env.max_steps,
|
| 230 |
num_sensors=env.initial_sensor_count,
|
| 231 |
-
negotiation_layer=
|
| 232 |
num_agents=4,
|
| 233 |
)
|
| 234 |
all_scores.append(task_result["overall_score"])
|
|
@@ -236,6 +215,8 @@ def run_deterministic_eval(
|
|
| 236 |
|
| 237 |
else:
|
| 238 |
# Single-agent mode — uses greedy baseline policy
|
|
|
|
|
|
|
| 239 |
try:
|
| 240 |
from agent.policy import select_action
|
| 241 |
except ImportError:
|
|
|
|
| 127 |
"""
|
| 128 |
try:
|
| 129 |
from env.environment import SentinelEnv
|
| 130 |
+
from env.multiagent import AryaXEnv
|
| 131 |
+
from tasks.easy_task import get_easy_env, get_easy_multi_env
|
| 132 |
+
from tasks.medium_task import get_medium_env, get_medium_multi_env
|
| 133 |
+
from tasks.hard_task import get_hard_env, get_hard_multi_env
|
| 134 |
except ImportError as exc:
|
| 135 |
raise ImportError(f"env/ or tasks/ package not found: {exc}") from exc
|
| 136 |
|
| 137 |
+
if mode == "multi":
|
| 138 |
+
TASKS = [
|
| 139 |
+
("easy", get_easy_multi_env, 42),
|
| 140 |
+
("medium", get_medium_multi_env, 7),
|
| 141 |
+
("hard", get_hard_multi_env, 13),
|
| 142 |
+
]
|
| 143 |
+
else:
|
| 144 |
+
TASKS = [
|
| 145 |
+
("easy", get_easy_env, 42),
|
| 146 |
+
("medium", get_medium_env, 7),
|
| 147 |
+
("hard", get_hard_env, 13),
|
| 148 |
+
]
|
| 149 |
|
| 150 |
results: dict = {}
|
| 151 |
all_scores: list[float] = []
|
| 152 |
all_conflict_rates: list[float] = []
|
| 153 |
|
| 154 |
for task_name, get_env_fn, seed in TASKS:
|
|
|
|
|
|
|
|
|
|
| 155 |
if mode == "multi":
|
| 156 |
+
# Multi-agent mode — uses AryaXEnv + NegotiationLayer
|
| 157 |
try:
|
| 158 |
from interaction import NegotiationLayer
|
| 159 |
from interaction.reward import RewardEngine
|
| 160 |
except ImportError as exc:
|
| 161 |
raise ImportError(f"interaction/ package not found: {exc}") from exc
|
| 162 |
|
| 163 |
+
env: AryaXEnv = get_env_fn()
|
| 164 |
+
obs_map = env.reset()
|
| 165 |
nl = NegotiationLayer()
|
| 166 |
reward_eng = RewardEngine()
|
| 167 |
+
agent_ids = ["satellite", "drone", "radar", "command"]
|
| 168 |
total_rewards: dict[str, float] = {aid: 0.0 for aid in agent_ids}
|
| 169 |
done = False
|
| 170 |
|
| 171 |
rng = __import__("random").Random(seed)
|
| 172 |
|
| 173 |
while not done:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
# Greedy proposals per agent
|
| 175 |
+
from env.multiagent import Proposal
|
| 176 |
proposals = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
used_sensors: set[str] = set()
|
| 178 |
used_targets: set[str] = set()
|
| 179 |
+
for agent_id in agent_ids:
|
| 180 |
+
agent_obs = obs_map[agent_id]
|
| 181 |
+
my_sensors = [
|
| 182 |
+
s for s in agent_obs.sensors
|
| 183 |
+
if s["available"] and s["id"] not in used_sensors
|
| 184 |
+
and (agent_id == "command" or s["type"] == agent_id)
|
| 185 |
+
]
|
| 186 |
+
targets = sorted(
|
| 187 |
+
[t for t in agent_obs.targets if t["active"]],
|
| 188 |
+
key=lambda t: -t["priority"]
|
| 189 |
+
)
|
| 190 |
+
for sensor in my_sensors:
|
| 191 |
+
for target in targets:
|
| 192 |
+
if target["id"] not in used_targets:
|
| 193 |
+
proposals.append(Proposal(
|
| 194 |
+
agent_id=agent_id,
|
| 195 |
+
sensor_id=sensor["id"],
|
| 196 |
+
target_id=target["id"]
|
| 197 |
+
))
|
| 198 |
+
used_sensors.add(sensor["id"])
|
| 199 |
+
used_targets.add(target["id"])
|
| 200 |
+
break
|
| 201 |
+
|
| 202 |
+
obs_map, step_r, done, info = env.step_multiagent(proposals)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
for aid, r in step_r.items():
|
| 204 |
total_rewards[aid] += r
|
| 205 |
|
|
|
|
| 207 |
per_agent_rewards=total_rewards,
|
| 208 |
step_count=env.max_steps,
|
| 209 |
num_sensors=env.initial_sensor_count,
|
| 210 |
+
negotiation_layer=env.negotiation,
|
| 211 |
num_agents=4,
|
| 212 |
)
|
| 213 |
all_scores.append(task_result["overall_score"])
|
|
|
|
| 215 |
|
| 216 |
else:
|
| 217 |
# Single-agent mode — uses greedy baseline policy
|
| 218 |
+
env: SentinelEnv = get_env_fn()
|
| 219 |
+
obs = env.reset()
|
| 220 |
try:
|
| 221 |
from agent.policy import select_action
|
| 222 |
except ImportError:
|
tasks/hard_task.py
CHANGED
|
@@ -61,3 +61,8 @@ def get_hard_task():
|
|
| 61 |
def get_hard_env():
|
| 62 |
from env import SentinelEnv
|
| 63 |
return SentinelEnv(max_steps=HARD_TASK_CONFIG["max_steps"], seed=13, config=HARD_TASK_CONFIG)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def get_hard_env():
|
| 62 |
from env import SentinelEnv
|
| 63 |
return SentinelEnv(max_steps=HARD_TASK_CONFIG["max_steps"], seed=13, config=HARD_TASK_CONFIG)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def get_hard_multi_env():
|
| 67 |
+
from env.multiagent import AryaXEnv
|
| 68 |
+
return AryaXEnv(max_steps=HARD_TASK_CONFIG["max_steps"], seed=13, density_factor=4.0, failure_prob=0.15, conflict_injection=True)
|
tasks/medium_task.py
CHANGED
|
@@ -45,3 +45,8 @@ def get_medium_task():
|
|
| 45 |
def get_medium_env():
|
| 46 |
from env import SentinelEnv
|
| 47 |
return SentinelEnv(max_steps=MEDIUM_TASK_CONFIG["max_steps"], seed=7, config=MEDIUM_TASK_CONFIG)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
def get_medium_env():
|
| 46 |
from env import SentinelEnv
|
| 47 |
return SentinelEnv(max_steps=MEDIUM_TASK_CONFIG["max_steps"], seed=7, config=MEDIUM_TASK_CONFIG)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def get_medium_multi_env():
|
| 51 |
+
from env.multiagent import AryaXEnv
|
| 52 |
+
return AryaXEnv(max_steps=MEDIUM_TASK_CONFIG["max_steps"], seed=7, density_factor=2.5, conflict_injection=True)
|
test_integration.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ARYA-X Full Integration Test
|
| 3 |
+
Tests every module connection described in the spec.
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import traceback
|
| 7 |
+
|
| 8 |
+
PASS = []
|
| 9 |
+
FAIL = []
|
| 10 |
+
|
| 11 |
+
def check(name, fn):
|
| 12 |
+
try:
|
| 13 |
+
fn()
|
| 14 |
+
PASS.append(name)
|
| 15 |
+
print(f" PASS {name}")
|
| 16 |
+
except Exception as e:
|
| 17 |
+
FAIL.append((name, str(e)))
|
| 18 |
+
print(f" FAIL {name}")
|
| 19 |
+
print(f" {e}")
|
| 20 |
+
|
| 21 |
+
# ── 1. Core imports ────────────────────────────────────────────────────────────
|
| 22 |
+
check("env/__init__ exports SentinelEnv", lambda: __import__("env").SentinelEnv)
|
| 23 |
+
check("env.models all 6 models", lambda: [
|
| 24 |
+
getattr(__import__("env.models", fromlist=["Sensor"]), x)
|
| 25 |
+
for x in ["Sensor","Target","Observation","Action","Proposal","AgentObservation","ConflictRecord","EpisodeMetrics"]
|
| 26 |
+
])
|
| 27 |
+
check("env.dynamics all 4 functions", lambda: [
|
| 28 |
+
getattr(__import__("env.dynamics", fromlist=["initialize_sensors"]), x)
|
| 29 |
+
for x in ["initialize_sensors","spawn_targets","spawn_targets_stochastic","apply_correlated_failures"]
|
| 30 |
+
])
|
| 31 |
+
check("env.world_model all 3 functions", lambda: [
|
| 32 |
+
getattr(__import__("env.world_model", fromlist=["add_observation_noise"]), x)
|
| 33 |
+
for x in ["add_observation_noise","apply_mask","get_priority_mapping"]
|
| 34 |
+
])
|
| 35 |
+
check("interaction package all 4 classes", lambda: [
|
| 36 |
+
getattr(__import__("interaction", fromlist=["NegotiationLayer"]), x)
|
| 37 |
+
for x in ["NegotiationLayer","ConflictDetector","ConflictResolver","RewardEngine"]
|
| 38 |
+
])
|
| 39 |
+
check("agents package all 4 agents", lambda: [
|
| 40 |
+
getattr(__import__("agents", fromlist=["SatelliteAgent"]), x)
|
| 41 |
+
for x in ["SatelliteAgent","DroneAgent","RadarAgent","CommandAgent"]
|
| 42 |
+
])
|
| 43 |
+
check("curriculum.CurriculumEngine", lambda: __import__("curriculum").CurriculumEngine)
|
| 44 |
+
check("trainer.ARYAXTrainer importable", lambda: __import__("trainer").ARYAXTrainer)
|
| 45 |
+
check("tasks.grader all 3 functions", lambda: [
|
| 46 |
+
getattr(__import__("tasks.grader", fromlist=["grade_episode"]), x)
|
| 47 |
+
for x in ["grade_episode","grade_multi_agent_episode","run_deterministic_eval"]
|
| 48 |
+
])
|
| 49 |
+
|
| 50 |
+
# ── 2. env/environment.py — both AryaXEnv classes ─────────────────────────────
|
| 51 |
+
check("env.environment.SentinelEnv exists", lambda: __import__("env.environment", fromlist=["SentinelEnv"]).SentinelEnv)
|
| 52 |
+
check("env.environment.AryaXEnv (rich) exists", lambda: __import__("env.environment", fromlist=["AryaXEnv"]).AryaXEnv)
|
| 53 |
+
check("env.multiagent.AryaXEnv (server) exists", lambda: __import__("env.multiagent", fromlist=["AryaXEnv"]).AryaXEnv)
|
| 54 |
+
|
| 55 |
+
# ── 3. SentinelEnv full episode ────────────────────────────────────────────────
|
| 56 |
+
def _sentinel_episode():
|
| 57 |
+
from env.environment import SentinelEnv
|
| 58 |
+
env = SentinelEnv(max_steps=3, seed=42)
|
| 59 |
+
obs = env.reset()
|
| 60 |
+
assert obs.sensors and obs.targets
|
| 61 |
+
from env.models import Action
|
| 62 |
+
obs, r, done, info = env.step(Action(sensor_id=obs.sensors[0].id, target_id=obs.targets[0].id))
|
| 63 |
+
assert "step_count" in info
|
| 64 |
+
check("SentinelEnv reset/step/info", _sentinel_episode)
|
| 65 |
+
|
| 66 |
+
# ── 4. env.environment.AryaXEnv (rich) — partial obs + world model ────────────
|
| 67 |
+
def _rich_aryax():
|
| 68 |
+
from env.environment import AryaXEnv
|
| 69 |
+
env = AryaXEnv(max_steps=3, seed=42, density_factor=2.5, failure_prob=0.1)
|
| 70 |
+
obs_map = env.reset()
|
| 71 |
+
assert set(obs_map.keys()) == {"satellite","drone","radar","command"}
|
| 72 |
+
# Each agent gets different target counts (partial obs)
|
| 73 |
+
sat_t = len(obs_map["satellite"].targets)
|
| 74 |
+
drone_t = len(obs_map["drone"].targets)
|
| 75 |
+
# satellite sees all, drone sees subset — may differ
|
| 76 |
+
assert sat_t >= 0 and drone_t >= 0
|
| 77 |
+
check("env.environment.AryaXEnv partial obs + world model", _rich_aryax)
|
| 78 |
+
|
| 79 |
+
# ── 5. env.multiagent.AryaXEnv (server) — interaction/ pipeline ───────────────
|
| 80 |
+
def _server_aryax():
|
| 81 |
+
from env.multiagent import AryaXEnv, Proposal
|
| 82 |
+
env = AryaXEnv(max_steps=3, seed=42)
|
| 83 |
+
obs = env.reset()
|
| 84 |
+
proposals = [
|
| 85 |
+
Proposal(agent_id="satellite", sensor_id=obs["satellite"].sensors[0]["id"],
|
| 86 |
+
target_id=obs["satellite"].targets[0]["id"]),
|
| 87 |
+
]
|
| 88 |
+
obs2, rewards, done, info = env.step_multiagent(proposals)
|
| 89 |
+
assert "conflict_rate" in info
|
| 90 |
+
assert "step_rewards" in info
|
| 91 |
+
assert set(rewards.keys()) == {"satellite","drone","radar","command"}
|
| 92 |
+
check("env.multiagent.AryaXEnv step_multiagent + conflict_rate", _server_aryax)
|
| 93 |
+
|
| 94 |
+
# ── 6. interaction/ pipeline end-to-end ───────────────────────────────────────
|
| 95 |
+
def _interaction_pipeline():
|
| 96 |
+
from interaction import NegotiationLayer
|
| 97 |
+
from interaction.reward import RewardEngine
|
| 98 |
+
nl = NegotiationLayer()
|
| 99 |
+
re = RewardEngine()
|
| 100 |
+
proposals = [
|
| 101 |
+
{"agent_id":"satellite","sensor_id":"S1","target_id":"T0_1","capability_score":0.9},
|
| 102 |
+
{"agent_id":"drone", "sensor_id":"S2","target_id":"T0_1","capability_score":0.5}, # conflict
|
| 103 |
+
]
|
| 104 |
+
world_state = {
|
| 105 |
+
"sensors": [{"id":"S1","type":"satellite","range":500,"available":True},
|
| 106 |
+
{"id":"S2","type":"drone","range":100,"available":True}],
|
| 107 |
+
"targets": [{"id":"T0_1","priority":3,"active":True,"type":"strategic"},
|
| 108 |
+
{"id":"T0_2","priority":1,"active":True,"type":"airspace"}],
|
| 109 |
+
"idle_sensors": [],
|
| 110 |
+
"step": 0,
|
| 111 |
+
"proposals": proposals,
|
| 112 |
+
}
|
| 113 |
+
result = nl.negotiate(proposals, world_state, lambda tied: max(tied, key=lambda p: p.get("capability_score",0)))
|
| 114 |
+
assert len(result.conflicts_detected) > 0, "Expected conflict"
|
| 115 |
+
assert nl.get_conflict_rate() > 0
|
| 116 |
+
rewards = re.compute_step_reward(result.final_assignments, result, world_state, ["satellite","drone","radar","command"])
|
| 117 |
+
assert isinstance(rewards, dict)
|
| 118 |
+
check("interaction/ NegotiationLayer + RewardEngine pipeline", _interaction_pipeline)
|
| 119 |
+
|
| 120 |
+
# ── 7. agents/ — observe/propose interface ────────────────────────────────────
|
| 121 |
+
def _agents_propose():
|
| 122 |
+
from agents import SatelliteAgent, DroneAgent, RadarAgent, CommandAgent
|
| 123 |
+
from env.models import AgentObservation, Sensor, Target
|
| 124 |
+
sensors = [Sensor(id="S1", type="satellite", range=500, available=True)]
|
| 125 |
+
targets = [Target(id="T0_1", priority=3, active=True),
|
| 126 |
+
Target(id="T0_2", priority=2, active=True)]
|
| 127 |
+
obs = AgentObservation(agent_id="satellite", agent_type="satellite",
|
| 128 |
+
sensors=sensors, targets=targets, timestep=0)
|
| 129 |
+
sat = SatelliteAgent()
|
| 130 |
+
sat.observe(obs)
|
| 131 |
+
props = sat.propose()
|
| 132 |
+
assert isinstance(props, list)
|
| 133 |
+
|
| 134 |
+
drone_obs = AgentObservation(agent_id="drone", agent_type="drone",
|
| 135 |
+
sensors=[Sensor(id="S2",type="drone",range=100,available=True)],
|
| 136 |
+
targets=targets, timestep=0)
|
| 137 |
+
d = DroneAgent()
|
| 138 |
+
d.observe(drone_obs)
|
| 139 |
+
assert isinstance(d.propose(), list)
|
| 140 |
+
|
| 141 |
+
radar_obs = AgentObservation(agent_id="radar", agent_type="radar",
|
| 142 |
+
sensors=[Sensor(id="S3",type="radar",range=300,available=True)],
|
| 143 |
+
targets=targets, timestep=0)
|
| 144 |
+
r = RadarAgent()
|
| 145 |
+
r.observe(radar_obs)
|
| 146 |
+
assert isinstance(r.propose(), list)
|
| 147 |
+
|
| 148 |
+
cmd_obs = AgentObservation(agent_id="command", agent_type="command",
|
| 149 |
+
sensors=sensors, targets=targets, timestep=0)
|
| 150 |
+
c = CommandAgent()
|
| 151 |
+
c.observe(cmd_obs)
|
| 152 |
+
assert isinstance(c.propose(), list)
|
| 153 |
+
check("agents/ all 4 observe()+propose() callable", _agents_propose)
|
| 154 |
+
|
| 155 |
+
# ── 8. CommandAgent.observe() accepts proposals kwarg ─────────────────────────
|
| 156 |
+
def _cmd_proposals_kwarg():
|
| 157 |
+
from agents import CommandAgent
|
| 158 |
+
from env.models import AgentObservation, Sensor, Target, Proposal
|
| 159 |
+
sensors = [Sensor(id="S1",type="satellite",range=500,available=True)]
|
| 160 |
+
targets = [Target(id="T0_1",priority=3,active=True)]
|
| 161 |
+
obs = AgentObservation(agent_id="command",agent_type="command",
|
| 162 |
+
sensors=sensors,targets=targets,timestep=0)
|
| 163 |
+
existing = [Proposal(sensor_id="S1",target_id="T0_1",agent_id="satellite",
|
| 164 |
+
priority_estimate=3,confidence=0.9)]
|
| 165 |
+
c = CommandAgent()
|
| 166 |
+
c.observe(obs, proposals=existing)
|
| 167 |
+
props = c.propose()
|
| 168 |
+
assert isinstance(props, list)
|
| 169 |
+
check("CommandAgent.observe(obs, proposals=...) signature", _cmd_proposals_kwarg)
|
| 170 |
+
|
| 171 |
+
# ── 9. DroneAgent theory-of-mind (loss tracking) ──────────────────────────────
|
| 172 |
+
def _drone_tom():
|
| 173 |
+
from agents import DroneAgent
|
| 174 |
+
from env.models import AgentObservation, Sensor, Target, ConflictRecord
|
| 175 |
+
sensors = [Sensor(id="S2",type="drone",range=100,available=True)]
|
| 176 |
+
targets = [Target(id="T0_2",priority=2,active=True)]
|
| 177 |
+
obs = AgentObservation(agent_id="drone",agent_type="drone",
|
| 178 |
+
sensors=sensors,targets=targets,timestep=0)
|
| 179 |
+
d = DroneAgent()
|
| 180 |
+
d.observe(obs)
|
| 181 |
+
# Simulate 3 losses to radar on redundant_coverage
|
| 182 |
+
d.losses["redundant_coverage"] = 3
|
| 183 |
+
props = d.propose()
|
| 184 |
+
# Boundary target (even index) should have reduced confidence
|
| 185 |
+
boundary_props = [p for p in props if d._is_boundary_target(p.target_id)]
|
| 186 |
+
if boundary_props:
|
| 187 |
+
assert boundary_props[0].confidence <= 0.5
|
| 188 |
+
check("DroneAgent theory-of-mind confidence reduction", _drone_tom)
|
| 189 |
+
|
| 190 |
+
# ── 10. CurriculumEngine → AryaXEnv config connection ─────────────────────────
|
| 191 |
+
def _curriculum_to_env():
|
| 192 |
+
from curriculum import CurriculumEngine
|
| 193 |
+
from env.multiagent import AryaXEnv
|
| 194 |
+
ce = CurriculumEngine()
|
| 195 |
+
cfg = ce.get_scenario_config()
|
| 196 |
+
assert "max_steps" in cfg
|
| 197 |
+
assert "sensor_failure_prob" in cfg
|
| 198 |
+
env = AryaXEnv(
|
| 199 |
+
max_steps=cfg["max_steps"],
|
| 200 |
+
seed=42,
|
| 201 |
+
density_factor=1.5 + ce.difficulty_level * 2.5,
|
| 202 |
+
failure_prob=cfg["sensor_failure_prob"],
|
| 203 |
+
)
|
| 204 |
+
obs = env.reset()
|
| 205 |
+
assert obs is not None
|
| 206 |
+
check("CurriculumEngine config -> AryaXEnv constructor", _curriculum_to_env)
|
| 207 |
+
|
| 208 |
+
# ── 11. grader.grade_multi_agent_episode ──────────────────────────────────────
|
| 209 |
+
def _grader_multi():
|
| 210 |
+
from tasks.grader import grade_multi_agent_episode
|
| 211 |
+
from interaction import NegotiationLayer
|
| 212 |
+
nl = NegotiationLayer()
|
| 213 |
+
result = grade_multi_agent_episode(
|
| 214 |
+
per_agent_rewards={"SAT":10.0,"UAV":5.0,"RDR":3.0,"CMD":2.0},
|
| 215 |
+
step_count=20,
|
| 216 |
+
num_sensors=4,
|
| 217 |
+
negotiation_layer=nl,
|
| 218 |
+
num_agents=4,
|
| 219 |
+
)
|
| 220 |
+
assert "overall_score" in result
|
| 221 |
+
assert "coordination_score" in result
|
| 222 |
+
assert "conflict_rate" in result
|
| 223 |
+
check("grader.grade_multi_agent_episode", _grader_multi)
|
| 224 |
+
|
| 225 |
+
# ── 12. server.py imports cleanly ─────────────────────────────────────────────
|
| 226 |
+
check("server.py imports without error", lambda: __import__("server"))
|
| 227 |
+
|
| 228 |
+
# ── 13. inference.py imports + run_multi_task callable ────────────────────────
|
| 229 |
+
def _inference_multi():
|
| 230 |
+
import inference
|
| 231 |
+
assert callable(inference.run_multi_task)
|
| 232 |
+
assert callable(inference._greedy_multi_proposals)
|
| 233 |
+
check("inference.py run_multi_task callable", _inference_multi)
|
| 234 |
+
|
| 235 |
+
# ── 14. trainer.py ARYAXTrainer instantiates (no model) ──────────────────────
|
| 236 |
+
def _trainer_init():
|
| 237 |
+
from trainer import ARYAXTrainer
|
| 238 |
+
# Should not raise even without GPU/model
|
| 239 |
+
t = ARYAXTrainer.__new__(ARYAXTrainer)
|
| 240 |
+
t.curriculum = __import__("curriculum").CurriculumEngine()
|
| 241 |
+
from interaction import NegotiationLayer
|
| 242 |
+
from interaction.reward import RewardEngine
|
| 243 |
+
t.negotiation = NegotiationLayer()
|
| 244 |
+
t.reward_engine = RewardEngine()
|
| 245 |
+
t._grpo_trainer = None
|
| 246 |
+
assert t._grpo_trainer is None
|
| 247 |
+
check("ARYAXTrainer attributes accessible", _trainer_init)
|
| 248 |
+
|
| 249 |
+
# ── 15. tasks/ easy/medium/hard envs return SentinelEnv ───────────────────────
|
| 250 |
+
def _task_envs():
|
| 251 |
+
from tasks.easy_task import get_easy_env
|
| 252 |
+
from tasks.medium_task import get_medium_env
|
| 253 |
+
from tasks.hard_task import get_hard_env
|
| 254 |
+
from env.environment import SentinelEnv
|
| 255 |
+
for fn in [get_easy_env, get_medium_env, get_hard_env]:
|
| 256 |
+
e = fn()
|
| 257 |
+
assert isinstance(e, SentinelEnv)
|
| 258 |
+
obs = e.reset()
|
| 259 |
+
assert obs.sensors
|
| 260 |
+
check("tasks/ easy/medium/hard envs", _task_envs)
|
| 261 |
+
|
| 262 |
+
# ── 16. env.multiagent NegotiationLayer is interaction/ (not duplicate) ────────
|
| 263 |
+
def _no_duplicate_nl():
|
| 264 |
+
from env.multiagent import NegotiationLayer as MaNL
|
| 265 |
+
from interaction import NegotiationLayer as INL
|
| 266 |
+
assert MaNL is INL, "env.multiagent.NegotiationLayer must BE interaction.NegotiationLayer"
|
| 267 |
+
check("env.multiagent.NegotiationLayer IS interaction.NegotiationLayer (no duplicate)", _no_duplicate_nl)
|
| 268 |
+
|
| 269 |
+
# ── Summary ────────────────────────────────────────────────────────────────────
|
| 270 |
+
print(f"\n{'='*55}")
|
| 271 |
+
print(f" PASSED: {len(PASS)}/{len(PASS)+len(FAIL)}")
|
| 272 |
+
if FAIL:
|
| 273 |
+
print(f" FAILED: {len(FAIL)}")
|
| 274 |
+
for name, err in FAIL:
|
| 275 |
+
print(f" - {name}")
|
| 276 |
+
print(f" {err}")
|
| 277 |
+
print(f"{'='*55}")
|
| 278 |
+
sys.exit(0 if not FAIL else 1)
|
train_colab.py
CHANGED
|
@@ -252,14 +252,11 @@ for ep in range(1, NUM_EPISODES + 1):
|
|
| 252 |
if grpo_trainer is not None and proposals:
|
| 253 |
queries = [json.dumps({"sensors": env_state["sensors"], "step": ep}) for _ in proposals]
|
| 254 |
responses = [json.dumps(p) for p in proposals]
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
)
|
| 261 |
-
except Exception as _e:
|
| 262 |
-
pass # graceful degradation if TRL API differs
|
| 263 |
|
| 264 |
if ep % 5 == 0 or ep == 1:
|
| 265 |
print(
|
|
|
|
| 252 |
if grpo_trainer is not None and proposals:
|
| 253 |
queries = [json.dumps({"sensors": env_state["sensors"], "step": ep}) for _ in proposals]
|
| 254 |
responses = [json.dumps(p) for p in proposals]
|
| 255 |
+
grpo_trainer.step(
|
| 256 |
+
queries=queries,
|
| 257 |
+
responses=responses,
|
| 258 |
+
rewards=[ep_reward / max(len(proposals), 1)] * len(proposals),
|
| 259 |
+
)
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
if ep % 5 == 0 or ep == 1:
|
| 262 |
print(
|
trainer.py
CHANGED
|
@@ -295,17 +295,24 @@ class ARYAXTrainer:
|
|
| 295 |
|
| 296 |
for episode in range(1, self.num_episodes + 1):
|
| 297 |
# ── Phase 1: Episode rollout ──────────────────────────
|
|
|
|
|
|
|
| 298 |
self.curriculum.reset_episode()
|
| 299 |
cfg = self.curriculum.get_scenario_config()
|
| 300 |
|
| 301 |
ep_seed = cfg.get("seed_override") or rng.randint(0, 10000)
|
| 302 |
conflict_injection = cfg.get("conflict_injection", False)
|
|
|
|
|
|
|
| 303 |
|
| 304 |
env = AryaXEnv(
|
| 305 |
max_steps=cfg["max_steps"],
|
| 306 |
seed=ep_seed,
|
| 307 |
density_factor=1.5 + self.curriculum.difficulty_level * 2.5,
|
| 308 |
failure_prob=cfg["sensor_failure_prob"],
|
|
|
|
|
|
|
|
|
|
| 309 |
)
|
| 310 |
# ── PARTIAL OBSERVABILITY: use AryaXEnv.reset() → per-agent obs ──
|
| 311 |
obs_map = env.reset() # Dict[agent_id → AgentObservation (dataclass)]
|
|
@@ -332,11 +339,11 @@ class ARYAXTrainer:
|
|
| 332 |
targets_list_by_agent: dict[str, list] = {}
|
| 333 |
|
| 334 |
# First pass: satellite, drone, radar observe & propose
|
|
|
|
| 335 |
for canonical_id in ["satellite", "drone", "radar"]:
|
| 336 |
agent = self._agents[canonical_id]
|
| 337 |
-
agent_obs = obs_map[canonical_id]
|
| 338 |
|
| 339 |
-
# Build models.AgentObservation for the agent's observe() method
|
| 340 |
from env.models import AgentObservation as ModelAgentObs, Sensor as ModelSensor, Target as ModelTarget
|
| 341 |
model_obs = ModelAgentObs(
|
| 342 |
agent_id=canonical_id,
|
|
@@ -347,7 +354,13 @@ class ARYAXTrainer:
|
|
| 347 |
conflict_history=[],
|
| 348 |
)
|
| 349 |
agent.observe(model_obs)
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
all_proposals_obj.extend(proposals_obj)
|
| 352 |
|
| 353 |
sensors_list_by_agent[canonical_id] = agent_obs.sensors
|
|
@@ -418,14 +431,16 @@ class ARYAXTrainer:
|
|
| 418 |
for canonical_id in AGENT_TYPES:
|
| 419 |
episode_rewards[canonical_id] += step_rewards.get(canonical_id, 0.0)
|
| 420 |
|
| 421 |
-
# Update agent conflict history
|
| 422 |
-
for
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
|
|
|
|
|
|
| 429 |
|
| 430 |
# Store GRPO experiences (keyed by short AGENT_IDS for backward compat)
|
| 431 |
for short_id in AGENT_IDS:
|
|
@@ -492,6 +507,8 @@ class ARYAXTrainer:
|
|
| 492 |
"difficulty": round(curr_update["difficulty"], 3),
|
| 493 |
"phase": curr_update["phase"],
|
| 494 |
"conflict_injection": conflict_injection,
|
|
|
|
|
|
|
| 495 |
})
|
| 496 |
# Flush metrics every 10 episodes (avoid losing data on crash)
|
| 497 |
if episode % 10 == 0:
|
|
@@ -586,7 +603,7 @@ class ARYAXTrainer:
|
|
| 586 |
try:
|
| 587 |
self._grpo_trainer.step(
|
| 588 |
queries=list(queries),
|
| 589 |
-
|
| 590 |
rewards=advantages,
|
| 591 |
)
|
| 592 |
print(
|
|
@@ -606,7 +623,7 @@ class ARYAXTrainer:
|
|
| 606 |
try:
|
| 607 |
from tasks.grader import run_deterministic_eval
|
| 608 |
# Pass None since we are using greedy agents, not LLM agents
|
| 609 |
-
result = run_deterministic_eval(agents=None, verbose=True, mode="
|
| 610 |
conflict_rate = self.negotiation.get_conflict_rate()
|
| 611 |
coordination_score = 1.0 - conflict_rate
|
| 612 |
result["conflict_rate"] = conflict_rate
|
|
|
|
| 295 |
|
| 296 |
for episode in range(1, self.num_episodes + 1):
|
| 297 |
# ── Phase 1: Episode rollout ──────────────────────────
|
| 298 |
+
# reset_episode() only checks self-play triggers; episode_count
|
| 299 |
+
# is incremented once by update() at episode end.
|
| 300 |
self.curriculum.reset_episode()
|
| 301 |
cfg = self.curriculum.get_scenario_config()
|
| 302 |
|
| 303 |
ep_seed = cfg.get("seed_override") or rng.randint(0, 10000)
|
| 304 |
conflict_injection = cfg.get("conflict_injection", False)
|
| 305 |
+
targets_range = cfg.get("targets_per_step_range", (0, 0))
|
| 306 |
+
freeze_agent = cfg.get("freeze_agent")
|
| 307 |
|
| 308 |
env = AryaXEnv(
|
| 309 |
max_steps=cfg["max_steps"],
|
| 310 |
seed=ep_seed,
|
| 311 |
density_factor=1.5 + self.curriculum.difficulty_level * 2.5,
|
| 312 |
failure_prob=cfg["sensor_failure_prob"],
|
| 313 |
+
conflict_injection=cfg["conflict_injection"],
|
| 314 |
+
min_targets=targets_range[0],
|
| 315 |
+
max_targets=targets_range[1],
|
| 316 |
)
|
| 317 |
# ── PARTIAL OBSERVABILITY: use AryaXEnv.reset() → per-agent obs ──
|
| 318 |
obs_map = env.reset() # Dict[agent_id → AgentObservation (dataclass)]
|
|
|
|
| 339 |
targets_list_by_agent: dict[str, list] = {}
|
| 340 |
|
| 341 |
# First pass: satellite, drone, radar observe & propose
|
| 342 |
+
# Frozen agent (self-play curriculum) skips propose() entirely
|
| 343 |
for canonical_id in ["satellite", "drone", "radar"]:
|
| 344 |
agent = self._agents[canonical_id]
|
| 345 |
+
agent_obs = obs_map[canonical_id]
|
| 346 |
|
|
|
|
| 347 |
from env.models import AgentObservation as ModelAgentObs, Sensor as ModelSensor, Target as ModelTarget
|
| 348 |
model_obs = ModelAgentObs(
|
| 349 |
agent_id=canonical_id,
|
|
|
|
| 354 |
conflict_history=[],
|
| 355 |
)
|
| 356 |
agent.observe(model_obs)
|
| 357 |
+
# freeze_agent uses short-code (SAT/UAV/RDR); map to canonical
|
| 358 |
+
_freeze_map = {"SAT": "satellite", "UAV": "drone", "RDR": "radar"}
|
| 359 |
+
frozen_canonical = _freeze_map.get(freeze_agent)
|
| 360 |
+
if frozen_canonical == canonical_id:
|
| 361 |
+
proposals_obj = [] # frozen — contributes nothing this step
|
| 362 |
+
else:
|
| 363 |
+
proposals_obj = agent.propose()
|
| 364 |
all_proposals_obj.extend(proposals_obj)
|
| 365 |
|
| 366 |
sensors_list_by_agent[canonical_id] = agent_obs.sensors
|
|
|
|
| 431 |
for canonical_id in AGENT_TYPES:
|
| 432 |
episode_rewards[canonical_id] += step_rewards.get(canonical_id, 0.0)
|
| 433 |
|
| 434 |
+
# Update agent conflict history with real conflict records
|
| 435 |
+
for canonical_id in AGENT_TYPES:
|
| 436 |
+
agent_conflicts = [
|
| 437 |
+
c for c in neg_result.conflicts_detected
|
| 438 |
+
if canonical_id in (getattr(c, "involved_agents", []) or [])
|
| 439 |
+
]
|
| 440 |
+
self._agents[canonical_id].update(
|
| 441 |
+
step_rewards.get(canonical_id, 0.0),
|
| 442 |
+
agent_conflicts
|
| 443 |
+
)
|
| 444 |
|
| 445 |
# Store GRPO experiences (keyed by short AGENT_IDS for backward compat)
|
| 446 |
for short_id in AGENT_IDS:
|
|
|
|
| 507 |
"difficulty": round(curr_update["difficulty"], 3),
|
| 508 |
"phase": curr_update["phase"],
|
| 509 |
"conflict_injection": conflict_injection,
|
| 510 |
+
"targets_range": list(targets_range),
|
| 511 |
+
"freeze_agent": freeze_agent,
|
| 512 |
})
|
| 513 |
# Flush metrics every 10 episodes (avoid losing data on crash)
|
| 514 |
if episode % 10 == 0:
|
|
|
|
| 603 |
try:
|
| 604 |
self._grpo_trainer.step(
|
| 605 |
queries=list(queries),
|
| 606 |
+
responses=list(responses),
|
| 607 |
rewards=advantages,
|
| 608 |
)
|
| 609 |
print(
|
|
|
|
| 623 |
try:
|
| 624 |
from tasks.grader import run_deterministic_eval
|
| 625 |
# Pass None since we are using greedy agents, not LLM agents
|
| 626 |
+
result = run_deterministic_eval(agents=None, verbose=True, mode="multi")
|
| 627 |
conflict_rate = self.negotiation.get_conflict_rate()
|
| 628 |
coordination_score = 1.0 - conflict_rate
|
| 629 |
result["conflict_rate"] = conflict_rate
|