agent_context_builder/
lib.rs1use std::collections::HashSet;
20
21#[derive(Debug, Clone)]
22struct Section {
23 name: String,
24 content: String,
25 priority: i32,
26 enabled: bool,
27}
28
29#[derive(Debug, Default)]
31pub struct ContextBuilder {
32 sections: Vec<Section>,
33 separator: String,
34}
35
36impl ContextBuilder {
37 pub fn new() -> Self {
38 Self { sections: Vec::new(), separator: "\n\n".into() }
39 }
40
41 pub fn separator(mut self, sep: impl Into<String>) -> Self {
43 self.separator = sep.into();
44 self
45 }
46
47 pub fn section(mut self, name: impl Into<String>, content: impl Into<String>) -> Self {
49 let name = name.into();
50 if let Some(s) = self.sections.iter_mut().find(|s| s.name == name) {
51 s.content = content.into();
52 } else {
53 self.sections.push(Section { name, content: content.into(), priority: 0, enabled: true });
54 }
55 self
56 }
57
58 pub fn section_with_priority(mut self, name: impl Into<String>, content: impl Into<String>, priority: i32) -> Self {
60 let name = name.into();
61 if let Some(s) = self.sections.iter_mut().find(|s| s.name == name) {
62 s.content = content.into();
63 s.priority = priority;
64 } else {
65 self.sections.push(Section { name, content: content.into(), priority, enabled: true });
66 }
67 self
68 }
69
70 pub fn disable(mut self, name: &str) -> Self {
72 if let Some(s) = self.sections.iter_mut().find(|s| s.name == name) {
73 s.enabled = false;
74 }
75 self
76 }
77
78 pub fn enable(mut self, name: &str) -> Self {
80 if let Some(s) = self.sections.iter_mut().find(|s| s.name == name) {
81 s.enabled = true;
82 }
83 self
84 }
85
86 pub fn remove(mut self, name: &str) -> Self {
88 self.sections.retain(|s| s.name != name);
89 self
90 }
91
92 pub fn get(&self, name: &str) -> Option<&str> {
94 self.sections.iter().find(|s| s.name == name).map(|s| s.content.as_str())
95 }
96
97 pub fn section_names(&self) -> Vec<&str> {
99 let mut sorted = self.sections.iter().collect::<Vec<_>>();
100 sorted.sort_by(|a, b| b.priority.cmp(&a.priority));
101 sorted.iter().map(|s| s.name.as_str()).collect()
102 }
103
104 pub fn enabled_count(&self) -> usize {
106 self.sections.iter().filter(|s| s.enabled).count()
107 }
108
109 pub fn build(&self) -> String {
111 let mut sorted: Vec<&Section> = self.sections.iter().filter(|s| s.enabled).collect();
112 sorted.sort_by(|a, b| b.priority.cmp(&a.priority));
114 sorted.iter().map(|s| s.content.as_str()).collect::<Vec<_>>().join(&self.separator)
115 }
116
117 pub fn has(&self, name: &str) -> bool {
119 self.sections.iter().any(|s| s.name == name)
120 }
121
122 pub fn duplicate_names(&self) -> Vec<String> {
124 let mut seen = HashSet::new();
125 let mut dups = Vec::new();
126 for s in &self.sections {
127 if !seen.insert(&s.name) {
128 dups.push(s.name.clone());
129 }
130 }
131 dups
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn single_section() {
141 let p = ContextBuilder::new().section("role", "You are helpful.").build();
142 assert_eq!(p, "You are helpful.");
143 }
144
145 #[test]
146 fn two_sections_joined() {
147 let p = ContextBuilder::new()
148 .section("a", "First.")
149 .section("b", "Second.")
150 .build();
151 assert!(p.contains("First."));
152 assert!(p.contains("Second."));
153 }
154
155 #[test]
156 fn default_separator_double_newline() {
157 let p = ContextBuilder::new()
158 .section("a", "A")
159 .section("b", "B")
160 .build();
161 assert!(p.contains("\n\nB") || p.contains("A\n\n"), "separator missing");
162 }
163
164 #[test]
165 fn custom_separator() {
166 let p = ContextBuilder::new()
167 .separator(" | ")
168 .section("a", "A")
169 .section("b", "B")
170 .build();
171 assert!(p.contains(" | "));
172 }
173
174 #[test]
175 fn replace_existing_section() {
176 let builder = ContextBuilder::new()
177 .section("role", "Old")
178 .section("role", "New");
179 assert_eq!(builder.get("role"), Some("New"));
180 }
181
182 #[test]
183 fn disable_omits_section() {
184 let p = ContextBuilder::new()
185 .section("a", "Show")
186 .section("b", "Hide")
187 .disable("b")
188 .build();
189 assert!(p.contains("Show"));
190 assert!(!p.contains("Hide"));
191 }
192
193 #[test]
194 fn enable_restores_section() {
195 let p = ContextBuilder::new()
196 .section("a", "Content")
197 .disable("a")
198 .enable("a")
199 .build();
200 assert!(p.contains("Content"));
201 }
202
203 #[test]
204 fn remove_deletes_section() {
205 let b = ContextBuilder::new()
206 .section("a", "keep")
207 .section("b", "gone")
208 .remove("b");
209 assert!(!b.has("b"));
210 assert!(b.has("a"));
211 }
212
213 #[test]
214 fn priority_ordering() {
215 let p = ContextBuilder::new()
216 .section_with_priority("low", "LOW", 1)
217 .section_with_priority("high", "HIGH", 10)
218 .build();
219 let high_pos = p.find("HIGH").unwrap();
220 let low_pos = p.find("LOW").unwrap();
221 assert!(high_pos < low_pos);
222 }
223
224 #[test]
225 fn has_section() {
226 let b = ContextBuilder::new().section("x", "v");
227 assert!(b.has("x"));
228 assert!(!b.has("y"));
229 }
230
231 #[test]
232 fn section_names_listed() {
233 let b = ContextBuilder::new().section("a", "").section("b", "");
234 let names = b.section_names();
235 assert!(names.contains(&"a"));
236 assert!(names.contains(&"b"));
237 }
238
239 #[test]
240 fn enabled_count() {
241 let b = ContextBuilder::new()
242 .section("a", "")
243 .section("b", "")
244 .disable("b");
245 assert_eq!(b.enabled_count(), 1);
246 }
247
248 #[test]
249 fn empty_builder_empty_string() {
250 assert_eq!(ContextBuilder::new().build(), "");
251 }
252
253 #[test]
254 fn no_duplicates_in_normal_use() {
255 let b = ContextBuilder::new().section("a", "1").section("b", "2");
256 assert!(b.duplicate_names().is_empty());
257 }
258}