1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7use lc_shared::tools::ToolCall;
8
9use super::audio::AudioContent;
10use super::file::FileContent;
11use super::image::ImageContent;
12use super::video::VideoContent;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Modality {
23 Image,
25 Audio,
27 Video,
29 File,
31}
32
33#[derive(Debug, Clone, PartialEq)]
40pub enum MediaPart<'a> {
41 Image(&'a ImageContent),
43 Audio(&'a AudioContent),
45 Video(&'a VideoContent),
47 File(&'a FileContent),
49}
50
51impl<'a> MediaPart<'a> {
52 pub fn modality(&self) -> Modality {
54 match self {
55 MediaPart::Image(_) => Modality::Image,
56 MediaPart::Audio(_) => Modality::Audio,
57 MediaPart::Video(_) => Modality::Video,
58 MediaPart::File(_) => Modality::File,
59 }
60 }
61
62 pub fn url(&self) -> &str {
64 match self {
65 MediaPart::Image(m) => &m.url,
66 MediaPart::Audio(m) => &m.url,
67 MediaPart::Video(m) => &m.url,
68 MediaPart::File(m) => &m.url,
69 }
70 }
71
72 pub fn mime_type(&self) -> Option<&str> {
77 match self {
78 MediaPart::File(f) => f.mime_type.as_deref().or_else(|| data_uri_mime(&f.url)),
79 MediaPart::Image(i) => data_uri_mime(&i.url),
80 MediaPart::Audio(a) => data_uri_mime(&a.url),
81 MediaPart::Video(v) => data_uri_mime(&v.url),
82 }
83 }
84
85 pub fn name(&self) -> Option<&str> {
87 match self {
88 MediaPart::File(f) => f.name.as_deref(),
89 _ => None,
90 }
91 }
92
93 pub fn base64_data(&self) -> Option<&str> {
95 let url = self.url();
96 url.split_once(',')
97 .filter(|(prefix, _)| prefix.contains("base64"))
98 .map(|(_, data)| data)
99 }
100
101 pub fn is_data_uri(&self) -> bool {
103 self.url().starts_with("data:")
104 }
105}
106
107pub(crate) fn data_uri_mime(url: &str) -> Option<&str> {
109 url.strip_prefix("data:")?
110 .split([';', ','])
111 .next()
112 .filter(|mime| !mime.is_empty())
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117#[serde(rename_all = "lowercase")]
118pub enum MessageType {
119 System,
121 Human,
123 AI,
125 Tool {
127 tool_call_id: String,
129 },
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134pub struct Message {
135 pub content: String,
137
138 #[serde(default)]
140 pub images: Vec<ImageContent>,
141
142 #[serde(default)]
144 pub audio: Vec<AudioContent>,
145
146 #[serde(default)]
148 pub videos: Vec<VideoContent>,
149
150 #[serde(default)]
152 pub files: Vec<FileContent>,
153
154 #[serde(rename = "type")]
156 pub message_type: MessageType,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub name: Option<String>,
161
162 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
164 pub additional_kwargs: HashMap<String, Value>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub id: Option<String>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub tool_calls: Option<Vec<ToolCall>>,
173}
174
175impl Message {
176 pub fn system(content: impl Into<String>) -> Self {
178 Self {
179 content: content.into(),
180 images: Vec::new(),
181 audio: Vec::new(),
182 videos: Vec::new(),
183 files: Vec::new(),
184 message_type: MessageType::System,
185 name: None,
186 additional_kwargs: HashMap::new(),
187 id: None,
188 tool_calls: None,
189 }
190 }
191
192 pub fn human(content: impl Into<String>) -> Self {
194 Self {
195 content: content.into(),
196 images: Vec::new(),
197 audio: Vec::new(),
198 videos: Vec::new(),
199 files: Vec::new(),
200 message_type: MessageType::Human,
201 name: None,
202 additional_kwargs: HashMap::new(),
203 id: None,
204 tool_calls: None,
205 }
206 }
207
208 pub fn human_with_image(content: impl Into<String>, image_url: impl Into<String>) -> Self {
210 Self {
211 content: content.into(),
212 images: vec![ImageContent::from_url(image_url)],
213 audio: Vec::new(),
214 videos: Vec::new(),
215 files: Vec::new(),
216 message_type: MessageType::Human,
217 name: None,
218 additional_kwargs: HashMap::new(),
219 id: None,
220 tool_calls: None,
221 }
222 }
223
224 pub fn human_with_images(content: impl Into<String>, images: Vec<ImageContent>) -> Self {
226 Self {
227 content: content.into(),
228 images,
229 audio: Vec::new(),
230 videos: Vec::new(),
231 files: Vec::new(),
232 message_type: MessageType::Human,
233 name: None,
234 additional_kwargs: HashMap::new(),
235 id: None,
236 tool_calls: None,
237 }
238 }
239
240 pub fn human_with_audio(content: impl Into<String>, audio: AudioContent) -> Self {
242 Self {
243 content: content.into(),
244 images: Vec::new(),
245 audio: vec![audio],
246 videos: Vec::new(),
247 files: Vec::new(),
248 message_type: MessageType::Human,
249 name: None,
250 additional_kwargs: HashMap::new(),
251 id: None,
252 tool_calls: None,
253 }
254 }
255
256 pub fn human_with_video(content: impl Into<String>, video: VideoContent) -> Self {
258 Self {
259 content: content.into(),
260 images: Vec::new(),
261 audio: Vec::new(),
262 videos: vec![video],
263 files: Vec::new(),
264 message_type: MessageType::Human,
265 name: None,
266 additional_kwargs: HashMap::new(),
267 id: None,
268 tool_calls: None,
269 }
270 }
271
272 pub fn human_with_file(content: impl Into<String>, file: FileContent) -> Self {
274 Self {
275 content: content.into(),
276 images: Vec::new(),
277 audio: Vec::new(),
278 videos: Vec::new(),
279 files: vec![file],
280 message_type: MessageType::Human,
281 name: None,
282 additional_kwargs: HashMap::new(),
283 id: None,
284 tool_calls: None,
285 }
286 }
287
288 pub fn ai(content: impl Into<String>) -> Self {
290 Self {
291 content: content.into(),
292 images: Vec::new(),
293 audio: Vec::new(),
294 videos: Vec::new(),
295 files: Vec::new(),
296 message_type: MessageType::AI,
297 name: None,
298 additional_kwargs: HashMap::new(),
299 id: None,
300 tool_calls: None,
301 }
302 }
303
304 pub fn ai_with_tool_calls(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
306 Self {
307 content: content.into(),
308 images: Vec::new(),
309 audio: Vec::new(),
310 videos: Vec::new(),
311 files: Vec::new(),
312 message_type: MessageType::AI,
313 name: None,
314 additional_kwargs: HashMap::new(),
315 id: None,
316 tool_calls: Some(tool_calls),
317 }
318 }
319
320 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
322 Self {
323 content: content.into(),
324 images: Vec::new(),
325 audio: Vec::new(),
326 videos: Vec::new(),
327 files: Vec::new(),
328 message_type: MessageType::Tool {
329 tool_call_id: tool_call_id.into(),
330 },
331 name: None,
332 additional_kwargs: HashMap::new(),
333 id: None,
334 tool_calls: None,
335 }
336 }
337
338 pub fn with_name(mut self, name: impl Into<String>) -> Self {
340 self.name = Some(name.into());
341 self
342 }
343
344 pub fn with_id(mut self, id: impl Into<String>) -> Self {
346 self.id = Some(id.into());
347 self
348 }
349
350 pub fn with_additional_kwarg(mut self, key: impl Into<String>, value: Value) -> Self {
352 self.additional_kwargs.insert(key.into(), value);
353 self
354 }
355
356 pub fn with_image(mut self, image: ImageContent) -> Self {
358 self.images.push(image);
359 self
360 }
361
362 pub fn with_audio(mut self, audio: AudioContent) -> Self {
364 self.audio.push(audio);
365 self
366 }
367
368 pub fn with_video(mut self, video: VideoContent) -> Self {
370 self.videos.push(video);
371 self
372 }
373
374 pub fn with_file(mut self, file: FileContent) -> Self {
376 self.files.push(file);
377 self
378 }
379
380 pub fn has_images(&self) -> bool {
382 !self.images.is_empty()
383 }
384
385 pub fn has_audio(&self) -> bool {
387 !self.audio.is_empty()
388 }
389
390 pub fn has_videos(&self) -> bool {
392 !self.videos.is_empty()
393 }
394
395 pub fn has_files(&self) -> bool {
397 !self.files.is_empty()
398 }
399
400 pub fn is_multimodal(&self) -> bool {
403 self.has_images() || self.has_audio() || self.has_videos() || self.has_files()
404 }
405
406 pub fn media_parts(&self) -> Vec<MediaPart<'_>> {
412 let mut parts: Vec<MediaPart<'_>> = Vec::with_capacity(
413 self.images.len() + self.audio.len() + self.videos.len() + self.files.len(),
414 );
415 parts.extend(self.images.iter().map(MediaPart::Image));
416 parts.extend(self.audio.iter().map(MediaPart::Audio));
417 parts.extend(self.videos.iter().map(MediaPart::Video));
418 parts.extend(self.files.iter().map(MediaPart::File));
419 parts
420 }
421
422 pub fn type_str(&self) -> String {
427 match &self.message_type {
428 MessageType::System => "system".to_string(),
429 MessageType::Human => "human".to_string(),
430 MessageType::AI => "ai".to_string(),
431 MessageType::Tool { tool_call_id } => format!("tool:{tool_call_id}"),
432 }
433 }
434
435 pub fn has_tool_calls(&self) -> bool {
437 self.tool_calls.as_deref().is_some_and(|t| !t.is_empty())
438 }
439
440 pub fn get_tool_calls(&self) -> Option<&[ToolCall]> {
442 self.tool_calls.as_deref()
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn test_human_with_image() {
452 let msg = Message::human_with_image("描述这张图", "https://example.com/img.jpg");
453 assert_eq!(msg.content, "描述这张图");
454 assert_eq!(msg.images.len(), 1);
455 assert_eq!(msg.images[0].url, "https://example.com/img.jpg");
456 assert!(msg.has_images());
457 }
458
459 #[test]
460 fn test_human_no_images_by_default() {
461 let msg = Message::human("纯文本");
462 assert!(msg.images.is_empty());
463 assert!(!msg.has_images());
464 }
465
466 #[test]
467 fn test_with_image_builder() {
468 let msg = Message::human("看图")
469 .with_image(ImageContent::from_url("https://example.com/a.png"))
470 .with_image(ImageContent::from_base64("abc"));
471 assert_eq!(msg.images.len(), 2);
472 }
473
474 #[test]
475 fn test_message_deserialize_without_images_field() {
476 let json = r#"{"content":"hi","type":"human"}"#;
478 let msg: Message = serde_json::from_str(json).unwrap();
479 assert_eq!(msg.content, "hi");
480 assert!(msg.images.is_empty());
481 }
482
483 #[test]
484 fn test_human_with_images_multiple() {
485 let msg = Message::human_with_images(
486 "多图",
487 vec![
488 ImageContent::from_url("https://example.com/1.jpg"),
489 ImageContent::from_url("https://example.com/2.jpg"),
490 ],
491 );
492 assert_eq!(msg.images.len(), 2);
493 }
494
495 #[test]
496 fn test_system_ai_no_images() {
497 assert!(Message::system("s").images.is_empty());
498 assert!(Message::ai("a").images.is_empty());
499 assert!(Message::tool("id", "c").images.is_empty());
500 }
501
502 #[test]
503 fn test_type_str_includes_tool_call_id() {
504 assert_eq!(Message::system("s").type_str(), "system");
505 assert_eq!(Message::human("h").type_str(), "human");
506 assert_eq!(Message::ai("a").type_str(), "ai");
507 assert_eq!(
508 Message::tool("call_123", "result").type_str(),
509 "tool:call_123"
510 );
511 }
512
513 #[test]
514 fn test_has_tool_calls_empty_and_present() {
515 let with_calls = Message::ai_with_tool_calls(
516 "call tool",
517 vec![ToolCall::builder("call_1")
518 .name("weather")
519 .arguments(r#"{"city":"beijing"}"#)
520 .build()],
521 );
522 assert!(with_calls.has_tool_calls());
523 assert_eq!(with_calls.get_tool_calls().unwrap().len(), 1);
524
525 assert!(!Message::ai("plain").has_tool_calls());
527 let empty = Message::ai_with_tool_calls("no calls", vec![]);
528 assert!(!empty.has_tool_calls());
529 }
530
531 #[test]
534 fn test_human_with_video() {
535 let msg = Message::human_with_video(
536 "看视频",
537 VideoContent::from_url("https://example.com/clip.mp4"),
538 );
539 assert!(msg.has_videos());
540 assert!(msg.is_multimodal());
541 assert_eq!(msg.videos.len(), 1);
542 assert_eq!(msg.videos[0].url, "https://example.com/clip.mp4");
543 }
544
545 #[test]
546 fn test_with_video_builder() {
547 let msg = Message::human("视频")
548 .with_video(VideoContent::from_url("https://example.com/a.mp4"))
549 .with_video(VideoContent::from_base64("abc"));
550 assert_eq!(msg.videos.len(), 2);
551 }
552
553 #[test]
554 fn test_video_field_defaults_on_old_json() {
555 let json = r#"{"content":"hi","type":"human","images":[],"audio":[],"files":[]}"#;
557 let msg: Message = serde_json::from_str(json).unwrap();
558 assert!(msg.videos.is_empty());
559 assert!(!msg.has_videos());
560 }
561
562 #[test]
563 fn test_media_parts_canonical_order_and_modality() {
564 let msg = Message::human("mixed")
565 .with_image(ImageContent::from_url("https://e.com/a.png"))
566 .with_audio(AudioContent::from_url("https://e.com/a.mp3"))
567 .with_video(VideoContent::from_url("https://e.com/a.mp4"))
568 .with_file(FileContent::from_base64("abc", "application/pdf"));
569
570 let parts = msg.media_parts();
571 assert_eq!(parts.len(), 4);
572 assert_eq!(parts[0].modality(), Modality::Image);
573 assert_eq!(parts[1].modality(), Modality::Audio);
574 assert_eq!(parts[2].modality(), Modality::Video);
575 assert_eq!(parts[3].modality(), Modality::File);
576 assert_eq!(parts[2].url(), "https://e.com/a.mp4");
577 assert_eq!(parts[3].name(), None);
578 assert_eq!(parts[3].mime_type(), Some("application/pdf"));
579 assert_eq!(parts[3].base64_data(), Some("abc"));
580 }
581
582 #[test]
583 fn test_media_part_data_uri_mime() {
584 let img = ImageContent::from_base64_with_mime("zzz", "image/webp");
585 let msg = Message::human("h").with_image(img);
586 let part = &msg.media_parts()[0];
587 assert!(part.is_data_uri());
588 assert_eq!(part.mime_type(), Some("image/webp"));
589 assert_eq!(part.base64_data(), Some("zzz"));
590
591 let plain = Message::human_with_image("h", "https://example.com/x.jpg");
593 let p = &plain.media_parts()[0];
594 assert!(!p.is_data_uri());
595 assert_eq!(p.mime_type(), None);
596 }
597
598 #[test]
599 fn test_file_part_explicit_mime_and_name() {
600 let file = FileContent::from_url_with_mime("https://e.com/d.pdf", "application/pdf")
601 .with_name("d.pdf");
602 let msg = Message::human_with_file("读文件", file);
603 let part = &msg.media_parts()[0];
604 assert_eq!(part.mime_type(), Some("application/pdf"));
605 assert_eq!(part.name(), Some("d.pdf"));
606 }
607}