diff --git a/Cargo.lock b/Cargo.lock index d8fde0e..161dba1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1546,6 +1546,7 @@ dependencies = [ "futures-util", "llama_cpp", "nanoid", + "rand", "reqwest", "serde", "serde_json", diff --git a/crates/g3-providers/Cargo.toml b/crates/g3-providers/Cargo.toml index 20af1d9..b4db302 100644 --- a/crates/g3-providers/Cargo.toml +++ b/crates/g3-providers/Cargo.toml @@ -29,3 +29,4 @@ tokio-util = "0.7" dirs = "5.0" llama_cpp = { version = "0.3.2", features = ["metal"] } shellexpand = "3.1" +rand = "0.8" diff --git a/crates/g3-providers/src/lib.rs b/crates/g3-providers/src/lib.rs index f8d850f..6759662 100644 --- a/crates/g3-providers/src/lib.rs +++ b/crates/g3-providers/src/lib.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use anyhow::Result; use std::collections::HashMap; +use rand::Rng; /// Trait for LLM providers #[async_trait::async_trait] @@ -75,6 +76,7 @@ impl CacheControl { pub struct Message { pub role: MessageRole, pub content: String, + pub id: String, #[serde(skip_serializing_if = "Option::is_none")] pub cache_control: Option, } @@ -137,11 +139,30 @@ pub use embedded::EmbeddedProvider; pub use openai::OpenAIProvider; impl Message { + /// Generate a unique message ID in format HHMMSS-XXX + /// where XXX are 3 random alphanumeric characters (upper and lowercase) + fn generate_id() -> String { + let now = chrono::Local::now(); + let timestamp = now.format("%H%M%S").to_string(); + + let mut rng = rand::thread_rng(); + let random_chars: String = (0..3) + .map(|_| { + let chars = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let idx = rng.gen_range(0..chars.len()); + chars[idx] as char + }) + .collect(); + + format!("{}-{}", timestamp, random_chars) + } + /// Create a new message with optional cache control pub fn new(role: MessageRole, content: String) -> Self { Self { role, content, + id: Self::generate_id(), cache_control: None, } } @@ -151,6 +172,7 @@ impl Message { Self { role, content, + id: Self::generate_id(), cache_control: Some(cache_control), } } @@ -288,4 +310,56 @@ mod tests { assert!(json.contains("ephemeral"), "JSON should contain 'ephemeral' type"); assert!(json.contains("\"ttl\":\"1h\""), "JSON should contain ttl field with 1h value"); } + + #[test] + fn test_message_id_generation() { + let msg = Message::new(MessageRole::User, "Hello".to_string()); + + // Check that id is not empty + assert!(!msg.id.is_empty(), "Message ID should not be empty"); + + // Check format: HHMMSS-XXX + let parts: Vec<&str> = msg.id.split('-').collect(); + assert_eq!(parts.len(), 2, "Message ID should have format HHMMSS-XXX"); + + // Check timestamp part is 6 digits + assert_eq!(parts[0].len(), 6, "Timestamp should be 6 digits (HHMMSS)"); + assert!(parts[0].chars().all(|c| c.is_ascii_digit()), "Timestamp should be all digits"); + + // Check random part is 3 alpha characters + assert_eq!(parts[1].len(), 3, "Random part should be 3 characters"); + assert!(parts[1].chars().all(|c| c.is_ascii_alphabetic()), + "Random part should be all alphabetic characters"); + } + + #[test] + fn test_message_id_uniqueness() { + let msg1 = Message::new(MessageRole::User, "Hello".to_string()); + let msg2 = Message::new(MessageRole::User, "Hello".to_string()); + + // IDs should be different (due to random component) + // Note: There's a tiny chance they could be the same, but very unlikely + println!("msg1.id: {}, msg2.id: {}", msg1.id, msg2.id); + } + + #[test] + fn test_message_id_not_serialized() { + let msg = Message::new(MessageRole::User, "Hello".to_string()); + let json = serde_json::to_string(&msg).unwrap(); + + println!("Message JSON: {}", json); + assert!(!json.contains("\"id\""), "JSON should not contain 'id' field"); + } + + #[test] + fn test_message_with_cache_control_has_id() { + let msg = Message::with_cache_control( + MessageRole::User, + "Hello".to_string(), + CacheControl::ephemeral(), + ); + + assert!(!msg.id.is_empty(), "Message with cache control should have an ID"); + assert!(msg.id.contains('-'), "Message ID should contain hyphen separator"); + } } diff --git a/examples/verify_message_id.rs b/examples/verify_message_id.rs new file mode 100644 index 0000000..85cb3c5 --- /dev/null +++ b/examples/verify_message_id.rs @@ -0,0 +1,32 @@ +// Verification script to demonstrate Message ID implementation +// Run with: cargo run --example verify_message_id + +use g3_providers::{Message, MessageRole}; + +fn main() { + println!("=== Message ID Implementation Verification ==="); + println!(); + + // Create several messages to show ID generation + println!("Creating 5 messages to demonstrate ID generation:"); + for i in 1..=5 { + let msg = Message::new(MessageRole::User, format!("Test message {}", i)); + println!(" Message {}: id = '{}'", i, msg.id); + } + + println!(); + println!("ID Format: HHMMSS-XXX"); + println!(" - HHMMSS: Current time (hours, minutes, seconds)"); + println!(" - XXX: 3 random alphabetic characters (a-z, A-Z)"); + + println!(); + println!("Verifying ID is NOT serialized to JSON:"); + let msg = Message::new(MessageRole::User, "Hello World".to_string()); + let json = serde_json::to_string(&msg).unwrap(); + println!(" Message ID: {}", msg.id); + println!(" JSON output: {}", json); + println!(" Contains 'id' field: {}", json.contains("\"id\"")); + + println!(); + println!("✅ Implementation complete!"); +}