Skip to main content

ferrin_message/
part.rs

1//! Content parts of application messages.
2
3use 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/// A part of a user message.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(tag = "type", rename_all = "kebab-case")]
25#[non_exhaustive]
26pub enum UserPart {
27    /// Text.
28    Text(TextPart),
29    /// An image; normalized to a file part with a detected media type during
30    /// conversion.
31    Image(ImagePart),
32    /// A file with an explicit media type.
33    File(FilePart),
34}
35
36impl UserPart {
37    /// A text part.
38    #[must_use]
39    pub fn text(text: impl Into<String>) -> Self {
40        Self::Text(TextPart::new(text))
41    }
42
43    /// An image from any source.
44    #[must_use]
45    pub fn image(source: impl Into<FileSource>) -> Self {
46        Self::Image(ImagePart::new(source))
47    }
48
49    /// An image from inline bytes.
50    #[must_use]
51    pub fn image_bytes(data: impl Into<Bytes>) -> Self {
52        Self::image(FileSource::bytes(data))
53    }
54
55    /// An image from base64 text.
56    #[must_use]
57    pub fn image_base64(data: impl Into<String>) -> Self {
58        Self::image(FileSource::base64(data))
59    }
60
61    /// An image from a URL.
62    #[must_use]
63    pub fn image_url(url: Url) -> Self {
64        Self::image(FileSource::url(url))
65    }
66
67    /// A file from any source.
68    #[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    /// A file from inline bytes.
74    #[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    /// A file from a URL.
80    #[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    /// A file previously uploaded to providers.
86    #[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    /// An inline text document.
92    #[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    /// A file read from a local path during conversion.
98    #[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    /// Sets the filename on a file part (no effect on other parts).
104    #[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    /// Sets provider options.
113    #[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    /// Returns the text of a text part.
124    #[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/// An image in a user message.
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct ImagePart {
166    /// Image source.
167    pub image: FileSource,
168    /// Media type, detected from the bytes when omitted.
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub media_type: Option<MediaType>,
171    /// Provider-specific options.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub provider_options: Option<ProviderOptions>,
174}
175
176impl ImagePart {
177    /// Creates an image part.
178    #[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    /// Sets the media type.
188    #[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/// A file in a user or assistant message.
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct FilePart {
198    /// File source.
199    pub data: FileSource,
200    /// Media type.
201    pub media_type: MediaType,
202    /// Optional filename.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub filename: Option<String>,
205    /// Provider-specific options.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub provider_options: Option<ProviderOptions>,
208}
209
210impl FilePart {
211    /// Creates a file part.
212    #[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    /// Sets the filename.
223    #[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/// A reasoning artefact (for example an image the model reasoned over) in an
231/// assistant message.
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct ReasoningFilePart {
234    /// File source (inline bytes or URL).
235    pub data: FileSource,
236    /// Media type.
237    pub media_type: MediaType,
238    /// Provider-specific options.
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub provider_options: Option<ProviderOptions>,
241}
242
243/// A request for user approval of a tool call, recorded in an assistant
244/// message.
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct ToolApprovalRequest {
247    /// Approval identifier.
248    pub approval_id: ApprovalId,
249    /// The tool call awaiting approval.
250    pub tool_call_id: ToolCallId,
251    /// Why approval is needed.
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub reason: Option<String>,
254    /// Whether the request was created automatically (for example by a
255    /// provider-executed tool) rather than by the tool's approval policy.
256    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
257    pub is_automatic: bool,
258    /// Tamper-detection signature over the request.
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub signature: Option<String>,
261}
262
263impl ToolApprovalRequest {
264    /// Creates a request.
265    #[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    /// Sets the reason.
277    #[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    /// Marks the request as automatic.
284    #[must_use]
285    pub fn automatic(mut self) -> Self {
286        self.is_automatic = true;
287        self
288    }
289
290    /// Sets the signature.
291    #[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/// The user's answer to a [`ToolApprovalRequest`], recorded in a tool
299/// message.
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301pub struct ToolApprovalResponse {
302    /// The approval being answered.
303    pub approval_id: ApprovalId,
304    /// Whether execution was approved.
305    pub approved: bool,
306    /// Optional explanation.
307    #[serde(default, skip_serializing_if = "Option::is_none")]
308    pub reason: Option<String>,
309    /// Whether the approved tool is executed by the provider.
310    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
311    pub provider_executed: bool,
312}
313
314impl ToolApprovalResponse {
315    /// An approval.
316    #[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    /// A denial.
327    #[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    /// Sets the reason.
338    #[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    /// Marks the tool as provider-executed.
345    #[must_use]
346    pub fn provider_executed(mut self) -> Self {
347        self.provider_executed = true;
348        self
349    }
350}
351
352/// A part of an assistant message.
353#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
354#[serde(tag = "type", rename_all = "kebab-case")]
355#[non_exhaustive]
356pub enum AssistantPart {
357    /// Text.
358    Text(TextPart),
359    /// Provider-specific content identified by kind.
360    Custom(CustomPart),
361    /// A generated file.
362    File(FilePart),
363    /// Reasoning text.
364    Reasoning(ReasoningPart),
365    /// A reasoning artefact.
366    ReasoningFile(ReasoningFilePart),
367    /// A tool call.
368    ToolCall(ToolCallPart),
369    /// A provider-executed tool result.
370    ToolResult(ToolResultPart),
371    /// A pending approval request.
372    ToolApprovalRequest(ToolApprovalRequest),
373}
374
375impl AssistantPart {
376    /// A text part.
377    #[must_use]
378    pub fn text(text: impl Into<String>) -> Self {
379        Self::Text(TextPart::new(text))
380    }
381
382    /// A reasoning part.
383    #[must_use]
384    pub fn reasoning(text: impl Into<String>) -> Self {
385        Self::Reasoning(ReasoningPart::new(text))
386    }
387
388    /// Returns the text of a text part.
389    #[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    /// Returns the tool call when this is one.
398    #[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    /// Returns the approval request when this is one.
407    #[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/// A part of a tool message.
477#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
478#[serde(tag = "type", rename_all = "kebab-case")]
479#[non_exhaustive]
480pub enum ToolPart {
481    /// A client-side tool result.
482    ToolResult(ToolResultPart),
483    /// An approval response; stripped before the prompt reaches the model.
484    ToolApprovalResponse(ToolApprovalResponse),
485}
486
487impl ToolPart {
488    /// Returns the tool result when this is one.
489    #[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    /// Returns the approval response when this is one.
498    #[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}