Skip to main content

turnframe_tasks/
instructions.rs

1//! Which instruction text a task call runs under, and the reference the record cites.
2//!
3//! A configured prompt source is asked for `<name>.<locale>`, then `<name>`. Without a
4//! source, or when it has neither, the task's built-in text is used under a reference
5//! whose version is `builtin` and whose hash is the text's, so a record always says
6//! which words a call ran under.
7
8use std::sync::Arc;
9
10use turnframe_core::locale::Locale;
11use turnframe_core::prompt::{PromptName, PromptRef, PromptSelector, PromptSource};
12
13/// The version every built-in instruction text is recorded under.
14pub const BUILT_IN_VERSION: &str = "builtin";
15
16/// Instruction text and the reference to record for it.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Instructions {
19    /// The text sent to the model.
20    pub text: String,
21    /// What the task record cites.
22    pub reference: PromptRef,
23}
24
25impl Instructions {
26    /// Built-in text under its own content-hash reference.
27    #[must_use]
28    pub fn built_in(name: &str, text: &str) -> Self {
29        Self {
30            text: text.to_owned(),
31            reference: PromptRef::of_text(name, BUILT_IN_VERSION, text),
32        }
33    }
34}
35
36/// Resolves the instructions for `name` in `locale`.
37pub async fn resolve(
38    source: Option<&Arc<dyn PromptSource>>,
39    selector: &PromptSelector,
40    name: &str,
41    locale: &Locale,
42    built_in: &str,
43) -> Instructions {
44    let Some(source) = source else {
45        return Instructions::built_in(name, built_in);
46    };
47    for candidate in [format!("{name}.{}", locale.as_str()), name.to_owned()] {
48        match source
49            .load(&PromptName::new(candidate.as_str()), selector)
50            .await
51        {
52            Ok(loaded) => {
53                let (reference, text) = loaded.into_parts();
54                return Instructions { text, reference };
55            }
56            Err(error) => {
57                tracing::debug!(
58                    target: "turnframe.tasks",
59                    prompt = candidate.as_str(),
60                    error_code = error.code(),
61                    "prompt source has no text for this name"
62                );
63            }
64        }
65    }
66    Instructions::built_in(name, built_in)
67}
68
69#[cfg(test)]
70mod tests {
71    use turnframe_core::prompt::{LoadedPrompt, PromptError};
72
73    use super::*;
74
75    #[derive(Debug)]
76    struct Italian;
77
78    #[async_trait::async_trait]
79    impl PromptSource for Italian {
80        async fn load(
81            &self,
82            name: &PromptName,
83            _selector: &PromptSelector,
84        ) -> Result<LoadedPrompt, PromptError> {
85            if name.as_str() == "understand.segment.it-IT" {
86                return Ok(LoadedPrompt::new(
87                    name.clone(),
88                    "v2",
89                    "Dividi il messaggio.",
90                ));
91            }
92            Err(PromptError::NotFound { name: name.clone() })
93        }
94
95        fn describe(&self) -> &'static str {
96            "italian"
97        }
98    }
99
100    #[tokio::test]
101    async fn a_locale_specific_prompt_wins_and_the_built_in_text_is_the_fallback() {
102        let source: Arc<dyn PromptSource> = Arc::new(Italian);
103        let italian = resolve(
104            Some(&source),
105            &PromptSelector::Latest,
106            "understand.segment",
107            &Locale::from("it-IT"),
108            "Split the message.",
109        )
110        .await;
111        assert_eq!(italian.text, "Dividi il messaggio.");
112        assert_eq!(italian.reference.version.as_str(), "v2");
113
114        let english = resolve(
115            Some(&source),
116            &PromptSelector::Latest,
117            "understand.segment",
118            &Locale::from("en-GB"),
119            "Split the message.",
120        )
121        .await;
122        assert_eq!(english.text, "Split the message.");
123        assert_eq!(english.reference.version.as_str(), BUILT_IN_VERSION);
124        assert!(english.reference.matches("Split the message."));
125    }
126}