Skip to content

Commit b679e8f

Browse files
committed
add logger examples + extra logging func
1 parent 4b5d7ca commit b679e8f

6 files changed

Lines changed: 156 additions & 33 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,20 @@ If you use this code or repository in your research, please cite:
135135
}
136136
```
137137

138+
## Optional: Trajectory metadata and logging
139+
`RLMChatCompletion` has an optional `metadata` field (default empty) that can hold the full trajectory (run config + all iterations and sub-calls) so you can reconstruct the run. Pass an `RLMLogger` to capture it:
140+
141+
- **In-memory only** (trajectory on `completion.metadata`): `logger=RLMLogger()` (no `log_dir`).
142+
- **Also save to disk** (JSONL for the visualizer): `logger=RLMLogger(log_dir="./logs")`.
143+
138144
## Optional Debugging: Visualizing RLM Trajectories
139-
We additionally provide a simple visualizer tool to examine and view the code, sub-LM, and root-LM calls of an RLM trajectory. To save log files (`.jsonl`) on every completion call that can be viewed in the visualizer, initialize the `RLMLogger` object and pass it into the `RLM` on initialization:
145+
We provide a simple visualizer to inspect code, sub-LM, and root-LM calls. Use `RLMLogger(log_dir="./logs")` so each completion writes a `.jsonl` file:
140146
```python
141147
from rlm.logger import RLMLogger
142148
from rlm import RLM
143149

144150
logger = RLMLogger(log_dir="./logs")
145-
rlm = RLM(
146-
...
147-
logger=logger
148-
)
151+
rlm = RLM(..., logger=logger)
149152
```
150153

151154
To run the visualizer locally, we use Node.js and shadcn/ui:

‎examples/logger_example.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Logger example: same structure as quickstart, with a different prompt.
3+
Shows trajectory capture in completion.metadata (and optional save to disk).
4+
"""
5+
6+
import os
7+
8+
from dotenv import load_dotenv
9+
10+
from rlm import RLM
11+
from rlm.logger import RLMLogger
12+
13+
load_dotenv()
14+
15+
# In-memory only: trajectory on completion.metadata (no disk write)
16+
logger = RLMLogger()
17+
# To also save JSONL for the visualizer: logger = RLMLogger(log_dir="./logs")
18+
19+
rlm = RLM(
20+
backend="portkey",
21+
backend_kwargs={
22+
"model_name": "@openai/gpt-5-nano",
23+
"api_key": os.getenv("PORTKEY_API_KEY"),
24+
},
25+
environment="local",
26+
environment_kwargs={},
27+
max_depth=1,
28+
logger=logger,
29+
verbose=True,
30+
)
31+
32+
prompt = "Compute the sum 1 + 2 + 3 + 4 + 5 and print the result using a REPL block, then return it with FINAL_VAR."
33+
result = rlm.completion(prompt)
34+
35+
print("Response:", result.response)
36+
if result.metadata:
37+
traj = result.metadata
38+
print("Trajectory: run_metadata +", len(traj.get("iterations", [])), "iterations")
39+
print("Trajectory:", traj)
40+
if traj.get("iterations"):
41+
first = traj["iterations"][0]
42+
print(" First iteration keys:", list(first.keys()))
43+
else:
44+
print("Trajectory: (none — no logger or metadata not captured)")
45+
46+
print("Full response:", result)

‎rlm/core/rlm.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ def completion(
229229
if self.depth >= self.max_depth:
230230
return self._fallback_answer(prompt)
231231

232+
if self.logger:
233+
self.logger.clear_iterations()
234+
232235
with self._spawn_completion_context(prompt) as (lm_handler, environment):
233236
message_history = self._setup_prompt(prompt)
234237

@@ -283,6 +286,7 @@ def completion(
283286
response=final_answer,
284287
usage_summary=usage,
285288
execution_time=time_end - time_start,
289+
metadata=self.logger.get_trajectory() if self.logger else None,
286290
)
287291

288292
# Format the iteration for the next prompt.
@@ -310,6 +314,7 @@ def completion(
310314
response=final_answer,
311315
usage_summary=usage,
312316
execution_time=time_end - time_start,
317+
metadata=self.logger.get_trajectory() if self.logger else None,
313318
)
314319

315320
def _completion_turn(

‎rlm/core/types.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,21 @@ class RLMChatCompletion:
9696
response: str
9797
usage_summary: UsageSummary
9898
execution_time: float
99+
metadata: dict | None = (
100+
None # Full trajectory (run_metadata + iterations) when logger captures it
101+
)
99102

100103
def to_dict(self):
101-
return {
104+
out = {
102105
"root_model": self.root_model,
103106
"prompt": self.prompt,
104107
"response": self.response,
105108
"usage_summary": self.usage_summary.to_dict(),
106109
"execution_time": self.execution_time,
107110
}
111+
if self.metadata is not None:
112+
out["metadata"] = self.metadata
113+
return out
108114

109115
@classmethod
110116
def from_dict(cls, data: dict) -> "RLMChatCompletion":
@@ -114,6 +120,7 @@ def from_dict(cls, data: dict) -> "RLMChatCompletion":
114120
response=data.get("response"),
115121
usage_summary=UsageSummary.from_dict(data.get("usage_summary")),
116122
execution_time=data.get("execution_time"),
123+
metadata=data.get("metadata"),
117124
)
118125

119126

‎rlm/logger/rlm_logger.py‎

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
Logger for RLM iterations.
33
4-
Writes RLMIteration data to JSON-lines files for analysis and debugging.
4+
Captures run metadata and iterations in memory so they can be attached to
5+
RLMChatCompletion.metadata. Optionally writes the same data to JSON-lines files.
56
"""
67

78
import json
@@ -13,50 +14,77 @@
1314

1415

1516
class RLMLogger:
16-
"""Logger that writes RLMIteration data to a JSON-lines file."""
17+
"""
18+
Captures trajectory (run metadata + iterations) for each completion.
19+
By default only captures in memory; set log_dir to also save to disk.
1720
18-
def __init__(self, log_dir: str, file_name: str = "rlm"):
19-
self.log_dir = log_dir
20-
os.makedirs(log_dir, exist_ok=True)
21-
22-
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
23-
run_id = str(uuid.uuid4())[:8]
24-
self.log_file_path = os.path.join(log_dir, f"{file_name}_{timestamp}_{run_id}.jsonl")
21+
- log_dir=None: trajectory is available via get_trajectory() and can be
22+
attached to RLMChatCompletion.metadata (no disk write).
23+
- log_dir="path": same capture plus appends to a JSONL file per run.
24+
"""
2525

26+
def __init__(self, log_dir: str | None = None, file_name: str = "rlm"):
27+
self._save_to_disk = log_dir is not None
28+
self.log_dir = log_dir
29+
self.log_file_path: str | None = None
30+
if self._save_to_disk and log_dir:
31+
os.makedirs(log_dir, exist_ok=True)
32+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
33+
run_id = str(uuid.uuid4())[:8]
34+
self.log_file_path = os.path.join(log_dir, f"{file_name}_{timestamp}_{run_id}.jsonl")
35+
36+
self._run_metadata: dict | None = None
37+
self._iterations: list[dict] = []
2638
self._iteration_count = 0
2739
self._metadata_logged = False
2840

29-
def log_metadata(self, metadata: RLMMetadata):
30-
"""Log RLM metadata as the first entry in the file."""
41+
def log_metadata(self, metadata: RLMMetadata) -> None:
42+
"""Capture run metadata (and optionally write to file)."""
3143
if self._metadata_logged:
3244
return
3345

34-
entry = {
35-
"type": "metadata",
36-
"timestamp": datetime.now().isoformat(),
37-
**metadata.to_dict(),
38-
}
39-
40-
with open(self.log_file_path, "a") as f:
41-
json.dump(entry, f)
42-
f.write("\n")
43-
46+
self._run_metadata = metadata.to_dict()
4447
self._metadata_logged = True
4548

46-
def log(self, iteration: RLMIteration):
47-
"""Log an RLMIteration to the file."""
49+
if self._save_to_disk and self.log_file_path:
50+
entry = {
51+
"type": "metadata",
52+
"timestamp": datetime.now().isoformat(),
53+
**self._run_metadata,
54+
}
55+
with open(self.log_file_path, "a") as f:
56+
json.dump(entry, f)
57+
f.write("\n")
58+
59+
def log(self, iteration: RLMIteration) -> None:
60+
"""Capture one iteration (and optionally append to file)."""
4861
self._iteration_count += 1
49-
5062
entry = {
5163
"type": "iteration",
5264
"iteration": self._iteration_count,
5365
"timestamp": datetime.now().isoformat(),
5466
**iteration.to_dict(),
5567
}
68+
self._iterations.append(entry)
69+
70+
if self._save_to_disk and self.log_file_path:
71+
with open(self.log_file_path, "a") as f:
72+
json.dump(entry, f)
73+
f.write("\n")
5674

57-
with open(self.log_file_path, "a") as f:
58-
json.dump(entry, f)
59-
f.write("\n")
75+
def clear_iterations(self) -> None:
76+
"""Reset iterations for the next completion (trajectory is per completion)."""
77+
self._iterations = []
78+
self._iteration_count = 0
79+
80+
def get_trajectory(self) -> dict | None:
81+
"""Return captured run_metadata + iterations for the current completion, or None if no metadata yet."""
82+
if self._run_metadata is None:
83+
return None
84+
return {
85+
"run_metadata": self._run_metadata,
86+
"iterations": list(self._iterations),
87+
}
6088

6189
@property
6290
def iteration_count(self) -> int:

‎tests/test_types.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
ModelUsageSummary,
66
QueryMetadata,
77
REPLResult,
8+
RLMChatCompletion,
89
RLMIteration,
910
RLMMetadata,
1011
UsageSummary,
@@ -156,6 +157,39 @@ def test_to_dict(self):
156157
assert d["iteration_time"] == 1.5
157158

158159

160+
class TestRLMChatCompletion:
161+
"""Tests for RLMChatCompletion."""
162+
163+
def test_metadata_default_none(self):
164+
usage = UsageSummary(model_usage_summaries={})
165+
c = RLMChatCompletion(
166+
root_model="gpt-4",
167+
prompt="hi",
168+
response="hello",
169+
usage_summary=usage,
170+
execution_time=1.0,
171+
)
172+
assert c.metadata is None
173+
d = c.to_dict()
174+
assert "metadata" not in d
175+
176+
def test_metadata_roundtrip(self):
177+
usage = UsageSummary(model_usage_summaries={})
178+
trajectory = {"run_metadata": {"root_model": "gpt-4"}, "iterations": []}
179+
c = RLMChatCompletion(
180+
root_model="gpt-4",
181+
prompt="hi",
182+
response="hello",
183+
usage_summary=usage,
184+
execution_time=1.0,
185+
metadata=trajectory,
186+
)
187+
d = c.to_dict()
188+
assert d["metadata"] == trajectory
189+
c2 = RLMChatCompletion.from_dict(d)
190+
assert c2.metadata == trajectory
191+
192+
159193
class TestQueryMetadata:
160194
"""Tests for QueryMetadata."""
161195

0 commit comments

Comments
 (0)