1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(tag = "kind", rename_all = "snake_case")]
6pub enum MediaSource {
7 Artifact { uri: String },
8}
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[serde(tag = "type", rename_all = "snake_case")]
12pub enum MessagePart {
13 Text {
14 text: String,
15 },
16 Image {
17 source: MediaSource,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 mime_type: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 alt_text: Option<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 detail: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 width_px: Option<u32>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 height_px: Option<u32>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 size_bytes: Option<u64>,
30 },
31 Audio {
32 source: MediaSource,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 mime_type: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 transcript: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 duration_ms: Option<u64>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 size_bytes: Option<u64>,
41 },
42 Video {
43 source: MediaSource,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 mime_type: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 alt_text: Option<String>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 duration_ms: Option<u64>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 width_px: Option<u32>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 height_px: Option<u32>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 size_bytes: Option<u64>,
56 },
57 File {
58 source: MediaSource,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 mime_type: Option<String>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 name: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 size_bytes: Option<u64>,
65 },
66 Json {
67 value: Value,
68 },
69}
70
71#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
72#[serde(rename_all = "camelCase")]
73pub struct MessageContent {
74 #[serde(default)]
75 pub parts: Vec<MessagePart>,
76}
77
78impl MessageContent {
79 pub fn text(text: impl Into<String>) -> Self {
80 Self {
81 parts: vec![MessagePart::Text { text: text.into() }],
82 }
83 }
84
85 pub fn plain_text(&self) -> String {
86 self.parts
87 .iter()
88 .filter_map(|part| match part {
89 MessagePart::Text { text } => Some(text.as_str()),
90 _ => None,
91 })
92 .collect::<Vec<_>>()
93 .join("\n")
94 }
95
96 pub fn has_non_text_parts(&self) -> bool {
97 self.parts
98 .iter()
99 .any(|part| !matches!(part, MessagePart::Text { .. }))
100 }
101}
102
103#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
104#[serde(rename_all = "snake_case")]
105pub enum ConversationMessageRole {
106 User,
107 Assistant,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111#[serde(rename_all = "camelCase")]
112pub struct ConversationMessage {
113 pub message_id: String,
114 pub request_id: String,
115 pub role: ConversationMessageRole,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub author_user_id: Option<String>,
118 pub content: MessageContent,
119 pub created_at: String,
120}