1use serde::{Deserialize, Serialize};
22
23#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32#[serde(rename_all = "snake_case")]
33pub enum Source {
34 Uri(String),
36 Data(String),
38}
39
40impl Source {
41 pub fn uri(&self) -> Option<&str> {
43 match self {
44 Source::Uri(u) => Some(u),
45 Source::Data(_) => None,
46 }
47 }
48
49 pub fn data(&self) -> Option<&str> {
51 match self {
52 Source::Data(d) => Some(d),
53 Source::Uri(_) => None,
54 }
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[serde(tag = "kind", rename_all = "snake_case")]
65pub enum Part {
66 Text { text: String },
69 Image { media_type: String, source: Source },
71 Audio { media_type: String, source: Source },
73 File {
76 #[serde(default, skip_serializing_if = "String::is_empty")]
77 name: String,
78 media_type: String,
79 source: Source,
80 },
81 Json { json: serde_json::Value },
84}
85
86impl Part {
87 pub fn text(text: impl Into<String>) -> Self {
89 Part::Text { text: text.into() }
90 }
91
92 pub fn image_uri(media_type: impl Into<String>, uri: impl Into<String>) -> Self {
94 Part::Image {
95 media_type: media_type.into(),
96 source: Source::Uri(uri.into()),
97 }
98 }
99
100 pub fn image_data(media_type: impl Into<String>, data: impl Into<String>) -> Self {
102 Part::Image {
103 media_type: media_type.into(),
104 source: Source::Data(data.into()),
105 }
106 }
107
108 pub fn audio_uri(media_type: impl Into<String>, uri: impl Into<String>) -> Self {
110 Part::Audio {
111 media_type: media_type.into(),
112 source: Source::Uri(uri.into()),
113 }
114 }
115
116 pub fn audio_data(media_type: impl Into<String>, data: impl Into<String>) -> Self {
118 Part::Audio {
119 media_type: media_type.into(),
120 source: Source::Data(data.into()),
121 }
122 }
123
124 pub fn file_uri(
126 name: impl Into<String>,
127 media_type: impl Into<String>,
128 uri: impl Into<String>,
129 ) -> Self {
130 Part::File {
131 name: name.into(),
132 media_type: media_type.into(),
133 source: Source::Uri(uri.into()),
134 }
135 }
136
137 pub fn file_data(
140 name: impl Into<String>,
141 media_type: impl Into<String>,
142 data: impl Into<String>,
143 ) -> Self {
144 Part::File {
145 name: name.into(),
146 media_type: media_type.into(),
147 source: Source::Data(data.into()),
148 }
149 }
150
151 pub fn json(value: impl Into<serde_json::Value>) -> Self {
153 Part::Json { json: value.into() }
154 }
155
156 pub fn kind(&self) -> &'static str {
160 match self {
161 Part::Text { .. } => "text",
162 Part::Image { .. } => "image",
163 Part::Audio { .. } => "audio",
164 Part::File { .. } => "file",
165 Part::Json { .. } => "json",
166 }
167 }
168
169 pub fn is_text(&self) -> bool {
171 matches!(self, Part::Text { .. })
172 }
173
174 pub fn as_text(&self) -> Option<&str> {
176 match self {
177 Part::Text { text } => Some(text),
178 _ => None,
179 }
180 }
181
182 pub fn media_type(&self) -> Option<&str> {
184 match self {
185 Part::Image { media_type, .. }
186 | Part::Audio { media_type, .. }
187 | Part::File { media_type, .. } => Some(media_type),
188 _ => None,
189 }
190 }
191
192 pub fn source(&self) -> Option<&Source> {
194 match self {
195 Part::Image { source, .. } | Part::Audio { source, .. } | Part::File { source, .. } => {
196 Some(source)
197 }
198 _ => None,
199 }
200 }
201
202 pub fn uri(&self) -> Option<&str> {
204 self.source().and_then(Source::uri)
205 }
206
207 pub fn data(&self) -> Option<&str> {
209 self.source().and_then(Source::data)
210 }
211}
212
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
216#[serde(rename_all = "snake_case")]
217pub enum Role {
218 User,
220 Assistant,
222}
223
224#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
231pub struct Message {
232 pub role: Role,
233 pub content: Vec<Part>,
234}
235
236impl Message {
237 pub fn new(role: Role, content: impl IntoIterator<Item = Part>) -> Self {
239 Self {
240 role,
241 content: content.into_iter().collect(),
242 }
243 }
244
245 pub fn user(text: impl Into<String>) -> Self {
247 Self::new(Role::User, [Part::text(text)])
248 }
249
250 pub fn assistant(text: impl Into<String>) -> Self {
252 Self::new(Role::Assistant, [Part::text(text)])
253 }
254
255 pub fn text(&self) -> String {
257 text_of(&self.content)
258 }
259}
260
261pub fn text_of(parts: &[Part]) -> String {
264 parts
265 .iter()
266 .filter_map(Part::as_text)
267 .collect::<Vec<_>>()
268 .join("\n")
269}
270
271pub fn modalities(parts: &[Part]) -> Vec<&'static str> {
274 let mut seen: Vec<&'static str> = Vec::new();
275 for p in parts {
276 let k = p.kind();
277 if !seen.contains(&k) {
278 seen.push(k);
279 }
280 }
281 seen
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn text_and_modalities() {
290 let parts = vec![
291 Part::text("describe this"),
292 Part::image_uri("image/png", "https://x/y.png"),
293 Part::text("and this"),
294 Part::image_data("image/jpeg", "QUJD"),
295 ];
296 assert_eq!(text_of(&parts), "describe this\nand this");
297 assert_eq!(modalities(&parts), vec!["text", "image"]);
299 }
300
301 #[test]
302 fn accessors() {
303 let img = Part::image_uri("image/png", "u");
304 assert_eq!(img.kind(), "image");
305 assert!(!img.is_text());
306 assert_eq!(img.as_text(), None);
307 assert_eq!(img.media_type(), Some("image/png"));
308 assert_eq!(img.uri(), Some("u"));
309 assert_eq!(img.data(), None);
310
311 let blob = Part::image_data("image/png", "QUJD");
312 assert_eq!(blob.uri(), None);
313 assert_eq!(blob.data(), Some("QUJD"));
314
315 let txt = Part::text("hi");
316 assert_eq!(txt.kind(), "text");
317 assert!(txt.is_text());
318 assert_eq!(txt.as_text(), Some("hi"));
319 assert_eq!(txt.media_type(), None);
320 assert_eq!(txt.source(), None);
321 }
322
323 #[test]
324 fn serializes_with_kind_tag_and_round_trips() {
325 let parts = vec![
326 Part::text("hello"),
327 Part::image_data("image/png", "QkFTRTY0"),
328 Part::file_uri("notes.pdf", "application/pdf", "https://x/n.pdf"),
329 Part::file_data("blob.bin", "application/octet-stream", "QUJD"),
330 Part::json(serde_json::json!({"label": "cat", "p": 0.9})),
331 ];
332 let line = serde_json::to_string(&parts).unwrap();
333 assert!(line.contains(r#""kind":"text""#));
335 assert!(line.contains(r#""kind":"image""#));
336 assert!(line.contains(r#""source":{"data":"QkFTRTY0"}"#));
337 let back: Vec<Part> = serde_json::from_str(&line).unwrap();
338 assert_eq!(back, parts);
339 }
340
341 #[test]
342 fn media_part_requires_a_source() {
343 let err = serde_json::from_str::<Part>(r#"{"kind":"image","media_type":"image/png"}"#);
346 assert!(err.is_err());
347 let ok = serde_json::from_str::<Part>(
349 r#"{"kind":"image","media_type":"image/png","source":{"uri":"u"}}"#,
350 );
351 assert_eq!(ok.unwrap(), Part::image_uri("image/png", "u"));
352 }
353}