1use crate::features::LinguisticFeatures;
10use serde::{Deserialize, Serialize};
11use std::collections::VecDeque;
12
13#[derive(Clone, Debug, Default, Serialize, Deserialize)]
15pub struct DeltaSignals {
16 pub length_z: f32,
18 pub complexity_z: f32,
20 pub emotional_z: f32,
22 pub formality_z: f32,
24 pub urgency_z: f32,
26 pub uncertainty_z: f32,
28 pub baseline_size: usize,
30}
31
32impl DeltaSignals {
33 pub fn has_significant_deviation(&self, threshold: f32) -> bool {
35 self.length_z.abs() > threshold
36 || self.complexity_z.abs() > threshold
37 || self.emotional_z.abs() > threshold
38 || self.formality_z.abs() > threshold
39 || self.urgency_z.abs() > threshold
40 || self.uncertainty_z.abs() > threshold
41 }
42
43 pub fn max_deviation(&self) -> (&'static str, f32) {
45 let signals = [
46 ("length", self.length_z),
47 ("complexity", self.complexity_z),
48 ("emotional", self.emotional_z),
49 ("formality", self.formality_z),
50 ("urgency", self.urgency_z),
51 ("uncertainty", self.uncertainty_z),
52 ];
53
54 signals
55 .into_iter()
56 .max_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).unwrap())
57 .unwrap_or(("none", 0.0))
58 }
59}
60
61#[derive(Clone, Debug, Default, Serialize, Deserialize)]
63struct RunningStat {
64 count: usize,
65 mean: f64,
66 m2: f64, }
68
69impl RunningStat {
70 fn new() -> Self {
71 Self::default()
72 }
73
74 fn update(&mut self, value: f64) {
76 self.count += 1;
77 let delta = value - self.mean;
78 self.mean += delta / self.count as f64;
79 let delta2 = value - self.mean;
80 self.m2 += delta * delta2;
81 }
82
83 fn remove(&mut self, value: f64) {
85 if self.count <= 1 {
86 *self = Self::new();
87 return;
88 }
89
90 let delta = value - self.mean;
92 self.mean = (self.mean * self.count as f64 - value) / (self.count - 1) as f64;
93 let delta2 = value - self.mean;
94 self.m2 -= delta * delta2;
95 self.m2 = self.m2.max(0.0); self.count -= 1;
97 }
98
99 fn variance(&self) -> f64 {
101 if self.count < 2 {
102 return 1.0; }
104 (self.m2 / (self.count - 1) as f64).max(0.001)
105 }
106
107 fn std_dev(&self) -> f64 {
109 self.variance().sqrt()
110 }
111
112 fn z_score(&self, value: f64) -> f64 {
114 if self.count < 3 {
115 return 0.0; }
117 (value - self.mean) / self.std_dev()
118 }
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize)]
123struct BaselinePoint {
124 length: f64,
125 complexity: f64,
126 emotional: f64,
127 formality: f64,
128 urgency: f64,
129 uncertainty: f64,
130}
131
132impl From<&LinguisticFeatures> for BaselinePoint {
133 fn from(f: &LinguisticFeatures) -> Self {
134 Self {
135 length: f.word_count as f64,
136 complexity: f.complexity_score() as f64,
137 emotional: f.emotional_intensity() as f64,
138 formality: f.formality_score() as f64,
139 urgency: f.urgency_score() as f64,
140 uncertainty: f.uncertainty_score() as f64,
141 }
142 }
143}
144
145#[derive(Clone, Debug, Serialize, Deserialize)]
147pub struct Baseline {
148 max_size: usize,
150 history: VecDeque<BaselinePoint>,
152 length_stat: RunningStat,
154 complexity_stat: RunningStat,
155 emotional_stat: RunningStat,
156 formality_stat: RunningStat,
157 urgency_stat: RunningStat,
158 uncertainty_stat: RunningStat,
159}
160
161impl Baseline {
162 pub fn new(max_size: usize) -> Self {
164 Self {
165 max_size,
166 history: VecDeque::with_capacity(max_size),
167 length_stat: RunningStat::new(),
168 complexity_stat: RunningStat::new(),
169 emotional_stat: RunningStat::new(),
170 formality_stat: RunningStat::new(),
171 urgency_stat: RunningStat::new(),
172 uncertainty_stat: RunningStat::new(),
173 }
174 }
175
176 pub fn add(&mut self, features: &LinguisticFeatures) {
178 let point = BaselinePoint::from(features);
179
180 if self.history.len() >= self.max_size {
182 if let Some(old) = self.history.pop_front() {
183 self.length_stat.remove(old.length);
184 self.complexity_stat.remove(old.complexity);
185 self.emotional_stat.remove(old.emotional);
186 self.formality_stat.remove(old.formality);
187 self.urgency_stat.remove(old.urgency);
188 self.uncertainty_stat.remove(old.uncertainty);
189 }
190 }
191
192 self.length_stat.update(point.length);
194 self.complexity_stat.update(point.complexity);
195 self.emotional_stat.update(point.emotional);
196 self.formality_stat.update(point.formality);
197 self.urgency_stat.update(point.urgency);
198 self.uncertainty_stat.update(point.uncertainty);
199
200 self.history.push_back(point);
201 }
202
203 pub fn len(&self) -> usize {
205 self.history.len()
206 }
207
208 pub fn is_empty(&self) -> bool {
210 self.history.is_empty()
211 }
212
213 pub fn is_ready(&self) -> bool {
215 self.history.len() >= 5 }
217
218 pub fn mean_length(&self) -> f64 {
220 self.length_stat.mean
221 }
222
223 pub fn mean_complexity(&self) -> f64 {
225 self.complexity_stat.mean
226 }
227}
228
229impl Default for Baseline {
230 fn default() -> Self {
231 Self::new(50) }
233}
234
235#[derive(Clone, Debug, Default)]
237pub struct DeltaAnalyzer {
238 pub significance_threshold: f32,
240}
241
242impl DeltaAnalyzer {
243 pub fn new() -> Self {
245 Self {
246 significance_threshold: 1.5, }
248 }
249
250 pub fn with_threshold(threshold: f32) -> Self {
252 Self {
253 significance_threshold: threshold,
254 }
255 }
256
257 pub fn analyze(&self, baseline: &Baseline, features: &LinguisticFeatures) -> DeltaSignals {
262 if !baseline.is_ready() {
263 return DeltaSignals {
264 baseline_size: baseline.len(),
265 ..Default::default()
266 };
267 }
268
269 let point = BaselinePoint::from(features);
270
271 DeltaSignals {
272 length_z: baseline.length_stat.z_score(point.length) as f32,
273 complexity_z: baseline.complexity_stat.z_score(point.complexity) as f32,
274 emotional_z: baseline.emotional_stat.z_score(point.emotional) as f32,
275 formality_z: baseline.formality_stat.z_score(point.formality) as f32,
276 urgency_z: baseline.urgency_stat.z_score(point.urgency) as f32,
277 uncertainty_z: baseline.uncertainty_stat.z_score(point.uncertainty) as f32,
278 baseline_size: baseline.len(),
279 }
280 }
281
282 pub fn analyze_and_update(
286 &self,
287 baseline: &mut Baseline,
288 features: &LinguisticFeatures,
289 ) -> DeltaSignals {
290 let signals = self.analyze(baseline, features);
291 baseline.add(features);
292 signals
293 }
294
295 pub fn to_axis_adjustments(&self, signals: &DeltaSignals) -> Vec<(&'static str, f32)> {
299 let mut adjustments = Vec::new();
300
301 let z_to_adj = |z: f32| -> f32 {
303 (z / (1.0 + z.abs()) * 0.3).clamp(-0.3, 0.3)
305 };
306
307 if signals.length_z.abs() > self.significance_threshold {
309 adjustments.push(("cognitive_load", z_to_adj(-signals.length_z)));
310 }
311
312 if signals.complexity_z.abs() > self.significance_threshold {
314 adjustments.push(("tolerance_for_complexity", z_to_adj(signals.complexity_z)));
315 }
316
317 if signals.emotional_z.abs() > self.significance_threshold {
319 adjustments.push(("emotional_intensity", z_to_adj(signals.emotional_z)));
320 adjustments.push(("emotional_stability", z_to_adj(-signals.emotional_z * 0.5)));
322 }
323
324 if signals.formality_z.abs() > self.significance_threshold {
326 adjustments.push(("formality", z_to_adj(signals.formality_z)));
327 }
328
329 if signals.urgency_z.abs() > self.significance_threshold {
331 adjustments.push(("urgency_sensitivity", z_to_adj(signals.urgency_z)));
332 }
333
334 if signals.uncertainty_z.abs() > self.significance_threshold {
336 adjustments.push(("anxiety_level", z_to_adj(signals.uncertainty_z)));
337 adjustments.push(("assertiveness", z_to_adj(-signals.uncertainty_z)));
338 }
339
340 adjustments
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::features::LinguisticExtractor;
348
349 fn make_features(text: &str) -> LinguisticFeatures {
350 LinguisticExtractor::new().extract(text)
351 }
352
353 #[test]
354 fn test_baseline_building() {
355 let mut baseline = Baseline::new(10);
356
357 for _ in 0..5 {
358 baseline.add(&make_features("This is a normal message."));
359 }
360
361 assert_eq!(baseline.len(), 5);
362 assert!(baseline.is_ready());
363 }
364
365 #[test]
366 fn test_baseline_fifo() {
367 let mut baseline = Baseline::new(3);
368
369 baseline.add(&make_features("Message one."));
370 baseline.add(&make_features("Message two."));
371 baseline.add(&make_features("Message three."));
372 baseline.add(&make_features("Message four.")); assert_eq!(baseline.len(), 3);
375 }
376
377 #[test]
378 fn test_detect_urgency_spike() {
379 let mut baseline = Baseline::default();
380 let analyzer = DeltaAnalyzer::new();
381
382 for _ in 0..10 {
384 baseline.add(&make_features(
385 "Here is my regular question about the product.",
386 ));
387 }
388
389 let urgent = make_features("URGENT!!! I need help RIGHT NOW! This is critical!!!");
391 let signals = analyzer.analyze(&baseline, &urgent);
392
393 assert!(signals.urgency_z > 1.0);
395 assert!(signals.emotional_z > 1.0);
396 }
397
398 #[test]
399 fn test_detect_terseness() {
400 let mut baseline = Baseline::default();
401 let analyzer = DeltaAnalyzer::new();
402
403 for _ in 0..10 {
405 baseline.add(&make_features(
406 "I wanted to ask about the features of your product and understand \
407 how it might help with my specific use case in data processing.",
408 ));
409 }
410
411 let terse = make_features("ok");
413 let signals = analyzer.analyze(&baseline, &terse);
414
415 assert!(signals.length_z < -1.0);
417 }
418
419 #[test]
420 fn test_not_enough_baseline() {
421 let baseline = Baseline::default();
422 let analyzer = DeltaAnalyzer::new();
423
424 let features = make_features("Hello world");
425 let signals = analyzer.analyze(&baseline, &features);
426
427 assert_eq!(signals.length_z, 0.0);
429 assert!(!baseline.is_ready());
430 }
431
432 #[test]
433 fn test_axis_adjustments() {
434 let analyzer = DeltaAnalyzer::new();
435
436 let signals = DeltaSignals {
437 urgency_z: 2.5,
438 emotional_z: 0.5, ..Default::default()
440 };
441
442 let adjustments = analyzer.to_axis_adjustments(&signals);
443
444 assert!(adjustments
446 .iter()
447 .any(|(axis, _)| *axis == "urgency_sensitivity"));
448 assert!(!adjustments
450 .iter()
451 .any(|(axis, _)| *axis == "emotional_intensity"));
452 }
453
454 #[test]
455 fn test_max_deviation() {
456 let signals = DeltaSignals {
457 length_z: 0.5,
458 urgency_z: 3.0,
459 emotional_z: -2.0,
460 ..Default::default()
461 };
462
463 let (metric, z) = signals.max_deviation();
464 assert_eq!(metric, "urgency");
465 assert_eq!(z, 3.0);
466 }
467}