1use crate::error::{Result, SomaError};
15use crate::value::Value;
16use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21#[non_exhaustive]
22pub enum Role {
23 System,
25 User,
28 Assistant,
30}
31
32impl Role {
33 pub fn as_str(&self) -> &'static str {
35 match self {
36 Self::System => "system",
37 Self::User => "user",
38 Self::Assistant => "assistant",
39 }
40 }
41}
42
43impl std::fmt::Display for Role {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.write_str(self.as_str())
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57#[serde(tag = "type", rename_all = "snake_case")]
58#[non_exhaustive]
59pub enum ContentBlock {
60 Text {
62 text: String,
64 },
65
66 ToolUse {
68 id: String,
70 name: String,
72 input: serde_json::Value,
74 },
75
76 ToolResult {
78 tool_use_id: String,
80 content: String,
82 #[serde(default)]
84 is_error: bool,
85 },
86}
87
88impl ContentBlock {
89 pub fn text(text: impl Into<String>) -> Self {
91 Self::Text { text: text.into() }
92 }
93
94 pub fn tool_use(
96 id: impl Into<String>,
97 name: impl Into<String>,
98 input: serde_json::Value,
99 ) -> Self {
100 Self::ToolUse {
101 id: id.into(),
102 name: name.into(),
103 input,
104 }
105 }
106
107 pub fn tool_result(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
109 Self::ToolResult {
110 tool_use_id: tool_use_id.into(),
111 content: content.into(),
112 is_error: false,
113 }
114 }
115
116 pub fn tool_error(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
119 Self::ToolResult {
120 tool_use_id: tool_use_id.into(),
121 content: content.into(),
122 is_error: true,
123 }
124 }
125
126 pub fn as_text(&self) -> Option<&str> {
128 match self {
129 Self::Text { text } => Some(text),
130 _ => None,
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub struct Message {
138 pub role: Role,
140 pub content: Vec<ContentBlock>,
142}
143
144impl Message {
145 pub fn new(role: Role, content: Vec<ContentBlock>) -> Self {
147 Self { role, content }
148 }
149
150 pub fn system(text: impl Into<String>) -> Self {
152 Self::new(Role::System, vec![ContentBlock::text(text)])
153 }
154
155 pub fn user(text: impl Into<String>) -> Self {
157 Self::new(Role::User, vec![ContentBlock::text(text)])
158 }
159
160 pub fn assistant(text: impl Into<String>) -> Self {
162 Self::new(Role::Assistant, vec![ContentBlock::text(text)])
163 }
164
165 pub fn text(&self) -> String {
167 self.content
168 .iter()
169 .filter_map(ContentBlock::as_text)
170 .collect::<Vec<_>>()
171 .join("")
172 }
173
174 pub fn tool_uses(&self) -> impl Iterator<Item = (&str, &str, &serde_json::Value)> {
176 self.content.iter().filter_map(|b| match b {
177 ContentBlock::ToolUse { id, name, input } => Some((id.as_str(), name.as_str(), input)),
178 _ => None,
179 })
180 }
181}
182
183#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
188#[serde(transparent)]
189pub struct Messages(pub Vec<Message>);
190
191impl Messages {
192 pub fn new() -> Self {
194 Self::default()
195 }
196
197 pub fn push(&mut self, message: Message) {
199 self.0.push(message);
200 }
201
202 pub fn len(&self) -> usize {
204 self.0.len()
205 }
206
207 pub fn is_empty(&self) -> bool {
209 self.0.is_empty()
210 }
211
212 pub fn iter(&self) -> std::slice::Iter<'_, Message> {
214 self.0.iter()
215 }
216
217 pub fn last(&self) -> Option<&Message> {
219 self.0.last()
220 }
221
222 pub fn to_value(&self) -> Value {
224 Value::json(serde_json::to_value(self).unwrap_or(serde_json::Value::Null))
225 }
226
227 pub fn from_value(value: &Value) -> Result<Self> {
234 match value {
235 Value::Text(s) => Ok(Self(vec![Message::user(s.as_ref())])),
236 Value::Json(j) => {
237 if let Some(s) = j.as_str() {
238 return Ok(Self(vec![Message::user(s)]));
239 }
240 serde_json::from_value((**j).clone()).map_err(|e| SomaError::SchemaMismatch {
241 expected: "messages".into(),
242 got: format!("json that is not a conversation: {e}"),
243 })
244 }
245 other => Err(SomaError::SchemaMismatch {
246 expected: "messages".into(),
247 got: other.type_name().to_string(),
248 }),
249 }
250 }
251}
252
253impl From<Vec<Message>> for Messages {
254 fn from(v: Vec<Message>) -> Self {
255 Self(v)
256 }
257}
258
259impl IntoIterator for Messages {
260 type Item = Message;
261 type IntoIter = std::vec::IntoIter<Message>;
262 fn into_iter(self) -> Self::IntoIter {
263 self.0.into_iter()
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn roundtrips_through_a_value() {
273 let mut msgs = Messages::new();
274 msgs.push(Message::system("You are terse."));
275 msgs.push(Message::user("What is 2+2?"));
276 msgs.push(Message::new(
277 Role::Assistant,
278 vec![
279 ContentBlock::text("Let me compute that."),
280 ContentBlock::tool_use("t1", "calc", serde_json::json!({"expr": "2+2"})),
281 ],
282 ));
283 msgs.push(Message::new(
284 Role::User,
285 vec![ContentBlock::tool_result("t1", "4")],
286 ));
287
288 let decoded = Messages::from_value(&msgs.to_value()).unwrap();
289 assert_eq!(decoded, msgs);
290 }
291
292 #[test]
295 fn promotes_a_bare_string_to_a_user_turn() {
296 for v in [
297 Value::text("Summarize this."),
298 Value::json(serde_json::json!("Summarize this.")),
299 ] {
300 let msgs = Messages::from_value(&v).unwrap();
301 assert_eq!(msgs.len(), 1);
302 assert_eq!(msgs.0[0].role, Role::User);
303 assert_eq!(msgs.0[0].text(), "Summarize this.");
304 }
305 }
306
307 #[test]
308 fn rejects_values_that_are_not_conversations() {
309 let err = Messages::from_value(&Value::tensor(vec![1.0], vec![1])).unwrap_err();
310 assert!(err.to_string().contains("messages"), "{err}");
311
312 let err = Messages::from_value(&Value::json(serde_json::json!({"a": 1}))).unwrap_err();
313 assert!(err.to_string().contains("messages"), "{err}");
314 }
315
316 #[test]
317 fn text_concatenates_prose_and_skips_tool_blocks() {
318 let m = Message::new(
319 Role::Assistant,
320 vec![
321 ContentBlock::text("a"),
322 ContentBlock::tool_use("t", "n", serde_json::json!({})),
323 ContentBlock::text("b"),
324 ],
325 );
326 assert_eq!(m.text(), "ab");
327 assert_eq!(m.tool_uses().count(), 1);
328 }
329
330 #[test]
331 fn tool_errors_are_marked() {
332 let ok = ContentBlock::tool_result("t", "fine");
333 let bad = ContentBlock::tool_error("t", "boom");
334 assert!(matches!(
335 ok,
336 ContentBlock::ToolResult {
337 is_error: false,
338 ..
339 }
340 ));
341 assert!(matches!(
342 bad,
343 ContentBlock::ToolResult { is_error: true, .. }
344 ));
345 }
346}