Skip to content

Commit f3f0713

Browse files
darindarin
authored andcommitted
Add RLM sub-LM integration coverage
1 parent 6e4e808 commit f3f0713

1 file changed

Lines changed: 57 additions & 15 deletions

File tree

crates/dspy-rs/tests/test_rlm_loop_integration.rs

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fn text_response(text: impl Into<String>) -> AssistantContent {
1313
AssistantContent::Text(Text { text: text.into() })
1414
}
1515

16-
async fn configure_test_lm(responses: Vec<String>) -> LM {
16+
async fn build_test_lm_with_client(responses: Vec<String>) -> (LM, TestCompletionModel) {
1717
let client = TestCompletionModel::new(responses.into_iter().map(text_response));
1818
let lm = temp_env::async_with_vars(
1919
[("OPENAI_API_KEY", Some("test"))],
@@ -24,27 +24,20 @@ async fn configure_test_lm(responses: Vec<String>) -> LM {
2424
)
2525
.await
2626
.expect("build lm")
27-
.with_client(LMClient::Test(client))
27+
.with_client(LMClient::Test(client.clone()))
2828
.await
2929
.expect("install test client");
30+
(lm, client)
31+
}
32+
33+
async fn configure_test_lm(responses: Vec<String>) -> LM {
34+
let (lm, _) = build_test_lm_with_client(responses).await;
3035
configure(lm.clone(), ChatAdapter::new());
3136
lm
3237
}
3338

3439
async fn configure_test_lm_with_client(responses: Vec<String>) -> (LM, TestCompletionModel) {
35-
let client = TestCompletionModel::new(responses.into_iter().map(text_response));
36-
let lm = temp_env::async_with_vars(
37-
[("OPENAI_API_KEY", Some("test"))],
38-
LM::builder()
39-
.model("openai:gpt-4o-mini".to_string())
40-
.temperature(0.0)
41-
.build(),
42-
)
43-
.await
44-
.expect("build lm")
45-
.with_client(LMClient::Test(client.clone()))
46-
.await
47-
.expect("install test client");
40+
let (lm, client) = build_test_lm_with_client(responses).await;
4841
configure(lm.clone(), ChatAdapter::new());
4942
(lm, client)
5043
}
@@ -206,3 +199,52 @@ async fn rlm_feedback_carries_truncation_marker_with_configured_budget() {
206199
"[output truncated at 10 chars - full content in variable. pass to llm_query() to analyze]"
207200
));
208201
}
202+
203+
#[cfg_attr(miri, ignore = "MIRI has issues with tokio's I/O driver")]
204+
#[tokio::test(flavor = "multi_thread")]
205+
async fn rlm_sub_lm_tools_persist_state_and_decrement_budget_across_turns() {
206+
let _lock = SETTINGS_LOCK.lock().await;
207+
let (_action_lm, action_client) = configure_test_lm_with_client(vec![
208+
"single = llm_query('single')\nbatch = llm_query_batched(['left', 'right'])\ncounter = 40 + len(batch)".to_string(),
209+
"try:\n llm_query('should_fail')\n budget_state = 'not_exhausted'\nexcept Exception as err:\n budget_state = 'exhausted' if 'budget exhausted' in str(err) else f'unexpected:{err}'\nSUBMIT(answer=f'{counter}:{budget_state}:{single}')".to_string(),
210+
])
211+
.await;
212+
let (sub_lm, _) = build_test_lm_with_client(vec![
213+
"single-ok".to_string(),
214+
"batch-a".to_string(),
215+
"batch-b".to_string(),
216+
])
217+
.await;
218+
219+
let rlm = Rlm::<RlmLoopSig>::builder()
220+
.runtime(Arc::new(PyO3Runtime))
221+
.sub_lm(Arc::new(sub_lm))
222+
.max_iterations(2)
223+
.max_llm_calls(3)
224+
.enable_extraction_fallback(false)
225+
.build();
226+
227+
let predicted = rlm
228+
.call(RlmLoopSigInput {
229+
prompt: "Use both sub-LM helpers, then submit on turn two.".to_string(),
230+
})
231+
.await
232+
.expect("rlm should complete with persisted state and enforced budget");
233+
234+
assert_eq!(predicted.answer, "42:exhausted:single-ok");
235+
assert!(
236+
predicted
237+
.metadata()
238+
.raw_response
239+
.contains("llm_query_batched")
240+
);
241+
242+
let last_request = action_client
243+
.last_request()
244+
.expect("expected second-turn request with feedback");
245+
let request_debug = format!("{last_request:?}");
246+
assert!(
247+
request_debug.contains("0/3 sub-model calls remaining"),
248+
"second turn should see depleted sub-LM budget"
249+
);
250+
}

0 commit comments

Comments
 (0)