1use alloc::{string::String, vec::Vec};
7use mime::Mime;
8use url::Url;
9
10use super::event::ToolCall;
11use super::reasoning::ReasoningState;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct Attachment {
20 url: Url,
21 #[cfg_attr(feature = "serde", serde(with = "mime_serde"))]
22 media_type: Mime,
23}
24
25impl Attachment {
26 #[must_use]
28 pub const fn new(url: Url, media_type: Mime) -> Self {
29 Self { url, media_type }
30 }
31
32 #[must_use]
34 pub const fn url(&self) -> &Url {
35 &self.url
36 }
37
38 #[must_use]
40 pub const fn media_type(&self) -> &Mime {
41 &self.media_type
42 }
43
44 #[must_use]
46 pub fn with_url(self, url: Url) -> Self {
47 Self {
48 url,
49 media_type: self.media_type,
50 }
51 }
52
53 #[must_use]
55 pub fn into_parts(self) -> (Url, Mime) {
56 (self.url, self.media_type)
57 }
58}
59
60#[cfg(feature = "serde")]
61mod mime_serde {
62 use alloc::string::String;
63 use core::str::FromStr;
64 use mime::Mime;
65 use serde::{Deserialize, Deserializer, Serializer, de::Error as _};
66
67 pub fn serialize<S>(media_type: &Mime, serializer: S) -> Result<S::Ok, S::Error>
68 where
69 S: Serializer,
70 {
71 serializer.serialize_str(media_type.as_ref())
72 }
73
74 pub fn deserialize<'de, D>(deserializer: D) -> Result<Mime, D::Error>
75 where
76 D: Deserializer<'de>,
77 {
78 let raw = String::deserialize(deserializer)?;
79 Mime::from_str(&raw).map_err(D::Error::custom)
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub enum Role {
87 User,
89 Assistant,
91 System,
93 Tool,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105#[cfg_attr(feature = "serde", serde(tag = "role", rename_all = "snake_case"))]
106pub enum Message {
107 User {
109 content: String,
111 #[cfg_attr(
113 feature = "serde",
114 serde(default, skip_serializing_if = "Vec::is_empty")
115 )]
116 attachments: Vec<Attachment>,
117 },
118 Assistant {
120 content: String,
122 #[cfg_attr(
124 feature = "serde",
125 serde(default, skip_serializing_if = "Vec::is_empty")
126 )]
127 tool_calls: Vec<ToolCall>,
128 #[cfg_attr(
134 feature = "serde",
135 serde(default, skip_serializing_if = "Vec::is_empty")
136 )]
137 reasoning: Vec<ReasoningState>,
138 },
139 System {
141 content: String,
143 },
144 Tool {
146 content: String,
148 tool_call_id: String,
150 },
151}
152
153impl Message {
154 #[must_use]
156 pub const fn role(&self) -> Role {
157 match self {
158 Self::User { .. } => Role::User,
159 Self::Assistant { .. } => Role::Assistant,
160 Self::System { .. } => Role::System,
161 Self::Tool { .. } => Role::Tool,
162 }
163 }
164
165 #[must_use]
167 pub fn content(&self) -> &str {
168 match self {
169 Self::User { content, .. }
170 | Self::Assistant { content, .. }
171 | Self::System { content }
172 | Self::Tool { content, .. } => content,
173 }
174 }
175
176 #[must_use]
178 pub fn attachments(&self) -> &[Attachment] {
179 match self {
180 Self::User { attachments, .. } => attachments,
181 _ => &[],
182 }
183 }
184
185 #[must_use]
187 pub fn tool_calls(&self) -> &[ToolCall] {
188 match self {
189 Self::Assistant { tool_calls, .. } => tool_calls,
190 _ => &[],
191 }
192 }
193
194 #[must_use]
196 pub fn tool_call_id(&self) -> Option<&str> {
197 match self {
198 Self::Tool { tool_call_id, .. } => Some(tool_call_id),
199 _ => None,
200 }
201 }
202
203 pub fn user(content: impl Into<String>) -> Self {
205 Self::User {
206 content: content.into(),
207 attachments: Vec::new(),
208 }
209 }
210
211 pub fn assistant(content: impl Into<String>) -> Self {
213 Self::Assistant {
214 content: content.into(),
215 tool_calls: Vec::new(),
216 reasoning: Vec::new(),
217 }
218 }
219
220 pub fn assistant_with_tool_calls(
222 content: impl Into<String>,
223 tool_calls: Vec<ToolCall>,
224 ) -> Self {
225 Self::Assistant {
226 content: content.into(),
227 tool_calls,
228 reasoning: Vec::new(),
229 }
230 }
231
232 pub fn assistant_with_reasoning(
238 content: impl Into<String>,
239 tool_calls: Vec<ToolCall>,
240 reasoning: Vec<ReasoningState>,
241 ) -> Self {
242 Self::Assistant {
243 content: content.into(),
244 tool_calls,
245 reasoning,
246 }
247 }
248
249 #[must_use]
253 pub fn reasoning(&self) -> &[ReasoningState] {
254 match self {
255 Self::Assistant { reasoning, .. } => reasoning,
256 _ => &[],
257 }
258 }
259
260 pub fn system(content: impl Into<String>) -> Self {
262 Self::System {
263 content: content.into(),
264 }
265 }
266
267 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
269 Self::Tool {
270 content: content.into(),
271 tool_call_id: tool_call_id.into(),
272 }
273 }
274
275 #[must_use]
277 pub fn with_attachment(mut self, attachment: Attachment) -> Self {
278 if let Self::User { attachments, .. } = &mut self {
279 attachments.push(attachment);
280 }
281 self
282 }
283
284 #[must_use]
286 pub fn with_attachments(mut self, values: impl IntoIterator<Item = Attachment>) -> Self {
287 if let Self::User { attachments, .. } = &mut self {
288 attachments.extend(values);
289 }
290 self
291 }
292
293 #[must_use]
295 pub fn with_tool_calls(mut self, calls: Vec<ToolCall>) -> Self {
296 if let Self::Assistant { tool_calls, .. } = &mut self {
297 *tool_calls = calls;
298 }
299 self
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use alloc::vec;
306
307 use super::*;
308
309 #[test]
310 fn role_equality() {
311 assert_eq!(Role::User, Role::User);
312 assert_eq!(Role::Assistant, Role::Assistant);
313 assert_eq!(Role::System, Role::System);
314 assert_eq!(Role::Tool, Role::Tool);
315 assert_ne!(Role::User, Role::Assistant);
316 }
317
318 #[test]
319 fn message_creation() {
320 let user = Message::user("Hello");
321 assert_eq!(user.role(), Role::User);
322 assert_eq!(user.content(), "Hello");
323
324 let assistant = Message::assistant("Hi there!");
325 assert_eq!(assistant.role(), Role::Assistant);
326 assert_eq!(assistant.content(), "Hi there!");
327
328 let system = Message::system("Be helpful");
329 assert_eq!(system.role(), Role::System);
330 assert_eq!(system.content(), "Be helpful");
331
332 let tool = Message::tool("call_123", "Success");
333 assert_eq!(tool.role(), Role::Tool);
334 assert_eq!(tool.content(), "Success");
335 assert_eq!(tool.tool_call_id(), Some("call_123"));
336 }
337
338 #[test]
339 fn assistant_with_tool_calls() {
340 let tool_calls = vec![ToolCall::new(
341 "call_1",
342 "get_weather",
343 serde_json::json!({"city": "NYC"}),
344 )];
345
346 let msg = Message::assistant_with_tool_calls("", tool_calls);
347 assert_eq!(msg.tool_calls().len(), 1);
348 assert_eq!(msg.tool_calls()[0].name, "get_weather");
349 }
350
351 #[test]
352 fn message_with_attachment() {
353 let attachment = Attachment::new(
354 "https://example.com/image.png".parse::<Url>().unwrap(),
355 mime::IMAGE_PNG,
356 );
357 let message = Message::user("Hello").with_attachment(attachment.clone());
358 assert_eq!(message.attachments(), &[attachment]);
359 }
360
361 #[test]
362 fn message_with_attachments() {
363 let attachments = vec![
364 Attachment::new(
365 "https://example.com/a.png".parse::<Url>().unwrap(),
366 mime::IMAGE_PNG,
367 ),
368 Attachment::new(
369 "https://example.com/b.pdf".parse::<Url>().unwrap(),
370 mime::APPLICATION_PDF,
371 ),
372 ];
373 let message = Message::user("Hello").with_attachments(attachments.clone());
374 assert_eq!(message.attachments(), attachments.as_slice());
375 }
376
377 #[test]
378 fn attachments_are_ignored_for_non_user_messages() {
379 let attachment = Attachment::new(
380 "https://example.com/a.png".parse::<Url>().unwrap(),
381 mime::IMAGE_PNG,
382 );
383 let message = Message::assistant("Hello").with_attachment(attachment);
384 assert!(
385 message.attachments().is_empty(),
386 "expected no attachments, got {:?}",
387 message.attachments()
388 );
389 }
390
391 #[test]
392 fn message_clone() {
393 let original = Message::user("Original");
394 let cloned = original.clone();
395 assert_eq!(original.content(), cloned.content());
396 }
397}