Skip to main content

ferrin_core/prompt/
standardize.rs

1//! Standardization of builder inputs into system instructions plus messages.
2
3use ferrin_message::Message;
4use ferrin_message::Role;
5use ferrin_message::SystemMessage;
6use ferrin_spec::ProviderOptions;
7
8use crate::error::Error;
9
10/// One system message or an ordered sequence of system messages.
11///
12/// String conversions create a single message. Use [`Self::messages`] for
13/// several instruction messages with independent provider options.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum Instructions {
17    /// A single system message.
18    System(SystemMessage),
19    /// System messages in prompt order (an empty vector adds none).
20    Messages(Vec<SystemMessage>),
21}
22
23impl Default for Instructions {
24    fn default() -> Self {
25        Self::new("")
26    }
27}
28
29impl Instructions {
30    /// Creates a single system instruction from text.
31    #[must_use]
32    pub fn new(content: impl Into<String>) -> Self {
33        Self::System(SystemMessage {
34            content: content.into(),
35            provider_options: None,
36        })
37    }
38
39    /// Creates ordered instructions with independent message metadata.
40    ///
41    /// # Examples
42    ///
43    /// ```
44    /// use ferrin_core::Instructions;
45    /// use ferrin_message::SystemMessage;
46    /// let instructions = Instructions::messages([
47    ///     SystemMessage { content: "Be concise.".into(), provider_options: None },
48    ///     SystemMessage { content: "Use supplied sources.".into(), provider_options: None },
49    /// ]);
50    /// assert_eq!(instructions.as_messages().len(), 2);
51    /// ```
52    #[must_use]
53    pub fn messages(messages: impl IntoIterator<Item = SystemMessage>) -> Self {
54        Self::Messages(messages.into_iter().collect())
55    }
56
57    /// Replaces provider options on every configured system message.
58    #[must_use]
59    pub fn with_provider_options(mut self, options: ProviderOptions) -> Self {
60        match &mut self {
61            Self::System(message) => message.provider_options = Some(options),
62            Self::Messages(messages) => {
63                for message in messages {
64                    message.provider_options = Some(options.clone());
65                }
66            }
67        }
68        self
69    }
70
71    /// Borrows instruction messages in prompt order.
72    #[must_use]
73    pub fn as_messages(&self) -> &[SystemMessage] {
74        match self {
75            Self::System(message) => std::slice::from_ref(message),
76            Self::Messages(messages) => messages,
77        }
78    }
79
80    /// Consumes the instructions into ordered system messages.
81    #[must_use]
82    pub fn into_messages(self) -> Vec<SystemMessage> {
83        match self {
84            Self::System(message) => vec![message],
85            Self::Messages(messages) => messages,
86        }
87    }
88}
89
90impl From<&str> for Instructions {
91    fn from(content: &str) -> Self {
92        Self::new(content)
93    }
94}
95
96impl From<String> for Instructions {
97    fn from(content: String) -> Self {
98        Self::new(content)
99    }
100}
101
102impl From<SystemMessage> for Instructions {
103    fn from(message: SystemMessage) -> Self {
104        Self::System(message)
105    }
106}
107
108impl From<Vec<SystemMessage>> for Instructions {
109    fn from(messages: Vec<SystemMessage>) -> Self {
110        Self::Messages(messages)
111    }
112}
113
114/// The standardized prompt: optional system instructions and non-empty
115/// messages without system role (unless explicitly allowed).
116#[derive(Debug, Clone, PartialEq)]
117pub(crate) struct StandardizedPrompt {
118    pub(crate) system: Option<Instructions>,
119    pub(crate) messages: Vec<Message>,
120}
121
122/// Standardizes builder inputs.
123///
124/// Exactly one of `prompt` and `messages` must be set; `messages` must not be
125/// empty and must not contain system messages unless
126/// `allow_system_in_messages` is set.
127pub(crate) fn standardize(
128    system: Option<Instructions>,
129    prompt: Option<String>,
130    messages: Option<Vec<Message>>,
131    allow_system_in_messages: bool,
132) -> Result<StandardizedPrompt, Error> {
133    let messages = match (prompt, messages) {
134        (Some(_), Some(_)) => {
135            return Err(Error::invalid_prompt(
136                "prompt and messages cannot be set at the same time",
137            ));
138        }
139        (None, None) => {
140            return Err(Error::invalid_prompt(
141                "either prompt or messages must be set",
142            ));
143        }
144        (Some(text), None) => vec![Message::user(text)],
145        (None, Some(messages)) => messages,
146    };
147    if messages.is_empty() {
148        return Err(Error::invalid_prompt("messages must not be empty"));
149    }
150    if !allow_system_in_messages
151        && let Some(index) = messages
152            .iter()
153            .position(|message| message.role() == Role::System)
154    {
155        return Err(Error::invalid_prompt(format!(
156            "messages must not contain system messages (found at index {index}); \
157             use `system` or enable `allow_system_in_messages`"
158        )));
159    }
160    Ok(StandardizedPrompt { system, messages })
161}