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 && last.agent.is_empty()
63 && last.project_id.is_none()
64 {
65 last.user = message.to_string();
66 last.timestamp = Utc::now();
67 return;
68 }
69
70 self.turns.push_back(turn);
71
72 while self.turns.len() > self.max_turns {
74 self.turns.pop_front();
75 }
76 }
77
78 pub fn push_agent(&mut self, response: &str, project_id: Option<Uuid>) {
80 if let Some(last) = self.turns.back_mut() {
81 last.agent = truncate_response(response, 200);
82 last.project_id = project_id;
83 }
84 }
85
86 pub fn recent(&self, n: usize) -> Vec<&ConversationTurn> {
88 self.turns.iter().rev().take(n).collect()
89 }
90
91 pub fn turns(&self) -> VecDeque<ConversationTurn> {
93 self.turns.clone()
94 }
95
96 pub fn len(&self) -> usize {
98 self.turns.len()
99 }
100
101 pub fn is_empty(&self) -> bool {
103 self.turns.is_empty()
104 }
105
106 pub fn should_check_topic(&self, min_turns: usize) -> bool {
108 self.turns_since_topic_check >= min_turns || self.pattern_changed()
109 }
110
111 pub fn mark_topic_checked(&mut self) {
113 self.turns_since_topic_check = 0;
114 }
115
116 pub fn record_turn(&mut self, min_turns: usize) -> bool {
118 self.turns_since_topic_check += 1;
119 self.should_check_topic(min_turns)
120 }
121
122 pub fn pattern_changed(&self) -> bool {
124 if self.turns.len() < 4 {
125 return false;
126 }
127
128 let all_turns: Vec<_> = self.turns.iter().collect();
129 let recent = &all_turns[all_turns.len() - 2..];
130 let previous = &all_turns[all_turns.len() - 4..all_turns.len() - 2];
131
132 let avg_recent =
133 recent.iter().map(|t| word_count(&t.user)).sum::<usize>() as f64 / recent.len() as f64;
134 let avg_prev = previous.iter().map(|t| word_count(&t.user)).sum::<usize>() as f64
135 / previous.len() as f64;
136
137 let ratio = avg_recent / avg_prev.max(1.0);
138 !(0.5..=2.0).contains(&ratio)
139 }
140
141 pub fn clear(&mut self) {
143 self.turns.clear();
144 self.turns_since_topic_check = 0;
145 }
146}
147
148fn word_count(s: &str) -> usize {
150 s.split_whitespace().count()
151}
152
153fn truncate_response(response: &str, max_len: usize) -> String {
155 if response.len() <= max_len {
156 response.to_string()
157 } else {
158 let end = response
159 .char_indices()
160 .take_while(|(idx, _)| *idx < max_len)
161 .last()
162 .map(|(idx, c)| idx + c.len_utf8())
163 .unwrap_or(0);
164 if end == 0 {
165 "...".to_string()
166 } else {
167 format!("{}...", &response[..end])
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_push_user_and_agent() {
178 let mut buf = ConversationBuffer::new(10);
179 assert!(buf.is_empty());
180
181 buf.push_user("Hello, how are you?");
182 assert_eq!(buf.len(), 1);
183
184 buf.push_agent("I'm doing well!", None);
185 assert_eq!(buf.turns[0].agent, "I'm doing well!");
186 }
187
188 #[test]
189 fn test_max_capacity() {
190 let mut buf = ConversationBuffer::new(3);
191
192 for i in 1..=5 {
193 buf.push_user(&format!("msg{}", i));
194 buf.push_agent("r", None);
195 }
196
197 assert_eq!(buf.len(), 3);
198 assert_eq!(buf.recent(1)[0].user, "msg5");
199 }
200
201 #[test]
202 fn test_pattern_changed() {
203 let mut buf = ConversationBuffer::new(10);
204
205 for _ in 0..3 {
206 buf.push_user("hi");
207 buf.push_agent("hi", None);
208 }
209 assert!(!buf.pattern_changed());
210
211 buf.push_user("This is a very long message with many many many words to trigger detection");
212 buf.push_agent("ok", None);
213 assert!(buf.pattern_changed());
214 }
215}