use crate::merkle::Hash;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompressionLevel {
Full,
Summary,
Abstract,
Minimal,
}
impl CompressionLevel {
pub fn ratio(&self) -> f64 {
match self {
Self::Full => 0.0,
Self::Summary => 0.3,
Self::Abstract => 0.6,
Self::Minimal => 0.9,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextPacket {
pub id: Uuid,
pub content: String,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub compression: CompressionLevel,
pub hash: Hash,
pub parent_hash: Option<Hash>,
pub trace_root: Option<Hash>,
pub source_agent: Option<Uuid>,
pub importance: f64,
}
pub const MAX_CONTENT_SIZE: usize = 50 * 1024;
impl ContextPacket {
pub fn new(content: &str) -> Self {
let content = if content.len() > MAX_CONTENT_SIZE {
tracing::warn!(
size = content.len(),
max = MAX_CONTENT_SIZE,
"Context content truncated"
);
&content[..MAX_CONTENT_SIZE]
} else {
content
};
let hash = Self::compute_hash(content);
Self {
id: Uuid::new_v4(),
content: content.to_string(),
created_at: Utc::now(),
expires_at: None,
compression: CompressionLevel::Full,
hash,
parent_hash: None,
trace_root: None,
source_agent: None,
importance: 0.5,
}
}
pub fn with_ttl(content: &str, ttl: Duration) -> Self {
let mut packet = Self::new(content);
packet.expires_at = Some(Utc::now() + ttl);
packet
}
pub fn compute_hash(content: &str) -> Hash {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
Hash(hasher.finalize().into())
}
pub fn is_expired(&self) -> bool {
self.expires_at.is_some_and(|exp| Utc::now() > exp)
}
pub fn age(&self) -> Duration {
Utc::now().signed_duration_since(self.created_at)
}
pub fn compress(&self, level: CompressionLevel) -> Self {
let max_len = ((1.0 - level.ratio()) * self.content.len() as f64) as usize;
let compressed_content = if max_len < self.content.len() {
format!("{}...", &self.content[..max_len.max(10)])
} else {
self.content.clone()
};
let mut packet = Self::new(&compressed_content);
packet.compression = level;
packet.parent_hash = Some(self.hash.clone());
packet.source_agent = self.source_agent;
packet.importance = self.importance;
packet
}
pub fn chain_to(&mut self, parent: &ContextPacket) {
self.parent_hash = Some(parent.hash.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_packet() {
let packet = ContextPacket::new("Hello, world!");
assert_eq!(packet.content, "Hello, world!");
assert_eq!(packet.compression, CompressionLevel::Full);
assert!(!packet.is_expired());
}
#[test]
fn test_packet_with_ttl() {
let packet = ContextPacket::with_ttl("Temporary data", Duration::hours(1));
assert!(packet.expires_at.is_some());
assert!(!packet.is_expired());
}
#[test]
fn test_compress_packet() {
let packet = ContextPacket::new(
"This is a long piece of content that should be compressed when needed.",
);
let compressed = packet.compress(CompressionLevel::Summary);
assert_eq!(compressed.compression, CompressionLevel::Summary);
assert!(compressed.content.len() <= packet.content.len());
}
}