alignscope 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ """
2
+ AlignScope — RLlib Auto-Patch
3
+
4
+ Provides an AlignScopeCallback for Ray RLlib that automatically
5
+ logs multi-agent data to AlignScope on every episode step.
6
+
7
+ Tier 1 (Zero Code):
8
+ alignscope patch rllib
9
+ python train.py # zero changes
10
+
11
+ Tier 3 (Explicit Plugin):
12
+ from alignscope.patches.rllib import AlignScopeCallback
13
+ config = PPOConfig().callbacks(AlignScopeCallback)
14
+ """
15
+
16
+ _original_callbacks = None
17
+
18
+ try:
19
+ import ray
20
+ RAY_VERSION = tuple(map(int, ray.__version__.split(".")[:2]))
21
+ if RAY_VERSION[0] >= 2:
22
+ from ray.rllib.algorithms.callbacks import DefaultCallbacks
23
+ BaseCallback = DefaultCallbacks
24
+ else:
25
+ # RLlib 1.x path
26
+ from ray.rllib.agents.callbacks import DefaultCallbacks
27
+ BaseCallback = DefaultCallbacks
28
+ except (ImportError, AttributeError, ValueError):
29
+ BaseCallback = object
30
+
31
+
32
+ class AlignScopeCallback(BaseCallback):
33
+ """
34
+ RLlib callback that streams agent data to AlignScope.
35
+
36
+ Works with both Ray RLlib 1.x and 2.x APIs using version-aware static inheritance.
37
+ Gracefully degrades if RLlib is not installed.
38
+ """
39
+
40
+ def __init__(self, *args, **kwargs):
41
+ if BaseCallback is not object:
42
+ super().__init__(*args, **kwargs)
43
+
44
+ import alignscope
45
+ if alignscope._tracker is None:
46
+ alignscope.init(project="rllib-run")
47
+
48
+ self._tracker = alignscope._tracker
49
+ self._step = 0
50
+
51
+ def on_episode_step(
52
+ self,
53
+ *,
54
+ worker=None,
55
+ base_env=None,
56
+ policies=None,
57
+ episode=None,
58
+ env_index=None,
59
+ **kwargs,
60
+ ):
61
+ """Called on every step of every episode."""
62
+ self._step += 1
63
+
64
+ try:
65
+ agents = []
66
+ actions = {}
67
+ rewards = {}
68
+
69
+ # Extract agent data from the episode
70
+ if episode is not None:
71
+ for agent_id in episode.get_agents():
72
+ last_action = episode.last_action_for(agent_id)
73
+ last_reward = episode.last_reward_for(agent_id)
74
+ last_obs = episode.last_observation_for(agent_id)
75
+
76
+ # Determine team from agent_id naming convention
77
+ team = self._infer_team(agent_id)
78
+
79
+ import math
80
+ all_agents = list(episode.get_agents())
81
+ a_idx = all_agents.index(agent_id) if agent_id in all_agents else 0
82
+ n_agents = len(all_agents)
83
+
84
+ agents.append({
85
+ "agent_id": str(agent_id),
86
+ "team": team,
87
+ "role": "agent",
88
+ "x": round(math.cos(2 * math.pi * a_idx / max(1, n_agents)) * 150, 2),
89
+ "y": round(math.sin(2 * math.pi * a_idx / max(1, n_agents)) * 150, 2),
90
+ "resources": 0,
91
+ "hearts": 0,
92
+ "energy": float(last_reward) if last_reward is not None else 0,
93
+ "is_defector": False,
94
+ "coalition_id": team,
95
+ })
96
+
97
+ actions[str(agent_id)] = str(last_action)
98
+ rewards[str(agent_id)] = float(last_reward) if last_reward is not None else 0
99
+
100
+ self._tracker.log(
101
+ step=self._step,
102
+ agents=agents,
103
+ actions=actions,
104
+ rewards=rewards,
105
+ )
106
+
107
+ except Exception as e:
108
+ # Never crash the training run
109
+ pass
110
+
111
+ def on_episode_start(self, *, episode=None, **kwargs):
112
+ """Reset step counter at episode start."""
113
+ self._step = 0
114
+
115
+ def on_episode_end(self, *, episode=None, **kwargs):
116
+ """Log episode completion."""
117
+ pass
118
+
119
+ @staticmethod
120
+ def _infer_team(agent_id) -> int:
121
+ """Infer team from agent ID naming patterns."""
122
+ agent_str = str(agent_id).lower()
123
+ # Common patterns: "agent_0", "team_1_agent_2", "red_0", etc.
124
+ if "team_1" in agent_str or "blue" in agent_str or "enemy" in agent_str:
125
+ return 1
126
+ if "team_2" in agent_str:
127
+ return 2
128
+ return 0
129
+
130
+
131
+ def apply():
132
+ """
133
+ Auto-patch RLlib to use AlignScope callbacks.
134
+
135
+ This monkey-patches the default callback class so existing
136
+ training code works without any changes.
137
+ """
138
+ global _original_callbacks
139
+
140
+ try:
141
+ from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
142
+
143
+ # Store original
144
+ _original_callbacks = AlgorithmConfig.callbacks
145
+
146
+ # Monkey-patch the default
147
+ original_callbacks_method = AlgorithmConfig.callbacks
148
+
149
+ def patched_callbacks(self, callbacks_class=None):
150
+ if callbacks_class is None:
151
+ callbacks_class = AlignScopeCallback
152
+ return original_callbacks_method.fget(self)
153
+
154
+ # Override the property/method
155
+ try:
156
+ AlgorithmConfig.callbacks = property(
157
+ lambda self: AlignScopeCallback,
158
+ original_callbacks_method.fset if hasattr(original_callbacks_method, 'fset') else None,
159
+ )
160
+ except (TypeError, AttributeError):
161
+ # Fallback: just inform user to use the callback manually
162
+ print("[AlignScope] Auto-patch applied. Use AlignScopeCallback in your config.")
163
+
164
+ print("[AlignScope] ✓ RLlib patched successfully")
165
+ return True
166
+
167
+ except ImportError:
168
+ raise ImportError(
169
+ "RLlib is not installed. Install with: pip install 'alignscope[rllib]'"
170
+ )