adk_core/
instruction_template.rs1use crate::{AdkError, InvocationContext, Result};
2
3fn is_ident_start(c: char) -> bool {
5 c.is_ascii_alphabetic() || c == '_'
6}
7
8fn is_ident_body(c: char) -> bool {
10 c.is_ascii_alphanumeric() || c == '_' || c == ':' || c == '.'
11}
12
13fn find_next_placeholder(template: &str, from: usize) -> Option<(usize, usize, &str)> {
18 let bytes = template.as_bytes();
19 let len = bytes.len();
20 let mut i = from;
21
22 while i < len {
23 if bytes[i] == b'{' {
24 let content_start = i + 1;
25 if content_start >= len {
26 break;
27 }
28 if !is_ident_start(bytes[content_start] as char) {
30 i += 1;
31 continue;
32 }
33 let mut j = content_start + 1;
35 while j < len && is_ident_body(bytes[j] as char) {
36 j += 1;
37 }
38 if j < len && bytes[j] == b'?' {
40 j += 1;
41 }
42 if j < len && bytes[j] == b'}' {
44 let content = &template[content_start..j];
45 return Some((i, j + 1, content));
46 }
47 }
48 i += 1;
49 }
50 None
51}
52
53fn is_identifier(s: &str) -> bool {
56 if s.is_empty() {
57 return false;
58 }
59
60 let mut chars = s.chars();
61 let first = chars.next().unwrap();
62
63 if !first.is_alphabetic() && first != '_' {
64 return false;
65 }
66
67 chars.all(|c| c.is_alphanumeric() || c == '_')
68}
69
70fn is_valid_state_name(var_name: &str) -> bool {
73 let parts: Vec<&str> = var_name.split(':').collect();
74
75 match parts.len() {
76 1 => is_identifier(var_name),
77 2 => {
78 let prefix = format!("{}:", parts[0]);
79 let valid_prefixes = ["app:", "user:", "temp:"];
80 valid_prefixes.contains(&prefix.as_str()) && is_identifier(parts[1])
81 }
82 _ => false,
83 }
84}
85
86async fn replace_match(ctx: &dyn InvocationContext, content: &str) -> Result<String> {
89 let var_name = content.trim();
90
91 let (var_name, optional) =
93 if let Some(name) = var_name.strip_suffix('?') { (name, true) } else { (var_name, false) };
94
95 if let Some(file_name) = var_name.strip_prefix("artifact.") {
97 if file_name.is_empty() {
98 return Err(AdkError::agent(
99 "Invalid artifact name '': must include a file name after 'artifact.'",
100 ));
101 }
102
103 if file_name.contains("..") || file_name.contains('/') || file_name.contains('\\') {
105 return Err(AdkError::agent(format!(
106 "Invalid artifact name '{file_name}': must not contain path separators or '..'"
107 )));
108 }
109
110 let artifacts = ctx
111 .artifacts()
112 .ok_or_else(|| AdkError::agent("Artifact service is not initialized"))?;
113
114 match artifacts.load(file_name).await {
115 Ok(part) => {
116 if let Some(text) = part.text() {
117 return Ok(text.to_string());
118 }
119 Ok(String::new())
120 }
121 Err(e) => {
122 if optional {
123 Ok(String::new())
124 } else {
125 Err(AdkError::agent(format!("Failed to load artifact {file_name}: {e}")))
126 }
127 }
128 }
129 } else if is_valid_state_name(var_name) {
130 let state_value = ctx.session().state().get(var_name);
131
132 match state_value {
133 Some(value) => {
134 if let Some(s) = value.as_str() {
135 Ok(s.to_string())
136 } else {
137 Ok(format!("{}", value))
138 }
139 }
140 None => {
141 if optional {
142 Ok(String::new())
143 } else {
144 Err(AdkError::agent(format!("State variable '{var_name}' not found")))
145 }
146 }
147 }
148 } else {
149 Ok(format!("{{{}}}", content))
151 }
152}
153
154pub async fn inject_session_state(ctx: &dyn InvocationContext, template: &str) -> Result<String> {
177 let mut result = String::with_capacity((template.len() as f32 * 1.2) as usize);
179 let mut last_end = 0;
180
181 while let Some((start, end, content)) = find_next_placeholder(template, last_end) {
182 result.push_str(&template[last_end..start]);
184
185 let replacement = replace_match(ctx, content).await?;
187 result.push_str(&replacement);
188
189 last_end = end;
190 }
191
192 result.push_str(&template[last_end..]);
194
195 Ok(result)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_is_identifier() {
204 assert!(is_identifier("valid_name"));
205 assert!(is_identifier("_private"));
206 assert!(is_identifier("name123"));
207 assert!(!is_identifier("123invalid"));
208 assert!(!is_identifier(""));
209 assert!(!is_identifier("with-dash"));
210 }
211
212 #[test]
213 fn test_is_valid_state_name() {
214 assert!(is_valid_state_name("valid_var"));
215 assert!(is_valid_state_name("app:config"));
216 assert!(is_valid_state_name("user:preference"));
217 assert!(is_valid_state_name("temp:data"));
218 assert!(!is_valid_state_name("invalid:prefix"));
219 assert!(!is_valid_state_name("app:invalid-name"));
220 assert!(!is_valid_state_name("too:many:parts"));
221 }
222
223 #[test]
224 fn test_find_placeholder_basic() {
225 let t = "Hello {name}, welcome!";
226 let (s, e, c) = find_next_placeholder(t, 0).unwrap();
227 assert_eq!(c, "name");
228 assert_eq!(&t[s..e], "{name}");
229 }
230
231 #[test]
232 fn test_find_placeholder_optional() {
233 let t = "Hello {name?}!";
234 let (_, _, c) = find_next_placeholder(t, 0).unwrap();
235 assert_eq!(c, "name?");
236 }
237
238 #[test]
239 fn test_find_placeholder_prefixed() {
240 let t = "Value: {app:config}";
241 let (_, _, c) = find_next_placeholder(t, 0).unwrap();
242 assert_eq!(c, "app:config");
243 }
244
245 #[test]
246 fn test_find_placeholder_artifact() {
247 let t = "Content: {artifact.readme}";
248 let (_, _, c) = find_next_placeholder(t, 0).unwrap();
249 assert_eq!(c, "artifact.readme");
250 }
251
252 #[test]
253 fn test_find_placeholder_skips_invalid() {
254 assert!(find_next_placeholder("{123}", 0).is_none());
256 assert!(find_next_placeholder("{}", 0).is_none());
258 assert!(find_next_placeholder("{\"key\": \"value\"}", 0).is_none());
260 }
261
262 #[test]
263 fn test_find_placeholder_multiple() {
264 let t = "{a} and {b}";
265 let (_, e1, c1) = find_next_placeholder(t, 0).unwrap();
266 assert_eq!(c1, "a");
267 let (_, _, c2) = find_next_placeholder(t, e1).unwrap();
268 assert_eq!(c2, "b");
269 }
270}