1use bytes::Bytes;
2use camino::Utf8PathBuf;
3use serde::{Deserialize, Serialize};
4use url::Url;
5
6#[derive(Clone, Debug, Serialize, Deserialize)]
7#[serde(tag = "type", rename_all = "snake_case")]
8pub enum Source {
9 Base64 { data: String },
10 Url { url: Url },
11 File { path: Utf8PathBuf },
12 #[serde(skip)]
13 Bytes(Bytes),
14}
15
16impl Source {
17 pub fn base64(data: impl Into<String>) -> Self {
18 Self::Base64 { data: data.into() }
19 }
20
21 pub fn url(url: Url) -> Self {
22 Self::Url { url }
23 }
24
25 pub fn file(path: impl Into<Utf8PathBuf>) -> Self {
26 Self::File { path: path.into() }
27 }
28
29 pub fn bytes(data: impl Into<Bytes>) -> Self {
30 Self::Bytes(data.into())
31 }
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
35pub struct ImageContent {
36 pub source: Source,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub media_type: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub alt: Option<String>,
41}
42
43impl ImageContent {
44 pub fn new(source: Source) -> Self {
45 Self {
46 source,
47 media_type: None,
48 alt: None,
49 }
50 }
51
52 pub fn with_media_type(mut self, media_type: impl Into<String>) -> Self {
53 self.media_type = Some(media_type.into());
54 self
55 }
56
57 pub fn with_alt(mut self, alt: impl Into<String>) -> Self {
58 self.alt = Some(alt.into());
59 self
60 }
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize)]
64pub struct AudioContent {
65 pub source: Source,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub media_type: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub duration_ms: Option<u64>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub transcript: Option<String>,
72}
73
74impl AudioContent {
75 pub fn new(source: Source) -> Self {
76 Self {
77 source,
78 media_type: None,
79 duration_ms: None,
80 transcript: None,
81 }
82 }
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize)]
86pub struct VideoContent {
87 pub source: Source,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub media_type: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub duration_ms: Option<u64>,
92}
93
94impl VideoContent {
95 pub fn new(source: Source) -> Self {
96 Self {
97 source,
98 media_type: None,
99 duration_ms: None,
100 }
101 }
102}
103
104#[derive(Clone, Debug, Serialize, Deserialize)]
105pub struct FileContent {
106 pub source: Source,
107 pub filename: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub media_type: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub size: Option<u64>,
112}
113
114impl FileContent {
115 pub fn new(source: Source, filename: impl Into<String>) -> Self {
116 Self {
117 source,
118 filename: filename.into(),
119 media_type: None,
120 size: None,
121 }
122 }
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct ToolUse {
127 pub id: String,
128 pub name: String,
129 pub input: serde_json::Value,
130}
131
132impl ToolUse {
133 pub fn new(id: impl Into<String>, name: impl Into<String>, input: serde_json::Value) -> Self {
134 Self {
135 id: id.into(),
136 name: name.into(),
137 input,
138 }
139 }
140}
141
142#[derive(Clone, Debug, Serialize, Deserialize)]
143pub struct ToolResult {
144 pub tool_use_id: String,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub content: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub error: Option<String>,
149 #[serde(default)]
150 pub is_error: bool,
151}
152
153impl ToolResult {
154 pub fn success(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
155 Self {
156 tool_use_id: tool_use_id.into(),
157 content: Some(content.into()),
158 error: None,
159 is_error: false,
160 }
161 }
162
163 pub fn error(tool_use_id: impl Into<String>, error: impl Into<String>) -> Self {
164 Self {
165 tool_use_id: tool_use_id.into(),
166 content: None,
167 error: Some(error.into()),
168 is_error: true,
169 }
170 }
171}
172
173#[derive(Clone, Debug, Serialize, Deserialize)]
174#[serde(tag = "type", rename_all = "snake_case")]
175pub enum ContentBlock {
176 Text { text: String },
177 Image(ImageContent),
178 Audio(AudioContent),
179 Video(VideoContent),
180 File(FileContent),
181 ToolUse(ToolUse),
182 ToolResult(ToolResult),
183 #[serde(rename = "x-custom")]
184 Custom {
185 #[serde(rename = "x-type")]
186 custom_type: String,
187 data: serde_json::Value,
188 },
189}
190
191impl ContentBlock {
192 pub fn text(s: impl Into<String>) -> Self {
193 Self::Text { text: s.into() }
194 }
195
196 pub fn image(content: ImageContent) -> Self {
197 Self::Image(content)
198 }
199
200 pub fn audio(content: AudioContent) -> Self {
201 Self::Audio(content)
202 }
203
204 pub fn video(content: VideoContent) -> Self {
205 Self::Video(content)
206 }
207
208 pub fn file(content: FileContent) -> Self {
209 Self::File(content)
210 }
211
212 pub fn tool_use(tool_use: ToolUse) -> Self {
213 Self::ToolUse(tool_use)
214 }
215
216 pub fn tool_result(result: ToolResult) -> Self {
217 Self::ToolResult(result)
218 }
219
220 pub fn custom(custom_type: impl Into<String>, data: serde_json::Value) -> Self {
221 Self::Custom {
222 custom_type: custom_type.into(),
223 data,
224 }
225 }
226
227 pub fn is_text(&self) -> bool {
228 matches!(self, Self::Text { .. })
229 }
230
231 pub fn is_image(&self) -> bool {
232 matches!(self, Self::Image(_))
233 }
234
235 pub fn is_tool_use(&self) -> bool {
236 matches!(self, Self::ToolUse(_))
237 }
238
239 pub fn is_tool_result(&self) -> bool {
240 matches!(self, Self::ToolResult(_))
241 }
242
243 pub fn as_text(&self) -> Option<&str> {
244 match self {
245 Self::Text { text } => Some(text),
246 _ => None,
247 }
248 }
249
250 pub fn as_tool_use(&self) -> Option<&ToolUse> {
251 match self {
252 Self::ToolUse(tu) => Some(tu),
253 _ => None,
254 }
255 }
256
257 pub fn as_tool_result(&self) -> Option<&ToolResult> {
258 match self {
259 Self::ToolResult(tr) => Some(tr),
260 _ => None,
261 }
262 }
263}