Skip to content

Commit 2b1561a

Browse files
authored
add depth component (for depth=1) to allow alternative backends (#44)
1 parent 3f3c854 commit 2b1561a

8 files changed

Lines changed: 92 additions & 33 deletions

File tree

‎rlm/core/comms_utils.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class LMRequest:
2828
prompt: str | dict[str, Any] | None = None
2929
prompts: list[str | dict[str, Any]] | None = None
3030
model: str | None = None
31+
depth: int = 0
3132

3233
@property
3334
def is_batched(self) -> bool:
@@ -43,6 +44,7 @@ def to_dict(self) -> dict:
4344
d["prompts"] = self.prompts
4445
if self.model is not None:
4546
d["model"] = self.model
47+
d["depth"] = self.depth
4648
return d
4749

4850
@classmethod
@@ -52,6 +54,7 @@ def from_dict(cls, data: dict) -> "LMRequest":
5254
prompt=data.get("prompt"),
5355
prompts=data.get("prompts"),
5456
model=data.get("model"),
57+
depth=data.get("depth", -1), # TODO: Default should throw an error
5558
)
5659

5760

@@ -198,18 +201,23 @@ def socket_request(address: tuple[str, int], data: dict, timeout: int = 300) ->
198201
# =============================================================================
199202

200203

201-
def send_lm_request(address: tuple[str, int], request: LMRequest, timeout: int = 300) -> LMResponse:
204+
def send_lm_request(
205+
address: tuple[str, int], request: LMRequest, timeout: int = 300, depth: int | None = None
206+
) -> LMResponse:
202207
"""Send an LM request and return typed response.
203208
204209
Args:
205210
address: (host, port) tuple of LM Handler server.
206211
request: LMRequest to send.
207212
timeout: Socket timeout in seconds.
213+
depth: Optional depth to override request depth.
208214
209215
Returns:
210216
LMResponse with content or error.
211217
"""
212218
try:
219+
if depth is not None:
220+
request.depth = depth
213221
response_data = socket_request(address, request.to_dict(), timeout)
214222
return LMResponse.from_dict(response_data)
215223
except Exception as e:
@@ -221,6 +229,7 @@ def send_lm_request_batched(
221229
prompts: list[str | dict[str, Any]],
222230
model: str | None = None,
223231
timeout: int = 300,
232+
depth: int = 0,
224233
) -> list[LMResponse]:
225234
"""Send a batched LM request and return a list of typed responses.
226235
@@ -229,12 +238,13 @@ def send_lm_request_batched(
229238
prompts: List of prompts to send.
230239
model: Optional model name to use.
231240
timeout: Socket timeout in seconds.
241+
depth: Depth for routing (default 0).
232242
233243
Returns:
234244
List of LMResponse objects, one per prompt, in the same order.
235245
"""
236246
try:
237-
request = LMRequest(prompts=prompts, model=model)
247+
request = LMRequest(prompts=prompts, model=model, depth=depth)
238248
response_data = socket_request(address, request.to_dict(), timeout)
239249
response = LMResponse.from_dict(response_data)
240250

‎rlm/core/lm_handler.py‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def handle(self):
4545

4646
def _handle_single(self, request: LMRequest, handler: "LMHandler") -> LMResponse:
4747
"""Handle a single prompt request."""
48-
client = handler.get_client(request.model)
48+
client = handler.get_client(request.model, request.depth)
4949

5050
start_time = time.perf_counter()
5151
content = client.completion(request.prompt)
@@ -64,7 +64,7 @@ def _handle_single(self, request: LMRequest, handler: "LMHandler") -> LMResponse
6464

6565
def _handle_batched(self, request: LMRequest, handler: "LMHandler") -> LMResponse:
6666
"""Handle a batched prompts request using async for concurrency."""
67-
client = handler.get_client(request.model)
67+
client = handler.get_client(request.model, request.depth)
6868

6969
start_time = time.perf_counter()
7070

@@ -112,8 +112,10 @@ def __init__(
112112
client: BaseLM,
113113
host: str = "127.0.0.1",
114114
port: int = 0, # auto-assign available port
115+
other_backend_client: BaseLM | None = None,
115116
):
116117
self.default_client = client
118+
self.other_backend_client = other_backend_client
117119
self.clients: dict[str, BaseLM] = {}
118120
self.host = host
119121
self._server: ThreadingLMServer | None = None
@@ -126,10 +128,21 @@ def register_client(self, model_name: str, client: BaseLM) -> None:
126128
"""Register a client for a specific model name."""
127129
self.clients[model_name] = client
128130

129-
def get_client(self, model: str | None = None) -> BaseLM:
130-
"""Get client by model name, or return default."""
131+
def get_client(self, model: str | None = None, depth: int = 0) -> BaseLM:
132+
"""Get client by model name or depth, or return default.
133+
134+
Routing logic:
135+
- depth=0: use default_client (main backend)
136+
- depth=1: use other_backend_client if it exists, otherwise default_client
137+
- If model is specified and exists in clients, use that (overrides depth routing)
138+
"""
131139
if model and model in self.clients:
132140
return self.clients[model]
141+
142+
# Route based on depth
143+
if depth == 1 and self.other_backend_client is not None:
144+
return self.other_backend_client
145+
133146
return self.default_client
134147

135148
@property
@@ -179,6 +192,14 @@ def __exit__(self, exc_type, exc_val, exc_tb):
179192
def get_usage_summary(self) -> UsageSummary:
180193
"""Get the usage summary for all clients, merged into a single dict."""
181194
merged = {}
195+
# Include default client
196+
default_summary = self.default_client.get_usage_summary()
197+
merged.update(default_summary.model_usage_summaries)
198+
# Include other backend client if it exists
199+
if self.other_backend_client is not None:
200+
other_summary = self.other_backend_client.get_usage_summary()
201+
merged.update(other_summary.model_usage_summaries)
202+
# Include all registered clients
182203
for client in self.clients.values():
183204
client_summary = client.get_usage_summary()
184205
merged.update(client_summary.model_usage_summaries)

‎rlm/core/rlm.py‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ def __init__(
7676
self.environment_kwargs = (
7777
environment_kwargs.copy() if environment_kwargs is not None else {}
7878
)
79+
# Validate other_backends: currently only support one additional backend
80+
if other_backends is not None:
81+
if len(other_backends) != 1:
82+
raise ValueError(
83+
"We currently only support one additional backend for the recursive sub-calls! "
84+
"This model will be the model used for recursive sub-calls, but this will change in the future"
85+
)
86+
7987
self.other_backends = other_backends
8088
self.other_backend_kwargs = other_backend_kwargs
8189

@@ -124,9 +132,15 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
124132
"""
125133
# Create client and wrap in handler
126134
client: BaseLM = get_client(self.backend, self.backend_kwargs)
127-
lm_handler = LMHandler(client)
128135

129-
# Register other clients to be available as sub-call options
136+
# Create other_backend_client if provided (for depth=1 routing)
137+
other_backend_client: BaseLM | None = None
138+
if self.other_backends and self.other_backend_kwargs:
139+
other_backend_client = get_client(self.other_backends[0], self.other_backend_kwargs[0])
140+
141+
lm_handler = LMHandler(client, other_backend_client=other_backend_client)
142+
143+
# Register other clients to be available as sub-call options (by model name)
130144
if self.other_backends and self.other_backend_kwargs:
131145
for backend, kwargs in zip(self.other_backends, self.other_backend_kwargs, strict=True):
132146
other_client: BaseLM = get_client(backend, kwargs)
@@ -150,6 +164,7 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
150164
env_kwargs = self.environment_kwargs.copy()
151165
env_kwargs["lm_handler_address"] = (lm_handler.host, lm_handler.port)
152166
env_kwargs["context_payload"] = prompt
167+
env_kwargs["depth"] = self.depth + 1 # Environment depth is RLM depth + 1
153168
environment: BaseEnv = get_environment(self.environment_type, env_kwargs)
154169

155170
if self.persistent:

‎rlm/environments/base_env.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ class BaseEnv(ABC):
1010
where isolated environments are on a separate machine from the LM.
1111
"""
1212

13-
def __init__(self, persistent: bool = False, **kwargs):
13+
def __init__(self, persistent: bool = False, depth: int = 1, **kwargs):
1414
self.persistent = persistent
15+
self.depth = depth
1516
self.kwargs = kwargs
1617

1718
@abstractmethod

‎rlm/environments/docker_repl.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class LLMProxyHandler(BaseHTTPRequestHandler):
2828
lm_handler_address: tuple[str, int] | None = None
2929
pending_calls: list[RLMChatCompletion] = []
3030
lock: threading.Lock = threading.Lock()
31+
depth: int = 1
3132

3233
def log_message(self, *args):
3334
pass
@@ -55,7 +56,7 @@ def _handle_single(self, body: dict) -> dict:
5556
if not self.lm_handler_address:
5657
return {"error": "No LM handler configured"}
5758

58-
request = LMRequest(prompt=body.get("prompt"), model=body.get("model"))
59+
request = LMRequest(prompt=body.get("prompt"), model=body.get("model"), depth=self.depth)
5960
response = send_lm_request(self.lm_handler_address, request)
6061

6162
if not response.success:
@@ -72,7 +73,7 @@ def _handle_batched(self, body: dict) -> dict:
7273

7374
prompts = body.get("prompts", [])
7475
responses = send_lm_request_batched(
75-
self.lm_handler_address, prompts, model=body.get("model")
76+
self.lm_handler_address, prompts, model=body.get("model"), depth=self.depth
7677
)
7778

7879
results = []
@@ -87,7 +88,7 @@ def _handle_batched(self, body: dict) -> dict:
8788
return {"responses": results}
8889

8990

90-
def _build_exec_script(code: str, proxy_port: int) -> str:
91+
def _build_exec_script(code: str, proxy_port: int, depth: int = 1) -> str:
9192
"""Build execution script for the container."""
9293
code_b64 = base64.b64encode(code.encode()).decode()
9394

@@ -104,15 +105,15 @@ def _build_exec_script(code: str, proxy_port: int) -> str:
104105
105106
def llm_query(prompt, model=None):
106107
try:
107-
r = requests.post(f"{{PROXY}}/llm_query", json={{"prompt": prompt, "model": model}}, timeout=300)
108+
r = requests.post(f"{{PROXY}}/llm_query", json={{"prompt": prompt, "model": model, "depth": {depth}}}, timeout=300)
108109
d = r.json()
109110
return d.get("response") or f"Error: {{d.get('error')}}"
110111
except Exception as e:
111112
return f"Error: {{e}}"
112113
113114
def llm_query_batched(prompts, model=None):
114115
try:
115-
r = requests.post(f"{{PROXY}}/llm_query_batched", json={{"prompts": prompts, "model": model}}, timeout=300)
116+
r = requests.post(f"{{PROXY}}/llm_query_batched", json={{"prompts": prompts, "model": model, "depth": {depth}}}, timeout=300)
116117
d = r.json()
117118
return d.get("responses") or [f"Error: {{d.get('error')}}"] * len(prompts)
118119
except Exception as e:
@@ -181,13 +182,14 @@ def __init__(
181182
context_payload: dict | list | str | None = None,
182183
setup_code: str | None = None,
183184
persistent: bool = False,
185+
depth: int = 1,
184186
**kwargs,
185187
):
186188
if persistent:
187189
raise NotImplementedError(
188190
"Persistent REPLs are currently not supported for environment: DockerREPL"
189191
)
190-
super().__init__(persistent=persistent, **kwargs)
192+
super().__init__(persistent=persistent, depth=depth, **kwargs)
191193

192194
self.image = image
193195
self.lm_handler_address = lm_handler_address
@@ -216,6 +218,7 @@ def setup(self):
216218
"lm_handler_address": self.lm_handler_address,
217219
"pending_calls": self.pending_calls,
218220
"lock": self._calls_lock,
221+
"depth": self.depth,
219222
},
220223
)
221224
self.proxy_server = HTTPServer(("127.0.0.1", 0), handler)
@@ -266,7 +269,7 @@ def execute_code(self, code: str) -> REPLResult:
266269
with self._calls_lock:
267270
self.pending_calls.clear()
268271

269-
script = _build_exec_script(code, self.proxy_port)
272+
script = _build_exec_script(code, self.proxy_port, self.depth)
270273
result = subprocess.run(
271274
["docker", "exec", self.container_id, "python", "-c", script],
272275
capture_output=True,

‎rlm/environments/local_repl.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,10 @@ def __init__(
124124
context_payload: dict | list | str | None = None,
125125
setup_code: str | None = None,
126126
persistent: bool = False,
127+
depth: int = 1,
127128
**kwargs,
128129
):
129-
super().__init__(persistent=persistent, **kwargs)
130+
super().__init__(persistent=persistent, depth=depth, **kwargs)
130131

131132
self.lm_handler_address = lm_handler_address
132133
self.original_cwd = os.getcwd()
@@ -181,7 +182,7 @@ def _llm_query(self, prompt: str, model: str | None = None) -> str:
181182
return "Error: No LM handler configured"
182183

183184
try:
184-
request = LMRequest(prompt=prompt, model=model)
185+
request = LMRequest(prompt=prompt, model=model, depth=self.depth)
185186
response = send_lm_request(self.lm_handler_address, request)
186187

187188
if not response.success:
@@ -210,7 +211,9 @@ def _llm_query_batched(self, prompts: list[str], model: str | None = None) -> li
210211
return ["Error: No LM handler configured"] * len(prompts)
211212

212213
try:
213-
responses = send_lm_request_batched(self.lm_handler_address, prompts, model=model)
214+
responses = send_lm_request_batched(
215+
self.lm_handler_address, prompts, model=model, depth=self.depth
216+
)
214217

215218
results = []
216219
for response in responses:

‎rlm/environments/modal_repl.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def respond():
112112
# =============================================================================
113113

114114

115-
def _build_exec_script(code: str, broker_port: int = 8080) -> str:
115+
def _build_exec_script(code: str, broker_port: int = 8080, depth: int = 1) -> str:
116116
"""
117117
Build a script that executes code with state persistence.
118118
LLM queries go through the local broker server.
@@ -145,7 +145,7 @@ def llm_query(prompt, model=None):
145145
try:
146146
response = requests.post(
147147
f"{{BROKER_URL}}/enqueue",
148-
json={{"type": "single", "prompt": prompt, "model": model}},
148+
json={{"type": "single", "prompt": prompt, "model": model, "depth": {depth}}},
149149
timeout=300,
150150
)
151151
data = response.json()
@@ -161,7 +161,7 @@ def llm_query_batched(prompts, model=None):
161161
try:
162162
response = requests.post(
163163
f"{{BROKER_URL}}/enqueue",
164-
json={{"type": "batched", "prompts": prompts, "model": model}},
164+
json={{"type": "batched", "prompts": prompts, "model": model, "depth": {depth}}},
165165
timeout=300,
166166
)
167167
data = response.json()
@@ -284,13 +284,14 @@ def __init__(
284284
context_payload: dict | list | str | None = None,
285285
setup_code: str | None = None,
286286
persistent: bool = False,
287+
depth: int = 1,
287288
**kwargs,
288289
):
289290
if persistent:
290291
raise NotImplementedError(
291292
"Persistent REPLs are currently not supported for environment: ModalREPL"
292293
)
293-
super().__init__(persistent=persistent, **kwargs)
294+
super().__init__(persistent=persistent, depth=depth, **kwargs)
294295

295296
self.app_name = app_name
296297
self.timeout = timeout
@@ -387,7 +388,7 @@ def _handle_llm_request(self, req_data: dict) -> dict:
387388

388389
if req_type == "single":
389390
prompt = req_data.get("prompt")
390-
request = LMRequest(prompt=prompt, model=model)
391+
request = LMRequest(prompt=prompt, model=model, depth=self.depth)
391392
response = send_lm_request(self.lm_handler_address, request)
392393

393394
if not response.success:
@@ -401,7 +402,9 @@ def _handle_llm_request(self, req_data: dict) -> dict:
401402

402403
elif req_type == "batched":
403404
prompts = req_data.get("prompts", [])
404-
responses = send_lm_request_batched(self.lm_handler_address, prompts, model=model)
405+
responses = send_lm_request_batched(
406+
self.lm_handler_address, prompts, model=model, depth=self.depth
407+
)
405408

406409
results = []
407410
for resp in responses:
@@ -437,7 +440,7 @@ def execute_code(self, code: str) -> REPLResult:
437440
self.pending_llm_calls.clear()
438441

439442
# Build and execute the script
440-
script = _build_exec_script(code, self.BROKER_PORT)
443+
script = _build_exec_script(code, self.BROKER_PORT, self.depth)
441444
process = self.sandbox.exec("python", "-c", script)
442445

443446
# Read output

0 commit comments

Comments
 (0)