use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::agent::PayloadMessage;
use crate::attachment::Attachment;
pub(super) fn current_unix_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time should be after UNIX_EPOCH")
.as_secs()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MessageId(u64);
static MESSAGE_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
impl MessageId {
pub fn new() -> Self {
Self(MESSAGE_ID_COUNTER.fetch_add(1, Ordering::SeqCst))
}
pub fn as_u64(&self) -> u64 {
self.0
}
}
impl Default for MessageId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DialogueMessage {
pub id: MessageId,
pub turn: usize,
pub speaker: Speaker,
pub content: String,
pub timestamp: u64,
#[serde(default)]
pub metadata: MessageMetadata,
#[serde(default)]
pub sent_agents: SentAgents,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SentAgents {
#[serde(rename = "agents")]
Agents {
agents: Vec<Speaker>,
},
All,
}
impl Default for SentAgents {
fn default() -> Self {
Self::Agents { agents: vec![] }
}
}
impl SentAgents {
pub fn sent(&mut self, speaker: Speaker) {
match self {
Self::Agents { agents } => agents.push(speaker),
Self::All => {} }
}
pub fn is_empty(&self) -> bool {
match self {
Self::Agents { agents } => agents.is_empty(),
Self::All => false,
}
}
}
impl From<DialogueMessage> for PayloadMessage {
fn from(msg: DialogueMessage) -> PayloadMessage {
PayloadMessage {
speaker: msg.speaker,
content: msg.content,
metadata: msg.metadata,
}
}
}
impl DialogueMessage {
pub fn new(turn: usize, speaker: Speaker, content: String) -> Self {
Self {
id: MessageId::new(),
turn,
speaker,
content,
timestamp: current_unix_timestamp(),
metadata: MessageMetadata::default(),
sent_agents: SentAgents::default(),
}
}
pub fn with_metadata(&mut self, metadata: &MessageMetadata) -> Self {
self.metadata = metadata.clone();
self.clone()
}
pub fn speaker_name(&self) -> &str {
self.speaker.name()
}
pub fn speaker_role(&self) -> Option<&str> {
self.speaker.role()
}
pub fn sent_to_agents(&self) -> bool {
!self.sent_agents.is_empty()
}
pub fn sent(&mut self, speaker: Speaker) {
self.sent_agents.sent(speaker);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Speaker {
System,
User {
name: String,
role: String,
},
Agent {
name: String,
role: String,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<String>,
},
}
impl Speaker {
pub fn name(&self) -> &str {
match self {
Speaker::System => "System",
Speaker::User { name, .. } => name,
Speaker::Agent { name, .. } => name,
}
}
pub fn role(&self) -> Option<&str> {
match self {
Speaker::System => None,
Speaker::User { role, .. } => Some(role),
Speaker::Agent { role, .. } => Some(role),
}
}
pub fn icon(&self) -> Option<&str> {
match self {
Speaker::Agent { icon, .. } => icon.as_deref(),
_ => None,
}
}
pub fn display_name(&self) -> String {
match self {
Speaker::System => "System".to_string(),
Speaker::User { name, .. } => name.clone(),
Speaker::Agent { name, icon, .. } => match icon {
Some(icon) => format!("{} {}", icon, name),
None => name.clone(),
},
}
}
pub fn user(name: impl Into<String>, role: impl Into<String>) -> Self {
Self::User {
name: name.into(),
role: role.into(),
}
}
pub fn agent(name: impl Into<String>, role: impl Into<String>) -> Self {
Self::Agent {
name: name.into(),
role: role.into(),
icon: None,
}
}
pub fn agent_with_icon(
name: impl Into<String>,
role: impl Into<String>,
icon: impl Into<String>,
) -> Self {
Self::Agent {
name: name.into(),
role: role.into(),
icon: Some(icon.into()),
}
}
#[deprecated(note = "Use `agent()` instead")]
pub fn participant(name: impl Into<String>, role: impl Into<String>) -> Self {
Self::agent(name, role)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageOrigin {
IncomingPayload,
AgentGenerated,
}
impl Default for MessageOrigin {
fn default() -> Self {
Self::IncomingPayload
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageType {
Conversational,
Notification,
System,
ContextInfo,
Custom(String),
}
impl Default for MessageType {
fn default() -> Self {
Self::Conversational
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub token_count: Option<usize>,
#[serde(default)]
pub has_attachments: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_type: Option<MessageType>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<Attachment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<MessageOrigin>,
#[serde(flatten)]
pub custom: HashMap<String, serde_json::Value>,
}
impl MessageMetadata {
pub fn new() -> Self {
Self::default()
}
pub fn with_type(mut self, message_type: MessageType) -> Self {
self.message_type = Some(message_type);
self
}
pub fn with_custom(
mut self,
key: impl Into<String>,
value: impl Into<serde_json::Value>,
) -> Self {
self.custom.insert(key.into(), value.into());
self
}
pub fn is_type(&self, message_type: &MessageType) -> bool {
self.message_type.as_ref() == Some(message_type)
}
pub fn is_context_only(&self) -> bool {
matches!(self.message_type, Some(MessageType::ContextInfo))
}
pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
if !attachments.is_empty() {
self.attachments.extend(attachments);
self.has_attachments = true;
}
self
}
pub fn attachments(&self) -> &[Attachment] {
&self.attachments
}
pub fn with_origin(mut self, origin: MessageOrigin) -> Self {
self.origin = Some(origin);
self
}
pub fn ensure_origin(mut self, origin: MessageOrigin) -> Self {
if self.origin.is_none() {
self.origin = Some(origin);
}
self
}
pub fn origin(&self) -> Option<MessageOrigin> {
self.origin
}
}
pub fn format_messages_to_prompt(messages: &[(Speaker, String)]) -> String {
const MULTIPART_THRESHOLD: usize = 1000;
let total_chars: usize = messages.iter().map(|(_, content)| content.len()).sum();
if total_chars >= MULTIPART_THRESHOLD {
format_messages_multipart(messages)
} else {
format_messages_simple(messages)
}
}
fn format_messages_simple(messages: &[(Speaker, String)]) -> String {
if messages.is_empty() {
return String::new();
}
let mut output = String::from("# Messages\n\n");
for (speaker, content) in messages {
let speaker_info = match speaker.role() {
Some(role) => format!("{} ({})", speaker.name(), role),
None => speaker.name().to_string(),
};
output.push_str(&format!("## {}\n{}\n\n", speaker_info, content));
}
output
}
fn format_messages_multipart(messages: &[(Speaker, String)]) -> String {
if messages.is_empty() {
return String::new();
}
let mut output = String::from(
"=================================================================================\n\
MESSAGES\n\
=================================================================================\n\n",
);
for (speaker, content) in messages {
let speaker_info = match speaker.role() {
Some(role) => format!("{} ({})", speaker.name(), role),
None => speaker.name().to_string(),
};
output.push_str(&format!(
"───────────────────────────────────────────────────────────────────────────────\n\
{}\n\
───────────────────────────────────────────────────────────────────────────────\n\
{}\n\n",
speaker_info, content
));
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_id_uniqueness() {
let id1 = MessageId::new();
let id2 = MessageId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_message_creation() {
let msg = DialogueMessage::new(1, Speaker::System, "Test message".to_string());
assert_eq!(msg.turn, 1);
assert_eq!(msg.speaker_name(), "System");
assert_eq!(msg.content, "Test message");
}
#[test]
fn test_participant_speaker() {
let speaker = Speaker::agent("Alice", "Engineer");
assert_eq!(speaker.name(), "Alice");
assert_eq!(speaker.role(), Some("Engineer"));
}
#[test]
fn test_system_speaker() {
let speaker = Speaker::System;
assert_eq!(speaker.name(), "System");
assert_eq!(speaker.role(), None);
}
#[test]
fn test_message_serialization() {
let msg = DialogueMessage::new(1, Speaker::agent("Bob", "Designer"), "Hello".to_string());
let json = serde_json::to_string(&msg).unwrap();
let deserialized: DialogueMessage = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, msg.id);
assert_eq!(deserialized.turn, msg.turn);
assert_eq!(deserialized.content, msg.content);
}
#[test]
fn test_format_messages_simple() {
let messages = vec![
(Speaker::System, "Task: Discuss architecture".to_string()),
(
Speaker::agent("Alice", "Engineer"),
"I suggest microservices".to_string(),
),
];
let prompt = format_messages_to_prompt(&messages);
assert!(prompt.contains("# Messages"));
assert!(prompt.contains("## System"));
assert!(prompt.contains("## Alice (Engineer)"));
assert!(prompt.contains("Task: Discuss architecture"));
assert!(prompt.contains("I suggest microservices"));
assert!(!prompt.contains("==="));
assert!(!prompt.contains("───"));
}
#[test]
fn test_format_messages_multipart() {
let long_content = "a".repeat(1500);
let messages = vec![
(Speaker::System, "Short task".to_string()),
(Speaker::agent("Alice", "Engineer"), long_content.clone()),
];
let prompt = format_messages_to_prompt(&messages);
assert!(prompt.contains("MESSAGES"));
assert!(prompt.contains("==="));
assert!(prompt.contains("───"));
assert!(prompt.contains("System"));
assert!(prompt.contains("Alice (Engineer)"));
assert!(!prompt.contains("# Messages"));
assert!(!prompt.contains("## System"));
}
#[test]
fn test_format_messages_empty() {
let messages: Vec<(Speaker, String)> = vec![];
let prompt = format_messages_to_prompt(&messages);
assert_eq!(prompt, "");
}
#[test]
fn test_format_messages_threshold() {
let content_999 = "a".repeat(999);
let messages_under = vec![(Speaker::System, content_999)];
let prompt_under = format_messages_to_prompt(&messages_under);
assert!(
prompt_under.contains("# Messages"),
"Should use simple format for 999 chars"
);
let content_1000 = "a".repeat(1000);
let messages_at = vec![(Speaker::System, content_1000)];
let prompt_at = format_messages_to_prompt(&messages_at);
assert!(
prompt_at.contains("==="),
"Should use multipart format for 1000 chars"
);
}
#[test]
fn test_sent_agents_default() {
let sent = SentAgents::default();
assert!(sent.is_empty());
assert_eq!(sent, SentAgents::Agents { agents: vec![] });
}
#[test]
fn test_sent_agents_add_single_agent() {
let mut sent = SentAgents::default();
let alice = Speaker::agent("Alice", "Engineer");
sent.sent(alice.clone());
assert!(!sent.is_empty());
match sent {
SentAgents::Agents { agents } => {
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].name(), "Alice");
}
SentAgents::All => panic!("Expected Agents variant"),
}
}
#[test]
fn test_sent_agents_add_multiple_agents() {
let mut sent = SentAgents::default();
let alice = Speaker::agent("Alice", "Engineer");
let bob = Speaker::agent("Bob", "Designer");
sent.sent(alice.clone());
sent.sent(bob.clone());
assert!(!sent.is_empty());
match sent {
SentAgents::Agents { agents } => {
assert_eq!(agents.len(), 2);
assert_eq!(agents[0].name(), "Alice");
assert_eq!(agents[1].name(), "Bob");
}
SentAgents::All => panic!("Expected Agents variant"),
}
}
#[test]
fn test_sent_agents_all_variant() {
let sent = SentAgents::All;
assert!(!sent.is_empty());
}
#[test]
fn test_sent_agents_all_ignores_additional_agents() {
let mut sent = SentAgents::All;
let alice = Speaker::agent("Alice", "Engineer");
sent.sent(alice);
assert_eq!(sent, SentAgents::All);
assert!(!sent.is_empty());
}
#[test]
fn test_sent_agents_serialize_empty() {
let sent = SentAgents::default();
let json = serde_json::to_string(&sent).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["type"], "agents");
assert!(value["agents"].is_array());
assert_eq!(value["agents"].as_array().unwrap().len(), 0);
}
#[test]
fn test_sent_agents_serialize_with_agents() {
let mut sent = SentAgents::default();
sent.sent(Speaker::agent("Alice", "Engineer"));
sent.sent(Speaker::agent("Bob", "Designer"));
let json = serde_json::to_string(&sent).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["type"], "agents");
assert!(value["agents"].is_array());
assert_eq!(value["agents"].as_array().unwrap().len(), 2);
}
#[test]
fn test_sent_agents_serialize_all() {
let sent = SentAgents::All;
let json = serde_json::to_string(&sent).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["type"], "all");
assert!(value.get("agents").is_none());
}
#[test]
fn test_sent_agents_deserialize_empty() {
let json = r#"{"type":"agents","agents":[]}"#;
let sent: SentAgents = serde_json::from_str(json).unwrap();
assert!(sent.is_empty());
assert_eq!(sent, SentAgents::Agents { agents: vec![] });
}
#[test]
fn test_sent_agents_deserialize_with_agents() {
let json = r#"{"type":"agents","agents":[
{"type":"agent","name":"Alice","role":"Engineer"},
{"type":"agent","name":"Bob","role":"Designer"}
]}"#;
let sent: SentAgents = serde_json::from_str(json).unwrap();
assert!(!sent.is_empty());
match sent {
SentAgents::Agents { agents } => {
assert_eq!(agents.len(), 2);
assert_eq!(agents[0].name(), "Alice");
assert_eq!(agents[1].name(), "Bob");
}
SentAgents::All => panic!("Expected Agents variant"),
}
}
#[test]
fn test_sent_agents_deserialize_all() {
let json = r#"{"type":"all"}"#;
let sent: SentAgents = serde_json::from_str(json).unwrap();
assert!(!sent.is_empty());
assert_eq!(sent, SentAgents::All);
}
#[test]
fn test_sent_agents_round_trip() {
let mut original = SentAgents::default();
original.sent(Speaker::agent("Alice", "Engineer"));
original.sent(Speaker::agent("Bob", "Designer"));
let json = serde_json::to_string(&original).unwrap();
let deserialized: SentAgents = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_sent_agents_all_round_trip() {
let original = SentAgents::All;
let json = serde_json::to_string(&original).unwrap();
let deserialized: SentAgents = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_dialogue_message_sent_to_agents() {
let mut msg = DialogueMessage::new(1, Speaker::System, "Test".to_string());
assert!(!msg.sent_to_agents());
msg.sent(Speaker::agent("Alice", "Engineer"));
assert!(msg.sent_to_agents());
}
#[test]
fn test_dialogue_message_serialization_with_sent_agents() {
let mut msg =
DialogueMessage::new(1, Speaker::agent("Alice", "Engineer"), "Hello".to_string());
msg.sent(Speaker::agent("Bob", "Designer"));
let json = serde_json::to_string(&msg).unwrap();
let deserialized: DialogueMessage = serde_json::from_str(&json).unwrap();
assert!(deserialized.sent_to_agents());
}
}