use crate::ir::Envelope;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct ConversationContext {
id: String,
history: Vec<Envelope>,
system_envelope: Option<Envelope>,
max_history_len: usize,
}
impl Default for ConversationContext {
fn default() -> Self {
Self::new()
}
}
impl ConversationContext {
const DEFAULT_MAX_HISTORY_LEN: usize = 50;
pub fn new() -> Self {
Self {
id: Uuid::new_v4().to_string(),
history: Vec::new(),
system_envelope: None,
max_history_len: Self::DEFAULT_MAX_HISTORY_LEN,
}
}
pub fn with_id(id: String) -> Self {
Self {
id,
history: Vec::new(),
system_envelope: None,
max_history_len: Self::DEFAULT_MAX_HISTORY_LEN,
}
}
pub fn with_max_history_len(mut self, len: usize) -> Self {
self.max_history_len = len;
self
}
pub fn with_system(mut self, envelope: Envelope) -> Self {
self.system_envelope = Some(envelope);
self
}
pub fn id(&self) -> &str {
&self.id
}
pub fn max_history_len(&self) -> usize {
self.max_history_len
}
pub fn system_envelope(&self) -> Option<&Envelope> {
self.system_envelope.as_ref()
}
pub fn push(&mut self, envelope: Envelope) {
self.history.push(envelope);
while self.history.len() > self.max_history_len {
self.history.remove(0);
}
}
pub fn history(&self) -> &[Envelope] {
&self.history
}
pub fn context_for_llm(&self) -> Vec<&Envelope> {
let mut result = Vec::with_capacity(self.history.len() + 1);
if let Some(ref system) = self.system_envelope {
result.push(system);
}
for envelope in &self.history {
result.push(envelope);
}
result
}
pub fn clear(&mut self) {
self.history.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{EnvelopeKind, MessageRole};
#[test]
fn test_new_creates_uuid() {
let ctx1 = ConversationContext::new();
let ctx2 = ConversationContext::new();
assert_ne!(ctx1.id(), ctx2.id());
assert_eq!(ctx1.id().len(), 36);
}
#[test]
fn test_new_has_empty_history() {
let ctx = ConversationContext::new();
assert!(ctx.history().is_empty());
assert!(ctx.system_envelope().is_none());
}
#[test]
fn test_default_max_history_len() {
let ctx = ConversationContext::new();
assert_eq!(ctx.max_history_len(), 50);
}
#[test]
fn test_with_id() {
let ctx = ConversationContext::with_id("custom-id".to_string());
assert_eq!(ctx.id(), "custom-id");
}
#[test]
fn test_with_max_history_len() {
let ctx = ConversationContext::new().with_max_history_len(100);
assert_eq!(ctx.max_history_len(), 100);
}
#[test]
fn test_with_system() {
let system_envelope = Envelope::new(EnvelopeKind::Text("System prompt".to_string()))
.with_role(MessageRole::System);
let ctx = ConversationContext::new().with_system(system_envelope.clone());
assert!(ctx.system_envelope().is_some());
assert!(ctx.system_envelope().unwrap().is_system_message());
}
#[test]
fn test_push_and_history() {
let mut ctx = ConversationContext::new();
ctx.push(
Envelope::new(EnvelopeKind::Text("Hello".to_string())).with_role(MessageRole::User),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("Hi there".to_string()))
.with_role(MessageRole::Assistant),
);
let history = ctx.history();
assert_eq!(history.len(), 2);
assert!(history[0].is_user_message());
assert!(history[1].is_assistant_message());
}
#[test]
fn test_fifo_pruning() {
let mut ctx = ConversationContext::new().with_max_history_len(3);
for i in 0..5 {
ctx.push(
Envelope::new(EnvelopeKind::Text(format!("Message {}", i)))
.with_role(MessageRole::User),
);
}
assert_eq!(ctx.history().len(), 3);
if let EnvelopeKind::Text(text) = &ctx.history()[0].kind {
assert_eq!(text, "Message 2");
}
if let EnvelopeKind::Text(text) = &ctx.history()[1].kind {
assert_eq!(text, "Message 3");
}
if let EnvelopeKind::Text(text) = &ctx.history()[2].kind {
assert_eq!(text, "Message 4");
}
}
#[test]
fn test_context_for_llm_with_system() {
let mut ctx = ConversationContext::new().with_system(
Envelope::new(EnvelopeKind::Text("You are helpful.".to_string()))
.with_role(MessageRole::System),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("Hello".to_string())).with_role(MessageRole::User),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("Hi!".to_string())).with_role(MessageRole::Assistant),
);
let messages = ctx.context_for_llm();
assert_eq!(messages.len(), 3);
assert!(messages[0].is_system_message());
assert!(messages[1].is_user_message());
assert!(messages[2].is_assistant_message());
}
#[test]
fn test_context_for_llm_without_system() {
let mut ctx = ConversationContext::new();
ctx.push(
Envelope::new(EnvelopeKind::Text("Hello".to_string())).with_role(MessageRole::User),
);
let messages = ctx.context_for_llm();
assert_eq!(messages.len(), 1);
assert!(messages[0].is_user_message());
}
#[test]
fn test_clear_preserves_system_and_id() {
let original_id = "test-id".to_string();
let mut ctx = ConversationContext::with_id(original_id.clone()).with_system(
Envelope::new(EnvelopeKind::Text("System".to_string())).with_role(MessageRole::System),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("User msg".to_string())).with_role(MessageRole::User),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("Assistant msg".to_string()))
.with_role(MessageRole::Assistant),
);
assert_eq!(ctx.history().len(), 2);
ctx.clear();
assert!(ctx.history().is_empty());
assert!(ctx.system_envelope().is_some());
assert_eq!(ctx.id(), original_id);
}
#[test]
fn test_default_trait() {
let ctx: ConversationContext = Default::default();
assert!(ctx.history().is_empty());
assert_eq!(ctx.max_history_len(), 50);
}
#[test]
fn test_context_for_llm_ordering() {
let mut ctx = ConversationContext::new().with_system(
Envelope::new(EnvelopeKind::Text("System".to_string())).with_role(MessageRole::System),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("User 1".to_string())).with_role(MessageRole::User),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("Assistant 1".to_string()))
.with_role(MessageRole::Assistant),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("User 2".to_string())).with_role(MessageRole::User),
);
ctx.push(
Envelope::new(EnvelopeKind::Text("Assistant 2".to_string()))
.with_role(MessageRole::Assistant),
);
let messages = ctx.context_for_llm();
assert_eq!(messages.len(), 5);
if let EnvelopeKind::Text(text) = &messages[0].kind {
assert_eq!(text, "System");
}
if let EnvelopeKind::Text(text) = &messages[1].kind {
assert_eq!(text, "User 1");
}
if let EnvelopeKind::Text(text) = &messages[2].kind {
assert_eq!(text, "Assistant 1");
}
if let EnvelopeKind::Text(text) = &messages[3].kind {
assert_eq!(text, "User 2");
}
if let EnvelopeKind::Text(text) = &messages[4].kind {
assert_eq!(text, "Assistant 2");
}
}
}