brainwires_knowledge/knowledge/
config.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum DispositionTrait {
17 Analytical,
19 Concise,
21 Cautious,
23 Creative,
25 Systematic,
27}
28
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct MemoryBankConfig {
53 pub mission: Option<String>,
55 pub directives: Vec<String>,
57 pub disposition: Vec<DispositionTrait>,
59}
60
61impl MemoryBankConfig {
62 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn with_mission(mut self, mission: impl Into<String>) -> Self {
69 self.mission = Some(mission.into());
70 self
71 }
72
73 pub fn with_directive(mut self, directive: impl Into<String>) -> Self {
75 self.directives.push(directive.into());
76 self
77 }
78
79 pub fn with_disposition(mut self, trait_: DispositionTrait) -> Self {
81 if !self.disposition.contains(&trait_) {
82 self.disposition.push(trait_);
83 }
84 self
85 }
86
87 pub fn is_noop(&self) -> bool {
89 self.mission.is_none() && self.directives.is_empty() && self.disposition.is_empty()
90 }
91
92 pub fn mission_tag(&self) -> Option<String> {
96 self.mission.as_ref().map(|m| {
97 let slug = m
98 .to_lowercase()
99 .split_whitespace()
100 .collect::<Vec<_>>()
101 .join("_");
102 format!("mission:{slug}")
103 })
104 }
105
106 pub fn blocks_content(&self, content: &str) -> bool {
114 let lower_content = content.to_lowercase();
115 for directive in &self.directives {
116 let object = if let Some(rest) = directive.strip_prefix("Never ") {
117 rest
118 } else if let Some(rest) = directive.strip_prefix("Do not ") {
119 rest
120 } else {
121 continue; };
123
124 let words: Vec<&str> = object.split_whitespace().collect();
125 if !words.is_empty()
126 && words
127 .iter()
128 .all(|w| lower_content.contains(&w.to_lowercase()))
129 {
130 return true;
131 }
132 }
133 false
134 }
135
136 pub fn disposition_score_delta(&self, content: &str) -> f32 {
139 if self.disposition.is_empty() {
140 return 0.0;
141 }
142
143 let lower = content.to_lowercase();
144 let mut delta: f32 = 0.0;
145
146 for trait_ in &self.disposition {
147 delta += match trait_ {
148 DispositionTrait::Analytical => {
149 let has_numbers = lower.chars().any(|c| c.is_ascii_digit());
151 let has_code = lower.contains("```") || lower.contains(" ");
152 let has_bullets = lower.contains("- ") || lower.contains("* ");
153 if has_numbers || has_code || has_bullets {
154 0.05
155 } else {
156 0.0
157 }
158 }
159 DispositionTrait::Concise => {
160 if content.len() > 500 { -0.05 } else { 0.0 }
162 }
163 DispositionTrait::Cautious => {
164 let hedges = ["might", "could", "consider", "perhaps", "possibly", "maybe"];
166 if hedges.iter().any(|h| lower.contains(h)) {
167 0.05
168 } else {
169 0.0
170 }
171 }
172 DispositionTrait::Creative => {
173 let creative = [
175 "idea",
176 "what if",
177 "novel",
178 "alternative",
179 "propose",
180 "imagine",
181 ];
182 if creative.iter().any(|c| lower.contains(c)) {
183 0.05
184 } else {
185 0.0
186 }
187 }
188 DispositionTrait::Systematic => {
189 let sequential = ["first", "then", "finally", "step ", "1.", "2.", "3."];
191 if sequential.iter().any(|s| lower.contains(s)) {
192 0.05
193 } else {
194 0.0
195 }
196 }
197 };
198 }
199
200 delta.clamp(-0.1, 0.1)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_default_is_noop() {
210 assert!(MemoryBankConfig::default().is_noop());
211 assert!(MemoryBankConfig::new().is_noop());
212 }
213
214 #[test]
215 fn test_builder_chain() {
216 let cfg = MemoryBankConfig::new()
217 .with_mission("Security assistant")
218 .with_directive("Never store PII")
219 .with_disposition(DispositionTrait::Analytical);
220 assert!(!cfg.is_noop());
221 assert_eq!(cfg.mission.as_deref(), Some("Security assistant"));
222 assert_eq!(cfg.directives.len(), 1);
223 assert_eq!(cfg.disposition.len(), 1);
224 }
225
226 #[test]
227 fn test_mission_tag() {
228 let cfg = MemoryBankConfig::new().with_mission("Security Assistant");
229 assert_eq!(cfg.mission_tag(), Some("mission:security_assistant".into()));
230 assert!(MemoryBankConfig::new().mission_tag().is_none());
231 }
232
233 #[test]
234 fn test_blocks_content_never() {
235 let cfg = MemoryBankConfig::new().with_directive("Never store PII");
236 assert!(cfg.blocks_content("we should store user PII here"));
237 assert!(!cfg.blocks_content("authentication token handling"));
238 }
239
240 #[test]
241 fn test_blocks_content_do_not() {
242 let cfg = MemoryBankConfig::new().with_directive("Do not log passwords");
243 assert!(cfg.blocks_content("log passwords to the debug output"));
244 assert!(!cfg.blocks_content("log request headers"));
245 }
246
247 #[test]
248 fn test_blocks_content_non_blocking_directive() {
249 let cfg = MemoryBankConfig::new().with_directive("Prefer Rust over Python");
250 assert!(!cfg.blocks_content("Prefer Rust over Python everywhere"));
252 }
253
254 #[test]
255 fn test_disposition_concise_penalty() {
256 let cfg = MemoryBankConfig::new().with_disposition(DispositionTrait::Concise);
257 let long_content = "x".repeat(501);
258 let short_content = "short";
259 assert!(cfg.disposition_score_delta(&long_content) < 0.0);
260 assert_eq!(cfg.disposition_score_delta(short_content), 0.0);
261 }
262
263 #[test]
264 fn test_disposition_analytical_boost() {
265 let cfg = MemoryBankConfig::new().with_disposition(DispositionTrait::Analytical);
266 assert!(cfg.disposition_score_delta("Step 1. Use 42 requests") > 0.0);
267 assert_eq!(cfg.disposition_score_delta("casual chat"), 0.0);
268 }
269
270 #[test]
271 fn test_disposition_delta_clamp() {
272 let cfg = MemoryBankConfig::new()
274 .with_disposition(DispositionTrait::Analytical)
275 .with_disposition(DispositionTrait::Cautious)
276 .with_disposition(DispositionTrait::Creative)
277 .with_disposition(DispositionTrait::Systematic)
278 .with_disposition(DispositionTrait::Concise);
279 let content = "first idea: might use 42 steps - consider alternatives";
280 let delta = cfg.disposition_score_delta(content);
281 assert!((-0.1..=0.1).contains(&delta));
282 }
283}