switchyard_libsy/algorithms/util/
prompts.rs1use switchyard_protocol::{ContentBlock, InstructionBlock, Message, Request, Role};
14
15pub fn append_note(request: &mut Request, note: &str) {
24 match request.llm_request.messages.last_mut() {
25 Some(last) if last.role == Role::User => last.content.push(ContentBlock::Text {
26 text: note.to_string(),
27 }),
28 _ => request
29 .llm_request
30 .messages
31 .push(Message::text(Role::User, note)),
32 }
33 drop_exact_replay(request);
34}
35
36pub(crate) fn drop_exact_replay(request: &mut Request) {
51 request.llm_request.preservation.requests.clear();
52}
53
54pub(crate) fn prepend_system_prompt(request: &mut Request, prompt: &str) {
56 request.llm_request.instructions.insert(
57 0,
58 InstructionBlock {
59 role: Role::System,
60 content: vec![ContentBlock::Text {
61 text: prompt.to_string(),
62 }],
63 },
64 );
65 drop_exact_replay(request);
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use switchyard_protocol::{LlmRequest, ToolResult, text_request};
72
73 const NOTE: &str = "recovering from an error";
74
75 fn request_with(messages: Vec<Message>) -> Request {
78 Request {
79 llm_request: LlmRequest {
80 messages,
81 preservation: preserved_body(),
82 ..LlmRequest::default()
83 },
84 raw_request: None,
85 metadata: None,
86 }
87 }
88
89 fn preserved_body() -> switchyard_protocol::PreservationMetadata {
90 let mut preservation = switchyard_protocol::PreservationMetadata::default();
91 preservation.requests.insert(
92 "openai_chat".into(),
93 serde_json::json!({"model": "weak", "messages": [{"role": "user", "content": "hi"}]}),
94 );
95 preservation
96 }
97
98 fn replays_exactly(request: &Request) -> bool {
99 !request.llm_request.preservation.requests.is_empty()
100 }
101
102 #[test]
103 fn a_note_joins_a_trailing_user_turn_after_its_tool_result() {
104 let tool_result = ContentBlock::ToolResult(ToolResult {
107 tool_call_id: "call_1".to_string(),
108 content: vec![ContentBlock::Text {
109 text: "exit 1".to_string(),
110 }],
111 is_error: Some(true),
112 });
113 let mut request = request_with(vec![Message {
114 role: Role::User,
115 content: vec![tool_result.clone()],
116 }]);
117
118 append_note(&mut request, NOTE);
119
120 let messages = &request.llm_request.messages;
121 assert_eq!(messages.len(), 1, "no second consecutive user turn");
122 assert_eq!(
123 messages[0].content,
124 vec![
125 tool_result,
126 ContentBlock::Text {
127 text: NOTE.to_string()
128 }
129 ]
130 );
131 }
132
133 #[test]
134 fn a_note_opens_a_user_turn_after_an_assistant_turn() {
135 let mut request = request_with(vec![Message::text(Role::Assistant, "done")]);
136
137 append_note(&mut request, NOTE);
138
139 let messages = &request.llm_request.messages;
140 assert_eq!(messages.len(), 2);
141 assert_eq!(messages[1].role, Role::User);
142 assert_eq!(messages[1].text_content(""), Some(NOTE.to_string()));
143 }
144
145 #[test]
146 fn a_note_leaves_the_rest_of_the_conversation_untouched() {
147 let mut request = Request {
148 llm_request: LlmRequest {
149 preservation: preserved_body(),
150 ..text_request(Some("auto".to_string()), "fix the build")
151 },
152 raw_request: None,
153 metadata: None,
154 };
155
156 append_note(&mut request, NOTE);
157
158 let trail: Vec<String> = request
159 .llm_request
160 .messages
161 .iter()
162 .filter_map(|message| message.text_content("|"))
163 .collect();
164 assert_eq!(trail, vec![format!("fix the build|{NOTE}")]);
165 assert!(
166 !replays_exactly(&request),
167 "a same-format hop would replay the body captured before the note"
168 );
169 }
170}