Skip to main content

lellm_core/
message.rs

1//! 消息与内容块类型。
2
3use crate::error::{ParseError, ToolResult};
4use serde::{Deserialize, Serialize};
5
6/// 缓存控制标记 — Provider 无关的语义抽象。
7///
8/// 由 Provider Codec 映射为各 Provider 的具体格式:
9/// - Anthropic: `{"type": "ephemeral"}`
10/// - OpenAI: ignore(隐式缓存)
11/// - Google: ignore
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13pub enum CacheControl {
14    /// 缓存断点 — 标记此处为缓存边界。
15    /// 业务层在稳定性递减的层边界处插入。
16    Breakpoint,
17}
18
19/// 纯文本块
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct TextBlock {
22    pub text: String,
23
24    /// 缓存控制标记。业务层在 System prompt 的稳定性层边界处设置。
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub cache_control: Option<CacheControl>,
27}
28
29/// 思考块(Claude thinking / OpenAI reasoning)
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct ThinkingBlock {
32    pub thinking: String,
33    /// 部分 provider 支持 redacted thinking
34    pub redacted: Option<String>,
35}
36
37/// 图片资源
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct ImageSource {
40    /// base64 编码的图片数据
41    pub data: String,
42    /// MIME 类型,如 "image/png"
43    pub media_type: String,
44}
45
46/// LLM 请求的工具调用。
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct ToolCall {
49    pub id: String,
50    pub name: String,
51    pub arguments: serde_json::Value,
52}
53
54/// 内容块 — Message 和 ChatResponse 的基本组成单元。
55/// 核心层极简,无 provider 特有标记。
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum ContentBlock {
59    Text(TextBlock),
60    Thinking(ThinkingBlock),
61    Image { source: ImageSource },
62    ToolCall(ToolCall),
63}
64
65impl ContentBlock {
66    /// 创建纯文本块。接受 `&str`、`String`。
67    pub fn text(s: impl Into<String>) -> Self {
68        ContentBlock::Text(TextBlock {
69            text: s.into(),
70            cache_control: None,
71        })
72    }
73
74    /// 创建带缓存标记的文本块。
75    pub fn text_with_cache(s: String, cache: CacheControl) -> Self {
76        ContentBlock::Text(TextBlock {
77            text: s,
78            cache_control: Some(cache),
79        })
80    }
81
82    pub fn as_text(&self) -> Option<&str> {
83        match self {
84            ContentBlock::Text(block) => Some(&block.text),
85            _ => None,
86        }
87    }
88}
89
90/// 对话中的单条消息。
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "type", rename_all = "snake_case")]
93pub enum Message {
94    System {
95        content: Vec<ContentBlock>,
96    },
97    User {
98        content: Vec<ContentBlock>,
99    },
100    Assistant {
101        content: Vec<ContentBlock>,
102    },
103    ToolResult {
104        tool_call_id: String,
105        /// 工具执行是否失败(供 Provider API 映射,如 Anthropic `is_error: true`)
106        is_error: bool,
107        content: Vec<ContentBlock>,
108    },
109}
110
111impl Message {
112    // =======================================================================
113    // 便捷构造方法 — 纯文本(最常见用法)
114    // =======================================================================
115
116    /// 便捷构造:纯文本 System 消息。
117    ///
118    /// ```
119    /// use lellm_core::Message;
120    ///
121    /// // 之前:
122    /// // Message::System { content: lellm_core::text_block("you are helpful".to_string()) }
123    ///
124    /// // 现在:
125    /// let msg = Message::system_text("you are helpful");
126    /// ```
127    pub fn system_text(s: &str) -> Self {
128        Message::System {
129            content: text_block(s.to_string()),
130        }
131    }
132
133    /// 便捷构造:纯文本 User 消息。
134    ///
135    /// ```
136    /// use lellm_core::Message;
137    ///
138    /// let msg = Message::user_text("hello");
139    /// ```
140    pub fn user_text(s: &str) -> Self {
141        Message::User {
142            content: text_block(s.to_string()),
143        }
144    }
145
146    /// 便捷构造:纯文本 Assistant 消息。
147    pub fn assistant_text(s: &str) -> Self {
148        Message::Assistant {
149            content: text_block(s.to_string()),
150        }
151    }
152
153    // =======================================================================
154    // 便捷构造方法 — 多模态
155    // =======================================================================
156
157    /// 便捷构造:带图片的 User 消息(文本 + 图片)。
158    ///
159    /// ```
160    /// use lellm_core::Message;
161    ///
162    /// let msg = Message::user_text_image(
163    ///     "what's in this image?",
164    ///     "image/png".to_string(),
165    ///     "base64_encoded_data".to_string(),
166    /// );
167    /// ```
168    pub fn user_text_image(text: &str, media_type: String, data: String) -> Self {
169        Message::User {
170            content: vec![
171                ContentBlock::text(text),
172                ContentBlock::Image {
173                    source: ImageSource { data, media_type },
174                },
175            ],
176        }
177    }
178
179    /// 便捷构造:仅图片的 User 消息。
180    pub fn user_image(media_type: String, data: String) -> Self {
181        Message::User {
182            content: vec![ContentBlock::Image {
183                source: ImageSource { data, media_type },
184            }],
185        }
186    }
187
188    // =======================================================================
189    // 便捷构造方法 — 自定义内容块
190    // =======================================================================
191
192    /// 便捷构造:System 消息(自定义 ContentBlock)。
193    pub fn system(content: Vec<ContentBlock>) -> Self {
194        Message::System { content }
195    }
196
197    /// 便捷构造:User 消息(自定义 ContentBlock)。
198    pub fn user(content: Vec<ContentBlock>) -> Self {
199        Message::User { content }
200    }
201
202    /// 便捷构造:Assistant 消息(自定义 ContentBlock)。
203    pub fn assistant(content: Vec<ContentBlock>) -> Self {
204        Message::Assistant { content }
205    }
206
207    /// 便捷构造:ToolResult 消息(成功)。
208    pub fn tool_result_ok(call_id: impl Into<String>, content: String) -> Self {
209        Message::ToolResult {
210            tool_call_id: call_id.into(),
211            is_error: false,
212            content: text_block(content),
213        }
214    }
215
216    /// 便捷构造:ToolResult 消息(失败)。
217    pub fn tool_error(call_id: impl Into<String>, error: String) -> Self {
218        Message::ToolResult {
219            tool_call_id: call_id.into(),
220            is_error: true,
221            content: text_block(error),
222        }
223    }
224
225    // =======================================================================
226    // 访问器
227    // =======================================================================
228
229    /// 返回内容块的引用(用于 provider 适配器序列化)
230    pub fn content(&self) -> &Vec<ContentBlock> {
231        match self {
232            Message::System { content }
233            | Message::User { content }
234            | Message::Assistant { content }
235            | Message::ToolResult { content, .. } => content,
236        }
237    }
238
239    /// 返回 ToolResult 的 tool_call_id(仅 ToolResult 变体有效,其他返回 None)
240    pub fn tool_call_id(&self) -> String {
241        match self {
242            Message::ToolResult { tool_call_id, .. } => tool_call_id.clone(),
243            _ => String::new(),
244        }
245    }
246
247    /// 返回 ToolResult 的 is_error 标记(仅 ToolResult 变体有效)
248    pub fn is_tool_error(&self) -> bool {
249        matches!(self, Message::ToolResult { is_error: true, .. })
250    }
251
252    /// 从工具调用结果构建 Message::ToolResult
253    ///
254    /// 成功 → 序列化 `serde_json::Value` 为文本,`is_error: false`
255    /// 失败 → `"tool error: {e}"` 文本 content,`is_error: true`
256    pub fn tool_result(call: &ToolCall, result: &ToolResult) -> Self {
257        let (content_str, is_error) = match result {
258            Ok(v) => (
259                serde_json::to_string(v).unwrap_or_else(|_| v.to_string()),
260                false,
261            ),
262            Err(e) => (format!("tool error: {e}"), true),
263        };
264        Message::ToolResult {
265            tool_call_id: call.id.clone(),
266            is_error,
267            content: text_block(content_str),
268        }
269    }
270
271    /// 语义校验 — 检查 Message 变体与 ContentBlock 的合法性。
272    ///
273    /// v0.1 核心规则:
274    /// 1. `ToolResult` 禁止包含 `ToolCall` 或 `Thinking`
275    /// 2. `ToolResult.tool_call_id` 非空
276    /// 3. `Assistant` 中的 `ToolCall.id` 非空
277    /// 4. `User` 禁止包含 `Thinking`
278    pub fn validate(&self) -> Result<(), ParseError> {
279        match self {
280            Message::ToolResult {
281                tool_call_id,
282                is_error: _,
283                content,
284            } => {
285                if tool_call_id.is_empty() {
286                    return Err(ParseError {
287                        detail: "ToolResult.tool_call_id must not be empty".into(),
288                    });
289                }
290                for block in content {
291                    match block {
292                        ContentBlock::ToolCall(_) => {
293                            return Err(ParseError {
294                                detail: "ToolResult must not contain ToolCall blocks".into(),
295                            });
296                        }
297                        ContentBlock::Thinking(_) => {
298                            return Err(ParseError {
299                                detail: "ToolResult must not contain Thinking blocks".into(),
300                            });
301                        }
302                        _ => {}
303                    }
304                }
305            }
306            Message::Assistant { content } => {
307                for block in content {
308                    if let ContentBlock::ToolCall(tc) = block
309                        && tc.id.is_empty()
310                    {
311                        return Err(ParseError {
312                            detail: "Assistant ToolCall.id must not be empty".into(),
313                        });
314                    }
315                }
316            }
317            Message::User { content } => {
318                for block in content {
319                    if let ContentBlock::Thinking(_) = block {
320                        return Err(ParseError {
321                            detail: "User must not contain Thinking blocks".into(),
322                        });
323                    }
324                }
325            }
326            Message::System { .. } => {}
327        }
328        Ok(())
329    }
330
331    /// 提取所有 ToolCall(仅 Assistant 消息包含)
332    pub fn extract_tool_calls(&self) -> Vec<ToolCall> {
333        match self {
334            Message::Assistant { content } => content
335                .iter()
336                .filter_map(|b| {
337                    if let ContentBlock::ToolCall(tc) = b {
338                        Some(tc.clone())
339                    } else {
340                        None
341                    }
342                })
343                .collect(),
344            _ => Vec::new(),
345        }
346    }
347}
348
349/// 便捷函数:创建纯文本块
350pub fn text_block(s: impl Into<String>) -> Vec<ContentBlock> {
351    vec![ContentBlock::text(s)]
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn test_content_block_text() {
360        let block = ContentBlock::text("hello");
361        assert_eq!(block.as_text(), Some("hello"));
362    }
363
364    #[test]
365    fn test_content_block_tool_call_no_as_text() {
366        let block = ContentBlock::ToolCall(ToolCall {
367            id: "1".into(),
368            name: "test".into(),
369            arguments: serde_json::json!({}),
370        });
371        assert_eq!(block.as_text(), None);
372    }
373
374    #[test]
375    fn test_message_content() {
376        let msg = Message::user_text("hello world");
377        assert_eq!(msg.content().len(), 1);
378        assert_eq!(msg.content()[0].as_text(), Some("hello world"));
379    }
380
381    #[test]
382    fn test_message_extract_tool_calls() {
383        let tc = ToolCall {
384            id: "1".into(),
385            name: "test".into(),
386            arguments: serde_json::json!({}),
387        };
388        let msg = Message::Assistant {
389            content: vec![ContentBlock::ToolCall(tc.clone())],
390        };
391        let calls = msg.extract_tool_calls();
392        assert_eq!(calls.len(), 1);
393        assert_eq!(calls[0].name, "test");
394    }
395
396    // ─── validate() 测试 ───
397
398    #[test]
399    fn test_validate_user_ok() {
400        let msg = Message::User {
401            content: text_block("hello".to_string()),
402        };
403        assert!(msg.validate().is_ok());
404    }
405
406    #[test]
407    fn test_validate_user_reject_thinking() {
408        let msg = Message::User {
409            content: vec![ContentBlock::Thinking(ThinkingBlock {
410                thinking: "hmm".into(),
411                redacted: None,
412            })],
413        };
414        assert!(matches!(msg.validate(), Err(ParseError { .. })));
415    }
416
417    #[test]
418    fn test_validate_assistant_ok() {
419        let msg = Message::Assistant {
420            content: text_block("hi".to_string()),
421        };
422        assert!(msg.validate().is_ok());
423    }
424
425    #[test]
426    fn test_validate_assistant_tool_call_empty_id() {
427        let msg = Message::Assistant {
428            content: vec![ContentBlock::ToolCall(ToolCall {
429                id: String::new(),
430                name: "test".into(),
431                arguments: serde_json::json!({}),
432            })],
433        };
434        assert!(matches!(msg.validate(), Err(ParseError { .. })));
435    }
436
437    #[test]
438    fn test_validate_tool_result_ok() {
439        let msg = Message::ToolResult {
440            tool_call_id: "call_1".to_string(),
441            is_error: false,
442            content: text_block("ok".to_string()),
443        };
444        assert!(msg.validate().is_ok());
445    }
446
447    #[test]
448    fn test_validate_tool_result_empty_id() {
449        let msg = Message::ToolResult {
450            tool_call_id: String::new(),
451            is_error: false,
452            content: text_block("ok".to_string()),
453        };
454        assert!(matches!(msg.validate(), Err(ParseError { .. })));
455    }
456
457    #[test]
458    fn test_validate_tool_result_reject_tool_call() {
459        let msg = Message::ToolResult {
460            tool_call_id: "call_1".to_string(),
461            is_error: false,
462            content: vec![ContentBlock::ToolCall(ToolCall {
463                id: "x".into(),
464                name: "y".into(),
465                arguments: serde_json::json!({}),
466            })],
467        };
468        assert!(matches!(msg.validate(), Err(ParseError { .. })));
469    }
470
471    #[test]
472    fn test_validate_tool_result_reject_thinking() {
473        let msg = Message::ToolResult {
474            tool_call_id: "call_1".to_string(),
475            is_error: false,
476            content: vec![ContentBlock::Thinking(ThinkingBlock {
477                thinking: "hmm".into(),
478                redacted: None,
479            })],
480        };
481        assert!(matches!(msg.validate(), Err(ParseError { .. })));
482    }
483
484    #[test]
485    fn test_validate_system_ok() {
486        let msg = Message::System {
487            content: text_block("you are helpful".to_string()),
488        };
489        assert!(msg.validate().is_ok());
490    }
491
492    // ─── 便捷构造方法测试 ───
493
494    #[test]
495    fn test_convenience_system_text() {
496        let msg = Message::system_text("you are helpful");
497        assert!(matches!(msg, Message::System { .. }));
498        assert_eq!(msg.content()[0].as_text(), Some("you are helpful"));
499    }
500
501    #[test]
502    fn test_convenience_user_text() {
503        let msg = Message::user_text("hello");
504        assert!(matches!(msg, Message::User { .. }));
505        assert_eq!(msg.content()[0].as_text(), Some("hello"));
506    }
507
508    #[test]
509    fn test_convenience_assistant_text() {
510        let msg = Message::assistant_text("the answer is 42");
511        assert!(matches!(msg, Message::Assistant { .. }));
512        assert_eq!(msg.content()[0].as_text(), Some("the answer is 42"));
513    }
514
515    #[test]
516    fn test_convenience_system_content() {
517        let msg = Message::system(vec![ContentBlock::text("prompt")]);
518        assert!(matches!(msg, Message::System { .. }));
519        assert_eq!(msg.content()[0].as_text(), Some("prompt"));
520    }
521
522    #[test]
523    fn test_convenience_user_content() {
524        let msg = Message::user(vec![ContentBlock::text("question")]);
525        assert!(matches!(msg, Message::User { .. }));
526        assert_eq!(msg.content()[0].as_text(), Some("question"));
527    }
528
529    #[test]
530    fn test_convenience_tool_result_ok() {
531        let msg = Message::tool_result_ok("call_1", "result data".to_string());
532        assert!(matches!(msg, Message::ToolResult { .. }));
533        assert!(!msg.is_tool_error());
534        assert_eq!(msg.tool_call_id(), "call_1");
535    }
536
537    #[test]
538    fn test_convenience_tool_error() {
539        let msg = Message::tool_error("call_2", "something failed".to_string());
540        assert!(matches!(msg, Message::ToolResult { .. }));
541        assert!(msg.is_tool_error());
542        assert_eq!(msg.tool_call_id(), "call_2");
543    }
544
545    #[test]
546    fn test_content_block_text_with_string() {
547        let s = String::from("dynamic");
548        let block = ContentBlock::text(s);
549        assert_eq!(block.as_text(), Some("dynamic"));
550    }
551
552    #[test]
553    fn test_text_block_with_str() {
554        let blocks = text_block("hello");
555        assert_eq!(blocks.len(), 1);
556        assert_eq!(blocks[0].as_text(), Some("hello"));
557    }
558
559    #[test]
560    fn test_text_block_with_string() {
561        let blocks = text_block(String::from("hello"));
562        assert_eq!(blocks.len(), 1);
563        assert_eq!(blocks[0].as_text(), Some("hello"));
564    }
565
566    // ─── 多模态便捷构造测试 ───
567
568    #[test]
569    fn test_convenience_user_text_image() {
570        let msg = Message::user_text_image("what's this?", "image/png".into(), "base64data".into());
571        assert!(matches!(msg, Message::User { .. }));
572        assert_eq!(msg.content().len(), 2);
573        assert_eq!(msg.content()[0].as_text(), Some("what's this?"));
574        match &msg.content()[1] {
575            ContentBlock::Image { source } => {
576                assert_eq!(source.media_type, "image/png");
577                assert_eq!(source.data, "base64data");
578            }
579            _ => panic!("expected Image block"),
580        }
581    }
582
583    #[test]
584    fn test_convenience_user_image() {
585        let msg = Message::user_image("image/jpeg".into(), "jpgdata".into());
586        assert!(matches!(msg, Message::User { .. }));
587        assert_eq!(msg.content().len(), 1);
588        match &msg.content()[0] {
589            ContentBlock::Image { source } => {
590                assert_eq!(source.media_type, "image/jpeg");
591                assert_eq!(source.data, "jpgdata");
592            }
593            _ => panic!("expected Image block"),
594        }
595    }
596}