turnframe_tasks/
instructions.rs1use std::sync::Arc;
9
10use turnframe_core::locale::Locale;
11use turnframe_core::prompt::{PromptName, PromptRef, PromptSelector, PromptSource};
12
13pub const BUILT_IN_VERSION: &str = "builtin";
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Instructions {
19 pub text: String,
21 pub reference: PromptRef,
23}
24
25impl Instructions {
26 #[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
36pub 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}