use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, VecDeque};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConversationRole {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GarrisonEntry {
pub id: Uuid,
pub role: ConversationRole,
pub content: String,
pub timestamp: DateTime<Utc>,
pub metadata: HashMap<String, Value>,
pub token_count: Option<u32>,
}
impl GarrisonEntry {
pub fn new(role: ConversationRole, content: String) -> Self {
Self {
id: Uuid::new_v4(),
role,
content,
timestamp: Utc::now(),
metadata: HashMap::new(),
token_count: None,
}
}
pub fn with_metadata(
role: ConversationRole,
content: String,
metadata: HashMap<String, Value>,
) -> Self {
Self {
id: Uuid::new_v4(),
role,
content,
timestamp: Utc::now(),
metadata,
token_count: None,
}
}
pub fn with_token_count(role: ConversationRole, content: String, token_count: u32) -> Self {
Self {
id: Uuid::new_v4(),
role,
content,
timestamp: Utc::now(),
metadata: HashMap::new(),
token_count: Some(token_count),
}
}
pub fn validate(&self) -> Result<(), String> {
if self.content.is_empty() {
return Err("Content cannot be empty".to_string());
}
Ok(())
}
pub fn set_token_count(&mut self, count: u32) {
self.token_count = Some(count);
}
pub fn add_metadata(&mut self, key: String, value: Value) {
self.metadata.insert(key, value);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GarrisonType {
ShortTerm,
LongTerm,
Episodic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EvictionStrategy {
FIFO,
ImportanceBased,
SlidingWindow,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GarrisonConfig {
pub max_entries: usize,
pub max_tokens: Option<u32>,
pub eviction_strategy: EvictionStrategy,
pub preserve_recent_count: usize,
}
impl Default for GarrisonConfig {
fn default() -> Self {
Self {
max_entries: 100,
max_tokens: Some(4000),
eviction_strategy: EvictionStrategy::ImportanceBased,
preserve_recent_count: 10,
}
}
}
impl GarrisonConfig {
pub fn new(max_entries: usize, max_tokens: Option<u32>) -> Self {
Self {
max_entries,
max_tokens,
..Default::default()
}
}
pub fn with_eviction_strategy(mut self, strategy: EvictionStrategy) -> Self {
self.eviction_strategy = strategy;
self
}
pub fn with_preserve_recent(mut self, count: usize) -> Self {
self.preserve_recent_count = count;
self
}
}
#[derive(Debug, Clone)]
pub struct ConversationHistory {
entries: VecDeque<GarrisonEntry>,
config: GarrisonConfig,
}
impl ConversationHistory {
pub fn new(config: GarrisonConfig) -> Self {
Self {
entries: VecDeque::new(),
config,
}
}
pub fn add(&mut self, entry: GarrisonEntry) {
self.entries.push_back(entry);
self.apply_windowing();
}
pub fn get_recent(&self, limit: usize) -> Vec<&GarrisonEntry> {
let start = self.entries.len().saturating_sub(limit);
self.entries.range(start..).collect()
}
pub fn get_all(&self) -> Vec<&GarrisonEntry> {
self.entries.iter().collect()
}
pub fn total_tokens(&self) -> u32 {
self.entries.iter().filter_map(|e| e.token_count).sum()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
fn apply_windowing(&mut self) {
while self.entries.len() > self.config.max_entries {
self.evict_entry();
}
if let Some(max_tokens) = self.config.max_tokens {
while self.total_tokens() > max_tokens && !self.entries.is_empty() {
self.evict_entry();
}
}
}
fn evict_entry(&mut self) {
match self.config.eviction_strategy {
EvictionStrategy::FIFO => {
self.entries.pop_front();
}
EvictionStrategy::SlidingWindow => {
self.entries.pop_front();
}
EvictionStrategy::ImportanceBased => {
self.evict_importance_based();
}
}
}
fn evict_importance_based(&mut self) {
let total_entries = self.entries.len();
if total_entries == 0 {
return;
}
let preserve_count = self.config.preserve_recent_count.min(total_entries);
let recent_start_idx = total_entries.saturating_sub(preserve_count);
for i in 0..recent_start_idx {
if self.entries[i].role != ConversationRole::System {
self.entries.remove(i);
return;
}
}
for i in recent_start_idx..total_entries {
if self.entries[i].role != ConversationRole::System {
self.entries.remove(i);
return;
}
}
self.entries.pop_front();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conversation_role_serialization() {
let role = ConversationRole::User;
let json = serde_json::to_string(&role).unwrap();
assert_eq!(json, "\"user\"");
let deserialized: ConversationRole = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, role);
}
#[test]
fn test_garrison_entry_creation() {
let entry = GarrisonEntry::new(ConversationRole::User, "Test message".to_string());
assert_eq!(entry.role, ConversationRole::User);
assert_eq!(entry.content, "Test message");
assert!(entry.token_count.is_none());
assert!(entry.metadata.is_empty());
}
#[test]
fn test_garrison_entry_validation() {
let valid_entry = GarrisonEntry::new(ConversationRole::User, "Valid content".to_string());
assert!(valid_entry.validate().is_ok());
let invalid_entry = GarrisonEntry::new(ConversationRole::User, String::new());
assert!(invalid_entry.validate().is_err());
}
#[test]
fn test_garrison_entry_with_token_count() {
let entry = GarrisonEntry::with_token_count(
ConversationRole::Assistant,
"Response".to_string(),
42,
);
assert_eq!(entry.token_count, Some(42));
}
#[test]
fn test_garrison_entry_serialization() {
let entry =
GarrisonEntry::with_token_count(ConversationRole::User, "Test message".to_string(), 10);
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("\"role\":\"user\""));
assert!(json.contains("\"content\":\"Test message\""));
assert!(json.contains("\"token_count\":10"));
let deserialized: GarrisonEntry = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.role, entry.role);
assert_eq!(deserialized.content, entry.content);
assert_eq!(deserialized.token_count, entry.token_count);
assert_eq!(deserialized.id, entry.id);
}
#[test]
fn test_conversation_history_add_and_get() {
let config = GarrisonConfig::default();
let mut history = ConversationHistory::new(config);
history.add(GarrisonEntry::new(
ConversationRole::User,
"First".to_string(),
));
history.add(GarrisonEntry::new(
ConversationRole::Assistant,
"Second".to_string(),
));
assert_eq!(history.len(), 2);
let recent = history.get_recent(2);
assert_eq!(recent.len(), 2);
}
#[test]
fn test_conversation_history_windowing_by_count() {
let config = GarrisonConfig::new(3, None);
let mut history = ConversationHistory::new(config);
for i in 0..5 {
history.add(GarrisonEntry::new(
ConversationRole::User,
format!("Message {}", i),
));
}
assert_eq!(history.len(), 3);
let entries = history.get_all();
assert_eq!(entries[0].content, "Message 2");
}
#[test]
fn test_conversation_history_token_counting() {
let config = GarrisonConfig::default();
let mut history = ConversationHistory::new(config);
history.add(GarrisonEntry::with_token_count(
ConversationRole::User,
"First".to_string(),
10,
));
history.add(GarrisonEntry::with_token_count(
ConversationRole::Assistant,
"Second".to_string(),
20,
));
assert_eq!(history.total_tokens(), 30);
}
#[test]
fn test_importance_based_eviction_preserves_system() {
let config = GarrisonConfig::new(3, None)
.with_eviction_strategy(EvictionStrategy::ImportanceBased)
.with_preserve_recent(1);
let mut history = ConversationHistory::new(config);
history.add(GarrisonEntry::new(
ConversationRole::System,
"System prompt".to_string(),
));
history.add(GarrisonEntry::new(
ConversationRole::User,
"User 1".to_string(),
));
history.add(GarrisonEntry::new(
ConversationRole::User,
"User 2".to_string(),
));
history.add(GarrisonEntry::new(
ConversationRole::User,
"User 3".to_string(),
));
assert_eq!(history.len(), 3);
let entries = history.get_all();
assert_eq!(entries[0].role, ConversationRole::System);
assert_eq!(entries[1].content, "User 2");
}
#[test]
fn test_fifo_eviction() {
let config = GarrisonConfig::new(3, None).with_eviction_strategy(EvictionStrategy::FIFO);
let mut history = ConversationHistory::new(config);
for i in 0..5 {
history.add(GarrisonEntry::new(
ConversationRole::User,
format!("Message {}", i),
));
}
assert_eq!(history.len(), 3);
let entries = history.get_all();
assert_eq!(entries[0].content, "Message 2");
assert_eq!(entries[1].content, "Message 3");
assert_eq!(entries[2].content, "Message 4");
}
#[test]
fn test_empty_history_operations() {
let config = GarrisonConfig::default();
let history = ConversationHistory::new(config);
assert_eq!(history.len(), 0);
assert_eq!(history.total_tokens(), 0);
let recent = history.get_recent(10);
assert!(recent.is_empty());
let all = history.get_all();
assert!(all.is_empty());
}
}