Spaces:
Sleeping
Sleeping
TM23-sanji
refactor: remove narrative drift metrics and implement a new reward structure based on propagandist flag frequency and pivot tracking.
1cb80e7 | """Graph state manager for the social network simulation.""" | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| from schemas import Post, round2 | |
| class GraphStateManager: | |
| """Mutable graph state for a single episode.""" | |
| def __init__(self, episode_id: int, neutral_ids: List[int] = None): | |
| self.episode_id = episode_id | |
| self.step: int = 0 | |
| self.posts: List[Post] = [] | |
| self.post_counter: int = 0 | |
| self.account_posts: Dict[int, List[int]] = {} | |
| self.account_citations_given: Dict[int, List[int]] = {} | |
| self.account_citations_received: Dict[int, List[int]] = {} | |
| self.flags: Dict[int, int] = {} | |
| self.burned_accounts: set = set() | |
| self.neutral_ids: set = set(neutral_ids) if neutral_ids else set() | |
| self.neutral_post_window: List[Post] = [] # last 10 neutral posts | |
| self.prop_pivot_log: Dict[int, List[int]] = {} # account_id -> [step, step, ...] | |
| def add_post( | |
| self, | |
| account_id: int, | |
| text: str, | |
| topic: str, | |
| framing: str, | |
| veracity: bool, | |
| cites_post_id: Optional[int] = None, | |
| cites_account_id: Optional[int] = None, | |
| ) -> Post: | |
| self.post_counter += 1 | |
| post = Post( | |
| post_id=self.post_counter, | |
| step=self.step, | |
| account_id=account_id, | |
| text=text, | |
| topic=topic, | |
| framing=framing, | |
| veracity=veracity, | |
| cites_post_id=cites_post_id, | |
| cites_account_id=cites_account_id, | |
| ) | |
| self.posts.append(post) | |
| self.account_posts.setdefault(account_id, []).append(post.post_id) | |
| # Track neutral posts in rolling window | |
| if account_id in self.neutral_ids and cites_post_id is None: | |
| self.neutral_post_window.append(post) | |
| if len(self.neutral_post_window) > 10: | |
| self.neutral_post_window.pop(0) | |
| if cites_post_id is not None: | |
| self.account_citations_given.setdefault(account_id, []).append(post.post_id) | |
| if cites_account_id is not None: | |
| self.account_citations_received.setdefault(cites_account_id, []).append(post.post_id) | |
| return post | |
| def flag_account(self, account_id: int, step: int) -> None: | |
| self.flags[account_id] = step | |
| def burn_account(self, account_id: int) -> None: | |
| self.burned_accounts.add(account_id) | |
| def is_burned(self, account_id: int) -> bool: | |
| return account_id in self.burned_accounts | |
| def is_flagged(self, account_id: int) -> bool: | |
| return account_id in self.flags | |
| def log_pivot(self, account_id: int, step: int) -> None: | |
| """Record when a propagandist takes MIMIC or PIVOT action.""" | |
| self.prop_pivot_log.setdefault(account_id, []).append(step) | |
| def get_account_velocity(self, account_id: int, window: int = 10) -> float: | |
| recent = [p for p in self.posts if p.account_id == account_id and self.step - p.step <= window] | |
| if window == 0: | |
| return 0.0 | |
| return round2(len(recent) / window) | |
| def get_recent_posts(self, n_steps: int = 8) -> List[Post]: | |
| return [p for p in self.posts if self.step - p.step < n_steps] | |
| def get_last_n_posts(self, n: int = 5) -> List[Post]: | |
| return self.posts[-n:] if len(self.posts) >= n else self.posts[:] | |
| def to_dict(self, full: bool = True) -> dict: | |
| base = { | |
| "episode_id": self.episode_id, | |
| "step": self.step, | |
| "num_posts": len(self.posts), | |
| "num_citations": sum(1 for p in self.posts if p.cites_post_id is not None), | |
| "flags": {str(k): v for k, v in self.flags.items()}, | |
| "burned": sorted(list(self.burned_accounts)), | |
| "prop_pivot_log": {str(k): v for k, v in self.prop_pivot_log.items()}, | |
| } | |
| if full: | |
| base["posts"] = [p.model_dump() for p in self.posts] | |
| else: | |
| base["recent_posts"] = [p.model_dump() for p in self.get_recent_posts(n_steps=10)] | |
| return base | |
| def save(self, path: Path, full: bool = True) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(path, "w") as f: | |
| json.dump(self.to_dict(full=full), f, indent=2) | |