1use std::path::PathBuf;
4
5use bytes::Bytes;
6use ferrin_spec::ApprovalId;
7use ferrin_spec::MediaType;
8use ferrin_spec::ProviderOptions;
9use ferrin_spec::ProviderReference;
10use ferrin_spec::ToolCallId;
11use ferrin_spec::language_model::prompt::CustomPart;
12use ferrin_spec::language_model::prompt::ReasoningPart;
13use ferrin_spec::language_model::prompt::TextPart;
14use ferrin_spec::language_model::prompt::ToolCallPart;
15use ferrin_spec::language_model::prompt::ToolResultPart;
16use serde::Deserialize;
17use serde::Serialize;
18use url::Url;
19
20use crate::file_source::FileSource;
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(tag = "type", rename_all = "kebab-case")]
25#[non_exhaustive]
26pub enum UserPart {
27 Text(TextPart),
29 Image(ImagePart),
32 File(FilePart),
34}
35
36impl UserPart {
37 #[must_use]
39 pub fn text(text: impl Into<String>) -> Self {
40 Self::Text(TextPart::new(text))
41 }
42
43 #[must_use]
45 pub fn image(source: impl Into<FileSource>) -> Self {
46 Self::Image(ImagePart::new(source))
47 }
48
49 #[must_use]
51 pub fn image_bytes(data: impl Into<Bytes>) -> Self {
52 Self::image(FileSource::bytes(data))
53 }
54
55 #[must_use]
57 pub fn image_base64(data: impl Into<String>) -> Self {
58 Self::image(FileSource::base64(data))
59 }
60
61 #[must_use]
63 pub fn image_url(url: Url) -> Self {
64 Self::image(FileSource::url(url))
65 }
66
67 #[must_use]
69 pub fn file(source: impl Into<FileSource>, media_type: impl Into<MediaType>) -> Self {
70 Self::File(FilePart::new(source, media_type))
71 }
72
73 #[must_use]
75 pub fn file_bytes(data: impl Into<Bytes>, media_type: impl Into<MediaType>) -> Self {
76 Self::file(FileSource::bytes(data), media_type)
77 }
78
79 #[must_use]
81 pub fn file_url(url: Url, media_type: impl Into<MediaType>) -> Self {
82 Self::file(FileSource::url(url), media_type)
83 }
84
85 #[must_use]
87 pub fn file_reference(reference: ProviderReference, media_type: impl Into<MediaType>) -> Self {
88 Self::file(FileSource::Reference { reference }, media_type)
89 }
90
91 #[must_use]
93 pub fn file_text(text: impl Into<String>, media_type: impl Into<MediaType>) -> Self {
94 Self::file(FileSource::text(text), media_type)
95 }
96
97 #[must_use]
99 pub fn file_path(path: impl Into<PathBuf>, media_type: impl Into<MediaType>) -> Self {
100 Self::file(FileSource::path(path), media_type)
101 }
102
103 #[must_use]
105 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
106 if let Self::File(file) = &mut self {
107 file.filename = Some(filename.into());
108 }
109 self
110 }
111
112 #[must_use]
114 pub fn with_provider_options(mut self, options: ProviderOptions) -> Self {
115 match &mut self {
116 Self::Text(part) => part.provider_options = Some(options),
117 Self::Image(part) => part.provider_options = Some(options),
118 Self::File(part) => part.provider_options = Some(options),
119 }
120 self
121 }
122
123 #[must_use]
125 pub fn as_text(&self) -> Option<&str> {
126 match self {
127 Self::Text(part) => Some(&part.text),
128 Self::Image(_) | Self::File(_) => None,
129 }
130 }
131}
132
133impl From<TextPart> for UserPart {
134 fn from(part: TextPart) -> Self {
135 Self::Text(part)
136 }
137}
138
139impl From<ImagePart> for UserPart {
140 fn from(part: ImagePart) -> Self {
141 Self::Image(part)
142 }
143}
144
145impl From<FilePart> for UserPart {
146 fn from(part: FilePart) -> Self {
147 Self::File(part)
148 }
149}
150
151impl From<&str> for UserPart {
152 fn from(text: &str) -> Self {
153 Self::text(text)
154 }
155}
156
157impl From<String> for UserPart {
158 fn from(text: String) -> Self {
159 Self::text(text)
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct ImagePart {
166 pub image: FileSource,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub media_type: Option<MediaType>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub provider_options: Option<ProviderOptions>,
174}
175
176impl ImagePart {
177 #[must_use]
179 pub fn new(image: impl Into<FileSource>) -> Self {
180 Self {
181 image: image.into(),
182 media_type: None,
183 provider_options: None,
184 }
185 }
186
187 #[must_use]
189 pub fn with_media_type(mut self, media_type: impl Into<MediaType>) -> Self {
190 self.media_type = Some(media_type.into());
191 self
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct FilePart {
198 pub data: FileSource,
200 pub media_type: MediaType,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub filename: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub provider_options: Option<ProviderOptions>,
208}
209
210impl FilePart {
211 #[must_use]
213 pub fn new(data: impl Into<FileSource>, media_type: impl Into<MediaType>) -> Self {
214 Self {
215 data: data.into(),
216 media_type: media_type.into(),
217 filename: None,
218 provider_options: None,
219 }
220 }
221
222 #[must_use]
224 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
225 self.filename = Some(filename.into());
226 self
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct ReasoningFilePart {
234 pub data: FileSource,
236 pub media_type: MediaType,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub provider_options: Option<ProviderOptions>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct ToolApprovalRequest {
247 pub approval_id: ApprovalId,
249 pub tool_call_id: ToolCallId,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub reason: Option<String>,
254 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
257 pub is_automatic: bool,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub signature: Option<String>,
261}
262
263impl ToolApprovalRequest {
264 #[must_use]
266 pub fn new(approval_id: impl Into<ApprovalId>, tool_call_id: impl Into<ToolCallId>) -> Self {
267 Self {
268 approval_id: approval_id.into(),
269 tool_call_id: tool_call_id.into(),
270 reason: None,
271 is_automatic: false,
272 signature: None,
273 }
274 }
275
276 #[must_use]
278 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
279 self.reason = Some(reason.into());
280 self
281 }
282
283 #[must_use]
285 pub fn automatic(mut self) -> Self {
286 self.is_automatic = true;
287 self
288 }
289
290 #[must_use]
292 pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
293 self.signature = Some(signature.into());
294 self
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301pub struct ToolApprovalResponse {
302 pub approval_id: ApprovalId,
304 pub approved: bool,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub reason: Option<String>,
309 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
311 pub provider_executed: bool,
312}
313
314impl ToolApprovalResponse {
315 #[must_use]
317 pub fn approved(approval_id: impl Into<ApprovalId>) -> Self {
318 Self {
319 approval_id: approval_id.into(),
320 approved: true,
321 reason: None,
322 provider_executed: false,
323 }
324 }
325
326 #[must_use]
328 pub fn denied(approval_id: impl Into<ApprovalId>) -> Self {
329 Self {
330 approval_id: approval_id.into(),
331 approved: false,
332 reason: None,
333 provider_executed: false,
334 }
335 }
336
337 #[must_use]
339 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
340 self.reason = Some(reason.into());
341 self
342 }
343
344 #[must_use]
346 pub fn provider_executed(mut self) -> Self {
347 self.provider_executed = true;
348 self
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
354#[serde(tag = "type", rename_all = "kebab-case")]
355#[non_exhaustive]
356pub enum AssistantPart {
357 Text(TextPart),
359 Custom(CustomPart),
361 File(FilePart),
363 Reasoning(ReasoningPart),
365 ReasoningFile(ReasoningFilePart),
367 ToolCall(ToolCallPart),
369 ToolResult(ToolResultPart),
371 ToolApprovalRequest(ToolApprovalRequest),
373}
374
375impl AssistantPart {
376 #[must_use]
378 pub fn text(text: impl Into<String>) -> Self {
379 Self::Text(TextPart::new(text))
380 }
381
382 #[must_use]
384 pub fn reasoning(text: impl Into<String>) -> Self {
385 Self::Reasoning(ReasoningPart::new(text))
386 }
387
388 #[must_use]
390 pub fn as_text(&self) -> Option<&str> {
391 match self {
392 Self::Text(part) => Some(&part.text),
393 _ => None,
394 }
395 }
396
397 #[must_use]
399 pub fn as_tool_call(&self) -> Option<&ToolCallPart> {
400 match self {
401 Self::ToolCall(part) => Some(part),
402 _ => None,
403 }
404 }
405
406 #[must_use]
408 pub fn as_tool_approval_request(&self) -> Option<&ToolApprovalRequest> {
409 match self {
410 Self::ToolApprovalRequest(part) => Some(part),
411 _ => None,
412 }
413 }
414}
415
416impl From<TextPart> for AssistantPart {
417 fn from(part: TextPart) -> Self {
418 Self::Text(part)
419 }
420}
421
422impl From<CustomPart> for AssistantPart {
423 fn from(part: CustomPart) -> Self {
424 Self::Custom(part)
425 }
426}
427
428impl From<FilePart> for AssistantPart {
429 fn from(part: FilePart) -> Self {
430 Self::File(part)
431 }
432}
433
434impl From<ReasoningPart> for AssistantPart {
435 fn from(part: ReasoningPart) -> Self {
436 Self::Reasoning(part)
437 }
438}
439
440impl From<ReasoningFilePart> for AssistantPart {
441 fn from(part: ReasoningFilePart) -> Self {
442 Self::ReasoningFile(part)
443 }
444}
445
446impl From<ToolCallPart> for AssistantPart {
447 fn from(part: ToolCallPart) -> Self {
448 Self::ToolCall(part)
449 }
450}
451
452impl From<ToolResultPart> for AssistantPart {
453 fn from(part: ToolResultPart) -> Self {
454 Self::ToolResult(part)
455 }
456}
457
458impl From<ToolApprovalRequest> for AssistantPart {
459 fn from(part: ToolApprovalRequest) -> Self {
460 Self::ToolApprovalRequest(part)
461 }
462}
463
464impl From<&str> for AssistantPart {
465 fn from(text: &str) -> Self {
466 Self::text(text)
467 }
468}
469
470impl From<String> for AssistantPart {
471 fn from(text: String) -> Self {
472 Self::text(text)
473 }
474}
475
476#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
478#[serde(tag = "type", rename_all = "kebab-case")]
479#[non_exhaustive]
480pub enum ToolPart {
481 ToolResult(ToolResultPart),
483 ToolApprovalResponse(ToolApprovalResponse),
485}
486
487impl ToolPart {
488 #[must_use]
490 pub fn as_tool_result(&self) -> Option<&ToolResultPart> {
491 match self {
492 Self::ToolResult(part) => Some(part),
493 Self::ToolApprovalResponse(_) => None,
494 }
495 }
496
497 #[must_use]
499 pub fn as_tool_approval_response(&self) -> Option<&ToolApprovalResponse> {
500 match self {
501 Self::ToolApprovalResponse(part) => Some(part),
502 Self::ToolResult(_) => None,
503 }
504 }
505}
506
507impl From<ToolResultPart> for ToolPart {
508 fn from(part: ToolResultPart) -> Self {
509 Self::ToolResult(part)
510 }
511}
512
513impl From<ToolApprovalResponse> for ToolPart {
514 fn from(part: ToolApprovalResponse) -> Self {
515 Self::ToolApprovalResponse(part)
516 }
517}