Spaces:
Running
Running
Download oceanus/models.py from aakarsh2007/Oceanus-AI: direct link, hf CLI and curl.
- Browser
- Download file 19.7 kB
-
https://huggingface.co/spaces/aakarsh2007/Oceanus-AI/resolve/main/oceanus/models.py
- Command line
-
hf download hf://spaces/aakarsh2007/Oceanus-AI/oceanus/models.py
-
curl -L -o models.py https://huggingface.co/spaces/aakarsh2007/Oceanus-AI/resolve/main/oceanus/models.py
19.7 kB
| """ | |
| OpenEnv Wrapper β reset(), step(), ObservationSpace, ActionSpace. | |
| """ | |
| import json | |
| import re | |
| from typing import Dict, Any, Tuple, List, Optional | |
| from oceanus.physics import GameState, GRID_SIZE | |
| def parse_asv_action(raw_output: str, schema_version: str = "v1") -> Tuple[Optional[Dict], bool]: | |
| """Extract JSON action from LLM output. Returns (action_dict, is_valid).""" | |
| if not raw_output or not isinstance(raw_output, str): | |
| return None, False | |
| try: | |
| # Greedy match to handle nested braces | |
| match = re.search(r'\{[^{}]*\}', raw_output, re.DOTALL) | |
| if not match: | |
| return None, False | |
| action = json.loads(match.group()) | |
| intent_key = "action_type" if schema_version == "v2" else "intent" | |
| intent = action.get(intent_key, "") | |
| if not intent: | |
| return None, False | |
| valid_intents = {"move", "scan", "clean", "broadcast"} | |
| if intent not in valid_intents: | |
| return None, False | |
| if intent == "move": | |
| direction = action.get("direction", "") | |
| if direction not in {"north", "south", "east", "west", "stay"}: | |
| return None, False | |
| if intent == "broadcast" and not action.get("message", "").strip(): | |
| return None, False | |
| # Normalize to v1 for internal engine use | |
| if schema_version == "v2": | |
| action["intent"] = action.pop("action_type") | |
| return action, True | |
| except (json.JSONDecodeError, AttributeError, TypeError): | |
| return None, False | |
| def parse_policy_action(raw_output: str, schema_version: str = "v1") -> Tuple[Optional[Dict], bool]: | |
| """Extract JSON action from policy LLM output.""" | |
| if not raw_output or not isinstance(raw_output, str): | |
| return None, False | |
| try: | |
| match = re.search(r'\{[^{}]*\}', raw_output, re.DOTALL) | |
| if not match: | |
| return None, False | |
| action = json.loads(match.group()) | |
| intent_key = "action_type" if schema_version == "v2" else "intent" | |
| target_key = "recipient" if schema_version == "v2" else "target" | |
| intent = action.get(intent_key, "") | |
| if not intent: | |
| return None, False | |
| valid_intents = {"reply_email", "propose_treaty", "accept_treaty", "reject_treaty"} | |
| if intent not in valid_intents: | |
| return None, False | |
| # Relaxed validation β only require target for reply/propose/accept | |
| if intent in ("reply_email", "propose_treaty", "accept_treaty"): | |
| if not action.get(target_key, "").strip(): | |
| return None, False | |
| # Normalize to v1 for internal engine use | |
| if schema_version == "v2": | |
| action["intent"] = action.pop("action_type") | |
| if target_key in action: | |
| action["target"] = action.pop("recipient") | |
| return action, True | |
| except (json.JSONDecodeError, AttributeError, TypeError): | |
| return None, False | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Observation Builders | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_asv_observation(state: GameState, asv_id: str) -> Dict: | |
| asv = state.asvs[asv_id] | |
| sonar = state.get_sonar_reading(asv) | |
| sector = state.get_sector(asv.row, asv.col) | |
| on_net = state.grid[asv.row, asv.col] > 0 | |
| return { | |
| "agent_id": asv_id, | |
| "position": {"row": asv.row, "col": asv.col, "sector": sector}, | |
| "battery": asv.battery, | |
| "sonar_3x3": sonar, | |
| "on_net": bool(on_net), | |
| "comms_inbox": list(asv.comms_inbox[-3:]), | |
| "step": state.step_count, | |
| "biodiversity_index": round(state.biodiversity_index, 1), | |
| "wind_hint": f"Wind direction: {state.wind_vector}", | |
| "schema_version": state.schema_version, | |
| } | |
| def build_policy_observation(state: GameState, agent_id: str) -> Dict: | |
| if agent_id == "Port_Authority": | |
| inbox_msgs = list(state.port_authority_inbox.messages)[:4] | |
| else: | |
| inbox_msgs = list(state.fleet_manager_inbox.messages)[:4] | |
| return { | |
| "agent_id": agent_id, | |
| "current_policy_status": state.treaty_status, | |
| "net_spawn_rate": state.net_spawn_rate, | |
| "biodiversity_index": round(state.biodiversity_index, 1), | |
| "inbox": inbox_msgs, | |
| "step": state.step_count, | |
| "unanswered_emails": ( | |
| state.port_authority_inbox.unanswered_count | |
| if agent_id == "Port_Authority" | |
| else state.fleet_manager_inbox.unanswered_count | |
| ), | |
| "schema_version": state.schema_version, | |
| } | |
| def build_asv_prompt(obs: Dict) -> str: | |
| sonar_str = "\n".join([" " + str(row) for row in obs["sonar_3x3"]]) | |
| inbox_str = "\n".join([f" - {m}" for m in obs["comms_inbox"]]) or " (empty)" | |
| if obs.get("schema_version") == "v2": | |
| schema_instructions = """ | |
| Respond with a single JSON action using schema v2 ('action_type' instead of 'intent'): | |
| - To move: {"action_type": "move", "direction": "north|south|east|west|stay"} | |
| - To clean a net at your position: {"action_type": "clean"} | |
| - To broadcast to other ASVs: {"action_type": "broadcast", "message": "your message here"} | |
| - To scan (reveal sonar): {"action_type": "scan"} | |
| """ | |
| else: | |
| schema_instructions = """ | |
| Respond with a single JSON action using schema v1: | |
| - To move: {"intent": "move", "direction": "north|south|east|west|stay"} | |
| - To clean a net at your position: {"intent": "clean"} | |
| - To broadcast to other ASVs: {"intent": "broadcast", "message": "your message here"} | |
| - To scan (reveal sonar): {"intent": "scan"} | |
| """ | |
| return f"""You are {obs['agent_id']}, an Autonomous Surface Vehicle on a ghost-net recovery mission. | |
| CURRENT STATE: | |
| - Position: Row {obs['position']['row']}, Col {obs['position']['col']} (Sector {obs['position']['sector']}) | |
| - Battery: {obs['battery']}% | |
| - Biodiversity Index: {obs['biodiversity_index']}% | |
| - On Net: {"YES" if obs.get("on_net") else "NO"} | |
| - Step: {obs['step']} | |
| - {obs['wind_hint']} | |
| SONAR (3x3 grid around you, 'net' = ghost net detected): | |
| {sonar_str} | |
| COMMS INBOX: | |
| {inbox_str} | |
| TASK: Recover ghost nets to restore ocean biodiversity. Coordinate with other ASVs. | |
| {schema_instructions} | |
| Your JSON action:""" | |
| def build_policy_prompt(obs: Dict) -> str: | |
| inbox_str = "" | |
| for msg in obs["inbox"]: | |
| inbox_str += f"\n FROM: {msg.get('from','?')} [Mood: {msg.get('mood','Neutral')}]\n MSG: {msg.get('message','')}\n" | |
| if not inbox_str: | |
| inbox_str = " (no messages)" | |
| if obs.get("schema_version") == "v2": | |
| schema_instructions = """ | |
| Respond with a single JSON action using schema v2 ('action_type' instead of 'intent', 'recipient' instead of 'target'): | |
| - To reply to an email: {"action_type": "reply_email", "recipient": "sender_name", "content": "your reply"} | |
| - To propose a treaty: {"action_type": "propose_treaty", "recipient": "Fleet_Manager", "content": "treaty terms"} | |
| - To accept a treaty: {"action_type": "accept_treaty", "recipient": "Port_Authority", "content": "acceptance message"} | |
| """ | |
| else: | |
| schema_instructions = """ | |
| Respond with a single JSON action using schema v1: | |
| - To reply to an email: {"intent": "reply_email", "target": "sender_name", "content": "your reply"} | |
| - To propose a treaty: {"intent": "propose_treaty", "target": "Fleet_Manager", "content": "treaty terms"} | |
| - To accept a treaty: {"intent": "accept_treaty", "target": "Port_Authority", "content": "acceptance message"} | |
| """ | |
| return f"""You are the {obs['agent_id']} in the Oceanus Ocean Recovery Program. | |
| CURRENT STATE: | |
| - Policy Status: {obs['current_policy_status']} | |
| - Net Spawn Rate: {obs['net_spawn_rate']} (lower is better) | |
| - Biodiversity Index: {obs['biodiversity_index']}% | |
| - Unanswered Emails: {obs['unanswered_emails']} | |
| - Step: {obs['step']} | |
| YOUR INBOX: | |
| {inbox_str} | |
| TASK: Negotiate a Tagging Mandate treaty to reduce ghost net spawning. Reply to stakeholders diplomatically. | |
| A successful treaty reduces net spawn rate from High to Low, saving the ecosystem. | |
| {schema_instructions} | |
| Your JSON action:""" | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Reward Engine | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class RewardEngine: | |
| """CTDE reward: R_total = Ξ±*Ξ£ R_local_asv + Ξ²*Ξ£ R_local_policy + Ξ³*R_global_ecosystem""" | |
| ALPHA = 1.0 | |
| BETA = 1.0 | |
| GAMMA = 2.0 | |
| def __init__(self): | |
| self.episode_rewards: Dict[str, List[float]] = {} | |
| def compute_asv_reward( | |
| self, action: Optional[Dict], valid: bool, cleaned: bool, broadcast_useful: bool | |
| ) -> float: | |
| r = 0.0 | |
| if not valid: | |
| r -= 1.0 | |
| if cleaned: | |
| r += 5.0 | |
| if action and action.get("intent") == "move": | |
| r -= 0.1 | |
| if broadcast_useful: | |
| r += 0.5 | |
| return r | |
| def compute_policy_reward( | |
| self, | |
| action: Optional[Dict], | |
| valid: bool, | |
| treaty_advanced: bool, | |
| email_replied: bool, | |
| email_ignored_penalty: bool, | |
| ) -> float: | |
| r = 0.0 | |
| if not valid: | |
| r -= 1.0 | |
| if treaty_advanced: | |
| r += 10.0 | |
| if email_replied: | |
| r += 2.0 | |
| if email_ignored_penalty: | |
| r -= 5.0 | |
| return r | |
| def compute_global_reward(self, state: GameState) -> float: | |
| if not state.done: | |
| return 0.0 | |
| if state.biodiversity_index >= 75.0: | |
| return 100.0 | |
| elif state.biodiversity_index >= 50.0: | |
| return 40.0 | |
| elif state.biodiversity_index >= 25.0: | |
| return 10.0 | |
| return -20.0 | |
| def blend(self, asv_rewards: List[float], policy_rewards: List[float], global_r: float) -> float: | |
| return self.ALPHA * sum(asv_rewards) + self.BETA * sum(policy_rewards) + self.GAMMA * global_r | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main OpenEnv Environment | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class OceanusEnv: | |
| """ | |
| OpenEnv-compatible environment for Oceanus. | |
| Observation space: JSON strings (text) | |
| Action space: Text output (parsed to JSON) | |
| """ | |
| metadata = { | |
| "name": "Oceanus-v1", | |
| "description": "Multi-layer ghost-gear recovery and treaty negotiation arena", | |
| "themes": ["multi-agent", "long-horizon", "world-modeling", "self-improvement"], | |
| "num_agents": 6, | |
| } | |
| def __init__(self, seed: int = 42, max_steps: int = 200): | |
| self.seed = seed | |
| self.max_steps = max_steps | |
| self.state: Optional[GameState] = None | |
| self.reward_engine = RewardEngine() | |
| self._episode_asv_rewards: List[float] = [] | |
| self._episode_policy_rewards: List[float] = [] | |
| self._step_log: List[Dict] = [] | |
| def reset(self) -> Dict[str, Any]: | |
| """Reset environment, return initial observations for all agents.""" | |
| self.state = GameState(seed=self.seed) | |
| self.state.max_steps = self.max_steps | |
| self._episode_asv_rewards = [] | |
| self._episode_policy_rewards = [] | |
| self._step_log = [] | |
| obs = {} | |
| for asv_id in self.state.asvs: | |
| raw_obs = build_asv_observation(self.state, asv_id) | |
| obs[asv_id] = {"observation": raw_obs, "prompt": build_asv_prompt(raw_obs)} | |
| for agent_id in ["Port_Authority", "Fleet_Manager"]: | |
| raw_obs = build_policy_observation(self.state, agent_id) | |
| obs[agent_id] = {"observation": raw_obs, "prompt": build_policy_prompt(raw_obs)} | |
| return obs | |
| def step(self, actions: Dict[str, str]) -> Tuple[Dict, Dict, bool, Dict]: | |
| """ | |
| Process one step for all agents. | |
| actions: {agent_id: raw_llm_output_string} | |
| Returns: (observations, rewards, done, info) | |
| """ | |
| assert self.state is not None, "Call reset() first" | |
| rewards: Dict[str, float] = {} | |
| info: Dict[str, Any] = {"events": [], "cleaned": [], "treaty_events": [], "broadcasts": []} | |
| # ββ ASV actions ββ | |
| asv_rewards = [] | |
| for asv_id, raw_output in actions.items(): | |
| if not asv_id.startswith("ASV"): | |
| continue | |
| asv = self.state.asvs.get(asv_id) | |
| if asv is None: | |
| continue | |
| action, valid = parse_asv_action(raw_output, schema_version=self.state.schema_version) | |
| cleaned = False | |
| broadcast_useful = False | |
| if valid and action: | |
| intent = action["intent"] | |
| if intent == "move": | |
| asv.move(action.get("direction", "stay")) | |
| elif intent == "clean": | |
| cleaned = self.state.clean_net_at(asv.row, asv.col) | |
| if cleaned: | |
| asv.nets_cleaned += 1 | |
| info["cleaned"].append(f"{asv_id} cleaned net at ({asv.row},{asv.col})") | |
| elif intent == "broadcast": | |
| msg = action.get("message", "").strip() | |
| if msg and len(msg) >= 10 and ("net" in msg.lower() or "sector" in msg.lower() or "at" in msg.lower()): | |
| broadcast_useful = True | |
| for other_id, other_asv in self.state.asvs.items(): | |
| if other_id != asv_id: | |
| other_asv.comms_inbox.append(f"{asv_id}: {msg}") | |
| if len(other_asv.comms_inbox) > 10: | |
| other_asv.comms_inbox.pop(0) | |
| info["broadcasts"].append(f"{asv_id}: {msg}") | |
| # "scan" β sonar is always active, no extra action needed | |
| r = self.reward_engine.compute_asv_reward(action, valid, cleaned, broadcast_useful) | |
| rewards[asv_id] = r | |
| asv_rewards.append(r) | |
| # ββ Policy actions ββ | |
| policy_rewards = [] | |
| for agent_id, raw_output in actions.items(): | |
| if agent_id.startswith("ASV"): | |
| continue | |
| if agent_id not in ("Port_Authority", "Fleet_Manager"): | |
| continue | |
| action, valid = parse_policy_action(raw_output, schema_version=self.state.schema_version) | |
| treaty_advanced = False | |
| email_replied = False | |
| email_ignored_penalty = False | |
| if valid and action: | |
| intent = action["intent"] | |
| target = action.get("target", "") | |
| content = action.get("content", "") | |
| if intent == "reply_email": | |
| inbox = ( | |
| self.state.port_authority_inbox | |
| if agent_id == "Port_Authority" | |
| else self.state.fleet_manager_inbox | |
| ) | |
| msg = inbox.pop_message(target) | |
| if msg and len(content) > 10: | |
| email_replied = True | |
| info["treaty_events"].append(f"{agent_id} replied to {target}") | |
| elif intent == "propose_treaty": | |
| if self.state.treaty_status == "No Tagging Mandate": | |
| self.state.treaty_status = "Treaty Proposed" | |
| treaty_advanced = True | |
| other_inbox = ( | |
| self.state.fleet_manager_inbox | |
| if agent_id == "Port_Authority" | |
| else self.state.port_authority_inbox | |
| ) | |
| other_inbox.add_message(agent_id, "Formal", f"Treaty proposal: {content}") | |
| info["treaty_events"].append( | |
| f"{agent_id} proposed treaty: {content[:60]}..." | |
| ) | |
| elif intent == "accept_treaty": | |
| if self.state.treaty_status == "Treaty Proposed": | |
| self.state.treaty_status = "Tagging Mandate Active" | |
| treaty_advanced = True | |
| info["treaty_events"].append( | |
| f"TREATY SIGNED by {agent_id}! Spawn rate dropping." | |
| ) | |
| elif intent == "reject_treaty": | |
| self.state.treaty_status = "No Tagging Mandate" | |
| info["treaty_events"].append(f"{agent_id} rejected treaty.") | |
| # Penalty for ignored emails > 5 steps old | |
| inbox = ( | |
| self.state.port_authority_inbox | |
| if agent_id == "Port_Authority" | |
| else self.state.fleet_manager_inbox | |
| ) | |
| if any(m["age"] > 5 for m in inbox.messages): | |
| email_ignored_penalty = True | |
| r = self.reward_engine.compute_policy_reward( | |
| action, valid, treaty_advanced, email_replied, email_ignored_penalty | |
| ) | |
| rewards[agent_id] = r | |
| policy_rewards.append(r) | |
| # ββ Tick physics ββ | |
| self.state.tick() | |
| # ββ Global reward at episode end ββ | |
| global_r = self.reward_engine.compute_global_reward(self.state) | |
| total_r = self.reward_engine.blend(asv_rewards, policy_rewards, global_r) | |
| rewards["__total__"] = total_r | |
| rewards["__global__"] = global_r | |
| self._episode_asv_rewards.extend(asv_rewards) | |
| self._episode_policy_rewards.extend(policy_rewards) | |
| # ββ Build next observations ββ | |
| obs: Dict[str, Any] = {} | |
| for asv_id in self.state.asvs: | |
| raw_obs = build_asv_observation(self.state, asv_id) | |
| obs[asv_id] = {"observation": raw_obs, "prompt": build_asv_prompt(raw_obs)} | |
| for agent_id in ["Port_Authority", "Fleet_Manager"]: | |
| raw_obs = build_policy_observation(self.state, agent_id) | |
| obs[agent_id] = {"observation": raw_obs, "prompt": build_policy_prompt(raw_obs)} | |
| info["step"] = self.state.step_count | |
| info["biodiversity"] = self.state.biodiversity_index | |
| info["active_nets"] = len(self.state.ghost_nets) | |
| info["treaty_status"] = self.state.treaty_status | |
| info["total_cleaned"] = self.state.total_cleaned | |
| self._step_log.append({ | |
| "step": self.state.step_count, | |
| "rewards": dict(rewards), | |
| "biodiversity": self.state.biodiversity_index, | |
| "active_nets": len(self.state.ghost_nets), | |
| "treaty": self.state.treaty_status, | |
| }) | |
| return obs, rewards, self.state.done, info | |
| def get_episode_summary(self) -> Dict: | |
| return { | |
| "total_steps": self.state.step_count if self.state else 0, | |
| "total_cleaned": self.state.total_cleaned if self.state else 0, | |
| "biodiversity_final": self.state.biodiversity_index if self.state else 0.0, | |
| "treaty_status": self.state.treaty_status if self.state else "N/A", | |
| "avg_asv_reward": ( | |
| sum(self._episode_asv_rewards) / max(1, len(self._episode_asv_rewards)) | |
| ), | |
| "avg_policy_reward": ( | |
| sum(self._episode_policy_rewards) / max(1, len(self._episode_policy_rewards)) | |
| ), | |
| "step_log": self._step_log, | |
| } | |