1use 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() } }