1use rustlavel_core::Json;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Role {
12 System,
14 User,
15 Assistant,
16 Tool,
18}
19
20impl Role {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 Role::System => "system",
24 Role::User => "user",
25 Role::Assistant => "assistant",
26 Role::Tool => "tool",
27 }
28 }
29
30 pub fn parse(text: &str) -> Option<Role> {
31 match text {
32 "system" => Some(Role::System),
33 "user" => Some(Role::User),
34 "assistant" => Some(Role::Assistant),
35 "tool" => Some(Role::Tool),
36 _ => None,
37 }
38 }
39}
40
41impl std::fmt::Display for Role {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str(self.as_str())
44 }
45}
46
47#[derive(Debug, Clone, PartialEq)]
53pub enum Content {
54 Text(String),
55 ToolUse { id: String, name: String, input: Json },
57 ToolResult { id: String, name: String, output: Json, is_error: bool },
59}
60
61impl Content {
62 pub fn text(text: impl Into<String>) -> Content {
63 Content::Text(text.into())
64 }
65
66 pub fn as_text(&self) -> Option<&str> {
68 match self {
69 Content::Text(text) => Some(text),
70 _ => None,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq)]
77pub struct Message {
78 pub role: Role,
79 pub content: Vec<Content>,
80}
81
82impl Message {
83 pub fn new(role: Role, content: Vec<Content>) -> Message {
84 Message { role, content }
85 }
86
87 pub fn system(text: impl Into<String>) -> Message {
88 Message::new(Role::System, vec![Content::text(text)])
89 }
90
91 pub fn user(text: impl Into<String>) -> Message {
92 Message::new(Role::User, vec![Content::text(text)])
93 }
94
95 pub fn assistant(text: impl Into<String>) -> Message {
96 Message::new(Role::Assistant, vec![Content::text(text)])
97 }
98
99 pub fn tool_result(id: impl Into<String>, name: impl Into<String>, output: Json) -> Message {
101 Message::new(
102 Role::Tool,
103 vec![Content::ToolResult {
104 id: id.into(),
105 name: name.into(),
106 output,
107 is_error: false,
108 }],
109 )
110 }
111
112 pub fn tool_error(
118 id: impl Into<String>,
119 name: impl Into<String>,
120 message: impl Into<String>,
121 ) -> Message {
122 Message::new(
123 Role::Tool,
124 vec![Content::ToolResult {
125 id: id.into(),
126 name: name.into(),
127 output: Json::from(message.into()),
128 is_error: true,
129 }],
130 )
131 }
132
133 pub fn with(mut self, part: Content) -> Message {
134 self.content.push(part);
135 self
136 }
137
138 pub fn text(&self) -> String {
141 self.content
142 .iter()
143 .filter_map(Content::as_text)
144 .collect::<Vec<_>>()
145 .join("")
146 }
147
148 pub fn tool_uses(&self) -> impl Iterator<Item = (&str, &str, &Json)> {
150 self.content.iter().filter_map(|part| match part {
151 Content::ToolUse { id, name, input } => Some((id.as_str(), name.as_str(), input)),
152 _ => None,
153 })
154 }
155
156 pub fn is_empty(&self) -> bool {
157 self.content.is_empty()
158 }
159}
160
161#[derive(Debug, Clone, Default, PartialEq)]
166pub struct Conversation {
167 messages: Vec<Message>,
168}
169
170impl Conversation {
171 pub fn new() -> Conversation {
172 Conversation::default()
173 }
174
175 pub fn system(mut self, text: impl Into<String>) -> Conversation {
176 self.messages.push(Message::system(text));
177 self
178 }
179
180 pub fn user(mut self, text: impl Into<String>) -> Conversation {
181 self.messages.push(Message::user(text));
182 self
183 }
184
185 pub fn assistant(mut self, text: impl Into<String>) -> Conversation {
186 self.messages.push(Message::assistant(text));
187 self
188 }
189
190 pub fn push(&mut self, message: Message) {
191 self.messages.push(message);
192 }
193
194 pub fn extend(&mut self, messages: impl IntoIterator<Item = Message>) {
195 self.messages.extend(messages);
196 }
197
198 pub fn messages(&self) -> &[Message] {
199 &self.messages
200 }
201
202 pub fn last(&self) -> Option<&Message> {
203 self.messages.last()
204 }
205
206 pub fn len(&self) -> usize {
207 self.messages.len()
208 }
209
210 pub fn is_empty(&self) -> bool {
211 self.messages.is_empty()
212 }
213
214 pub fn into_messages(self) -> Vec<Message> {
215 self.messages
216 }
217}
218
219impl From<Conversation> for Vec<Message> {
220 fn from(conversation: Conversation) -> Vec<Message> {
221 conversation.messages
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn a_message_joins_its_text_parts_and_keeps_tool_parts_apart() {
231 let message = Message::assistant("Let me check. ")
232 .with(Content::text("One moment."))
233 .with(Content::ToolUse {
234 id: "toolu_1".into(),
235 name: "get_weather".into(),
236 input: Json::object([("city", "Oslo".into())]),
237 });
238
239 assert_eq!(message.text(), "Let me check. One moment.");
240
241 let uses: Vec<_> = message.tool_uses().collect();
242 assert_eq!(uses.len(), 1);
243 assert_eq!(uses[0].0, "toolu_1");
244 assert_eq!(uses[0].1, "get_weather");
245 assert_eq!(uses[0].2.get("city").unwrap().as_str(), Some("Oslo"));
246 }
247
248 #[test]
249 fn a_failing_tool_becomes_a_message_rather_than_an_error() {
250 let message = Message::tool_error("toolu_1", "get_weather", "no such city");
251
252 assert_eq!(message.role, Role::Tool);
253 match &message.content[0] {
254 Content::ToolResult { is_error, output, .. } => {
255 assert!(is_error);
256 assert_eq!(output.as_str(), Some("no such city"));
257 }
258 other => panic!("expected a tool result, got {other:?}"),
259 }
260 }
261
262 #[test]
263 fn a_conversation_builds_up_in_order() {
264 let mut conversation = Conversation::new().system("Be brief.").user("Hello");
265 conversation.push(Message::assistant("Hi."));
266
267 assert_eq!(conversation.len(), 3);
268 assert_eq!(conversation.messages()[0].role, Role::System);
269 assert_eq!(conversation.last().unwrap().text(), "Hi.");
270 assert!(!conversation.is_empty());
271 }
272
273 #[test]
274 fn roles_round_trip_through_their_wire_names() {
275 for role in [Role::System, Role::User, Role::Assistant, Role::Tool] {
276 assert_eq!(Role::parse(role.as_str()), Some(role));
277 assert_eq!(role.to_string(), role.as_str());
278 }
279 assert_eq!(Role::parse("wizard"), None);
280 }
281}