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