Skip to main content

ferrin_message/
message.rs

1//! Application messages.
2
3use ferrin_spec::ProviderOptions;
4use serde::Deserialize;
5use serde::Serialize;
6
7use crate::part::AssistantPart;
8use crate::part::ToolApprovalResponse;
9use crate::part::ToolPart;
10use crate::part::UserPart;
11
12/// A message in an application conversation, tagged by `role`.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(tag = "role", rename_all = "lowercase")]
15#[non_exhaustive]
16pub enum Message {
17    /// Instructions for the model.
18    System(SystemMessage),
19    /// Input from the user.
20    User(UserMessage),
21    /// Output from the model.
22    Assistant(AssistantMessage),
23    /// Tool results and approval responses.
24    Tool(ToolMessage),
25}
26
27/// The role of a [`Message`].
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30#[non_exhaustive]
31pub enum Role {
32    /// System.
33    System,
34    /// User.
35    User,
36    /// Assistant.
37    Assistant,
38    /// Tool.
39    Tool,
40}
41
42impl Role {
43    /// The wire name of the role.
44    #[must_use]
45    pub fn as_str(self) -> &'static str {
46        match self {
47            Self::System => "system",
48            Self::User => "user",
49            Self::Assistant => "assistant",
50            Self::Tool => "tool",
51        }
52    }
53}
54
55impl std::fmt::Display for Role {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.write_str(self.as_str())
58    }
59}
60
61/// A system message.
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct SystemMessage {
64    /// Instruction text.
65    pub content: String,
66    /// Provider-specific options.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub provider_options: Option<ProviderOptions>,
69}
70
71/// A user message.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct UserMessage {
74    /// Text or parts.
75    pub content: UserContent,
76    /// Provider-specific options.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub provider_options: Option<ProviderOptions>,
79}
80
81/// An assistant message.
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct AssistantMessage {
84    /// Text or parts.
85    pub content: AssistantContent,
86    /// Provider-specific options.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub provider_options: Option<ProviderOptions>,
89}
90
91/// A tool message.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93pub struct ToolMessage {
94    /// Tool results and approval responses.
95    pub content: Vec<ToolPart>,
96    /// Provider-specific options.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub provider_options: Option<ProviderOptions>,
99}
100
101/// Content of a user message: a string or a list of parts.
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103#[serde(untagged)]
104pub enum UserContent {
105    /// Plain text, equivalent to a single text part.
106    Text(String),
107    /// Parts.
108    Parts(Vec<UserPart>),
109}
110
111impl UserContent {
112    /// Returns `true` when the content has no text and no parts.
113    #[must_use]
114    pub fn is_empty(&self) -> bool {
115        match self {
116            Self::Text(text) => text.is_empty(),
117            Self::Parts(parts) => parts.is_empty(),
118        }
119    }
120
121    /// Returns the plain text when the content is a string.
122    #[must_use]
123    pub fn as_text(&self) -> Option<&str> {
124        match self {
125            Self::Text(text) => Some(text),
126            Self::Parts(_) => None,
127        }
128    }
129
130    /// Returns the parts, converting plain text into a single text part.
131    #[must_use]
132    pub fn into_parts(self) -> Vec<UserPart> {
133        match self {
134            Self::Text(text) => vec![UserPart::text(text)],
135            Self::Parts(parts) => parts,
136        }
137    }
138}
139
140impl From<&str> for UserContent {
141    fn from(text: &str) -> Self {
142        Self::Text(text.to_owned())
143    }
144}
145
146impl From<String> for UserContent {
147    fn from(text: String) -> Self {
148        Self::Text(text)
149    }
150}
151
152impl From<Vec<UserPart>> for UserContent {
153    fn from(parts: Vec<UserPart>) -> Self {
154        Self::Parts(parts)
155    }
156}
157
158/// Content of an assistant message: a string or a list of parts.
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160#[serde(untagged)]
161pub enum AssistantContent {
162    /// Plain text, equivalent to a single text part.
163    Text(String),
164    /// Parts.
165    Parts(Vec<AssistantPart>),
166}
167
168impl AssistantContent {
169    /// Returns `true` when the content has no text and no parts.
170    #[must_use]
171    pub fn is_empty(&self) -> bool {
172        match self {
173            Self::Text(text) => text.is_empty(),
174            Self::Parts(parts) => parts.is_empty(),
175        }
176    }
177
178    /// Returns the plain text when the content is a string.
179    #[must_use]
180    pub fn as_text(&self) -> Option<&str> {
181        match self {
182            Self::Text(text) => Some(text),
183            Self::Parts(_) => None,
184        }
185    }
186
187    /// Returns the parts, converting plain text into a single text part.
188    #[must_use]
189    pub fn into_parts(self) -> Vec<AssistantPart> {
190        match self {
191            Self::Text(text) => vec![AssistantPart::text(text)],
192            Self::Parts(parts) => parts,
193        }
194    }
195
196    /// Returns the parts when the content is a list.
197    #[must_use]
198    pub fn as_parts(&self) -> Option<&[AssistantPart]> {
199        match self {
200            Self::Text(_) => None,
201            Self::Parts(parts) => Some(parts),
202        }
203    }
204}
205
206impl From<&str> for AssistantContent {
207    fn from(text: &str) -> Self {
208        Self::Text(text.to_owned())
209    }
210}
211
212impl From<String> for AssistantContent {
213    fn from(text: String) -> Self {
214        Self::Text(text)
215    }
216}
217
218impl From<Vec<AssistantPart>> for AssistantContent {
219    fn from(parts: Vec<AssistantPart>) -> Self {
220        Self::Parts(parts)
221    }
222}
223
224impl Message {
225    /// A system message.
226    #[must_use]
227    pub fn system(content: impl Into<String>) -> Self {
228        Self::System(SystemMessage {
229            content: content.into(),
230            provider_options: None,
231        })
232    }
233
234    /// A user message with plain text.
235    #[must_use]
236    pub fn user(text: impl Into<String>) -> Self {
237        Self::User(UserMessage {
238            content: UserContent::Text(text.into()),
239            provider_options: None,
240        })
241    }
242
243    /// A user message with parts.
244    #[must_use]
245    pub fn user_parts(parts: impl IntoIterator<Item = impl Into<UserPart>>) -> Self {
246        Self::User(UserMessage {
247            content: UserContent::Parts(parts.into_iter().map(Into::into).collect()),
248            provider_options: None,
249        })
250    }
251
252    /// An assistant message with plain text.
253    #[must_use]
254    pub fn assistant(text: impl Into<String>) -> Self {
255        Self::Assistant(AssistantMessage {
256            content: AssistantContent::Text(text.into()),
257            provider_options: None,
258        })
259    }
260
261    /// An assistant message with parts.
262    #[must_use]
263    pub fn assistant_parts(parts: impl IntoIterator<Item = impl Into<AssistantPart>>) -> Self {
264        Self::Assistant(AssistantMessage {
265            content: AssistantContent::Parts(parts.into_iter().map(Into::into).collect()),
266            provider_options: None,
267        })
268    }
269
270    /// A tool message.
271    #[must_use]
272    pub fn tool(parts: impl IntoIterator<Item = impl Into<ToolPart>>) -> Self {
273        Self::Tool(ToolMessage {
274            content: parts.into_iter().map(Into::into).collect(),
275            provider_options: None,
276        })
277    }
278
279    /// The role.
280    #[must_use]
281    pub fn role(&self) -> Role {
282        match self {
283            Self::System(_) => Role::System,
284            Self::User(_) => Role::User,
285            Self::Assistant(_) => Role::Assistant,
286            Self::Tool(_) => Role::Tool,
287        }
288    }
289
290    /// Returns `true` when the message carries no content (empty string or
291    /// no parts).
292    #[must_use]
293    pub fn is_empty(&self) -> bool {
294        match self {
295            Self::System(message) => message.content.is_empty(),
296            Self::User(message) => message.content.is_empty(),
297            Self::Assistant(message) => message.content.is_empty(),
298            Self::Tool(message) => message.content.is_empty(),
299        }
300    }
301
302    /// Provider-specific options.
303    #[must_use]
304    pub fn provider_options(&self) -> Option<&ProviderOptions> {
305        match self {
306            Self::System(message) => message.provider_options.as_ref(),
307            Self::User(message) => message.provider_options.as_ref(),
308            Self::Assistant(message) => message.provider_options.as_ref(),
309            Self::Tool(message) => message.provider_options.as_ref(),
310        }
311    }
312
313    /// Sets provider-specific options.
314    #[must_use]
315    pub fn with_provider_options(mut self, options: ProviderOptions) -> Self {
316        let slot = match &mut self {
317            Self::System(message) => &mut message.provider_options,
318            Self::User(message) => &mut message.provider_options,
319            Self::Assistant(message) => &mut message.provider_options,
320            Self::Tool(message) => &mut message.provider_options,
321        };
322        *slot = Some(options);
323        self
324    }
325
326    /// Returns the system message when this is one.
327    #[must_use]
328    pub fn as_system(&self) -> Option<&SystemMessage> {
329        match self {
330            Self::System(message) => Some(message),
331            _ => None,
332        }
333    }
334
335    /// Returns the user message when this is one.
336    #[must_use]
337    pub fn as_user(&self) -> Option<&UserMessage> {
338        match self {
339            Self::User(message) => Some(message),
340            _ => None,
341        }
342    }
343
344    /// Returns the assistant message when this is one.
345    #[must_use]
346    pub fn as_assistant(&self) -> Option<&AssistantMessage> {
347        match self {
348            Self::Assistant(message) => Some(message),
349            _ => None,
350        }
351    }
352
353    /// Returns the tool message when this is one.
354    #[must_use]
355    pub fn as_tool(&self) -> Option<&ToolMessage> {
356        match self {
357            Self::Tool(message) => Some(message),
358            _ => None,
359        }
360    }
361}
362
363impl From<SystemMessage> for Message {
364    fn from(message: SystemMessage) -> Self {
365        Self::System(message)
366    }
367}
368
369impl From<UserMessage> for Message {
370    fn from(message: UserMessage) -> Self {
371        Self::User(message)
372    }
373}
374
375impl From<AssistantMessage> for Message {
376    fn from(message: AssistantMessage) -> Self {
377        Self::Assistant(message)
378    }
379}
380
381impl From<ToolMessage> for Message {
382    fn from(message: ToolMessage) -> Self {
383        Self::Tool(message)
384    }
385}
386
387/// Helpers for message histories.
388pub trait MessagesExt {
389    /// Appends an approval response to the trailing tool message, creating
390    /// one when the history does not end with a tool message.
391    fn push_approval_response(&mut self, response: ToolApprovalResponse);
392
393    /// Returns the approval requests that have no response yet.
394    fn pending_approval_requests(&self) -> Vec<&crate::part::ToolApprovalRequest>;
395}
396
397impl MessagesExt for Vec<Message> {
398    fn push_approval_response(&mut self, response: ToolApprovalResponse) {
399        if let Some(Message::Tool(tool)) = self.last_mut() {
400            tool.content.push(ToolPart::ToolApprovalResponse(response));
401        } else {
402            self.push(Message::tool([ToolPart::ToolApprovalResponse(response)]));
403        }
404    }
405
406    fn pending_approval_requests(&self) -> Vec<&crate::part::ToolApprovalRequest> {
407        let answered: std::collections::HashSet<&ferrin_spec::ApprovalId> = self
408            .iter()
409            .filter_map(Message::as_tool)
410            .flat_map(|tool| tool.content.iter())
411            .filter_map(ToolPart::as_tool_approval_response)
412            .map(|response| &response.approval_id)
413            .collect();
414        self.iter()
415            .filter_map(Message::as_assistant)
416            .filter_map(|assistant| assistant.content.as_parts())
417            .flatten()
418            .filter_map(AssistantPart::as_tool_approval_request)
419            .filter(|request| !answered.contains(&request.approval_id))
420            .collect()
421    }
422}