Skip to main content

chasm/intelligence/
mod.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: AGPL-3.0-only
3//! AI Intelligence Module
4
5use crate::models::ChatSession;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Topic {
12    pub name: String,
13    pub confidence: f32,
14    pub keywords: Vec<String>,
15}
16
17pub struct TopicExtractor {
18    patterns: HashMap<String, Vec<String>>,
19    min_confidence: f32,
20}
21
22impl TopicExtractor {
23    pub fn new() -> Self {
24        let mut patterns = HashMap::new();
25        patterns.insert("rust".to_string(), vec!["rust", "cargo", "crate"].into_iter().map(String::from).collect());
26        patterns.insert("python".to_string(), vec!["python", "pip", "django"].into_iter().map(String::from).collect());
27        Self { patterns, min_confidence: 0.1 }
28    }
29
30    pub fn extract(&self, session: &ChatSession) -> Vec<Topic> {
31        let content = session.collect_all_text().to_lowercase();
32        let word_count = content.split_whitespace().count().max(1) as f32;
33        let mut topics = Vec::new();
34        for (name, keywords) in &self.patterns {
35            let mut matched = Vec::new();
36            let mut count = 0;
37            for kw in keywords {
38                if content.contains(kw) {
39                    matched.push(kw.clone());
40                    count += content.matches(kw).count();
41                }
42            }
43            if !matched.is_empty() {
44                let confidence = (count as f32 / word_count).min(1.0);
45                if confidence >= self.min_confidence {
46                    topics.push(Topic { name: name.clone(), confidence, keywords: matched });
47                }
48            }
49        }
50        topics
51    }
52}
53
54impl Default for TopicExtractor { fn default() -> Self { Self::new() } }
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ConversationInsights {
58    pub session_id: String,
59    pub generated_at: DateTime<Utc>,
60    pub key_points: Vec<KeyPoint>,
61    pub questions: Vec<String>,
62    pub stats: ConversationStats,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct KeyPoint {
67    pub summary: String,
68    pub importance: f32,
69    pub category: String,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ConversationStats {
74    pub message_count: usize,
75    pub user_messages: usize,
76    pub assistant_messages: usize,
77    pub turns: usize,
78}
79
80pub struct InsightsGenerator;
81
82impl InsightsGenerator {
83    pub fn new() -> Self { Self }
84    
85    pub fn generate(&self, session: &ChatSession) -> ConversationInsights {
86        let user_msgs = session.user_messages();
87        let asst_msgs = session.assistant_responses();
88        ConversationInsights {
89            session_id: session.session_id.clone().unwrap_or_default(),
90            generated_at: Utc::now(),
91            key_points: Vec::new(),
92            questions: user_msgs.iter().flat_map(|m| m.split('.').filter(|s| s.contains('?'))).map(String::from).collect(),
93            stats: ConversationStats {
94                message_count: user_msgs.len() + asst_msgs.len(),
95                user_messages: user_msgs.len(),
96                assistant_messages: asst_msgs.len(),
97                turns: session.requests.len(),
98            },
99        }
100    }
101}
102
103impl Default for InsightsGenerator { fn default() -> Self { Self::new() } }
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct Sentiment { pub score: f32, pub label: String, pub confidence: f32 }
107
108pub struct SentimentAnalyzer {
109    positive: HashSet<String>,
110    negative: HashSet<String>,
111}
112
113impl SentimentAnalyzer {
114    pub fn new() -> Self {
115        Self {
116            positive: vec!["good", "great", "thanks", "helpful", "works"].into_iter().map(String::from).collect(),
117            negative: vec!["bad", "error", "fail", "wrong", "bug"].into_iter().map(String::from).collect(),
118        }
119    }
120    
121    pub fn analyze(&self, session: &ChatSession) -> Sentiment {
122        let text = session.collect_all_text().to_lowercase();
123        let words: Vec<&str> = text.split_whitespace().collect();
124        let pos = words.iter().filter(|w| self.positive.contains(**w)).count();
125        let neg = words.iter().filter(|w| self.negative.contains(**w)).count();
126        let total = pos + neg;
127        let score = if total > 0 { (pos as f32 - neg as f32) / total as f32 } else { 0.0 };
128        let label = if score > 0.3 { "positive" } else if score < -0.3 { "negative" } else { "neutral" };
129        Sentiment { score, label: label.to_string(), confidence: if total > 0 { 0.8 } else { 0.5 } }
130    }
131}
132
133impl Default for SentimentAnalyzer { fn default() -> Self { Self::new() } }
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct QualityScore { pub overall: u8, pub clarity: u8, pub completeness: u8 }
137
138pub struct QualityScorer;
139
140impl QualityScorer {
141    pub fn new() -> Self { Self }
142    pub fn score(&self, session: &ChatSession) -> QualityScore {
143        let msgs = session.user_messages();
144        let clarity = if msgs.is_empty() { 50 } else { 70 };
145        let completeness = if session.assistant_responses().is_empty() { 0 } else { 80 };
146        QualityScore { overall: (clarity + completeness) / 2, clarity, completeness }
147    }
148}
149
150impl Default for QualityScorer { fn default() -> Self { Self::new() } }
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct SimilarityResult { pub session_a_id: String, pub session_b_id: String, pub score: f32 }
154
155pub struct SimilarityDetector;
156
157impl SimilarityDetector {
158    pub fn new() -> Self { Self }
159    pub fn compare(&self, a: &ChatSession, b: &ChatSession) -> SimilarityResult {
160        let ta = a.collect_all_text().to_lowercase();
161        let tb = b.collect_all_text().to_lowercase();
162        let wa: HashSet<&str> = ta.split_whitespace().collect();
163        let wb: HashSet<&str> = tb.split_whitespace().collect();
164        let inter = wa.intersection(&wb).count();
165        let union = wa.union(&wb).count();
166        let score = if union > 0 { inter as f32 / union as f32 } else { 0.0 };
167        SimilarityResult {
168            session_a_id: a.session_id.clone().unwrap_or_default(),
169            session_b_id: b.session_id.clone().unwrap_or_default(),
170            score,
171        }
172    }
173}
174
175impl Default for SimilarityDetector { fn default() -> Self { Self::new() } }