1use alloc::{string::String, vec::Vec};
7use mime::Mime;
8use url::Url;
9
10use super::event::ToolCall;
11use super::model::ReasoningEffort;
12use super::reasoning::ReasoningState;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct Attachment {
21 url: Url,
22 #[cfg_attr(feature = "serde", serde(with = "mime_serde"))]
23 media_type: Mime,
24}
25
26impl Attachment {
27 #[must_use]
29 pub const fn new(url: Url, media_type: Mime) -> Self {
30 Self { url, media_type }
31 }
32
33 #[must_use]
35 pub const fn url(&self) -> &Url {
36 &self.url
37 }
38
39 #[must_use]
41 pub const fn media_type(&self) -> &Mime {
42 &self.media_type
43 }
44
45 #[must_use]
47 pub fn with_url(self, url: Url) -> Self {
48 Self {
49 url,
50 media_type: self.media_type,
51 }
52 }
53
54 #[must_use]
56 pub fn into_parts(self) -> (Url, Mime) {
57 (self.url, self.media_type)
58 }
59}
60
61#[cfg(feature = "serde")]
62mod mime_serde {
63 use alloc::string::String;
64 use core::str::FromStr;
65 use mime::Mime;
66 use serde::{Deserialize, Deserializer, Serializer, de::Error as _};
67
68 pub fn serialize<S>(media_type: &Mime, serializer: S) -> Result<S::Ok, S::Error>
69 where
70 S: Serializer,
71 {
72 serializer.serialize_str(media_type.as_ref())
73 }
74
75 pub fn deserialize<'de, D>(deserializer: D) -> Result<Mime, D::Error>
76 where
77 D: Deserializer<'de>,
78 {
79 let raw = String::deserialize(deserializer)?;
80 Mime::from_str(&raw).map_err(D::Error::custom)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87pub enum Role {
88 User,
90 Assistant,
92 System,
94 Tool,
96 Configuration,
99}
100
101impl core::fmt::Display for Role {
102 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103 f.write_str(match self {
104 Self::User => "User",
105 Self::Assistant => "Assistant",
106 Self::System => "System",
107 Self::Tool => "Tool",
108 Self::Configuration => "Configuration",
109 })
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121#[cfg_attr(feature = "serde", serde(tag = "role", rename_all = "snake_case"))]
122pub enum Message {
123 User {
125 content: String,
127 #[cfg_attr(
129 feature = "serde",
130 serde(default, skip_serializing_if = "Vec::is_empty")
131 )]
132 attachments: Vec<Attachment>,
133 },
134 Assistant {
136 content: String,
138 #[cfg_attr(
140 feature = "serde",
141 serde(default, skip_serializing_if = "Vec::is_empty")
142 )]
143 tool_calls: Vec<ToolCall>,
144 #[cfg_attr(
150 feature = "serde",
151 serde(default, skip_serializing_if = "Vec::is_empty")
152 )]
153 reasoning: Vec<ReasoningState>,
154 },
155 System {
157 content: String,
159 },
160 Tool {
162 content: String,
164 tool_call_id: String,
166 },
167 Configuration {
177 reasoning_effort: ReasoningEffort,
179 },
180}
181
182impl Message {
183 #[must_use]
185 pub const fn role(&self) -> Role {
186 match self {
187 Self::User { .. } => Role::User,
188 Self::Assistant { .. } => Role::Assistant,
189 Self::System { .. } => Role::System,
190 Self::Tool { .. } => Role::Tool,
191 Self::Configuration { .. } => Role::Configuration,
192 }
193 }
194
195 #[must_use]
198 pub fn content(&self) -> &str {
199 match self {
200 Self::User { content, .. }
201 | Self::Assistant { content, .. }
202 | Self::System { content }
203 | Self::Tool { content, .. } => content,
204 Self::Configuration { .. } => "",
205 }
206 }
207
208 #[must_use]
211 pub const fn configuration(reasoning_effort: ReasoningEffort) -> Self {
212 Self::Configuration { reasoning_effort }
213 }
214
215 #[must_use]
218 pub const fn configured_effort(&self) -> Option<ReasoningEffort> {
219 match self {
220 Self::Configuration { reasoning_effort } => Some(*reasoning_effort),
221 _ => None,
222 }
223 }
224
225 #[must_use]
227 pub fn attachments(&self) -> &[Attachment] {
228 match self {
229 Self::User { attachments, .. } => attachments,
230 _ => &[],
231 }
232 }
233
234 #[must_use]
236 pub fn tool_calls(&self) -> &[ToolCall] {
237 match self {
238 Self::Assistant { tool_calls, .. } => tool_calls,
239 _ => &[],
240 }
241 }
242
243 #[must_use]
245 pub fn tool_call_id(&self) -> Option<&str> {
246 match self {
247 Self::Tool { tool_call_id, .. } => Some(tool_call_id),
248 _ => None,
249 }
250 }
251
252 pub fn user(content: impl Into<String>) -> Self {
254 Self::User {
255 content: content.into(),
256 attachments: Vec::new(),
257 }
258 }
259
260 pub fn assistant(content: impl Into<String>) -> Self {
262 Self::Assistant {
263 content: content.into(),
264 tool_calls: Vec::new(),
265 reasoning: Vec::new(),
266 }
267 }
268
269 pub fn assistant_with_tool_calls(
271 content: impl Into<String>,
272 tool_calls: Vec<ToolCall>,
273 ) -> Self {
274 Self::Assistant {
275 content: content.into(),
276 tool_calls,
277 reasoning: Vec::new(),
278 }
279 }
280
281 pub fn assistant_with_reasoning(
287 content: impl Into<String>,
288 tool_calls: Vec<ToolCall>,
289 reasoning: Vec<ReasoningState>,
290 ) -> Self {
291 Self::Assistant {
292 content: content.into(),
293 tool_calls,
294 reasoning,
295 }
296 }
297
298 #[must_use]
302 pub fn reasoning(&self) -> &[ReasoningState] {
303 match self {
304 Self::Assistant { reasoning, .. } => reasoning,
305 _ => &[],
306 }
307 }
308
309 pub fn system(content: impl Into<String>) -> Self {
311 Self::System {
312 content: content.into(),
313 }
314 }
315
316 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
318 Self::Tool {
319 content: content.into(),
320 tool_call_id: tool_call_id.into(),
321 }
322 }
323
324 #[must_use]
326 pub fn with_attachment(mut self, attachment: Attachment) -> Self {
327 if let Self::User { attachments, .. } = &mut self {
328 attachments.push(attachment);
329 }
330 self
331 }
332
333 #[must_use]
335 pub fn with_attachments(mut self, values: impl IntoIterator<Item = Attachment>) -> Self {
336 if let Self::User { attachments, .. } = &mut self {
337 attachments.extend(values);
338 }
339 self
340 }
341
342 #[must_use]
344 pub fn with_tool_calls(mut self, calls: Vec<ToolCall>) -> Self {
345 if let Self::Assistant { tool_calls, .. } = &mut self {
346 *tool_calls = calls;
347 }
348 self
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use alloc::vec;
355
356 use super::*;
357
358 #[test]
359 fn role_equality() {
360 assert_eq!(Role::User, Role::User);
361 assert_eq!(Role::Assistant, Role::Assistant);
362 assert_eq!(Role::System, Role::System);
363 assert_eq!(Role::Tool, Role::Tool);
364 assert_ne!(Role::User, Role::Assistant);
365 }
366
367 #[test]
368 fn message_creation() {
369 let user = Message::user("Hello");
370 assert_eq!(user.role(), Role::User);
371 assert_eq!(user.content(), "Hello");
372
373 let assistant = Message::assistant("Hi there!");
374 assert_eq!(assistant.role(), Role::Assistant);
375 assert_eq!(assistant.content(), "Hi there!");
376
377 let system = Message::system("Be helpful");
378 assert_eq!(system.role(), Role::System);
379 assert_eq!(system.content(), "Be helpful");
380
381 let tool = Message::tool("call_123", "Success");
382 assert_eq!(tool.role(), Role::Tool);
383 assert_eq!(tool.content(), "Success");
384 assert_eq!(tool.tool_call_id(), Some("call_123"));
385 }
386
387 #[test]
388 fn assistant_with_tool_calls() {
389 let tool_calls = vec![ToolCall::new(
390 "call_1",
391 "get_weather",
392 serde_json::json!({"city": "NYC"}),
393 )];
394
395 let msg = Message::assistant_with_tool_calls("", tool_calls);
396 assert_eq!(msg.tool_calls().len(), 1);
397 assert_eq!(msg.tool_calls()[0].name, "get_weather");
398 }
399
400 #[test]
401 fn message_with_attachment() {
402 let attachment = Attachment::new(
403 "https://example.com/image.png".parse::<Url>().unwrap(),
404 mime::IMAGE_PNG,
405 );
406 let message = Message::user("Hello").with_attachment(attachment.clone());
407 assert_eq!(message.attachments(), &[attachment]);
408 }
409
410 #[test]
411 fn message_with_attachments() {
412 let attachments = vec![
413 Attachment::new(
414 "https://example.com/a.png".parse::<Url>().unwrap(),
415 mime::IMAGE_PNG,
416 ),
417 Attachment::new(
418 "https://example.com/b.pdf".parse::<Url>().unwrap(),
419 mime::APPLICATION_PDF,
420 ),
421 ];
422 let message = Message::user("Hello").with_attachments(attachments.clone());
423 assert_eq!(message.attachments(), attachments.as_slice());
424 }
425
426 #[test]
427 fn attachments_are_ignored_for_non_user_messages() {
428 let attachment = Attachment::new(
429 "https://example.com/a.png".parse::<Url>().unwrap(),
430 mime::IMAGE_PNG,
431 );
432 let message = Message::assistant("Hello").with_attachment(attachment);
433 assert!(
434 message.attachments().is_empty(),
435 "expected no attachments, got {:?}",
436 message.attachments()
437 );
438 }
439
440 #[test]
441 fn message_clone() {
442 let original = Message::user("Original");
443 let cloned = original.clone();
444 assert_eq!(original.content(), cloned.content());
445 }
446}