switchyard_translation/
conversation.rs1use std::borrow::Cow;
7
8use crate::{ContentBlock, InstructionBlock, LlmRequest, Message, Role, ToolCall, ToolResult};
9
10pub struct Conversation<'a> {
12 pub instructions: Cow<'a, [InstructionBlock]>,
14 pub messages: Cow<'a, [Message]>,
16}
17
18pub fn conversation(request: &LlmRequest) -> Conversation<'_> {
22 if !request
23 .messages
24 .iter()
25 .any(|message| message.responses.is_some())
26 {
27 return Conversation {
28 instructions: Cow::Borrowed(&request.instructions),
29 messages: Cow::Borrowed(&request.messages),
30 };
31 }
32 let fields = &request.extensions.fields;
34 let stored_state = fields
35 .get("previous_response_id")
36 .and_then(serde_json::Value::as_str)
37 .is_some_and(|id| !id.is_empty())
38 || fields.get("conversation").is_some_and(|conversation| {
39 conversation
40 .as_str()
41 .or_else(|| conversation.get("id").and_then(serde_json::Value::as_str))
42 .is_some_and(|id| !id.is_empty())
43 });
44 let mut instructions = request.instructions.clone();
45 let mut turns = ConversationTurns::default();
46 for message in &request.messages {
47 if matches!(message.content.as_slice(), [ContentBlock::Unknown { raw, .. }]
49 if raw.get("type").and_then(serde_json::Value::as_str) == Some("additional_tools"))
50 {
51 continue;
52 }
53 if matches!(message.role, Role::System | Role::Developer) {
55 instructions.push(InstructionBlock {
56 role: message.role,
57 content: message.content.clone(),
58 });
59 continue;
60 }
61 match message.content.as_slice() {
62 [ContentBlock::Reasoning { .. }] if message.role == Role::Assistant => {
63 if !turns.outputs.is_empty() {
65 turns.flush_tools();
66 }
67 turns.reasoning.extend(message.content.clone());
68 }
69 [ContentBlock::ToolCall(call)] => {
70 if !turns.outputs.is_empty() {
71 turns.flush_tools();
72 }
73 turns.calls.push(call.clone());
74 }
75 [ContentBlock::ToolResult(result)] => {
76 if stored_state
77 || turns
78 .calls
79 .iter()
80 .any(|call| call.id == result.tool_call_id)
81 {
82 turns.outputs.push(result.clone());
83 } else {
84 let text = result
85 .content
86 .iter()
87 .filter_map(|block| match block {
88 ContentBlock::Text { text, .. } | ContentBlock::Refusal { text } => {
89 Some(text.as_str())
90 }
91 ContentBlock::Unknown { raw, .. } => raw.as_str(),
92 _ => None,
93 })
94 .collect::<Vec<_>>()
95 .join(" ");
96 turns.push_message(Message::text(
97 Role::User,
98 format!("Tool result {}: {text}", result.tool_call_id),
99 ));
100 }
101 }
102 _ => {
103 let unknown_item =
104 matches!(message.content.as_slice(), [ContentBlock::Unknown { .. }])
105 && message.responses.as_ref().is_some_and(|source| {
106 source
107 .fields
108 .get("type")
109 .and_then(serde_json::Value::as_str)
110 .is_some_and(|kind| kind != "message")
111 });
112 let mut message = message.clone();
113 message.responses = None;
114 if message.role == Role::Assistant && !unknown_item && turns.calls.is_empty() {
115 let mut content = std::mem::take(&mut turns.reasoning);
116 content.append(&mut message.content);
117 message.content = content;
118 } else if turns.calls.is_empty() {
119 turns.flush_reasoning();
120 }
121 turns.push_message(message);
122 }
123 }
124 }
125 turns.flush_tools();
126 turns.flush_reasoning();
127 Conversation {
128 instructions: Cow::Owned(instructions),
129 messages: Cow::Owned(turns.messages),
130 }
131}
132
133#[derive(Default)]
135struct ConversationTurns {
136 messages: Vec<Message>,
137 calls: Vec<ToolCall>,
138 outputs: Vec<ToolResult>,
139 deferred: Vec<Message>,
140 reasoning: Vec<ContentBlock>,
141}
142
143impl ConversationTurns {
144 fn push_message(&mut self, message: Message) {
145 if !self.calls.is_empty() && self.outputs.is_empty() {
146 self.deferred.push(message);
147 return;
148 }
149 self.flush_tools();
150 self.messages.push(message);
151 }
152
153 fn flush_tools(&mut self) {
154 if !self.calls.is_empty() {
155 let mut content = std::mem::take(&mut self.reasoning);
156 content.extend(
157 std::mem::take(&mut self.calls)
158 .into_iter()
159 .map(ContentBlock::ToolCall),
160 );
161 self.messages.push(Message {
162 role: Role::Assistant,
163 content,
164 responses: None,
165 });
166 }
167 for result in std::mem::take(&mut self.outputs) {
168 self.messages.push(Message {
169 role: Role::User,
170 content: vec![ContentBlock::ToolResult(result)],
171 responses: None,
172 });
173 }
174 self.messages.append(&mut self.deferred);
175 }
176
177 fn flush_reasoning(&mut self) {
178 if !self.reasoning.is_empty() {
179 let content = std::mem::take(&mut self.reasoning);
180 self.push_message(Message {
181 role: Role::Assistant,
182 content,
183 responses: None,
184 });
185 }
186 }
187}