Skip to main content

rust_genai_types/
content.rs

1use crate::base64_serde;
2use crate::enums::{FunctionResponseScheduling, Language, Outcome, PartMediaResolutionLevel};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[cfg(feature = "mcp")]
7use rmcp::model::CallToolResult;
8
9/// 对话内容。
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Content {
13    /// 角色:user/model/function。
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub role: Option<Role>,
16    /// 消息内容片段。
17    #[serde(default)]
18    pub parts: Vec<Part>,
19}
20
21impl Content {
22    /// 创建用户文本消息。
23    pub fn user(text: impl Into<String>) -> Self {
24        Self::from_text(text, Role::User)
25    }
26
27    /// 创建模型文本消息。
28    pub fn model(text: impl Into<String>) -> Self {
29        Self::from_text(text, Role::Model)
30    }
31
32    /// 创建文本消息。
33    pub fn text(text: impl Into<String>) -> Self {
34        Self::from_text(text, Role::User)
35    }
36
37    /// 从 parts 构建内容。
38    #[must_use]
39    pub const fn from_parts(parts: Vec<Part>, role: Role) -> Self {
40        Self {
41            role: Some(role),
42            parts,
43        }
44    }
45
46    /// 提取第一段文本。
47    #[must_use]
48    pub fn first_text(&self) -> Option<&str> {
49        self.parts.iter().find_map(|part| part.text_value())
50    }
51
52    fn from_text(text: impl Into<String>, role: Role) -> Self {
53        Self {
54            role: Some(role),
55            parts: vec![Part::text(text)],
56        }
57    }
58}
59
60/// 内容角色。
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "lowercase")]
63pub enum Role {
64    User,
65    Model,
66    Function,
67}
68
69/// 内容部分。
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct Part {
73    /// 具体内容变体。
74    #[serde(flatten)]
75    pub kind: PartKind,
76    /// 是否为思考内容。
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub thought: Option<bool>,
79    /// 思考签名(base64 编码)。
80    #[serde(
81        default,
82        skip_serializing_if = "Option::is_none",
83        with = "base64_serde::option"
84    )]
85    pub thought_signature: Option<Vec<u8>>,
86    /// 媒体分辨率设置(按 part)。
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub media_resolution: Option<PartMediaResolution>,
89    /// 视频元数据。
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub video_metadata: Option<VideoMetadata>,
92}
93
94impl Part {
95    /// 创建文本 Part。
96    pub fn text(text: impl Into<String>) -> Self {
97        Self {
98            kind: PartKind::Text { text: text.into() },
99            thought: None,
100            thought_signature: None,
101            media_resolution: None,
102            video_metadata: None,
103        }
104    }
105
106    /// 创建内联二进制数据 Part。
107    pub fn inline_data(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
108        Self {
109            kind: PartKind::InlineData {
110                inline_data: Blob {
111                    mime_type: mime_type.into(),
112                    data,
113                    display_name: None,
114                },
115            },
116            thought: None,
117            thought_signature: None,
118            media_resolution: None,
119            video_metadata: None,
120        }
121    }
122
123    /// 创建文件 URI Part。
124    pub fn file_data(file_uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
125        Self {
126            kind: PartKind::FileData {
127                file_data: FileData {
128                    file_uri: file_uri.into(),
129                    mime_type: mime_type.into(),
130                    display_name: None,
131                },
132            },
133            thought: None,
134            thought_signature: None,
135            media_resolution: None,
136            video_metadata: None,
137        }
138    }
139
140    /// 创建函数调用 Part。
141    #[must_use]
142    pub const fn function_call(function_call: FunctionCall) -> Self {
143        Self {
144            kind: PartKind::FunctionCall { function_call },
145            thought: None,
146            thought_signature: None,
147            media_resolution: None,
148            video_metadata: None,
149        }
150    }
151
152    /// 创建函数响应 Part。
153    #[must_use]
154    pub const fn function_response(function_response: FunctionResponse) -> Self {
155        Self {
156            kind: PartKind::FunctionResponse { function_response },
157            thought: None,
158            thought_signature: None,
159            media_resolution: None,
160            video_metadata: None,
161        }
162    }
163
164    /// 创建可执行代码 Part。
165    pub fn executable_code(code: impl Into<String>, language: Language) -> Self {
166        Self {
167            kind: PartKind::ExecutableCode {
168                executable_code: ExecutableCode {
169                    code: code.into(),
170                    language,
171                },
172            },
173            thought: None,
174            thought_signature: None,
175            media_resolution: None,
176            video_metadata: None,
177        }
178    }
179
180    /// 创建代码执行结果 Part。
181    pub fn code_execution_result(outcome: Outcome, output: impl Into<String>) -> Self {
182        Self {
183            kind: PartKind::CodeExecutionResult {
184                code_execution_result: CodeExecutionResult {
185                    outcome,
186                    output: Some(output.into()),
187                },
188            },
189            thought: None,
190            thought_signature: None,
191            media_resolution: None,
192            video_metadata: None,
193        }
194    }
195
196    /// 设置是否为思考内容。
197    #[must_use]
198    pub const fn with_thought(mut self, thought: bool) -> Self {
199        self.thought = Some(thought);
200        self
201    }
202
203    /// 设置 thought signature。
204    #[must_use]
205    pub fn with_thought_signature(mut self, signature: Vec<u8>) -> Self {
206        self.thought_signature = Some(signature);
207        self
208    }
209
210    /// 设置媒体分辨率。
211    #[must_use]
212    pub const fn with_media_resolution(mut self, resolution: PartMediaResolution) -> Self {
213        self.media_resolution = Some(resolution);
214        self
215    }
216
217    /// 设置视频元数据。
218    #[must_use]
219    pub fn with_video_metadata(mut self, metadata: VideoMetadata) -> Self {
220        self.video_metadata = Some(metadata);
221        self
222    }
223
224    /// 获取文本内容(仅当为 Text Part)。
225    #[must_use]
226    pub const fn text_value(&self) -> Option<&str> {
227        match &self.kind {
228            PartKind::Text { text } => Some(text.as_str()),
229            _ => None,
230        }
231    }
232
233    /// 获取函数调用引用(仅当为 `FunctionCall` Part)。
234    #[must_use]
235    pub const fn function_call_ref(&self) -> Option<&FunctionCall> {
236        match &self.kind {
237            PartKind::FunctionCall { function_call } => Some(function_call),
238            _ => None,
239        }
240    }
241}
242
243/// 内容部分的具体变体。
244#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase", untagged)]
246pub enum PartKind {
247    Text {
248        text: String,
249    },
250    InlineData {
251        #[serde(rename = "inlineData")]
252        inline_data: Blob,
253    },
254    FileData {
255        #[serde(rename = "fileData")]
256        file_data: FileData,
257    },
258    FunctionCall {
259        #[serde(rename = "functionCall")]
260        function_call: FunctionCall,
261    },
262    FunctionResponse {
263        #[serde(rename = "functionResponse")]
264        function_response: FunctionResponse,
265    },
266    ExecutableCode {
267        #[serde(rename = "executableCode")]
268        executable_code: ExecutableCode,
269    },
270    CodeExecutionResult {
271        #[serde(rename = "codeExecutionResult")]
272        code_execution_result: CodeExecutionResult,
273    },
274}
275
276/// 媒体分辨率设置(按 part,Gemini 3 支持)。
277#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct PartMediaResolution {
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub level: Option<PartMediaResolutionLevel>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub num_tokens: Option<i32>,
284}
285
286/// 二进制数据。
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct Blob {
290    pub mime_type: String,
291    #[serde(with = "base64_serde")]
292    pub data: Vec<u8>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub display_name: Option<String>,
295}
296
297/// URI 文件数据。
298#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct FileData {
301    pub file_uri: String,
302    pub mime_type: String,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub display_name: Option<String>,
305}
306
307/// 部分参数值(函数调用流式参数)。
308#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct PartialArg {
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub null_value: Option<String>,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub number_value: Option<f64>,
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub string_value: Option<String>,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub bool_value: Option<bool>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub json_path: Option<String>,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub will_continue: Option<bool>,
323}
324
325/// 函数调用。
326#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct FunctionCall {
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub id: Option<String>,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub name: Option<String>,
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub args: Option<Value>,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub partial_args: Option<Vec<PartialArg>>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub will_continue: Option<bool>,
339}
340
341/// 函数响应内容中的二进制数据。
342#[derive(Debug, Clone, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct FunctionResponseBlob {
345    pub mime_type: String,
346    #[serde(with = "base64_serde")]
347    pub data: Vec<u8>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub display_name: Option<String>,
350}
351
352/// 函数响应内容中的文件引用。
353#[derive(Debug, Clone, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub struct FunctionResponseFileData {
356    pub file_uri: String,
357    pub mime_type: String,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub display_name: Option<String>,
360}
361
362/// 函数响应的多模态 part。
363#[derive(Debug, Clone, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct FunctionResponsePart {
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub inline_data: Option<FunctionResponseBlob>,
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub file_data: Option<FunctionResponseFileData>,
370}
371
372/// 函数响应。
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct FunctionResponse {
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub will_continue: Option<bool>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub scheduling: Option<FunctionResponseScheduling>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub parts: Option<Vec<FunctionResponsePart>>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub id: Option<String>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub name: Option<String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub response: Option<Value>,
388}
389
390impl FunctionResponse {
391    /// 从 MCP `CallToolResult` 构造 FunctionResponse(需要启用 `mcp` feature)。
392    ///
393    /// # Errors
394    /// 当序列化 MCP 响应失败时返回错误。
395    #[cfg(feature = "mcp")]
396    pub fn from_mcp_response(
397        name: impl Into<String>,
398        response: &CallToolResult,
399    ) -> Result<Self, serde_json::Error> {
400        let value = serde_json::to_value(response)?;
401        let is_error = response.is_error.unwrap_or(false);
402        let response_value = if is_error {
403            let mut wrapper = serde_json::Map::new();
404            wrapper.insert("error".to_string(), value);
405            Value::Object(wrapper)
406        } else {
407            value
408        };
409        Ok(Self {
410            will_continue: None,
411            scheduling: None,
412            parts: None,
413            id: None,
414            name: Some(name.into()),
415            response: Some(response_value),
416        })
417    }
418}
419
420/// 可执行代码。
421#[derive(Debug, Clone, Serialize, Deserialize)]
422#[serde(rename_all = "camelCase")]
423pub struct ExecutableCode {
424    pub code: String,
425    pub language: Language,
426}
427
428/// 代码执行结果。
429#[derive(Debug, Clone, Serialize, Deserialize)]
430#[serde(rename_all = "camelCase")]
431pub struct CodeExecutionResult {
432    pub outcome: Outcome,
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub output: Option<String>,
435}
436
437/// 视频元数据。
438#[derive(Debug, Clone, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct VideoMetadata {
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub start_offset: Option<String>,
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub end_offset: Option<String>,
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub fps: Option<f32>,
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use serde_json::json;
453
454    #[test]
455    fn content_first_text_skips_non_text() {
456        let content = Content::from_parts(
457            vec![
458                Part::inline_data(vec![1, 2, 3], "image/png"),
459                Part::text("first"),
460                Part::text("second"),
461            ],
462            Role::User,
463        );
464        assert_eq!(content.first_text(), Some("first"));
465    }
466
467    #[test]
468    fn part_builders_and_accessors() {
469        let call = FunctionCall {
470            id: Some("call-1".into()),
471            name: Some("lookup".into()),
472            args: Some(json!({"q": "rust"})),
473            partial_args: None,
474            will_continue: None,
475        };
476        let response = FunctionResponse {
477            will_continue: None,
478            scheduling: None,
479            parts: None,
480            id: Some("resp-1".into()),
481            name: Some("lookup".into()),
482            response: Some(json!({"ok": true})),
483        };
484        let metadata = VideoMetadata {
485            start_offset: Some("0s".into()),
486            end_offset: Some("1s".into()),
487            fps: Some(30.0),
488        };
489
490        let part = Part::text("hello")
491            .with_thought(true)
492            .with_thought_signature(vec![1, 2, 3])
493            .with_media_resolution(PartMediaResolution {
494                level: Some(PartMediaResolutionLevel::MediaResolutionLow),
495                num_tokens: None,
496            })
497            .with_video_metadata(metadata);
498        assert_eq!(part.text_value(), Some("hello"));
499
500        let call_part = Part::function_call(call);
501        assert_eq!(
502            call_part.function_call_ref().unwrap().name.as_deref(),
503            Some("lookup")
504        );
505
506        let response_part = Part::function_response(response);
507        let json = serde_json::to_value(&response_part).unwrap();
508        assert!(json.get("functionResponse").is_some());
509
510        let exec_part = Part::executable_code("print('hi')", Language::Python);
511        let exec_json = serde_json::to_value(&exec_part).unwrap();
512        assert_eq!(exec_json["executableCode"]["language"], "PYTHON");
513
514        let result_part = Part::code_execution_result(Outcome::OutcomeOk, "ok");
515        let result_json = serde_json::to_value(&result_part).unwrap();
516        assert_eq!(result_json["codeExecutionResult"]["outcome"], "OUTCOME_OK");
517
518        let file_part = Part::file_data("files/abc", "application/pdf");
519        let file_json = serde_json::to_value(&file_part).unwrap();
520        assert_eq!(file_json["fileData"]["mimeType"], "application/pdf");
521    }
522
523    #[test]
524    fn content_roundtrip() {
525        let content = Content::user("hello");
526        let json = serde_json::to_string(&content).unwrap();
527        let decoded: Content = serde_json::from_str(&json).unwrap();
528        assert_eq!(decoded.parts.len(), 1);
529    }
530
531    #[test]
532    fn blob_base64_serialization() {
533        let blob = Blob {
534            mime_type: "image/png".into(),
535            data: vec![1, 2, 3],
536            display_name: None,
537        };
538        let value = serde_json::to_value(&blob).unwrap();
539        assert!(value["data"].is_string());
540    }
541
542    #[test]
543    fn function_response_media_roundtrip() {
544        let response = FunctionResponse {
545            will_continue: None,
546            scheduling: None,
547            parts: Some(vec![FunctionResponsePart {
548                inline_data: Some(FunctionResponseBlob {
549                    mime_type: "image/png".into(),
550                    data: vec![1, 2, 3],
551                    display_name: None,
552                }),
553                file_data: None,
554            }]),
555            id: Some("fn-1".into()),
556            name: Some("render_chart".into()),
557            response: None,
558        };
559
560        let part = Part::function_response(response);
561        let json = serde_json::to_string(&part).unwrap();
562        assert!(json.contains("inlineData"));
563    }
564
565    #[test]
566    fn function_call_part_deserializes_from_camel_case() {
567        let value = json!({
568            "functionCall": {
569                "name": "add_numbers",
570                "args": { "a": 2.5, "b": 3.1 }
571            },
572            "thoughtSignature": "AQID"
573        });
574        let part: Part = serde_json::from_value(value).unwrap();
575        let call = part.function_call_ref().expect("missing function call");
576        assert_eq!(call.name.as_deref(), Some("add_numbers"));
577    }
578}