gemini_adk_rs/
instruction.rs1use regex::Regex;
7use std::sync::LazyLock;
8
9use crate::state::State;
10
11static PLACEHOLDER_RE: LazyLock<Regex> =
12 LazyLock::new(|| Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_:]*)\??\}").unwrap());
13
14pub fn inject_session_state(template: &str, state: &State) -> String {
21 PLACEHOLDER_RE
22 .replace_all(template, |caps: ®ex::Captures| {
23 let full_match = &caps[0];
24 let key = &caps[1];
25 let optional = full_match.ends_with("?}");
26
27 match state.get_raw(key) {
28 Some(value) => value_to_string(&value),
29 None => {
30 if optional {
31 String::new()
32 } else {
33 full_match.to_string()
34 }
35 }
36 }
37 })
38 .into_owned()
39}
40
41pub trait InstructionProvider: Send + Sync {
47 fn provide(&self, state: &State) -> String;
49}
50
51impl<F> InstructionProvider for F
52where
53 F: Fn(&State) -> String + Send + Sync,
54{
55 fn provide(&self, state: &State) -> String {
56 self(state)
57 }
58}
59
60impl<T: InstructionProvider + ?Sized> InstructionProvider for std::sync::Arc<T> {
61 fn provide(&self, state: &State) -> String {
62 (**self).provide(state)
63 }
64}
65
66#[cfg(feature = "templates")]
83pub struct TemplateInstruction {
84 source: String,
85}
86
87#[cfg(feature = "templates")]
88impl TemplateInstruction {
89 pub fn new(source: impl Into<String>) -> Result<Self, String> {
92 let source = source.into();
93 let env = minijinja::Environment::new();
94 env.template_from_str(&source)
95 .map_err(|e| format!("template error: {e}"))?;
96 Ok(Self { source })
97 }
98
99 fn context(state: &State) -> minijinja::Value {
100 let mut all = serde_json::Map::new();
101 let mut top = serde_json::Map::new();
102 for key in state.keys() {
103 if let Some(value) = state.get_raw(&key) {
104 let bare_identifier = !key.is_empty()
105 && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
106 && !key.starts_with(|c: char| c.is_ascii_digit());
107 if bare_identifier {
108 top.insert(key.clone(), value.clone());
109 }
110 all.insert(key, value);
111 }
112 }
113 top.insert("state".into(), serde_json::Value::Object(all));
114 minijinja::Value::from_serialize(serde_json::Value::Object(top))
115 }
116}
117
118#[cfg(feature = "templates")]
119impl InstructionProvider for TemplateInstruction {
120 fn provide(&self, state: &State) -> String {
121 let env = minijinja::Environment::new();
122 env.render_str(&self.source, Self::context(state))
123 .unwrap_or_else(|e| format!("[template render error: {e}]"))
124 }
125}
126
127fn value_to_string(value: &serde_json::Value) -> String {
128 match value {
129 serde_json::Value::String(s) => s.clone(),
130 serde_json::Value::Null => String::new(),
131 other => other.to_string(),
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn closure_is_an_instruction_provider() {
141 let state = State::new();
142 let _ = state.set("persona", "pirate");
143 let provider = |s: &State| {
144 format!(
145 "Speak like a {}.",
146 s.get::<String>("persona").unwrap_or_default()
147 )
148 };
149 assert_eq!(
150 InstructionProvider::provide(&provider, &state),
151 "Speak like a pirate."
152 );
153 }
154
155 #[cfg(feature = "templates")]
156 #[test]
157 fn template_instruction_renders_conditionals_and_prefixed_keys() {
158 let state = State::new();
159 let _ = state.set("name", "Alice");
160 let _ = state.set("derived:risk", 0.9);
161 let inst = TemplateInstruction::new(
162 "Hello {{ name }}.{% if state[\"derived:risk\"] > 0.8 %} Escalate.{% endif %}",
163 )
164 .unwrap();
165 assert_eq!(inst.provide(&state), "Hello Alice. Escalate.");
166 }
167
168 #[cfg(feature = "templates")]
169 #[test]
170 fn template_instruction_rejects_bad_syntax_at_build() {
171 assert!(TemplateInstruction::new("{% if x %}unclosed").is_err());
172 }
173
174 #[test]
175 fn simple_substitution() {
176 let state = State::new();
177 let _ = state.set("name", "Alice");
178 let result = inject_session_state("Hello, {name}!", &state);
179 assert_eq!(result, "Hello, Alice!");
180 }
181
182 #[test]
183 fn optional_key_present() {
184 let state = State::new();
185 let _ = state.set("title", "Dr.");
186 let result = inject_session_state("Hello, {title?} Smith!", &state);
187 assert_eq!(result, "Hello, Dr. Smith!");
188 }
189
190 #[test]
191 fn optional_key_missing() {
192 let state = State::new();
193 let result = inject_session_state("Hello, {title?}Smith!", &state);
194 assert_eq!(result, "Hello, Smith!");
195 }
196
197 #[test]
198 fn missing_required_key_left_as_is() {
199 let state = State::new();
200 let result = inject_session_state("Hello, {unknown}!", &state);
201 assert_eq!(result, "Hello, {unknown}!");
202 }
203
204 #[test]
205 fn multiple_keys() {
206 let state = State::new();
207 let _ = state.set("first", "Alice");
208 let _ = state.set("last", "Smith");
209 let result = inject_session_state("{first} {last}", &state);
210 assert_eq!(result, "Alice Smith");
211 }
212
213 #[test]
214 fn prefix_key() {
215 let state = State::new();
216 let _ = state.app().set("flag", true);
217 let result = inject_session_state("Flag is {app:flag}", &state);
218 assert_eq!(result, "Flag is true");
219 }
220
221 #[test]
222 fn no_placeholders_passthrough() {
223 let state = State::new();
224 let template = "No placeholders here.";
225 assert_eq!(inject_session_state(template, &state), template);
226 }
227
228 #[test]
229 fn numeric_value() {
230 let state = State::new();
231 let _ = state.set("count", 42);
232 let result = inject_session_state("Count: {count}", &state);
233 assert_eq!(result, "Count: 42");
234 }
235
236 #[test]
237 fn empty_template() {
238 let state = State::new();
239 assert_eq!(inject_session_state("", &state), "");
240 }
241}