Skip to main content

lc_schema/messages/
message.rs

1//! Message data structures for chat models.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7use lc_shared::tools::ToolCall;
8
9use super::image::ImageContent;
10
11/// Message type classification.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(rename_all = "lowercase")]
14pub enum MessageType {
15    System,
16    Human,
17    AI,
18    Tool { tool_call_id: String },
19}
20
21/// Complete message structure for chat interactions.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Message {
24    pub content: String,
25
26    /// 图片内容(多模态 vision)
27    #[serde(default)]
28    pub images: Vec<ImageContent>,
29
30    #[serde(rename = "type")]
31    pub message_type: MessageType,
32
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub name: Option<String>,
35
36    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
37    pub additional_kwargs: HashMap<String, Value>,
38
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub id: Option<String>,
41
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub tool_calls: Option<Vec<ToolCall>>,
44}
45
46impl Message {
47    /// Creates a system message.
48    pub fn system(content: impl Into<String>) -> Self {
49        Self {
50            content: content.into(),
51            images: Vec::new(),
52            message_type: MessageType::System,
53            name: None,
54            additional_kwargs: HashMap::new(),
55            id: None,
56            tool_calls: None,
57        }
58    }
59
60    /// Creates a human (user) message.
61    pub fn human(content: impl Into<String>) -> Self {
62        Self {
63            content: content.into(),
64            images: Vec::new(),
65            message_type: MessageType::Human,
66            name: None,
67            additional_kwargs: HashMap::new(),
68            id: None,
69            tool_calls: None,
70        }
71    }
72
73    /// Creates a human message with an image (vision).
74    pub fn human_with_image(content: impl Into<String>, image_url: impl Into<String>) -> Self {
75        Self {
76            content: content.into(),
77            images: vec![ImageContent::from_url(image_url)],
78            message_type: MessageType::Human,
79            name: None,
80            additional_kwargs: HashMap::new(),
81            id: None,
82            tool_calls: None,
83        }
84    }
85
86    /// Creates a human message with multiple images.
87    pub fn human_with_images(content: impl Into<String>, images: Vec<ImageContent>) -> Self {
88        Self {
89            content: content.into(),
90            images,
91            message_type: MessageType::Human,
92            name: None,
93            additional_kwargs: HashMap::new(),
94            id: None,
95            tool_calls: None,
96        }
97    }
98
99    /// Creates an AI (assistant) message.
100    pub fn ai(content: impl Into<String>) -> Self {
101        Self {
102            content: content.into(),
103            images: Vec::new(),
104            message_type: MessageType::AI,
105            name: None,
106            additional_kwargs: HashMap::new(),
107            id: None,
108            tool_calls: None,
109        }
110    }
111
112    /// Creates an AI message with tool calls.
113    pub fn ai_with_tool_calls(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
114        Self {
115            content: content.into(),
116            images: Vec::new(),
117            message_type: MessageType::AI,
118            name: None,
119            additional_kwargs: HashMap::new(),
120            id: None,
121            tool_calls: Some(tool_calls),
122        }
123    }
124
125    /// Creates a tool result message.
126    pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
127        Self {
128            content: content.into(),
129            images: Vec::new(),
130            message_type: MessageType::Tool {
131                tool_call_id: tool_call_id.into(),
132            },
133            name: None,
134            additional_kwargs: HashMap::new(),
135            id: None,
136            tool_calls: None,
137        }
138    }
139
140    /// Sets the message name.
141    pub fn with_name(mut self, name: impl Into<String>) -> Self {
142        self.name = Some(name.into());
143        self
144    }
145
146    /// Sets the message ID.
147    pub fn with_id(mut self, id: impl Into<String>) -> Self {
148        self.id = Some(id.into());
149        self
150    }
151
152    /// Adds an additional keyword argument.
153    pub fn with_additional_kwarg(mut self, key: impl Into<String>, value: Value) -> Self {
154        self.additional_kwargs.insert(key.into(), value);
155        self
156    }
157
158    /// Adds an image to the message (vision).
159    pub fn with_image(mut self, image: ImageContent) -> Self {
160        self.images.push(image);
161        self
162    }
163
164    /// Returns whether the message has images.
165    pub fn has_images(&self) -> bool {
166        !self.images.is_empty()
167    }
168
169    /// Returns the message type as a string.
170    pub fn type_str(&self) -> &str {
171        match &self.message_type {
172            MessageType::System => "system",
173            MessageType::Human => "human",
174            MessageType::AI => "ai",
175            MessageType::Tool { .. } => "tool",
176        }
177    }
178
179    /// Returns whether the message has tool calls.
180    pub fn has_tool_calls(&self) -> bool {
181        self.tool_calls.is_some() && !self.tool_calls.as_ref().unwrap().is_empty()
182    }
183
184    /// Returns the tool calls if present.
185    pub fn get_tool_calls(&self) -> Option<&[ToolCall]> {
186        self.tool_calls.as_deref()
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_human_with_image() {
196        let msg = Message::human_with_image("描述这张图", "https://example.com/img.jpg");
197        assert_eq!(msg.content, "描述这张图");
198        assert_eq!(msg.images.len(), 1);
199        assert_eq!(msg.images[0].url, "https://example.com/img.jpg");
200        assert!(msg.has_images());
201    }
202
203    #[test]
204    fn test_human_no_images_by_default() {
205        let msg = Message::human("纯文本");
206        assert!(msg.images.is_empty());
207        assert!(!msg.has_images());
208    }
209
210    #[test]
211    fn test_with_image_builder() {
212        let msg = Message::human("看图")
213            .with_image(ImageContent::from_url("https://example.com/a.png"))
214            .with_image(ImageContent::from_base64("abc"));
215        assert_eq!(msg.images.len(), 2);
216    }
217
218    #[test]
219    fn test_message_deserialize_without_images_field() {
220        // 旧格式(无 images 字段)应能反序列化(#[serde(default)])
221        let json = r#"{"content":"hi","type":"human"}"#;
222        let msg: Message = serde_json::from_str(json).unwrap();
223        assert_eq!(msg.content, "hi");
224        assert!(msg.images.is_empty());
225    }
226
227    #[test]
228    fn test_human_with_images_multiple() {
229        let msg = Message::human_with_images(
230            "多图",
231            vec![
232                ImageContent::from_url("https://example.com/1.jpg"),
233                ImageContent::from_url("https://example.com/2.jpg"),
234            ],
235        );
236        assert_eq!(msg.images.len(), 2);
237    }
238
239    #[test]
240    fn test_system_ai_no_images() {
241        assert!(Message::system("s").images.is_empty());
242        assert!(Message::ai("a").images.is_empty());
243        assert!(Message::tool("id", "c").images.is_empty());
244    }
245}