1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use starweaver_core::Metadata;
6use starweaver_model::{CachePointTtl, ContentPart};
7use thiserror::Error;
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
11pub struct FileRef {
12 pub uri: String,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub media_type: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub name: Option<String>,
20}
21
22impl FileRef {
23 #[must_use]
25 pub fn new(uri: impl Into<String>) -> Self {
26 Self {
27 uri: uri.into(),
28 media_type: None,
29 name: None,
30 }
31 }
32}
33
34#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36pub struct BinaryRef {
37 pub uri: String,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub media_type: Option<String>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub bytes: Option<u64>,
45}
46
47impl BinaryRef {
48 #[must_use]
50 pub fn new(uri: impl Into<String>) -> Self {
51 Self {
52 uri: uri.into(),
53 media_type: None,
54 bytes: None,
55 }
56 }
57}
58
59#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
65#[serde(tag = "kind", rename_all = "snake_case")]
66pub enum InputPart {
67 CachePoint {
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 ttl: Option<CachePointTtl>,
72 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
74 metadata: Metadata,
75 },
76 Text {
78 text: String,
80 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
82 metadata: Metadata,
83 },
84 ImageUrl {
86 url: String,
88 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
90 metadata: Metadata,
91 },
92 FileUrl {
94 url: String,
96 media_type: String,
98 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
100 metadata: Metadata,
101 },
102 InlineBinary {
104 data: Vec<u8>,
106 media_type: String,
108 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
110 metadata: Metadata,
111 },
112 ResourceRef {
114 uri: String,
116 media_type: String,
118 resource_type: String,
120 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
122 resource_metadata: Metadata,
123 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
125 metadata: Metadata,
126 },
127 DataUrl {
129 data_url: String,
131 media_type: String,
133 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
135 metadata: Metadata,
136 },
137 Url {
139 url: String,
141 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
143 metadata: Metadata,
144 },
145 File {
147 file: FileRef,
149 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
151 metadata: Metadata,
152 },
153 Binary {
155 binary: BinaryRef,
157 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
159 metadata: Metadata,
160 },
161 Mode {
163 mode: String,
165 #[serde(default, skip_serializing_if = "Value::is_null")]
167 config: Value,
168 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
170 metadata: Metadata,
171 },
172 Command {
174 command: String,
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 args: Vec<String>,
179 #[serde(default, skip_serializing_if = "Value::is_null")]
181 payload: Value,
182 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
184 metadata: Metadata,
185 },
186}
187
188impl InputPart {
189 #[must_use]
191 pub fn text(text: impl Into<String>) -> Self {
192 Self::Text {
193 text: text.into(),
194 metadata: Metadata::default(),
195 }
196 }
197
198 #[must_use]
200 pub fn image_url(url: impl Into<String>) -> Self {
201 Self::ImageUrl {
202 url: url.into(),
203 metadata: Metadata::default(),
204 }
205 }
206
207 #[must_use]
209 pub fn url(url: impl Into<String>) -> Self {
210 Self::Url {
211 url: url.into(),
212 metadata: Metadata::default(),
213 }
214 }
215
216 #[must_use]
218 pub fn command(command: impl Into<String>, args: Vec<String>) -> Self {
219 Self::Command {
220 command: command.into(),
221 args,
222 payload: Value::Null,
223 metadata: Metadata::default(),
224 }
225 }
226
227 #[must_use]
229 pub const fn is_legacy_product_input(&self) -> bool {
230 matches!(self, Self::Mode { .. } | Self::Command { .. })
231 }
232}
233
234impl From<ContentPart> for InputPart {
235 fn from(part: ContentPart) -> Self {
236 let metadata = Metadata::default();
237 match part {
238 ContentPart::CachePoint { ttl } => Self::CachePoint { ttl, metadata },
239 ContentPart::Text { text } => Self::Text { text, metadata },
240 ContentPart::ImageUrl { url } => Self::ImageUrl { url, metadata },
241 ContentPart::FileUrl { url, media_type } => Self::FileUrl {
242 url,
243 media_type,
244 metadata,
245 },
246 ContentPart::Binary { data, media_type } => Self::InlineBinary {
247 data,
248 media_type,
249 metadata,
250 },
251 ContentPart::ResourceRef {
252 uri,
253 media_type,
254 resource_type,
255 metadata: resource_metadata,
256 } => Self::ResourceRef {
257 uri,
258 media_type,
259 resource_type,
260 resource_metadata,
261 metadata,
262 },
263 ContentPart::DataUrl {
264 data_url,
265 media_type,
266 } => Self::DataUrl {
267 data_url,
268 media_type,
269 metadata,
270 },
271 }
272 }
273}
274
275impl TryFrom<InputPart> for ContentPart {
276 type Error = InputConversionError;
277
278 fn try_from(part: InputPart) -> Result<Self, Self::Error> {
279 match part {
280 InputPart::CachePoint { ttl, .. } => Ok(Self::CachePoint { ttl }),
281 InputPart::Text { text, .. } => Ok(Self::Text { text }),
282 InputPart::ImageUrl { url, .. } | InputPart::Url { url, .. } => {
283 Ok(Self::ImageUrl { url })
284 }
285 InputPart::FileUrl {
286 url, media_type, ..
287 } => Ok(Self::FileUrl { url, media_type }),
288 InputPart::InlineBinary {
289 data, media_type, ..
290 } => Ok(Self::Binary { data, media_type }),
291 InputPart::ResourceRef {
292 uri,
293 media_type,
294 resource_type,
295 resource_metadata,
296 ..
297 } => Ok(Self::ResourceRef {
298 uri,
299 media_type,
300 resource_type,
301 metadata: resource_metadata,
302 }),
303 InputPart::DataUrl {
304 data_url,
305 media_type,
306 ..
307 } => Ok(Self::DataUrl {
308 data_url,
309 media_type,
310 }),
311 InputPart::File { file, .. } => {
312 let mut metadata = Metadata::default();
313 if let Some(name) = file.name {
314 metadata.insert("name".to_string(), Value::String(name));
315 }
316 Ok(Self::ResourceRef {
317 uri: file.uri,
318 media_type: file
319 .media_type
320 .unwrap_or_else(|| "application/octet-stream".to_string()),
321 resource_type: "file".to_string(),
322 metadata,
323 })
324 }
325 InputPart::Binary { binary, .. } => {
326 let mut metadata = Metadata::default();
327 if let Some(bytes) = binary.bytes {
328 metadata.insert("bytes".to_string(), Value::from(bytes));
329 }
330 Ok(Self::ResourceRef {
331 uri: binary.uri,
332 media_type: binary
333 .media_type
334 .unwrap_or_else(|| "application/octet-stream".to_string()),
335 resource_type: "binary".to_string(),
336 metadata,
337 })
338 }
339 InputPart::Mode { mode, .. } => Err(InputConversionError::ProductMode(mode)),
340 InputPart::Command { command, .. } => {
341 Err(InputConversionError::ProductCommand(command))
342 }
343 }
344 }
345}
346
347#[derive(Debug, Error)]
349pub enum InputConversionError {
350 #[error("legacy content_part input is invalid: {0}")]
352 LegacyContent(serde_json::Error),
353 #[error("product mode {0} is not model content")]
355 ProductMode(String),
356 #[error("product command {0} is not model content")]
358 ProductCommand(String),
359}