language_barrier_core/
message.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Represents the content of a message, which can be text or other structured data
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6#[serde(untagged)]
7pub enum Content {
8    /// Simple text content
9    Text(String),
10    /// Structured content with parts (for multimodal models)
11    Parts(Vec<ContentPart>),
12}
13
14impl Content {
15    /// Creates a new text content
16    ///
17    /// # Examples
18    ///
19    /// ```
20    /// use language_barrier_core::message::Content;
21    ///
22    /// let content = Content::text("Hello, world!");
23    /// ```
24    pub fn text(text: impl Into<String>) -> Self {
25        Content::Text(text.into())
26    }
27
28    /// Creates a new parts content
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use language_barrier_core::message::{Content, ContentPart};
34    ///
35    /// let parts = vec![ContentPart::text("Hello"), ContentPart::text("world")];
36    /// let content = Content::parts(parts);
37    /// ```
38    #[must_use]
39    pub fn parts(parts: Vec<ContentPart>) -> Self {
40        Content::Parts(parts)
41    }
42
43    /// Returns true if the content is empty
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use language_barrier_core::message::Content;
49    ///
50    /// let content = Content::text("");
51    /// assert!(content.is_empty());
52    ///
53    /// let content = Content::text("Hello");
54    /// assert!(!content.is_empty());
55    /// ```
56    #[must_use]
57    pub fn is_empty(&self) -> bool {
58        match self {
59            Content::Text(text) => text.is_empty(),
60            Content::Parts(parts) => parts.is_empty() || parts.iter().all(ContentPart::is_empty),
61        }
62    }
63}
64
65/// Represents a part of structured content for multimodal models
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(tag = "type")]
68pub enum ContentPart {
69    /// Text part
70    #[serde(rename = "text")]
71    Text {
72        /// The text content
73        text: String,
74    },
75    /// Image part
76    #[serde(rename = "image_url")]
77    ImageUrl {
78        /// The image URL and metadata
79        image_url: ImageUrl,
80    },
81}
82
83impl ContentPart {
84    /// Creates a new text part
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use language_barrier_core::message::ContentPart;
90    ///
91    /// let part = ContentPart::text("Hello, world!");
92    /// ```
93    pub fn text(text: impl Into<String>) -> Self {
94        ContentPart::Text { text: text.into() }
95    }
96
97    /// Creates a new image URL part
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use language_barrier_core::message::ContentPart;
103    ///
104    /// let part = ContentPart::image_url("https://example.com/image.jpg");
105    /// ```
106    pub fn image_url(url: impl Into<String>) -> Self {
107        ContentPart::ImageUrl {
108            image_url: ImageUrl::new(url),
109        }
110    }
111
112    /// Returns true if the part is empty
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use language_barrier_core::message::ContentPart;
118    ///
119    /// let part = ContentPart::text("");
120    /// assert!(part.is_empty());
121    ///
122    /// let part = ContentPart::text("Hello");
123    /// assert!(!part.is_empty());
124    /// ```
125    #[must_use]
126    pub fn is_empty(&self) -> bool {
127        match self {
128            ContentPart::Text { text } => text.is_empty(),
129            ContentPart::ImageUrl { .. } => false,
130        }
131    }
132}
133
134/// Represents an image URL with optional metadata
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct ImageUrl {
137    /// The URL of the image
138    pub url: String,
139    /// Optional detail level (for some providers)
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub detail: Option<String>,
142}
143
144impl ImageUrl {
145    /// Creates a new image URL
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use language_barrier_core::message::ImageUrl;
151    ///
152    /// let image_url = ImageUrl::new("https://example.com/image.jpg");
153    /// ```
154    pub fn new(url: impl Into<String>) -> Self {
155        Self {
156            url: url.into(),
157            detail: None,
158        }
159    }
160
161    /// Sets the detail level and returns self for method chaining
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use language_barrier_core::message::ImageUrl;
167    ///
168    /// let image_url = ImageUrl::new("https://example.com/image.jpg")
169    ///     .with_detail("high");
170    /// ```
171    #[must_use]
172    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
173        self.detail = Some(detail.into());
174        self
175    }
176}
177
178/// Represents a function definition within a tool call
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct Function {
181    /// The name of the function
182    pub name: String,
183    /// The arguments to the function (typically JSON)
184    pub arguments: String,
185}
186
187/// Represents a tool call
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct ToolCall {
190    /// The ID of the tool call
191    pub id: String,
192    /// The type of the tool call
193    #[serde(rename = "type")]
194    pub tool_type: String,
195    /// The function definition
196    pub function: Function,
197}
198
199/// Represents a message in a conversation
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201#[serde(tag = "role")]
202pub enum Message {
203    /// Message from the system (instructions)
204    #[serde(rename = "system")]
205    System {
206        /// The content of the system message
207        content: String,
208        /// Additional provider-specific metadata
209        #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
210        metadata: HashMap<String, serde_json::Value>,
211    },
212
213    /// Message from the user
214    #[serde(rename = "user")]
215    User {
216        /// The content of the user message
217        content: Content,
218        /// The name of the user (optional)
219        #[serde(skip_serializing_if = "Option::is_none")]
220        name: Option<String>,
221        /// Additional provider-specific metadata
222        #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
223        metadata: HashMap<String, serde_json::Value>,
224    },
225
226    /// Message from the assistant
227    #[serde(rename = "assistant")]
228    Assistant {
229        /// The content of the assistant message
230        #[serde(skip_serializing_if = "Option::is_none")]
231        content: Option<Content>,
232        /// The tool calls made by the assistant
233        #[serde(skip_serializing_if = "Vec::is_empty", default)]
234        tool_calls: Vec<ToolCall>,
235        /// Additional provider-specific metadata
236        #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
237        metadata: HashMap<String, serde_json::Value>,
238    },
239
240    /// Message from a tool
241    #[serde(rename = "tool")]
242    Tool {
243        /// The ID of the tool call this message is responding to
244        tool_call_id: String,
245        /// The content of the tool response
246        content: String,
247        /// Additional provider-specific metadata
248        #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
249        metadata: HashMap<String, serde_json::Value>,
250    },
251}
252
253impl Message {
254    /// Creates a new system message
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use language_barrier_core::message::Message;
260    ///
261    /// let msg = Message::system("You are a helpful assistant.");
262    /// ```
263    pub fn system(content: impl Into<String>) -> Self {
264        Message::System {
265            content: content.into(),
266            metadata: HashMap::new(),
267        }
268    }
269
270    /// Creates a new user message
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use language_barrier_core::message::Message;
276    ///
277    /// let msg = Message::user("Hello, can you help me?");
278    /// ```
279    pub fn user(content: impl Into<String>) -> Self {
280        Message::User {
281            content: Content::Text(content.into()),
282            name: None,
283            metadata: HashMap::new(),
284        }
285    }
286
287    /// Creates a new user message with a name
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use language_barrier_core::message::Message;
293    ///
294    /// let msg = Message::user_with_name("John", "Hello, can you help me?");
295    /// ```
296    pub fn user_with_name(name: impl Into<String>, content: impl Into<String>) -> Self {
297        Message::User {
298            content: Content::Text(content.into()),
299            name: Some(name.into()),
300            metadata: HashMap::new(),
301        }
302    }
303
304    /// Creates a new user message with multimodal content
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// use language_barrier_core::message::{Message, Content, ContentPart};
310    ///
311    /// let parts = vec![
312    ///     ContentPart::text("Look at this image:"),
313    ///     ContentPart::image_url("https://example.com/image.jpg"),
314    /// ];
315    /// let msg = Message::user_with_parts(parts);
316    /// ```
317    #[must_use]
318    pub fn user_with_parts(parts: Vec<ContentPart>) -> Self {
319        Message::User {
320            content: Content::Parts(parts),
321            name: None,
322            metadata: HashMap::new(),
323        }
324    }
325
326    /// Creates a new assistant message
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use language_barrier_core::message::Message;
332    ///
333    /// let msg = Message::assistant("I'm here to help you.");
334    /// ```
335    pub fn assistant(content: impl Into<String>) -> Self {
336        Message::Assistant {
337            content: Some(Content::Text(content.into())),
338            tool_calls: Vec::new(),
339            metadata: HashMap::new(),
340        }
341    }
342
343    /// Creates a new assistant message with tool calls
344    ///
345    /// # Examples
346    ///
347    /// ```
348    /// use language_barrier_core::message::{Message, ToolCall, Function};
349    ///
350    /// let tool_call = ToolCall {
351    ///     id: "call_123".to_string(),
352    ///     tool_type: "function".to_string(),
353    ///     function: Function {
354    ///         name: "get_weather".to_string(),
355    ///         arguments: "{\"location\":\"San Francisco\"}".to_string(),
356    ///     },
357    /// };
358    /// let msg = Message::assistant_with_tool_calls(vec![tool_call]);
359    /// ```
360    #[must_use]
361    pub fn assistant_with_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
362        Message::Assistant {
363            content: None,
364            tool_calls,
365            metadata: HashMap::new(),
366        }
367    }
368
369    /// Creates a new tool message
370    ///
371    /// # Examples
372    ///
373    /// ```
374    /// use language_barrier_core::message::Message;
375    ///
376    /// let msg = Message::tool("tool123", "The result is 42.");
377    /// ```
378    pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
379        Message::Tool {
380            tool_call_id: tool_call_id.into(),
381            content: content.into(),
382            metadata: HashMap::new(),
383        }
384    }
385
386    /// Creates a new tool message from the tool call that originated it.
387    pub fn tool_from_call(tool_call: &ToolCall, content: impl Into<String>) -> Self {
388        Message::Tool {
389            tool_call_id: tool_call.id.clone(),
390            content: content.into(),
391            metadata: HashMap::new(),
392        }
393    }
394
395    /// Returns the role of the message as a string
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use language_barrier_core::message::Message;
401    ///
402    /// let msg = Message::user("Hello");
403    /// assert_eq!(msg.role_str(), "user");
404    /// ```
405    #[must_use]
406    pub fn role_str(&self) -> &'static str {
407        match self {
408            Message::System { .. } => "system",
409            Message::User { .. } => "user",
410            Message::Assistant { .. } => "assistant",
411            Message::Tool { .. } => "tool",
412        }
413    }
414
415    /// Adds metadata and returns a new message
416    ///
417    /// # Examples
418    ///
419    /// ```
420    /// use language_barrier_core::message::Message;
421    /// use serde_json::json;
422    ///
423    /// let msg = Message::user("Hello")
424    ///     .with_metadata("priority", json!(5));
425    /// ```
426    #[must_use]
427    pub fn with_metadata(self, key: impl Into<String>, value: serde_json::Value) -> Self {
428        match self {
429            Message::System {
430                content,
431                mut metadata,
432            } => {
433                metadata.insert(key.into(), value);
434                Message::System { content, metadata }
435            }
436            Message::User {
437                content,
438                name,
439                mut metadata,
440            } => {
441                metadata.insert(key.into(), value);
442                Message::User {
443                    content,
444                    name,
445                    metadata,
446                }
447            }
448            Message::Assistant {
449                content,
450                tool_calls,
451                mut metadata,
452            } => {
453                metadata.insert(key.into(), value);
454                Message::Assistant {
455                    content,
456                    tool_calls,
457                    metadata,
458                }
459            }
460            Message::Tool {
461                tool_call_id,
462                content,
463                mut metadata,
464            } => {
465                metadata.insert(key.into(), value);
466                Message::Tool {
467                    tool_call_id,
468                    content,
469                    metadata,
470                }
471            }
472        }
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use serde_json::json;
480
481    #[test]
482    fn test_content_serialization() {
483        let text_content = Content::text("Hello, world!");
484        let serialized = serde_json::to_string(&text_content).unwrap();
485        assert_eq!(serialized, "\"Hello, world!\"");
486
487        let parts_content = Content::parts(vec![
488            ContentPart::text("Hello"),
489            ContentPart::image_url("https://example.com/image.jpg"),
490        ]);
491        let serialized = serde_json::to_string(&parts_content).unwrap();
492        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
493
494        assert!(parsed.is_array());
495        assert_eq!(parsed.as_array().unwrap().len(), 2);
496    }
497
498    #[test]
499    fn test_message_serialization() {
500        // Test user message serialization
501        let msg = Message::user("Hello, world!");
502        let serialized = serde_json::to_string(&msg).unwrap();
503        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
504
505        // Check externally tagged enum serialization
506        assert_eq!(parsed["role"], "user");
507
508        // In the new format, content is a property within the User variant,
509        // and for text content it's serialized as a string directly
510        assert!(parsed.get("content").is_some());
511
512        // Test with metadata and name
513        let msg = Message::user_with_name("John", "Hello").with_metadata("priority", json!(5));
514        let serialized = serde_json::to_string(&msg).unwrap();
515        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
516
517        assert_eq!(parsed["role"], "user");
518        assert!(parsed.get("name").is_some());
519        assert_eq!(parsed["name"], "John");
520        assert!(parsed.get("content").is_some());
521        assert_eq!(parsed["priority"], 5);
522    }
523
524    #[test]
525    fn test_system_message() {
526        let msg = Message::system("You are a helpful assistant");
527        match msg {
528            Message::System { content, metadata } => {
529                assert_eq!(content, "You are a helpful assistant");
530                assert!(metadata.is_empty());
531            }
532            _ => panic!("Expected System variant"),
533        }
534    }
535
536    #[test]
537    fn test_user_message() {
538        let msg = Message::user_with_name("John", "Hello");
539        match msg {
540            Message::User {
541                content,
542                name,
543                metadata,
544            } => {
545                assert_eq!(content, Content::Text("Hello".to_string()));
546                assert_eq!(name, Some("John".to_string()));
547                assert!(metadata.is_empty());
548            }
549            _ => panic!("Expected User variant"),
550        }
551    }
552
553    #[test]
554    fn test_assistant_message() {
555        let msg = Message::assistant("I'll help you");
556        match msg {
557            Message::Assistant {
558                content,
559                tool_calls,
560                metadata,
561            } => {
562                assert_eq!(content, Some(Content::Text("I'll help you".to_string())));
563                assert!(tool_calls.is_empty());
564                assert!(metadata.is_empty());
565            }
566            _ => panic!("Expected Assistant variant"),
567        }
568
569        let tool_call = ToolCall {
570            id: "call_123".to_string(),
571            tool_type: "function".to_string(),
572            function: Function {
573                name: "get_weather".to_string(),
574                arguments: "{\"location\":\"San Francisco\"}".to_string(),
575            },
576        };
577
578        let msg = Message::assistant_with_tool_calls(vec![tool_call]);
579        match msg {
580            Message::Assistant {
581                content,
582                tool_calls,
583                metadata,
584            } => {
585                assert_eq!(content, None);
586                assert_eq!(tool_calls.len(), 1);
587                assert_eq!(tool_calls[0].id, "call_123");
588                assert!(metadata.is_empty());
589            }
590            _ => panic!("Expected Assistant variant"),
591        }
592    }
593
594    #[test]
595    fn test_tool_message() {
596        let msg = Message::tool("call_123", "The weather is sunny");
597        match msg {
598            Message::Tool {
599                tool_call_id,
600                content,
601                metadata,
602            } => {
603                assert_eq!(tool_call_id, "call_123");
604                assert_eq!(content, "The weather is sunny");
605                assert!(metadata.is_empty());
606            }
607            _ => panic!("Expected Tool variant"),
608        }
609    }
610}