Build delegated realtime handoff text from all messages (#13395)
## Summary - Route delegated realtime handoff turns from all handoff message texts, preserving order - Fallback to input_transcript only when no messages are present - Add regression coverage for multi-message handoff requests
This commit is contained in:
parent
d7eb195b62
commit
6bee02a346
2 changed files with 110 additions and 8 deletions
|
|
@ -373,7 +373,15 @@ pub(crate) async fn handle_audio(
|
|||
}
|
||||
|
||||
fn realtime_text_from_handoff_request(handoff: &RealtimeHandoffRequested) -> Option<String> {
|
||||
(!handoff.input_transcript.is_empty()).then(|| handoff.input_transcript.clone())
|
||||
let messages = handoff
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.text.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
(!messages.is_empty()).then_some(messages).or_else(|| {
|
||||
(!handoff.input_transcript.is_empty()).then(|| handoff.input_transcript.clone())
|
||||
})
|
||||
}
|
||||
|
||||
fn realtime_api_key(
|
||||
|
|
@ -579,19 +587,39 @@ mod tests {
|
|||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn extracts_text_from_handoff_request_input_transcript() {
|
||||
fn extracts_text_from_handoff_request_messages() {
|
||||
let handoff = RealtimeHandoffRequested {
|
||||
handoff_id: "handoff_1".to_string(),
|
||||
item_id: "item_1".to_string(),
|
||||
input_transcript: "hello".to_string(),
|
||||
messages: vec![RealtimeHandoffMessage {
|
||||
role: "user".to_string(),
|
||||
text: "hello".to_string(),
|
||||
}],
|
||||
input_transcript: "ignored".to_string(),
|
||||
messages: vec![
|
||||
RealtimeHandoffMessage {
|
||||
role: "user".to_string(),
|
||||
text: "hello".to_string(),
|
||||
},
|
||||
RealtimeHandoffMessage {
|
||||
role: "assistant".to_string(),
|
||||
text: "hi there".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
realtime_text_from_handoff_request(&handoff),
|
||||
Some("hello".to_string())
|
||||
Some("hello\nhi there".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_text_from_handoff_request_input_transcript_if_messages_missing() {
|
||||
let handoff = RealtimeHandoffRequested {
|
||||
handoff_id: "handoff_1".to_string(),
|
||||
item_id: "item_1".to_string(),
|
||||
input_transcript: "ignored".to_string(),
|
||||
messages: vec![],
|
||||
};
|
||||
assert_eq!(
|
||||
realtime_text_from_handoff_request(&handoff),
|
||||
Some("ignored".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -918,6 +918,80 @@ async fn inbound_handoff_request_starts_turn() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn inbound_handoff_request_uses_all_messages() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let api_server = start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_once(
|
||||
&api_server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "ok"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let realtime_server = start_websocket_server(vec![vec![vec![
|
||||
json!({
|
||||
"type": "session.updated",
|
||||
"session": { "id": "sess_inbound_multi", "instructions": "backend prompt" }
|
||||
}),
|
||||
json!({
|
||||
"type": "conversation.handoff.requested",
|
||||
"handoff_id": "handoff_inbound_multi",
|
||||
"item_id": "item_inbound_multi",
|
||||
"input_transcript": "ignored",
|
||||
"messages": [
|
||||
{ "role": "assistant", "text": "assistant context" },
|
||||
{ "role": "user", "text": "delegated query" },
|
||||
{ "role": "assistant", "text": "assist confirm" },
|
||||
]
|
||||
}),
|
||||
]]])
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_config({
|
||||
let realtime_base_url = realtime_server.uri().to_string();
|
||||
move |config| {
|
||||
config.experimental_realtime_ws_base_url = Some(realtime_base_url);
|
||||
}
|
||||
});
|
||||
let test = builder.build(&api_server).await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let _ = wait_for_event_match(&test.codex, |msg| match msg {
|
||||
EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent {
|
||||
payload: RealtimeEvent::SessionUpdated { session_id, .. },
|
||||
}) => Some(session_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let request = response_mock.single_request();
|
||||
let user_texts = request.message_input_texts("user");
|
||||
assert!(
|
||||
user_texts
|
||||
.iter()
|
||||
.any(|text| text == "assistant context\ndelegated query\nassist confirm")
|
||||
);
|
||||
|
||||
realtime_server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue