expt-logger 0.1.0.dev19__tar.gz → 0.1.0.dev21__tar.gz
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.
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/PKG-INFO +1 -1
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/pyproject.toml +1 -1
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/__init__.py +22 -2
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/client.py +41 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/run.py +44 -2
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/types.py +9 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_client.py +12 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_global_api.py +23 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_integration_e2e.py +130 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_run.py +74 -37
- expt_logger-0.1.0.dev19/.claude/settings.local.json +0 -30
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/.gitignore +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/.pre-commit-config.yaml +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/DEVELOPMENT.md +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/README.md +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/buffer.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/config.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/env.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/exceptions.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/py.typed +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/src/expt_logger/validation.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/conftest.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_buffer.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_client_integration.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_config.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_env.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_exceptions.py +0 -0
- {expt_logger-0.1.0.dev19 → expt_logger-0.1.0.dev21}/tests/test_validation.py +0 -0
|
@@ -31,8 +31,7 @@ def init(
|
|
|
31
31
|
When attaching to an existing experiment:
|
|
32
32
|
- Validates the experiment exists (raises APIError if not found)
|
|
33
33
|
- Sets experiment status to "active"
|
|
34
|
-
-
|
|
35
|
-
- Subprocess (is_main_process=False): Config parameter is ignored to avoid conflicts
|
|
34
|
+
- Syncs provided config to server (if any)
|
|
36
35
|
|
|
37
36
|
Args:
|
|
38
37
|
name: Optional experiment name (used when creating new experiment)
|
|
@@ -113,6 +112,7 @@ def log_rollout(
|
|
|
113
112
|
step: int | None = None,
|
|
114
113
|
mode: str | None = None,
|
|
115
114
|
commit: bool = True,
|
|
115
|
+
id: str | None = None,
|
|
116
116
|
) -> None:
|
|
117
117
|
"""Log a rollout (conversation + rewards).
|
|
118
118
|
|
|
@@ -152,9 +152,28 @@ def log_rollout(
|
|
|
152
152
|
step=step,
|
|
153
153
|
mode=mode,
|
|
154
154
|
commit=commit,
|
|
155
|
+
id=id
|
|
155
156
|
)
|
|
156
157
|
|
|
157
158
|
|
|
159
|
+
def log_environment(
|
|
160
|
+
rollout_id: str,
|
|
161
|
+
content: str,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Log an environment log entry associated with a rollout.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
rollout_id: ID of the rollout this log entry is associated with
|
|
167
|
+
content: Log content string
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
RuntimeError: If no active run exists
|
|
171
|
+
"""
|
|
172
|
+
if _active_run is None:
|
|
173
|
+
raise RuntimeError("No active run. Call init() first.")
|
|
174
|
+
_active_run.log_environment(rollout_id=rollout_id, content=content)
|
|
175
|
+
|
|
176
|
+
|
|
158
177
|
def log_error(
|
|
159
178
|
error: Exception | str,
|
|
160
179
|
step: int | None = None,
|
|
@@ -298,6 +317,7 @@ __all__ = [
|
|
|
298
317
|
"init",
|
|
299
318
|
"log",
|
|
300
319
|
"log_rollout",
|
|
320
|
+
"log_environment",
|
|
301
321
|
"log_error",
|
|
302
322
|
"commit",
|
|
303
323
|
"end",
|
|
@@ -280,6 +280,47 @@ class APIClient:
|
|
|
280
280
|
payload = {"rollouts": chunk}
|
|
281
281
|
self._request("POST", url, json=payload)
|
|
282
282
|
|
|
283
|
+
def log_env_logs(
|
|
284
|
+
self,
|
|
285
|
+
experiment_id: str,
|
|
286
|
+
rollout_id: str,
|
|
287
|
+
content: str,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Log an environment log entry for an experiment.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
experiment_id: Experiment ID
|
|
293
|
+
rollout_id: Rollout ID this log entry is associated with
|
|
294
|
+
content: Log content string
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
APIError: If request fails
|
|
298
|
+
"""
|
|
299
|
+
url = f"{self.base_url}/api/experiments/{experiment_id}/env-logs"
|
|
300
|
+
payload = {"rolloutId": rollout_id, "content": content}
|
|
301
|
+
self._request("POST", url, json=payload)
|
|
302
|
+
|
|
303
|
+
def get_env_logs(
|
|
304
|
+
self,
|
|
305
|
+
experiment_id: str,
|
|
306
|
+
rollout_id: str,
|
|
307
|
+
) -> list[dict]:
|
|
308
|
+
"""Fetch environment logs for a specific rollout.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
experiment_id: Experiment ID
|
|
312
|
+
rollout_id: Rollout ID to filter logs by
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
List of env log entries
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
APIError: If request fails
|
|
319
|
+
"""
|
|
320
|
+
url = f"{self.base_url}/api/experiments/{experiment_id}/env-logs"
|
|
321
|
+
response = self._request("GET", url, params={"rolloutId": rollout_id})
|
|
322
|
+
return response.json()["logs"]
|
|
323
|
+
|
|
283
324
|
def log_errors(
|
|
284
325
|
self,
|
|
285
326
|
experiment_id: str,
|
|
@@ -23,6 +23,7 @@ from expt_logger.types import (
|
|
|
23
23
|
ConfigUpdateCommand,
|
|
24
24
|
ErrorItem,
|
|
25
25
|
LogCommand,
|
|
26
|
+
LogEnvironmentCommand,
|
|
26
27
|
LogErrorCommand,
|
|
27
28
|
LogRolloutCommand,
|
|
28
29
|
MessageItem,
|
|
@@ -81,8 +82,9 @@ class Run:
|
|
|
81
82
|
resolved_experiment_id = get_experiment_id(experiment_id, is_main_process=False)
|
|
82
83
|
if resolved_experiment_id is not None:
|
|
83
84
|
self._experiment_id = resolved_experiment_id
|
|
84
|
-
#
|
|
85
|
-
|
|
85
|
+
# Do not set the config if already attached to an experiment to avoid
|
|
86
|
+
# overwriting existing settings
|
|
87
|
+
self._validate_and_attach_experiment()
|
|
86
88
|
logger.info(
|
|
87
89
|
f"[expt_logger] Attached to experiment ID: {self._experiment_id} (subprocess)"
|
|
88
90
|
)
|
|
@@ -229,6 +231,7 @@ class Run:
|
|
|
229
231
|
step: int | None = None,
|
|
230
232
|
mode: str | None = None,
|
|
231
233
|
commit: bool = True,
|
|
234
|
+
id: str | None = None,
|
|
232
235
|
) -> None:
|
|
233
236
|
"""Log a rollout (conversation + rewards).
|
|
234
237
|
|
|
@@ -239,6 +242,7 @@ class Run:
|
|
|
239
242
|
step: Optional step number (overrides worker's step counter if provided)
|
|
240
243
|
mode: Optional mode (defaults to "train")
|
|
241
244
|
commit: Whether to flush buffer after logging
|
|
245
|
+
id: Optional identifier for this rollout
|
|
242
246
|
"""
|
|
243
247
|
from expt_logger.validation import (
|
|
244
248
|
validate_messages,
|
|
@@ -274,6 +278,7 @@ class Run:
|
|
|
274
278
|
"rewards": reward_items,
|
|
275
279
|
"mode": mode or "train",
|
|
276
280
|
"step": step,
|
|
281
|
+
"id": id,
|
|
277
282
|
}
|
|
278
283
|
|
|
279
284
|
try:
|
|
@@ -284,6 +289,28 @@ class Run:
|
|
|
284
289
|
if commit:
|
|
285
290
|
self.commit()
|
|
286
291
|
|
|
292
|
+
def log_environment(
|
|
293
|
+
self,
|
|
294
|
+
rollout_id: str,
|
|
295
|
+
content: str,
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Log an environment log entry associated with a rollout.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
rollout_id: ID of the rollout this log entry is associated with
|
|
301
|
+
content: Log content string
|
|
302
|
+
"""
|
|
303
|
+
env_cmd: LogEnvironmentCommand = {
|
|
304
|
+
"rollout_id": rollout_id,
|
|
305
|
+
"content": content,
|
|
306
|
+
}
|
|
307
|
+
try:
|
|
308
|
+
self._queue.put_nowait(("log_environment", env_cmd))
|
|
309
|
+
except Full:
|
|
310
|
+
logger.warning(
|
|
311
|
+
f"Command queue full, dropping environment log for rollout: {rollout_id}"
|
|
312
|
+
)
|
|
313
|
+
|
|
287
314
|
def log_error(
|
|
288
315
|
self,
|
|
289
316
|
error: Exception | str,
|
|
@@ -423,6 +450,8 @@ class Run:
|
|
|
423
450
|
self._handle_log(cast(LogCommand, data))
|
|
424
451
|
elif command == "log_rollout":
|
|
425
452
|
self._handle_log_rollout(cast(LogRolloutCommand, data))
|
|
453
|
+
elif command == "log_environment":
|
|
454
|
+
self._handle_log_environment(cast(LogEnvironmentCommand, data))
|
|
426
455
|
elif command == "log_error":
|
|
427
456
|
self._handle_log_error(cast(LogErrorCommand, data))
|
|
428
457
|
elif command == "commit":
|
|
@@ -533,9 +562,22 @@ class Run:
|
|
|
533
562
|
"messages": data["messages"],
|
|
534
563
|
"rewards": data["rewards"],
|
|
535
564
|
}
|
|
565
|
+
if data["id"] is not None:
|
|
566
|
+
rollout["id"] = data["id"]
|
|
536
567
|
|
|
537
568
|
self._buffer.add_rollout(rollout)
|
|
538
569
|
|
|
570
|
+
def _handle_log_environment(self, data: LogEnvironmentCommand) -> None:
|
|
571
|
+
"""Handle log_environment command.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
data: Log environment command data with rollout_id and content
|
|
575
|
+
"""
|
|
576
|
+
try:
|
|
577
|
+
self._client.log_env_logs(self._experiment_id, data["rollout_id"], data["content"])
|
|
578
|
+
except Exception as e:
|
|
579
|
+
logger.error(f"Error logging environment log: {e}", exc_info=True)
|
|
580
|
+
|
|
539
581
|
def _handle_log_error(self, data: LogErrorCommand) -> None:
|
|
540
582
|
"""Handle log_error command.
|
|
541
583
|
|
|
@@ -36,6 +36,7 @@ class RolloutItem(TypedDict):
|
|
|
36
36
|
promptText: str
|
|
37
37
|
messages: list[MessageItem]
|
|
38
38
|
rewards: list[RewardItem]
|
|
39
|
+
id: NotRequired[str]
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
class ErrorItem(TypedDict):
|
|
@@ -75,6 +76,7 @@ class LogRolloutCommand(TypedDict):
|
|
|
75
76
|
rewards: list[RewardItem]
|
|
76
77
|
mode: str
|
|
77
78
|
step: int | None
|
|
79
|
+
id: str | None
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
class LogErrorCommand(TypedDict):
|
|
@@ -87,6 +89,13 @@ class LogErrorCommand(TypedDict):
|
|
|
87
89
|
step: int | None
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
class LogEnvironmentCommand(TypedDict):
|
|
93
|
+
"""Command to log an environment log entry."""
|
|
94
|
+
|
|
95
|
+
rollout_id: str
|
|
96
|
+
content: str
|
|
97
|
+
|
|
98
|
+
|
|
90
99
|
class CommitCommand(TypedDict):
|
|
91
100
|
"""Command to commit (flush) the buffer."""
|
|
92
101
|
|
|
@@ -294,6 +294,18 @@ def test_log_rollouts(client):
|
|
|
294
294
|
assert call_args[1]["json"] == {"rollouts": rollouts}
|
|
295
295
|
|
|
296
296
|
|
|
297
|
+
def test_log_env_logs(client):
|
|
298
|
+
"""Test logging environment logs."""
|
|
299
|
+
with patch.object(client, "_request") as mock_request:
|
|
300
|
+
client.log_env_logs("exp-123", "rollout-456", "step 1 observation")
|
|
301
|
+
|
|
302
|
+
mock_request.assert_called_once()
|
|
303
|
+
call_args = mock_request.call_args
|
|
304
|
+
assert call_args[0][0] == "POST"
|
|
305
|
+
assert call_args[0][1] == "https://test.example.com/api/experiments/exp-123/env-logs"
|
|
306
|
+
assert call_args[1]["json"] == {"rolloutId": "rollout-456", "content": "step 1 observation"}
|
|
307
|
+
|
|
308
|
+
|
|
297
309
|
def test_log_errors(client):
|
|
298
310
|
"""Test logging errors."""
|
|
299
311
|
with patch.object(client, "_request") as mock_request:
|
|
@@ -134,6 +134,28 @@ def test_log_rollout_raises_if_no_active_run(mock_run):
|
|
|
134
134
|
assert "No active run" in str(exc_info.value)
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
def test_log_environment_delegates_to_run(mock_run):
|
|
138
|
+
"""Test log_environment() delegates to the active run."""
|
|
139
|
+
_, run_instance = mock_run
|
|
140
|
+
|
|
141
|
+
expt_logger.init()
|
|
142
|
+
expt_logger.log_environment(rollout_id="rollout-abc", content="step 1 observation")
|
|
143
|
+
|
|
144
|
+
run_instance.log_environment.assert_called_once_with(
|
|
145
|
+
rollout_id="rollout-abc",
|
|
146
|
+
content="step 1 observation",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_log_environment_raises_if_no_active_run():
|
|
151
|
+
"""Test log_environment() raises RuntimeError if no active run."""
|
|
152
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
153
|
+
expt_logger.log_environment("rollout-abc", "some content")
|
|
154
|
+
|
|
155
|
+
assert "No active run" in str(exc_info.value)
|
|
156
|
+
assert "init()" in str(exc_info.value)
|
|
157
|
+
|
|
158
|
+
|
|
137
159
|
def test_log_error_delegates_to_run(mock_run):
|
|
138
160
|
"""Test log_error() delegates to the active run."""
|
|
139
161
|
_, run_instance = mock_run
|
|
@@ -393,6 +415,7 @@ def test_all_exports():
|
|
|
393
415
|
"init",
|
|
394
416
|
"log",
|
|
395
417
|
"log_rollout",
|
|
418
|
+
"log_environment",
|
|
396
419
|
"log_error",
|
|
397
420
|
"commit",
|
|
398
421
|
"end",
|
|
@@ -112,6 +112,22 @@ def fetch_rollouts(shared_api_key: str, base_url: str):
|
|
|
112
112
|
return _fetch
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
@pytest.fixture
|
|
116
|
+
def fetch_env_logs(shared_api_key: str, base_url: str):
|
|
117
|
+
"""Factory fixture for fetching environment log data."""
|
|
118
|
+
|
|
119
|
+
def _fetch(experiment_id: str, rollout_id: str):
|
|
120
|
+
response = requests.get(
|
|
121
|
+
f"{base_url}/api/experiments/{experiment_id}/env-logs",
|
|
122
|
+
params={"rolloutId": rollout_id},
|
|
123
|
+
headers={"Authorization": f"Bearer {shared_api_key}"},
|
|
124
|
+
)
|
|
125
|
+
assert response.status_code == 200
|
|
126
|
+
return response.json()["logs"]
|
|
127
|
+
|
|
128
|
+
return _fetch
|
|
129
|
+
|
|
130
|
+
|
|
115
131
|
# ============================================================================
|
|
116
132
|
# Test Class 1: Basic Workflow Tests
|
|
117
133
|
# ============================================================================
|
|
@@ -1054,3 +1070,117 @@ class TestMultiProcess:
|
|
|
1054
1070
|
# Worker metrics
|
|
1055
1071
|
for i in range(num_workers):
|
|
1056
1072
|
assert f"worker-{i + 1}-metric" in scalars
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
# ============================================================================
|
|
1076
|
+
# Test Class 12: Environment Log Tests
|
|
1077
|
+
# ============================================================================
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
@pytest.mark.integration
|
|
1081
|
+
class TestEnvironmentLogs:
|
|
1082
|
+
"""Environment log functionality."""
|
|
1083
|
+
|
|
1084
|
+
def test_log_environment_basic(
|
|
1085
|
+
self,
|
|
1086
|
+
shared_api_key: str,
|
|
1087
|
+
base_url: str,
|
|
1088
|
+
cleanup_experiments: list[str],
|
|
1089
|
+
fetch_env_logs,
|
|
1090
|
+
) -> None:
|
|
1091
|
+
"""Test basic environment log persists to server."""
|
|
1092
|
+
run = expt_logger.init(
|
|
1093
|
+
name="test-env-log-basic",
|
|
1094
|
+
api_key=shared_api_key,
|
|
1095
|
+
base_url=base_url,
|
|
1096
|
+
)
|
|
1097
|
+
cleanup_experiments.append(run._experiment_id)
|
|
1098
|
+
|
|
1099
|
+
expt_logger.log_environment("00000000-0000-0000-0000-000000000001", "observation: step=1, reward=0.9")
|
|
1100
|
+
time.sleep(0.5)
|
|
1101
|
+
|
|
1102
|
+
env_logs = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-000000000001")
|
|
1103
|
+
assert len(env_logs) == 1
|
|
1104
|
+
assert env_logs[0]["content"] == "observation: step=1, reward=0.9"
|
|
1105
|
+
|
|
1106
|
+
expt_logger.end()
|
|
1107
|
+
|
|
1108
|
+
def test_log_environment_multiple_for_same_rollout(
|
|
1109
|
+
self,
|
|
1110
|
+
shared_api_key: str,
|
|
1111
|
+
base_url: str,
|
|
1112
|
+
cleanup_experiments: list[str],
|
|
1113
|
+
fetch_env_logs,
|
|
1114
|
+
) -> None:
|
|
1115
|
+
"""Test multiple environment logs for the same rollout are all stored."""
|
|
1116
|
+
run = expt_logger.init(
|
|
1117
|
+
name="test-env-log-multiple",
|
|
1118
|
+
api_key=shared_api_key,
|
|
1119
|
+
base_url=base_url,
|
|
1120
|
+
)
|
|
1121
|
+
cleanup_experiments.append(run._experiment_id)
|
|
1122
|
+
|
|
1123
|
+
expt_logger.log_environment("00000000-0000-0000-0000-000000000002", "step 1: action=left")
|
|
1124
|
+
expt_logger.log_environment("00000000-0000-0000-0000-000000000002", "step 2: action=right")
|
|
1125
|
+
expt_logger.log_environment("00000000-0000-0000-0000-000000000002", "step 3: action=jump")
|
|
1126
|
+
time.sleep(0.5)
|
|
1127
|
+
|
|
1128
|
+
env_logs = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-000000000002")
|
|
1129
|
+
assert len(env_logs) == 3
|
|
1130
|
+
contents = {log["content"] for log in env_logs}
|
|
1131
|
+
assert contents == {"step 1: action=left", "step 2: action=right", "step 3: action=jump"}
|
|
1132
|
+
|
|
1133
|
+
expt_logger.end()
|
|
1134
|
+
|
|
1135
|
+
def test_log_environment_different_rollouts(
|
|
1136
|
+
self,
|
|
1137
|
+
shared_api_key: str,
|
|
1138
|
+
base_url: str,
|
|
1139
|
+
cleanup_experiments: list[str],
|
|
1140
|
+
fetch_env_logs,
|
|
1141
|
+
) -> None:
|
|
1142
|
+
"""Test environment logs for different rollouts stay separate."""
|
|
1143
|
+
run = expt_logger.init(
|
|
1144
|
+
name="test-env-log-separate",
|
|
1145
|
+
api_key=shared_api_key,
|
|
1146
|
+
base_url=base_url,
|
|
1147
|
+
)
|
|
1148
|
+
cleanup_experiments.append(run._experiment_id)
|
|
1149
|
+
|
|
1150
|
+
expt_logger.log_environment("00000000-0000-0000-0000-00000000003a", "log for rollout A")
|
|
1151
|
+
expt_logger.log_environment("00000000-0000-0000-0000-00000000003b", "log for rollout B")
|
|
1152
|
+
time.sleep(0.5)
|
|
1153
|
+
|
|
1154
|
+
logs_a = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-00000000003a")
|
|
1155
|
+
logs_b = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-00000000003b")
|
|
1156
|
+
assert len(logs_a) == 1
|
|
1157
|
+
assert logs_a[0]["content"] == "log for rollout A"
|
|
1158
|
+
assert len(logs_b) == 1
|
|
1159
|
+
assert logs_b[0]["content"] == "log for rollout B"
|
|
1160
|
+
|
|
1161
|
+
expt_logger.end()
|
|
1162
|
+
|
|
1163
|
+
def test_log_environment_multiline_content(
|
|
1164
|
+
self,
|
|
1165
|
+
shared_api_key: str,
|
|
1166
|
+
base_url: str,
|
|
1167
|
+
cleanup_experiments: list[str],
|
|
1168
|
+
fetch_env_logs,
|
|
1169
|
+
) -> None:
|
|
1170
|
+
"""Test environment log with multiline content."""
|
|
1171
|
+
run = expt_logger.init(
|
|
1172
|
+
name="test-env-log-multiline",
|
|
1173
|
+
api_key=shared_api_key,
|
|
1174
|
+
base_url=base_url,
|
|
1175
|
+
)
|
|
1176
|
+
cleanup_experiments.append(run._experiment_id)
|
|
1177
|
+
|
|
1178
|
+
content = "obs: {x: 1.0, y: 2.0}\nreward: 0.5\ndone: false\ninfo: {step: 10}"
|
|
1179
|
+
expt_logger.log_environment("00000000-0000-0000-0000-000000000004", content)
|
|
1180
|
+
time.sleep(0.5)
|
|
1181
|
+
|
|
1182
|
+
env_logs = fetch_env_logs(run._experiment_id, "00000000-0000-0000-0000-000000000004")
|
|
1183
|
+
assert len(env_logs) == 1
|
|
1184
|
+
assert env_logs[0]["content"] == content
|
|
1185
|
+
|
|
1186
|
+
expt_logger.end()
|
|
@@ -1482,49 +1482,86 @@ def test_log_error_with_invalid_mode_empty(mock_client):
|
|
|
1482
1482
|
run.end()
|
|
1483
1483
|
|
|
1484
1484
|
|
|
1485
|
-
|
|
1486
|
-
"""Test subprocess (is_main_process=False) does not update config even if provided."""
|
|
1487
|
-
import os
|
|
1488
|
-
import tempfile
|
|
1485
|
+
# ========== log_environment Tests ==========
|
|
1489
1486
|
|
|
1487
|
+
|
|
1488
|
+
def test_log_environment_calls_client(mock_client):
|
|
1489
|
+
"""Test log_environment() calls client with correct args."""
|
|
1490
1490
|
_, client_instance = mock_client
|
|
1491
1491
|
|
|
1492
|
-
|
|
1493
|
-
temp_dir = tempfile.gettempdir()
|
|
1494
|
-
experiment_id_file = os.path.join(temp_dir, "expt-logger-experiment-id.txt")
|
|
1495
|
-
with open(experiment_id_file, "w") as f:
|
|
1496
|
-
f.write("existing-exp-123")
|
|
1492
|
+
run = Run(name="test-run", api_key="test-key", base_url="https://test.example.com")
|
|
1497
1493
|
|
|
1498
|
-
|
|
1499
|
-
# Subprocess tries to init with a config
|
|
1500
|
-
run = Run(
|
|
1501
|
-
config={"subprocess_key": "should_be_ignored"},
|
|
1502
|
-
api_key="test-key",
|
|
1503
|
-
base_url="https://test.example.com",
|
|
1504
|
-
is_main_process=False,
|
|
1505
|
-
)
|
|
1494
|
+
run.log_environment("rollout-abc", "step 1 observation")
|
|
1506
1495
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1496
|
+
# Give worker time to process
|
|
1497
|
+
time.sleep(0.1)
|
|
1509
1498
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1499
|
+
assert client_instance.log_env_logs.called
|
|
1500
|
+
call_args = client_instance.log_env_logs.call_args
|
|
1501
|
+
assert call_args[0][0] == "test-exp-id"
|
|
1502
|
+
assert call_args[0][1] == "rollout-abc"
|
|
1503
|
+
assert call_args[0][2] == "step 1 observation"
|
|
1512
1504
|
|
|
1513
|
-
|
|
1514
|
-
update_calls = client_instance.update_experiment.call_args_list
|
|
1515
|
-
# Find the call that sets status to active (during attach)
|
|
1516
|
-
attach_call = None
|
|
1517
|
-
for call in update_calls:
|
|
1518
|
-
if call.kwargs.get("status") == "active":
|
|
1519
|
-
attach_call = call
|
|
1520
|
-
break
|
|
1505
|
+
run.end()
|
|
1521
1506
|
|
|
1522
|
-
assert attach_call is not None, "Should have called update_experiment with status='active'"
|
|
1523
|
-
# Verify config was NOT passed (should be None)
|
|
1524
|
-
assert attach_call.kwargs.get("config") is None
|
|
1525
1507
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1508
|
+
def test_log_environment_multiple_calls(mock_client):
|
|
1509
|
+
"""Test log_environment() can be called multiple times for different rollouts."""
|
|
1510
|
+
_, client_instance = mock_client
|
|
1511
|
+
|
|
1512
|
+
run = Run(name="test-run", api_key="test-key", base_url="https://test.example.com")
|
|
1513
|
+
|
|
1514
|
+
run.log_environment("rollout-1", "log content 1")
|
|
1515
|
+
run.log_environment("rollout-2", "log content 2")
|
|
1516
|
+
|
|
1517
|
+
# Give worker time to process
|
|
1518
|
+
time.sleep(0.1)
|
|
1519
|
+
|
|
1520
|
+
assert client_instance.log_env_logs.call_count == 2
|
|
1521
|
+
first_call = client_instance.log_env_logs.call_args_list[0]
|
|
1522
|
+
second_call = client_instance.log_env_logs.call_args_list[1]
|
|
1523
|
+
assert first_call[0][1] == "rollout-1"
|
|
1524
|
+
assert first_call[0][2] == "log content 1"
|
|
1525
|
+
assert second_call[0][1] == "rollout-2"
|
|
1526
|
+
assert second_call[0][2] == "log content 2"
|
|
1527
|
+
|
|
1528
|
+
run.end()
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def test_log_environment_queue_full_handling(mock_client):
|
|
1532
|
+
"""Test that queue full is handled gracefully for environment logs."""
|
|
1533
|
+
_, _ = mock_client
|
|
1534
|
+
|
|
1535
|
+
run = Run(name="test-run", api_key="test-key", base_url="https://test.example.com")
|
|
1536
|
+
run._queue = queue.Queue(maxsize=1)
|
|
1537
|
+
|
|
1538
|
+
with patch("expt_logger.run.logger") as mock_logger:
|
|
1539
|
+
run.log_environment("rollout-1", "log 1")
|
|
1540
|
+
run.log_environment("rollout-2", "log 2") # Should trigger queue full
|
|
1541
|
+
|
|
1542
|
+
assert mock_logger.warning.called
|
|
1543
|
+
warning_msg = mock_logger.warning.call_args[0][0]
|
|
1544
|
+
assert "queue full" in warning_msg.lower()
|
|
1545
|
+
|
|
1546
|
+
run.end()
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def test_log_environment_api_error_is_logged(mock_client):
|
|
1550
|
+
"""Test that API errors from log_environment are caught and logged, not raised."""
|
|
1551
|
+
_, client_instance = mock_client
|
|
1552
|
+
|
|
1553
|
+
client_instance.log_env_logs.side_effect = APIError("Server error")
|
|
1554
|
+
|
|
1555
|
+
run = Run(name="test-run", api_key="test-key", base_url="https://test.example.com")
|
|
1556
|
+
|
|
1557
|
+
with patch("expt_logger.run.logger") as mock_logger:
|
|
1558
|
+
run.log_environment("rollout-abc", "some content")
|
|
1559
|
+
|
|
1560
|
+
# Give worker time to process and handle error
|
|
1561
|
+
time.sleep(0.2)
|
|
1562
|
+
|
|
1563
|
+
assert run._worker_thread is not None
|
|
1564
|
+
assert run._worker_thread.is_alive()
|
|
1565
|
+
assert mock_logger.error.called
|
|
1566
|
+
|
|
1567
|
+
run.end()
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(uv run mypy:*)",
|
|
5
|
-
"Bash(uv run pytest:*)",
|
|
6
|
-
"Bash(python -m pytest:*)",
|
|
7
|
-
"Bash(python example_config_validation.py:*)",
|
|
8
|
-
"Bash(uv run python:*)",
|
|
9
|
-
"Bash(ls:*)",
|
|
10
|
-
"Bash(curl:*)",
|
|
11
|
-
"Bash(cat:*)",
|
|
12
|
-
"Bash(EXPT_LOGGER_LOG_LEVEL=DEBUG uv run pytest:*)",
|
|
13
|
-
"Bash(EXPT_LOGGER_API_KEY=test EXPT_LOGGER_BASE_URL=http://localhost:3000 uv run python:*)",
|
|
14
|
-
"Bash(EXPT_LOGGER_API_KEY=test uv run python:*)",
|
|
15
|
-
"Bash(uv sync:*)",
|
|
16
|
-
"Bash(python -m json.tool:*)",
|
|
17
|
-
"Bash(uv run ruff:*)",
|
|
18
|
-
"Bash(find:*)",
|
|
19
|
-
"Bash(uv add:*)",
|
|
20
|
-
"Bash(python:*)",
|
|
21
|
-
"Bash(EXPT_LOGGER_LOG_LEVEL=DEBUG uv run python:*)",
|
|
22
|
-
"Bash(uv run:*)",
|
|
23
|
-
"Bash(git restore:*)",
|
|
24
|
-
"Bash(git checkout:*)",
|
|
25
|
-
"Bash(git rebase:*)",
|
|
26
|
-
"Bash(git add:*)",
|
|
27
|
-
"Bash(pip install:*)"
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|