Oceanus-AI / oceanus /runner.py
here2rule
initial deploy
ed79989
Raw History Blame Contribute Delete
12.5 kB
"""
Full episode runner — wires OceanusEnv + AdversaryAgent + agents.
"""
import json
import time
import asyncio
from typing import Dict, List, Callable, Optional
from oceanus.models import OceanusEnv
from oceanus.adversary import AdversaryAgent
class MockASVAgent:
"""Heuristic ASV agent for baseline comparison."""
def __init__(self, agent_id: str):
self.agent_id = agent_id
self._directions = ["north", "south", "east", "west"]
self._step = 0
def act(self, prompt: str, obs: Dict) -> str:
self._step += 1
sonar = obs.get("sonar_3x3", [])
# Scan sonar for nets and move toward / clean them
net_direction = None
for dr in range(3):
for dc in range(3):
if len(sonar) > dr and len(sonar[dr]) > dc and sonar[dr][dc] == "net":
row_diff = dr - 1
col_diff = dc - 1
if row_diff == 0 and col_diff == 0:
# Net is exactly at our position — clean it
return json.dumps({"intent": "clean"})
elif row_diff < 0:
net_direction = "north"
elif row_diff > 0:
net_direction = "south"
elif col_diff < 0:
net_direction = "west"
elif col_diff > 0:
net_direction = "east"
if net_direction:
return json.dumps({"intent": "move", "direction": net_direction})
# Broadcast position every 10 steps
if self._step % 10 == 0:
pos = obs.get("position", {})
return json.dumps({
"intent": "broadcast",
"message": f"At ({pos.get('row',0)},{pos.get('col',0)}) sector {pos.get('sector','?')}. Scanning.",
})
# Sweep pattern
direction = self._directions[self._step % 4]
return json.dumps({"intent": "move", "direction": direction})
async def async_act(self, prompt: str, obs: Dict) -> str:
await asyncio.sleep(0.01) # Simulate network latency
return self.act(prompt, obs)
class MockPolicyAgent:
"""Heuristic policy agent for baseline comparison."""
def __init__(self, agent_id: str):
self.agent_id = agent_id
self._step = 0
self._proposed = False
self._accepted = False
self._replied_to: set = set()
def act(self, prompt: str, obs: Dict) -> str:
self._step += 1
inbox = obs.get("inbox", [])
policy_status = obs.get("current_policy_status", "No Tagging Mandate")
# Fleet_Manager: accept treaty if proposed
if (
policy_status == "Treaty Proposed"
and self.agent_id == "Fleet_Manager"
and not self._accepted
):
self._accepted = True
return json.dumps({
"intent": "accept_treaty",
"target": "Port_Authority",
"content": "Agreed. Fleet will comply with tagging mandate under subsidy terms.",
})
# Reply to new emails
for msg in inbox:
sender = msg.get("from", "Unknown")
if sender not in self._replied_to:
self._replied_to.add(sender)
mood = msg.get("mood", "Neutral")
if mood in ("Angry", "Hostile"):
reply = "We hear your concerns and are working on a subsidy program to offset costs."
elif mood in ("Urgent", "Critical"):
reply = "Immediate action is being taken. Emergency protocols activated."
else:
reply = "Thank you for your message. We are reviewing the situation carefully."
return json.dumps({"intent": "reply_email", "target": sender, "content": reply})
# Port_Authority: propose treaty once
if not self._proposed and self.agent_id == "Port_Authority":
self._proposed = True
return json.dumps({
"intent": "propose_treaty",
"target": "Fleet_Manager",
"content": "50% subsidy on tracking tags in exchange for mandatory tagging compliance.",
})
# Default idle action — valid JSON so no penalty
return json.dumps({"intent": "scan"}) if self.agent_id.startswith("ASV") else json.dumps({
"intent": "reply_email",
"target": "General_Inbox",
"content": "Monitoring situation.",
})
async def async_act(self, prompt: str, obs: Dict) -> str:
await asyncio.sleep(0.02) # Policy agents have slightly higher simulated latency
return self.act(prompt, obs)
class LLMAgent:
"""Wraps any callable LLM inference function."""
def __init__(self, agent_id: str, llm_fn: Callable[[str], str]):
self.agent_id = agent_id
self.llm_fn = llm_fn
def act(self, prompt: str, obs: Dict) -> str:
try:
return self.llm_fn(prompt)
except Exception:
if self.agent_id.startswith("ASV"):
return json.dumps({"intent": "scan"})
return json.dumps({
"intent": "reply_email",
"target": "General_Inbox",
"content": "Processing...",
})
async def async_act(self, prompt: str, obs: Dict) -> str:
# If llm_fn is actually an async function, we can await it directly.
# For this hackathon, we simulate wrapping it:
if asyncio.iscoroutinefunction(self.llm_fn):
try:
return await self.llm_fn(prompt)
except Exception:
return json.dumps({"intent": "scan"})
else:
return await asyncio.to_thread(self.act, prompt, obs)
class OceanusRunner:
"""Runs full episodes of Oceanus with any combination of agents."""
def __init__(
self,
env: OceanusEnv,
adversary: AdversaryAgent,
agents: Optional[Dict] = None,
use_mock: bool = True,
llm_fn: Optional[Callable] = None,
verbose: bool = True,
step_delay: float = 0.0,
):
self.env = env
self.adversary = adversary
self.verbose = verbose
self.step_delay = step_delay
if agents:
self.agents = agents
elif use_mock:
self.agents = {
"ASV-1": MockASVAgent("ASV-1"),
"ASV-2": MockASVAgent("ASV-2"),
"ASV-3": MockASVAgent("ASV-3"),
"ASV-4": MockASVAgent("ASV-4"),
"Port_Authority": MockPolicyAgent("Port_Authority"),
"Fleet_Manager": MockPolicyAgent("Fleet_Manager"),
}
else:
assert llm_fn is not None, "Provide llm_fn for LLM agents"
self.agents = {
aid: LLMAgent(aid, llm_fn)
for aid in ["ASV-1", "ASV-2", "ASV-3", "ASV-4", "Port_Authority", "Fleet_Manager"]
}
self.episode_logs: List[Dict] = []
def run_episode(self, episode_id: int = 0) -> Dict:
"""Run a full episode. Returns episode summary."""
obs_all = self.env.reset()
done = False
total_reward = 0.0
chaos_events_all: List[str] = []
if self.verbose:
print(f"\n{'='*60}")
print(f" OCEANUS EPISODE {episode_id} STARTING")
print(f" Agents: {list(self.agents.keys())}")
print(f"{'='*60}")
while not done:
actions = {}
for agent_id, agent in self.agents.items():
if agent_id in obs_all:
agent_obs = obs_all[agent_id]
actions[agent_id] = agent.act(
agent_obs["prompt"], agent_obs["observation"]
)
obs_all, rewards, done, info = self.env.step(actions)
step_reward = rewards.get("__total__", 0.0)
total_reward += step_reward
if self.adversary.should_inject(info["step"]):
chaos = self.adversary.inject(self.env.state)
chaos_events_all.extend(chaos)
if self.verbose:
for event in chaos:
print(f" CHAOS [{info['step']}]: {event}")
if self.verbose and info["step"] % 20 == 0:
print(
f" Step {info['step']:3d} | "
f"Nets: {info['active_nets']:3d} | "
f"Cleaned: {info['total_cleaned']:3d} | "
f"Bio: {info['biodiversity']:5.1f}% | "
f"Treaty: {info['treaty_status'][:25]} | "
f"R: {step_reward:+.1f}"
)
for c in info.get("cleaned", []):
if self.verbose:
print(f" CLEANED: {c}")
for te in info.get("treaty_events", []):
if self.verbose:
print(f" TREATY: {te}")
if self.step_delay > 0:
time.sleep(self.step_delay)
summary = self.env.get_episode_summary()
summary["episode_id"] = episode_id
summary["total_reward"] = total_reward
summary["chaos_events"] = chaos_events_all
summary["adversary_summary"] = self.adversary.get_curriculum_summary()
if self.verbose:
print(f"\n{'='*60}")
print(f" EPISODE {episode_id} COMPLETE")
print(f" Total Reward: {total_reward:.2f}")
print(f" Biodiversity: {summary['biodiversity_final']:.1f}%")
print(f" Nets Cleaned: {summary['total_cleaned']}")
print(f" Treaty Status: {summary['treaty_status']}")
print(f" Chaos Events: {len(chaos_events_all)}")
print(f"{'='*60}\n")
self.episode_logs.append(summary)
return summary
def run_baseline(self, n_episodes: int = 3) -> List[Dict]:
return [self.run_episode(i) for i in range(n_episodes)]
async def async_run_episode(self, episode_id: int = 0, on_step_callback: Optional[Callable] = None) -> Dict:
"""Run a full episode asynchronously, simulating real multi-agent latency."""
obs_all = self.env.reset()
done = False
total_reward = 0.0
chaos_events_all: List[str] = []
if self.verbose:
print(f"\n{'='*60}")
print(f" OCEANUS ASYNC EPISODE {episode_id} STARTING")
print(f"{'='*60}")
while not done:
# Prepare async tasks
tasks = []
agent_ids = []
for agent_id, agent in self.agents.items():
if agent_id in obs_all:
agent_obs = obs_all[agent_id]
if hasattr(agent, "async_act"):
tasks.append(agent.async_act(agent_obs["prompt"], agent_obs["observation"]))
else:
tasks.append(asyncio.to_thread(agent.act, agent_obs["prompt"], agent_obs["observation"]))
agent_ids.append(agent_id)
# Execute all agent inferences concurrently
results = await asyncio.gather(*tasks)
actions = dict(zip(agent_ids, results))
obs_all, rewards, done, info = self.env.step(actions)
step_reward = rewards.get("__total__", 0.0)
total_reward += step_reward
if self.adversary.should_inject(info["step"]):
chaos = self.adversary.inject(self.env.state)
chaos_events_all.extend(chaos)
if self.verbose:
for event in chaos:
print(f" CHAOS [{info['step']}]: {event}")
if on_step_callback:
if asyncio.iscoroutinefunction(on_step_callback):
await on_step_callback(self.env.state, info)
else:
on_step_callback(self.env.state, info)
if self.step_delay > 0:
await asyncio.sleep(self.step_delay)
summary = self.env.get_episode_summary()
summary["episode_id"] = episode_id
summary["total_reward"] = total_reward
summary["chaos_events"] = chaos_events_all
summary["adversary_summary"] = self.adversary.get_curriculum_summary()
self.episode_logs.append(summary)
return summary