Skip to main content

lellm_core/
message.rs

1//! 消息与内容块类型。
2
3use crate::error::{ParseError, ToolResult};
4use serde::{Deserialize, Serialize};
5
6/// 纯文本块
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct TextBlock {
9    pub text: String,
10}
11
12/// 思考块(Claude thinking / OpenAI reasoning)
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct ThinkingBlock {
15    pub thinking: String,
16    /// 部分 provider 支持 redacted thinking
17    pub redacted: Option<String>,
18}
19
20/// 图片资源
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub struct ImageSource {
23    /// base64 编码的图片数据
24    pub data: String,
25    /// MIME 类型,如 "image/png"
26    pub media_type: String,
27}
28
29/// LLM 请求的工具调用。
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct ToolCall {
32    pub id: String,
33    pub name: String,
34    pub arguments: serde_json::Value,
35}
36
37/// 内容块 — Message 和 ChatResponse 的基本组成单元。
38/// 核心层极简,无 provider 特有标记。
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40#[serde(tag = "type", rename_all = "snake_case")]
41pub enum ContentBlock {
42    Text(TextBlock),
43    Thinking(ThinkingBlock),
44    Image { source: ImageSource },
45    ToolCall(ToolCall),
46}
47
48impl ContentBlock {
49    pub fn text(s: String) -> Self {
50        ContentBlock::Text(TextBlock { text: s })
51    }
52
53    pub fn as_text(&self) -> Option<&str> {
54        match self {
55            ContentBlock::Text(block) => Some(&block.text),
56            _ => None,
57        }
58    }
59}
60
61/// 对话中的单条消息。
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(tag = "type", rename_all = "snake_case")]
64pub enum Message {
65    System {
66        content: Vec<ContentBlock>,
67    },
68    User {
69        content: Vec<ContentBlock>,
70    },
71    Assistant {
72        content: Vec<ContentBlock>,
73    },
74    ToolResult {
75        tool_call_id: String,
76        /// 工具执行是否失败(供 Provider API 映射,如 Anthropic `is_error: true`)
77        is_error: bool,
78        content: Vec<ContentBlock>,
79    },
80}
81
82impl Message {
83    /// 返回内容块的引用(用于 provider 适配器序列化)
84    pub fn content(&self) -> &Vec<ContentBlock> {
85        match self {
86            Message::System { content }
87            | Message::User { content }
88            | Message::Assistant { content }
89            | Message::ToolResult { content, .. } => content,
90        }
91    }
92
93    /// 返回 ToolResult 的 tool_call_id(仅 ToolResult 变体有效,其他返回 None)
94    pub fn tool_call_id(&self) -> String {
95        match self {
96            Message::ToolResult { tool_call_id, .. } => tool_call_id.clone(),
97            _ => String::new(),
98        }
99    }
100
101    /// 返回 ToolResult 的 is_error 标记(仅 ToolResult 变体有效)
102    pub fn is_tool_error(&self) -> bool {
103        matches!(self, Message::ToolResult { is_error: true, .. })
104    }
105
106    /// 从工具调用结果构建 Message::ToolResult
107    ///
108    /// 成功 → 文本 content,`is_error: false`
109    /// 失败 → `"tool error: {e}"` 文本 content,`is_error: true`
110    pub fn tool_result(call: &ToolCall, result: &ToolResult) -> Self {
111        let (content_str, is_error) = match result {
112            Ok(s) => (s.clone(), false),
113            Err(e) => (format!("tool error: {e}"), true),
114        };
115        Message::ToolResult {
116            tool_call_id: call.id.clone(),
117            is_error,
118            content: text_block(content_str),
119        }
120    }
121
122    /// 语义校验 — 检查 Message 变体与 ContentBlock 的合法性。
123    ///
124    /// v0.1 核心规则:
125    /// 1. `ToolResult` 禁止包含 `ToolCall` 或 `Thinking`
126    /// 2. `ToolResult.tool_call_id` 非空
127    /// 3. `Assistant` 中的 `ToolCall.id` 非空
128    /// 4. `User` 禁止包含 `Thinking`
129    pub fn validate(&self) -> Result<(), ParseError> {
130        match self {
131            Message::ToolResult {
132                tool_call_id,
133                is_error: _,
134                content,
135            } => {
136                if tool_call_id.is_empty() {
137                    return Err(ParseError {
138                        detail: "ToolResult.tool_call_id must not be empty".into(),
139                    });
140                }
141                for block in content {
142                    match block {
143                        ContentBlock::ToolCall(_) => {
144                            return Err(ParseError {
145                                detail: "ToolResult must not contain ToolCall blocks".into(),
146                            });
147                        }
148                        ContentBlock::Thinking(_) => {
149                            return Err(ParseError {
150                                detail: "ToolResult must not contain Thinking blocks".into(),
151                            });
152                        }
153                        _ => {}
154                    }
155                }
156            }
157            Message::Assistant { content } => {
158                for block in content {
159                    if let ContentBlock::ToolCall(tc) = block
160                        && tc.id.is_empty()
161                    {
162                        return Err(ParseError {
163                            detail: "Assistant ToolCall.id must not be empty".into(),
164                        });
165                    }
166                }
167            }
168            Message::User { content } => {
169                for block in content {
170                    if let ContentBlock::Thinking(_) = block {
171                        return Err(ParseError {
172                            detail: "User must not contain Thinking blocks".into(),
173                        });
174                    }
175                }
176            }
177            Message::System { .. } => {}
178        }
179        Ok(())
180    }
181
182    /// 提取所有 ToolCall(仅 Assistant 消息包含)
183    pub fn extract_tool_calls(&self) -> Vec<ToolCall> {
184        match self {
185            Message::Assistant { content } => content
186                .iter()
187                .filter_map(|b| {
188                    if let ContentBlock::ToolCall(tc) = b {
189                        Some(tc.clone())
190                    } else {
191                        None
192                    }
193                })
194                .collect(),
195            _ => Vec::new(),
196        }
197    }
198}
199
200/// 便捷函数:创建纯文本块
201pub fn text_block(s: String) -> Vec<ContentBlock> {
202    vec![ContentBlock::text(s)]
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_content_block_text() {
211        let block = ContentBlock::text("hello".to_string());
212        assert_eq!(block.as_text(), Some("hello"));
213    }
214
215    #[test]
216    fn test_content_block_tool_call_no_as_text() {
217        let block = ContentBlock::ToolCall(ToolCall {
218            id: "1".into(),
219            name: "test".into(),
220            arguments: serde_json::json!({}),
221        });
222        assert_eq!(block.as_text(), None);
223    }
224
225    #[test]
226    fn test_message_content() {
227        let msg = Message::User {
228            content: text_block("hello world".to_string()),
229        };
230        assert_eq!(msg.content().len(), 1);
231        assert_eq!(msg.content()[0].as_text(), Some("hello world"));
232    }
233
234    #[test]
235    fn test_message_extract_tool_calls() {
236        let tc = ToolCall {
237            id: "1".into(),
238            name: "test".into(),
239            arguments: serde_json::json!({}),
240        };
241        let msg = Message::Assistant {
242            content: vec![ContentBlock::ToolCall(tc.clone())],
243        };
244        let calls = msg.extract_tool_calls();
245        assert_eq!(calls.len(), 1);
246        assert_eq!(calls[0].name, "test");
247    }
248
249    // ─── validate() 测试 ───
250
251    #[test]
252    fn test_validate_user_ok() {
253        let msg = Message::User {
254            content: text_block("hello".to_string()),
255        };
256        assert!(msg.validate().is_ok());
257    }
258
259    #[test]
260    fn test_validate_user_reject_thinking() {
261        let msg = Message::User {
262            content: vec![ContentBlock::Thinking(ThinkingBlock {
263                thinking: "hmm".into(),
264                redacted: None,
265            })],
266        };
267        assert!(matches!(msg.validate(), Err(ParseError { .. })));
268    }
269
270    #[test]
271    fn test_validate_assistant_ok() {
272        let msg = Message::Assistant {
273            content: text_block("hi".to_string()),
274        };
275        assert!(msg.validate().is_ok());
276    }
277
278    #[test]
279    fn test_validate_assistant_tool_call_empty_id() {
280        let msg = Message::Assistant {
281            content: vec![ContentBlock::ToolCall(ToolCall {
282                id: String::new(),
283                name: "test".into(),
284                arguments: serde_json::json!({}),
285            })],
286        };
287        assert!(matches!(msg.validate(), Err(ParseError { .. })));
288    }
289
290    #[test]
291    fn test_validate_tool_result_ok() {
292        let msg = Message::ToolResult {
293            tool_call_id: "call_1".to_string(),
294            is_error: false,
295            content: text_block("ok".to_string()),
296        };
297        assert!(msg.validate().is_ok());
298    }
299
300    #[test]
301    fn test_validate_tool_result_empty_id() {
302        let msg = Message::ToolResult {
303            tool_call_id: String::new(),
304            is_error: false,
305            content: text_block("ok".to_string()),
306        };
307        assert!(matches!(msg.validate(), Err(ParseError { .. })));
308    }
309
310    #[test]
311    fn test_validate_tool_result_reject_tool_call() {
312        let msg = Message::ToolResult {
313            tool_call_id: "call_1".to_string(),
314            is_error: false,
315            content: vec![ContentBlock::ToolCall(ToolCall {
316                id: "x".into(),
317                name: "y".into(),
318                arguments: serde_json::json!({}),
319            })],
320        };
321        assert!(matches!(msg.validate(), Err(ParseError { .. })));
322    }
323
324    #[test]
325    fn test_validate_tool_result_reject_thinking() {
326        let msg = Message::ToolResult {
327            tool_call_id: "call_1".to_string(),
328            is_error: false,
329            content: vec![ContentBlock::Thinking(ThinkingBlock {
330                thinking: "hmm".into(),
331                redacted: None,
332            })],
333        };
334        assert!(matches!(msg.validate(), Err(ParseError { .. })));
335    }
336
337    #[test]
338    fn test_validate_system_ok() {
339        let msg = Message::System {
340            content: text_block("you are helpful".to_string()),
341        };
342        assert!(msg.validate().is_ok());
343    }
344}