eptan commited on
Commit
3407691
·
verified ·
1 Parent(s): 46245ca

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN pip install uv
6
+
7
+ COPY pyproject.toml uv.lock ./
8
+ COPY __init__.py client.py models.py openenv.yaml ./
9
+ COPY server/ server/
10
+
11
+ RUN uv pip install --system .
12
+
13
+ ENV ENABLE_WEB_INTERFACE=true
14
+
15
+ EXPOSE 8000
16
+
17
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
18
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
19
+
20
+ CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
@@ -1,10 +1,24 @@
1
  ---
2
- title: Crisis Inbox
3
- emoji: 📈
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: CrisisInbox
3
+ emoji: 🚨
4
+ colorFrom: red
5
+ colorTo: yellow
6
  sdk: docker
7
+ app_port: 8000
8
  ---
9
 
10
+ # CrisisInbox
11
+
12
+ An RL environment built on OpenEnv 0.2.1 for training language models to manage personal task overload during natural disasters.
13
+
14
+ ## Quick Start
15
+
16
+ ```python
17
+ from crisis_inbox import CrisisInboxEnv
18
+
19
+ with CrisisInboxEnv(base_url="https://eptan-crisis-inbox.hf.space") as env:
20
+ env.reset()
21
+ tools = env.list_tools()
22
+ inbox = env.call_tool("get_inbox")
23
+ result = env.call_tool("respond_to_message", message_id="msg_001", response="Evacuating now")
24
+ ```
__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CrisisInbox - An RL environment for training LLMs to manage personal task
3
+ overload during natural disasters.
4
+ """
5
+
6
+ from openenv.core.env_server.mcp_types import CallToolAction, ListToolsAction
7
+
8
+ from .client import CrisisInboxEnv
9
+ from .models import Channel, Message, Urgency
10
+
11
+ __all__ = [
12
+ "CrisisInboxEnv",
13
+ "CallToolAction",
14
+ "ListToolsAction",
15
+ "Message",
16
+ "Channel",
17
+ "Urgency",
18
+ ]
client.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CrisisInbox Environment Client.
3
+
4
+ Connects to a running CrisisInbox server via WebSocket.
5
+ """
6
+
7
+ from openenv.core.mcp_client import MCPToolClient
8
+
9
+
10
+ class CrisisInboxEnv(MCPToolClient):
11
+ """
12
+ Client for the CrisisInbox Environment.
13
+
14
+ Provides MCP tool-calling interface:
15
+ - list_tools(): Discover available tools
16
+ - call_tool(name, **kwargs): Call a tool by name
17
+ - reset(): Reset the environment
18
+ - step(action): Execute an action
19
+
20
+ Example:
21
+ >>> with CrisisInboxEnv(base_url="http://localhost:8000").sync() as env:
22
+ ... env.reset()
23
+ ... tools = env.list_tools()
24
+ ... inbox = env.call_tool("get_inbox")
25
+ ... print(inbox)
26
+ """
27
+ pass
models.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CrisisInbox Data Models.
3
+
4
+ Core data model for the crisis inbox environment: messages arriving across
5
+ multiple channels during a 48-hour post-disaster scenario.
6
+ """
7
+
8
+ from enum import Enum
9
+ from typing import List, Optional
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class Channel(str, Enum):
15
+ """Communication channel a message arrives on."""
16
+ EMAIL = "email"
17
+ SMS = "sms"
18
+ PHONE = "phone"
19
+ GOVERNMENT_ALERT = "government_alert"
20
+ APP_NOTIFICATION = "app_notification"
21
+ SOCIAL_MEDIA = "social_media"
22
+
23
+
24
+ class Urgency(str, Enum):
25
+ """Urgency level of a message."""
26
+ CRITICAL = "critical" # Life-safety, immediate action required
27
+ HIGH = "high" # Time-sensitive deadline (hours)
28
+ MEDIUM = "medium" # Important but can wait (days)
29
+ LOW = "low" # Informational, no deadline
30
+
31
+
32
+ class Message(BaseModel):
33
+ """
34
+ A message arriving in the crisis inbox.
35
+
36
+ Represents a single communication from any sender across any channel
37
+ during a post-disaster scenario. Messages may have deadlines, depend
38
+ on other messages/actions, and may be affected by schema drift (changing
39
+ rules mid-episode).
40
+ """
41
+ id: str = Field(description="Unique message identifier")
42
+ sender: str = Field(description="Who sent the message (e.g. 'FEMA', 'Mom', 'State Farm')")
43
+ channel: Channel = Field(description="Communication channel")
44
+ subject: str = Field(description="Brief subject line")
45
+ content: str = Field(description="Full message body")
46
+ urgency: Urgency = Field(description="Urgency level")
47
+ timestamp_hours: float = Field(
48
+ description="Hours since disaster onset (0-48)",
49
+ ge=0.0,
50
+ le=48.0,
51
+ )
52
+ deadline_hours: Optional[float] = Field(
53
+ default=None,
54
+ description="Hours since disaster onset by which action must be taken",
55
+ )
56
+ dependencies: List[str] = Field(
57
+ default_factory=list,
58
+ description="IDs of messages that must be handled before this one can be resolved",
59
+ )
60
+ drift_flag: bool = Field(
61
+ default=False,
62
+ description="Whether this message represents a schema drift event "
63
+ "(policy change, deadline shift, rule update)",
64
+ )
65
+ supersedes: Optional[str] = Field(
66
+ default=None,
67
+ description="ID of a previous message this one replaces (due to drift)",
68
+ )
openenv.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: crisis_inbox
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
pyproject.toml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=45", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "crisis-inbox"
7
+ version = "0.1.0"
8
+ description = "RL environment for training LLMs to manage personal task overload during natural disasters"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "openenv-core[core]>=0.2.1",
12
+ "fastapi>=0.115.0",
13
+ "pydantic>=2.0.0",
14
+ "uvicorn>=0.24.0",
15
+ "requests>=2.31.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.0.0",
21
+ "pytest-cov>=4.0.0",
22
+ ]
23
+
24
+ [project.scripts]
25
+ server = "crisis_inbox.server.app:main"
26
+
27
+ [tool.setuptools]
28
+ include-package-data = true
29
+ packages = ["crisis_inbox", "crisis_inbox.server"]
server/Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install uv for fast dependency management
6
+ RUN pip install uv
7
+
8
+ # Copy project files
9
+ COPY pyproject.toml /app/
10
+ COPY crisis_inbox/ /app/crisis_inbox/
11
+
12
+ # Install dependencies
13
+ RUN uv pip install --system -e .
14
+
15
+ # Expose port
16
+ EXPOSE 8000
17
+
18
+ # Health check
19
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
20
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
21
+
22
+ CMD ["uvicorn", "crisis_inbox.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
server/__init__.py ADDED
File without changes
server/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (160 Bytes). View file
 
server/__pycache__/app.cpython-310.pyc ADDED
Binary file (1.72 kB). View file
 
server/__pycache__/crisis_inbox_environment.cpython-310.pyc ADDED
Binary file (9.02 kB). View file
 
server/app.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI application for the CrisisInbox Environment.
3
+
4
+ Usage:
5
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
6
+ uv run --project . server
7
+ """
8
+
9
+ from typing import Any, Dict, Union
10
+
11
+ from openenv.core.env_server.http_server import create_app
12
+ from openenv.core.env_server.mcp_types import (
13
+ CallToolAction,
14
+ CallToolObservation,
15
+ ListToolsAction,
16
+ )
17
+ from openenv.core.env_server.types import Action
18
+
19
+ from .crisis_inbox_environment import CrisisInboxEnvironment
20
+
21
+
22
+ class MCPAction(Action):
23
+ """Action class that deserializes both ListToolsAction and CallToolAction."""
24
+
25
+ model_config = Action.model_config.copy()
26
+ model_config["extra"] = "allow"
27
+
28
+ @classmethod
29
+ def model_validate(cls, obj: Any, **kwargs: Any) -> Action:
30
+ if isinstance(obj, dict):
31
+ action_type = obj.get("type", "")
32
+ if action_type == "list_tools":
33
+ return ListToolsAction(**obj)
34
+ elif action_type == "call_tool":
35
+ return CallToolAction(**obj)
36
+ return super().model_validate(obj, **kwargs)
37
+
38
+
39
+ app = create_app(
40
+ CrisisInboxEnvironment,
41
+ MCPAction,
42
+ CallToolObservation,
43
+ env_name="crisis_inbox",
44
+ )
45
+
46
+
47
+ def main():
48
+ import uvicorn
49
+ uvicorn.run(app, host="0.0.0.0", port=8000)
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
server/crisis_inbox_environment.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CrisisInbox Environment Implementation.
3
+
4
+ An MCP environment simulating a 48-hour post-disaster inbox where an agent
5
+ must triage and respond to messages across multiple channels.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Optional
10
+ from uuid import uuid4
11
+
12
+ from openenv.core.env_server.mcp_environment import MCPEnvironment
13
+ from openenv.core.env_server.types import Action, Observation, State
14
+ from fastmcp import FastMCP
15
+
16
+ from ..models import Channel, Message, Urgency
17
+
18
+
19
+ # Seed messages for the initial scenario
20
+ SEED_MESSAGES = [
21
+ Message(
22
+ id="msg_001",
23
+ sender="National Weather Service",
24
+ channel=Channel.GOVERNMENT_ALERT,
25
+ subject="Mandatory Evacuation Order - Zone A",
26
+ content=(
27
+ "A mandatory evacuation order has been issued for Zone A effective immediately. "
28
+ "All residents must evacuate within 6 hours. Failure to comply may result in "
29
+ "inability to receive emergency services. Evacuation shelters are open at "
30
+ "Lincoln High School and the Convention Center."
31
+ ),
32
+ urgency=Urgency.CRITICAL,
33
+ timestamp_hours=0.0,
34
+ deadline_hours=6.0,
35
+ drift_flag=False,
36
+ ),
37
+ Message(
38
+ id="msg_002",
39
+ sender="Mom",
40
+ channel=Channel.SMS,
41
+ subject="Are you safe?",
42
+ content=(
43
+ "Honey are you ok?? I saw the news about the hurricane. Your father and I "
44
+ "are worried sick. Please call us when you can. We can drive down to help "
45
+ "if you need us."
46
+ ),
47
+ urgency=Urgency.HIGH,
48
+ timestamp_hours=0.5,
49
+ drift_flag=False,
50
+ ),
51
+ Message(
52
+ id="msg_003",
53
+ sender="State Farm Insurance",
54
+ channel=Channel.EMAIL,
55
+ subject="Important: File your claim within 72 hours",
56
+ content=(
57
+ "Dear Policyholder, if you have experienced property damage due to the "
58
+ "recent disaster, please file your claim within 72 hours to ensure timely "
59
+ "processing. You will need: policy number, photos of damage, and a list "
60
+ "of damaged items. File at statefarm.com/claims or call 1-800-STATE-FARM."
61
+ ),
62
+ urgency=Urgency.HIGH,
63
+ timestamp_hours=1.0,
64
+ deadline_hours=73.0,
65
+ dependencies=[],
66
+ drift_flag=False,
67
+ ),
68
+ Message(
69
+ id="msg_004",
70
+ sender="HR Department",
71
+ channel=Channel.EMAIL,
72
+ subject="Remote work policy during emergency",
73
+ content=(
74
+ "Due to the declared state of emergency, all employees in affected areas "
75
+ "may work remotely. Please log into the HR portal and submit your emergency "
76
+ "status form by end of day tomorrow. If you are unable to work, file for "
77
+ "emergency leave. Contact your manager with your status."
78
+ ),
79
+ urgency=Urgency.MEDIUM,
80
+ timestamp_hours=2.0,
81
+ deadline_hours=26.0,
82
+ drift_flag=False,
83
+ ),
84
+ Message(
85
+ id="msg_005",
86
+ sender="Delta Airlines",
87
+ channel=Channel.APP_NOTIFICATION,
88
+ subject="Flight DL1847 Cancelled",
89
+ content=(
90
+ "Your flight DL1847 on March 10 has been cancelled due to airport closure. "
91
+ "You may rebook at no charge or request a full refund. Rebooking must be "
92
+ "completed within 48 hours."
93
+ ),
94
+ urgency=Urgency.MEDIUM,
95
+ timestamp_hours=3.0,
96
+ deadline_hours=51.0,
97
+ drift_flag=False,
98
+ ),
99
+ ]
100
+
101
+
102
+ class CrisisInboxEnvironment(MCPEnvironment):
103
+ """
104
+ Simulates a 48-hour post-disaster inbox triage scenario.
105
+
106
+ The agent receives messages from family, employers, government agencies,
107
+ insurance companies, and service providers. It must prioritize safety,
108
+ meet deadlines, and adapt to changing rules (schema drift).
109
+
110
+ MCP tools:
111
+ - get_inbox: View current unread messages
112
+ - read_message: Read a specific message in full
113
+ - respond_to_message: Respond to / take action on a message
114
+ - get_status: See current time, score, and handled messages
115
+ """
116
+
117
+ def __init__(self):
118
+ mcp = FastMCP("crisis_inbox")
119
+
120
+ # Environment state (will be set on reset)
121
+ self._messages: list[Message] = []
122
+ self._handled: dict[str, str] = {} # msg_id -> response
123
+ self._current_hour: float = 0.0
124
+ self._score: float = 0.0
125
+ self._state = State(episode_id=str(uuid4()), step_count=0)
126
+
127
+ @mcp.tool
128
+ def get_inbox() -> str:
129
+ """
130
+ View all messages currently in the inbox.
131
+ Returns a JSON list of message summaries (id, sender, subject, urgency, channel, timestamp).
132
+ """
133
+ summaries = []
134
+ for msg in self._messages:
135
+ summaries.append({
136
+ "id": msg.id,
137
+ "sender": msg.sender,
138
+ "subject": msg.subject,
139
+ "urgency": msg.urgency.value,
140
+ "channel": msg.channel.value,
141
+ "timestamp_hours": msg.timestamp_hours,
142
+ "deadline_hours": msg.deadline_hours,
143
+ "handled": msg.id in self._handled,
144
+ "drift_flag": msg.drift_flag,
145
+ })
146
+ return json.dumps(summaries, indent=2)
147
+
148
+ @mcp.tool
149
+ def read_message(message_id: str) -> str:
150
+ """
151
+ Read the full content of a specific message.
152
+
153
+ Args:
154
+ message_id: The ID of the message to read (e.g. 'msg_001')
155
+
156
+ Returns:
157
+ Full message details as JSON, or error if not found.
158
+ """
159
+ for msg in self._messages:
160
+ if msg.id == message_id:
161
+ return msg.model_dump_json(indent=2)
162
+ return json.dumps({"error": f"Message '{message_id}' not found"})
163
+
164
+ @mcp.tool
165
+ def respond_to_message(message_id: str, response: str) -> str:
166
+ """
167
+ Respond to or take action on a message.
168
+
169
+ Args:
170
+ message_id: The ID of the message to respond to
171
+ response: Your response or action description
172
+
173
+ Returns:
174
+ Result of the action including any reward earned.
175
+ """
176
+ msg = None
177
+ for m in self._messages:
178
+ if m.id == message_id:
179
+ msg = m
180
+ break
181
+
182
+ if msg is None:
183
+ return json.dumps({"error": f"Message '{message_id}' not found"})
184
+
185
+ if message_id in self._handled:
186
+ return json.dumps({"error": f"Message '{message_id}' already handled"})
187
+
188
+ # Check dependencies
189
+ unmet = [dep for dep in msg.dependencies if dep not in self._handled]
190
+ if unmet:
191
+ return json.dumps({
192
+ "error": f"Unmet dependencies: {unmet}. Handle those messages first."
193
+ })
194
+
195
+ # Calculate reward based on urgency and timing
196
+ reward = _calculate_reward(msg, self._current_hour, response)
197
+ self._handled[message_id] = response
198
+ self._score += reward
199
+
200
+ # Advance time slightly per action
201
+ self._current_hour += 0.25
202
+
203
+ done = len(self._handled) == len(self._messages) or self._current_hour >= 48.0
204
+
205
+ return json.dumps({
206
+ "status": "handled",
207
+ "message_id": message_id,
208
+ "reward": reward,
209
+ "total_score": self._score,
210
+ "current_hour": self._current_hour,
211
+ "done": done,
212
+ })
213
+
214
+ @mcp.tool
215
+ def get_status() -> str:
216
+ """
217
+ Get current environment status: time elapsed, score, and progress.
218
+ """
219
+ return json.dumps({
220
+ "current_hour": self._current_hour,
221
+ "total_score": self._score,
222
+ "messages_total": len(self._messages),
223
+ "messages_handled": len(self._handled),
224
+ "messages_remaining": len(self._messages) - len(self._handled),
225
+ "done": self._current_hour >= 48.0,
226
+ })
227
+
228
+ super().__init__(mcp)
229
+
230
+ def reset(
231
+ self,
232
+ seed: Optional[int] = None,
233
+ episode_id: Optional[str] = None,
234
+ **kwargs: Any,
235
+ ) -> Observation:
236
+ self._messages = [msg.model_copy() for msg in SEED_MESSAGES]
237
+ self._handled = {}
238
+ self._current_hour = 0.0
239
+ self._score = 0.0
240
+ self._state = State(
241
+ episode_id=episode_id or str(uuid4()),
242
+ step_count=0,
243
+ )
244
+
245
+ return Observation(
246
+ done=False,
247
+ reward=0.0,
248
+ metadata={
249
+ "status": "ready",
250
+ "message": "Crisis inbox loaded. You have 48 hours to triage incoming messages.",
251
+ "messages_count": len(self._messages),
252
+ "scenario": "Post-hurricane evacuation and recovery",
253
+ },
254
+ )
255
+
256
+ def _step_impl(
257
+ self,
258
+ action: Action,
259
+ timeout_s: Optional[float] = None,
260
+ **kwargs: Any,
261
+ ) -> Observation:
262
+ return Observation(
263
+ done=False,
264
+ reward=0.0,
265
+ metadata={
266
+ "error": f"Unknown action type: {type(action).__name__}. "
267
+ "Use ListToolsAction or CallToolAction for MCP interactions."
268
+ },
269
+ )
270
+
271
+ def step(
272
+ self,
273
+ action: Action,
274
+ timeout_s: Optional[float] = None,
275
+ **kwargs: Any,
276
+ ) -> Observation:
277
+ self._state.step_count += 1
278
+ return super().step(action, timeout_s=timeout_s, **kwargs)
279
+
280
+ @property
281
+ def state(self) -> State:
282
+ return self._state
283
+
284
+
285
+ def _calculate_reward(msg: Message, current_hour: float, response: str) -> float:
286
+ """Calculate reward for handling a message based on urgency, timing, and response quality."""
287
+ base_rewards = {
288
+ Urgency.CRITICAL: 10.0,
289
+ Urgency.HIGH: 5.0,
290
+ Urgency.MEDIUM: 3.0,
291
+ Urgency.LOW: 1.0,
292
+ }
293
+ reward = base_rewards.get(msg.urgency, 1.0)
294
+
295
+ # Bonus for handling before deadline
296
+ if msg.deadline_hours is not None:
297
+ if current_hour <= msg.deadline_hours:
298
+ # Earlier = more bonus (up to 50%)
299
+ time_remaining_frac = (msg.deadline_hours - current_hour) / msg.deadline_hours
300
+ reward *= 1.0 + 0.5 * time_remaining_frac
301
+ else:
302
+ # Penalty for missing deadline
303
+ reward *= 0.25
304
+
305
+ # Penalty for very short/empty responses
306
+ if len(response.strip()) < 10:
307
+ reward *= 0.5
308
+
309
+ # Bonus for handling drift-flagged messages (schema adaptation)
310
+ if msg.drift_flag:
311
+ reward *= 1.5
312
+
313
+ return round(reward, 2)
uv.lock ADDED
The diff for this file is too large to render. See raw diff