1use serde::{Deserialize, Serialize};
5use serde_json::json;
6
7use super::{AnnotateAble, Annotated, resource::ResourceContents};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
12#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
13pub struct RawTextContent {
14 pub text: String,
15 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
17 pub meta: Option<super::Meta>,
18}
19pub type TextContent = Annotated<RawTextContent>;
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
24pub struct RawImageContent {
25 pub data: String,
27 pub mime_type: String,
28 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
30 pub meta: Option<super::Meta>,
31}
32
33pub type ImageContent = Annotated<RawImageContent>;
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
37#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
38pub struct RawEmbeddedResource {
39 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
41 pub meta: Option<super::Meta>,
42 pub resource: ResourceContents,
43}
44
45impl RawEmbeddedResource {
46 pub fn new(resource: ResourceContents) -> Self {
48 Self {
49 meta: None,
50 resource,
51 }
52 }
53}
54
55pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
56
57impl EmbeddedResource {
58 pub fn get_text(&self) -> String {
59 match &self.resource {
60 ResourceContents::TextResourceContents { text, .. } => text.clone(),
61 _ => String::new(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
69#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
70pub struct RawAudioContent {
71 pub data: String,
72 pub mime_type: String,
73}
74
75pub type AudioContent = Annotated<RawAudioContent>;
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
81#[non_exhaustive]
82pub struct ToolUseContent {
83 pub id: String,
85 pub name: String,
87 pub input: super::JsonObject,
89 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
91 pub meta: Option<super::Meta>,
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
98#[non_exhaustive]
99pub struct ToolResultContent {
100 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
102 pub meta: Option<super::Meta>,
103 pub tool_use_id: String,
105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
107 pub content: Vec<Content>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub structured_content: Option<super::JsonObject>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub is_error: Option<bool>,
114}
115
116impl ToolUseContent {
117 pub fn new(id: impl Into<String>, name: impl Into<String>, input: super::JsonObject) -> Self {
118 Self {
119 id: id.into(),
120 name: name.into(),
121 input,
122 meta: None,
123 }
124 }
125}
126
127impl ToolResultContent {
128 pub fn new(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
129 Self {
130 meta: None,
131 tool_use_id: tool_use_id.into(),
132 content,
133 structured_content: None,
134 is_error: None,
135 }
136 }
137
138 pub fn error(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
139 Self {
140 meta: None,
141 tool_use_id: tool_use_id.into(),
142 content,
143 structured_content: None,
144 is_error: Some(true),
145 }
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150#[serde(tag = "type", rename_all = "snake_case")]
151#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
152#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
153pub enum RawContent {
154 Text(RawTextContent),
155 Image(RawImageContent),
156 Resource(RawEmbeddedResource),
157 Audio(RawAudioContent),
158 ResourceLink(super::resource::RawResource),
159}
160
161pub type Content = Annotated<RawContent>;
162
163impl RawContent {
164 pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {
165 let json = serde_json::to_string(&json).map_err(|e| {
166 crate::ErrorData::internal_error(
167 "fail to serialize response to json",
168 Some(json!(
169 {"reason": e.to_string()}
170 )),
171 )
172 })?;
173 Ok(RawContent::text(json))
174 }
175
176 pub fn text<S: Into<String>>(text: S) -> Self {
177 RawContent::Text(RawTextContent {
178 text: text.into(),
179 meta: None,
180 })
181 }
182
183 pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
184 RawContent::Image(RawImageContent {
185 data: data.into(),
186 mime_type: mime_type.into(),
187 meta: None,
188 })
189 }
190
191 pub fn resource(resource: ResourceContents) -> Self {
192 RawContent::Resource(RawEmbeddedResource {
193 meta: None,
194 resource,
195 })
196 }
197
198 pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
199 RawContent::Resource(RawEmbeddedResource {
200 meta: None,
201 resource: ResourceContents::TextResourceContents {
202 uri: uri.into(),
203 mime_type: Some("text".to_string()),
204 text: content.into(),
205 meta: None,
206 },
207 })
208 }
209
210 pub fn as_text(&self) -> Option<&RawTextContent> {
212 match self {
213 RawContent::Text(text) => Some(text),
214 _ => None,
215 }
216 }
217
218 pub fn as_image(&self) -> Option<&RawImageContent> {
220 match self {
221 RawContent::Image(image) => Some(image),
222 _ => None,
223 }
224 }
225
226 pub fn as_resource(&self) -> Option<&RawEmbeddedResource> {
228 match self {
229 RawContent::Resource(resource) => Some(resource),
230 _ => None,
231 }
232 }
233
234 pub fn as_resource_link(&self) -> Option<&super::resource::RawResource> {
236 match self {
237 RawContent::ResourceLink(link) => Some(link),
238 _ => None,
239 }
240 }
241
242 pub fn resource_link(resource: super::resource::RawResource) -> Self {
244 RawContent::ResourceLink(resource)
245 }
246}
247
248impl Content {
249 pub fn text<S: Into<String>>(text: S) -> Self {
250 RawContent::text(text).no_annotation()
251 }
252
253 pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
254 RawContent::image(data, mime_type).no_annotation()
255 }
256
257 pub fn resource(resource: ResourceContents) -> Self {
258 RawContent::resource(resource).no_annotation()
259 }
260
261 pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
262 RawContent::embedded_text(uri, content).no_annotation()
263 }
264
265 pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {
266 RawContent::json(json).map(|c| c.no_annotation())
267 }
268
269 pub fn resource_link(resource: super::resource::RawResource) -> Self {
271 RawContent::resource_link(resource).no_annotation()
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276pub struct JsonContent<S: Serialize>(S);
277pub trait IntoContents {
279 fn into_contents(self) -> Vec<Content>;
280}
281
282impl IntoContents for Content {
283 fn into_contents(self) -> Vec<Content> {
284 vec![self]
285 }
286}
287
288impl IntoContents for String {
289 fn into_contents(self) -> Vec<Content> {
290 vec![Content::text(self)]
291 }
292}
293
294impl IntoContents for () {
295 fn into_contents(self) -> Vec<Content> {
296 vec![]
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use serde_json;
303
304 use super::*;
305
306 #[test]
307 fn test_image_content_serialization() {
308 let image_content = RawImageContent {
309 data: "base64data".to_string(),
310 mime_type: "image/png".to_string(),
311 meta: None,
312 };
313
314 let json = serde_json::to_string(&image_content).unwrap();
315 println!("ImageContent JSON: {}", json);
316
317 assert!(json.contains("mimeType"));
319 assert!(!json.contains("mime_type"));
320 }
321
322 #[test]
323 fn test_audio_content_serialization() {
324 let audio_content = RawAudioContent {
325 data: "base64audiodata".to_string(),
326 mime_type: "audio/wav".to_string(),
327 };
328
329 let json = serde_json::to_string(&audio_content).unwrap();
330 println!("AudioContent JSON: {}", json);
331
332 assert!(json.contains("mimeType"));
334 assert!(!json.contains("mime_type"));
335 }
336
337 #[test]
338 fn test_resource_link_serialization() {
339 use super::super::resource::RawResource;
340
341 let resource_link = RawContent::ResourceLink(RawResource {
342 uri: "file:///test.txt".to_string(),
343 name: "test.txt".to_string(),
344 title: None,
345 description: Some("A test file".to_string()),
346 mime_type: Some("text/plain".to_string()),
347 size: Some(100),
348 icons: None,
349 meta: None,
350 });
351
352 let json = serde_json::to_string(&resource_link).unwrap();
353 println!("ResourceLink JSON: {}", json);
354
355 assert!(json.contains("\"type\":\"resource_link\""));
357 assert!(json.contains("\"uri\":\"file:///test.txt\""));
358 assert!(json.contains("\"name\":\"test.txt\""));
359 }
360
361 #[test]
362 fn test_resource_link_deserialization() {
363 let json = r#"{
364 "type": "resource_link",
365 "uri": "file:///example.txt",
366 "name": "example.txt",
367 "description": "Example file",
368 "mimeType": "text/plain"
369 }"#;
370
371 let content: RawContent = serde_json::from_str(json).unwrap();
372
373 if let RawContent::ResourceLink(resource) = content {
374 assert_eq!(resource.uri, "file:///example.txt");
375 assert_eq!(resource.name, "example.txt");
376 assert_eq!(resource.description, Some("Example file".to_string()));
377 assert_eq!(resource.mime_type, Some("text/plain".to_string()));
378 } else {
379 panic!("Expected ResourceLink variant");
380 }
381 }
382}