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}