oxios_kernel/project/
conversation_buffer.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::VecDeque;
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ConversationTurn {
14 pub user: String,
16 pub agent: String,
18 pub project_id: Option<Uuid>,
20 pub timestamp: DateTime<Utc>,
22}
23
24#[derive(Debug, Clone)]
26pub struct ConversationBuffer {
27 turns: VecDeque<ConversationTurn>,
29 max_turns: usize,
31 turns_since_topic_check: usize,
33}
34
35impl Default for ConversationBuffer {
36 fn default() -> Self {
37 Self::new(50)
38 }
39}
40
41impl ConversationBuffer {
42 pub fn new(max_turns: usize) -> Self {
44 Self {
45 turns: VecDeque::with_capacity(max_turns),
46 max_turns,
47 turns_since_topic_check: 0,
48 }
49 }
50
51 pub fn push_user(&mut self, message: &str) {
53 let turn = ConversationTurn {
54 user: message.to_string(),
55 agent: String::new(),
56 project_id: None,
57 timestamp: Utc::now(),
58 };
59
60 if let Some(last) = self.turns.back_mut() {
62 if last.agent.is_empty() && last.project_id.is_none() {
63 last.user = message.to_string();
64 last.timestamp = Utc::now();
65 return;
66 }
67 }
68
69 self.turns.push_back(turn);
70
71 while self.turns.len() > self.max_turns {
73 self.turns.pop_front();
74 }
75 }
76
77 pub fn push_agent(&mut self, response: &str, project_id: Option<Uuid>) {
79 if let Some(last) = self.turns.back_mut() {
80 last.agent = truncate_response(response, 200);
81 last.project_id = project_id;
82 }
83 }
84
85 pub fn recent(&self, n: usize) -> Vec<&ConversationTurn> {
87 self.turns.iter().rev().take(n).collect()
88 }
89
90 pub fn turns(&self) -> VecDeque<ConversationTurn> {
92 self.turns.clone()
93 }
94
95 pub fn len(&self) -> usize {
97 self.turns.len()
98 }
99
100 pub fn is_empty(&self) -> bool {
102 self.turns.is_empty()
103 }
104
105 pub fn should_check_topic(&self, min_turns: usize) -> bool {
107 self.turns_since_topic_check >= min_turns || self.pattern_changed()
108 }
109
110 pub fn mark_topic_checked(&mut self) {
112 self.turns_since_topic_check = 0;
113 }
114
115 pub fn record_turn(&mut self, min_turns: usize) -> bool {
117 self.turns_since_topic_check += 1;
118 self.should_check_topic(min_turns)
119 }
120
121 pub fn pattern_changed(&self) -> bool {
123 if self.turns.len() < 4 {
124 return false;
125 }
126
127 let all_turns: Vec<_> = self.turns.iter().collect();
128 let recent = &all_turns[all_turns.len() - 2..];
129 let previous = &all_turns[all_turns.len() - 4..all_turns.len() - 2];
130
131 let avg_recent =
132 recent.iter().map(|t| word_count(&t.user)).sum::<usize>() as f64 / recent.len() as f64;
133 let avg_prev = previous.iter().map(|t| word_count(&t.user)).sum::<usize>() as f64
134 / previous.len() as f64;
135
136 let ratio = avg_recent / avg_prev.max(1.0);
137 !(0.5..=2.0).contains(&ratio)
138 }
139
140 pub fn clear(&mut self) {
142 self.turns.clear();
143 self.turns_since_topic_check = 0;
144 }
145}
146
147fn word_count(s: &str) -> usize {
149 s.split_whitespace().count()
150}
151
152fn truncate_response(response: &str, max_len: usize) -> String {
154 if response.len() <= max_len {
155 response.to_string()
156 } else {
157 let end = response
158 .char_indices()
159 .take_while(|(idx, _)| *idx < max_len)
160 .last()
161 .map(|(idx, c)| idx + c.len_utf8())
162 .unwrap_or(0);
163 if end == 0 {
164 "...".to_string()
165 } else {
166 format!("{}...", &response[..end])
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_push_user_and_agent() {
177 let mut buf = ConversationBuffer::new(10);
178 assert!(buf.is_empty());
179
180 buf.push_user("Hello, how are you?");
181 assert_eq!(buf.len(), 1);
182
183 buf.push_agent("I'm doing well!", None);
184 assert_eq!(buf.turns[0].agent, "I'm doing well!");
185 }
186
187 #[test]
188 fn test_max_capacity() {
189 let mut buf = ConversationBuffer::new(3);
190
191 for i in 1..=5 {
192 buf.push_user(&format!("msg{}", i));
193 buf.push_agent("r", None);
194 }
195
196 assert_eq!(buf.len(), 3);
197 assert_eq!(buf.recent(1)[0].user, "msg5");
198 }
199
200 #[test]
201 fn test_pattern_changed() {
202 let mut buf = ConversationBuffer::new(10);
203
204 for _ in 0..3 {
205 buf.push_user("hi");
206 buf.push_agent("hi", None);
207 }
208 assert!(!buf.pattern_changed());
209
210 buf.push_user("This is a very long message with many many many words to trigger detection");
211 buf.push_agent("ok", None);
212 assert!(buf.pattern_changed());
213 }
214}