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/// System instructions: text with optional provider options.
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
12pub struct Instructions {
13    /// The instruction text.
14    pub content: String,
15    /// Provider-specific options for the system message.
16    pub provider_options: Option<ProviderOptions>,
17}
18
19impl Instructions {
20    /// Creates instructions from text.
21    #[must_use]
22    pub fn new(content: impl Into<String>) -> Self {
23        Self {
24            content: content.into(),
25            provider_options: None,
26        }
27    }
28
29    /// Sets provider options.
30    #[must_use]
31    pub fn with_provider_options(mut self, options: ProviderOptions) -> Self {
32        self.provider_options = Some(options);
33        self
34    }
35
36    /// Converts into a system message.
37    #[must_use]
38    pub fn into_message(self) -> SystemMessage {
39        SystemMessage {
40            content: self.content,
41            provider_options: self.provider_options,
42        }
43    }
44}
45
46impl From<&str> for Instructions {
47    fn from(content: &str) -> Self {
48        Self::new(content)
49    }
50}
51
52impl From<String> for Instructions {
53    fn from(content: String) -> Self {
54        Self::new(content)
55    }
56}
57
58impl From<SystemMessage> for Instructions {
59    fn from(message: SystemMessage) -> Self {
60        Self {
61            content: message.content,
62            provider_options: message.provider_options,
63        }
64    }
65}
66
67/// The standardized prompt: optional system instructions and non-empty
68/// messages without system role (unless explicitly allowed).
69#[derive(Debug, Clone, PartialEq)]
70pub(crate) struct StandardizedPrompt {
71    pub(crate) system: Option<Instructions>,
72    pub(crate) messages: Vec<Message>,
73}
74
75/// Standardizes builder inputs.
76///
77/// Exactly one of `prompt` and `messages` must be set; `messages` must not be
78/// empty and must not contain system messages unless
79/// `allow_system_in_messages` is set.
80pub(crate) fn standardize(
81    system: Option<Instructions>,
82    prompt: Option<String>,
83    messages: Option<Vec<Message>>,
84    allow_system_in_messages: bool,
85) -> Result<StandardizedPrompt, Error> {
86    let messages = match (prompt, messages) {
87        (Some(_), Some(_)) => {
88            return Err(Error::invalid_prompt(
89                "prompt and messages cannot be set at the same time",
90            ));
91        }
92        (None, None) => {
93            return Err(Error::invalid_prompt(
94                "either prompt or messages must be set",
95            ));
96        }
97        (Some(text), None) => vec![Message::user(text)],
98        (None, Some(messages)) => messages,
99    };
100    if messages.is_empty() {
101        return Err(Error::invalid_prompt("messages must not be empty"));
102    }
103    if !allow_system_in_messages
104        && let Some(index) = messages
105            .iter()
106            .position(|message| message.role() == Role::System)
107    {
108        return Err(Error::invalid_prompt(format!(
109            "messages must not contain system messages (found at index {index}); \
110             use `system` or enable `allow_system_in_messages`"
111        )));
112    }
113    Ok(StandardizedPrompt { system, messages })
114}