use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
pub use crate::platform::container::battalion::BattalionResult;
pub use crate::platform::container::execution_result::PaladinResult;
pub use crate::platform::container::paladin_error::PaladinError;
pub use crate::platform::container::token_usage::TokenUsage;
pub use super::herald_error::HeraldError;
pub trait Herald: Send + Sync {
fn format_paladin_result(&self, result: &PaladinResult) -> Result<String, HeraldError>;
fn format_battalion_result(&self, result: &BattalionResult) -> Result<String, HeraldError>;
fn format_stream_chunk(&self, chunk: &StreamChunk) -> Result<Option<String>, HeraldError>;
fn finalize_stream(&self, metadata: &ExecutionMetadata) -> Result<String, HeraldError>;
fn format_error(&self, error: &PaladinError) -> String;
fn name(&self) -> &str;
fn mime_type(&self) -> &str;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamChunk {
pub chunk_id: Uuid,
pub sequence_number: u64,
pub timestamp: DateTime<Utc>,
pub content: String,
pub token_count: Option<u32>,
pub is_final: bool,
#[serde(flatten)]
pub metadata: HashMap<String, serde_json::Value>,
}
impl StreamChunk {
pub fn builder() -> StreamChunkBuilder {
StreamChunkBuilder::default()
}
}
#[derive(Default)]
pub struct StreamChunkBuilder {
chunk_id: Option<Uuid>,
sequence_number: Option<u64>,
timestamp: Option<DateTime<Utc>>,
content: Option<String>,
token_count: Option<u32>,
is_final: Option<bool>,
metadata: HashMap<String, serde_json::Value>,
}
impl StreamChunkBuilder {
pub fn chunk_id(mut self, chunk_id: Uuid) -> Self {
self.chunk_id = Some(chunk_id);
self
}
pub fn sequence_number(mut self, sequence_number: u64) -> Self {
self.sequence_number = Some(sequence_number);
self
}
pub fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = Some(timestamp);
self
}
pub fn content(mut self, content: String) -> Self {
self.content = Some(content);
self
}
pub fn token_count(mut self, token_count: u32) -> Self {
self.token_count = Some(token_count);
self
}
pub fn is_final(mut self, is_final: bool) -> Self {
self.is_final = Some(is_final);
self
}
pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
self.metadata.insert(key, value);
self
}
pub fn build(self) -> Result<StreamChunk, HeraldError> {
Ok(StreamChunk {
chunk_id: self
.chunk_id
.ok_or_else(|| HeraldError::InvalidResult("chunk_id is required".to_string()))?,
sequence_number: self.sequence_number.ok_or_else(|| {
HeraldError::InvalidResult("sequence_number is required".to_string())
})?,
timestamp: self
.timestamp
.ok_or_else(|| HeraldError::InvalidResult("timestamp is required".to_string()))?,
content: self
.content
.ok_or_else(|| HeraldError::InvalidResult("content is required".to_string()))?,
token_count: self.token_count,
is_final: self
.is_final
.ok_or_else(|| HeraldError::InvalidResult("is_final is required".to_string()))?,
metadata: self.metadata,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionMetadata {
pub execution_id: Uuid,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub duration_ms: Option<u64>,
pub model_used: String,
pub token_usage: TokenUsage,
pub cost_estimate: Option<f64>,
pub error_count: u32,
#[serde(flatten)]
pub metadata: HashMap<String, serde_json::Value>,
}
impl ExecutionMetadata {
pub fn builder() -> ExecutionMetadataBuilder {
ExecutionMetadataBuilder::default()
}
pub fn calculate_duration(&mut self) {
if let Some(end) = self.end_time {
let duration = end.signed_duration_since(self.start_time);
self.duration_ms = Some(duration.num_milliseconds() as u64);
}
}
pub fn total_cost(&self) -> Option<f64> {
self.cost_estimate
}
}
#[derive(Default)]
pub struct ExecutionMetadataBuilder {
execution_id: Option<Uuid>,
start_time: Option<DateTime<Utc>>,
end_time: Option<DateTime<Utc>>,
duration_ms: Option<u64>,
model_used: Option<String>,
token_usage: Option<TokenUsage>,
cost_estimate: Option<f64>,
error_count: u32,
metadata: HashMap<String, serde_json::Value>,
}
impl ExecutionMetadataBuilder {
pub fn execution_id(mut self, execution_id: Uuid) -> Self {
self.execution_id = Some(execution_id);
self
}
pub fn start_time(mut self, start_time: DateTime<Utc>) -> Self {
self.start_time = Some(start_time);
self
}
pub fn end_time(mut self, end_time: DateTime<Utc>) -> Self {
self.end_time = Some(end_time);
self
}
pub fn duration_ms(mut self, duration_ms: u64) -> Self {
self.duration_ms = Some(duration_ms);
self
}
pub fn model_used(mut self, model_used: String) -> Self {
self.model_used = Some(model_used);
self
}
pub fn token_usage(mut self, token_usage: TokenUsage) -> Self {
self.token_usage = Some(token_usage);
self
}
pub fn cost_estimate(mut self, cost_estimate: f64) -> Self {
self.cost_estimate = Some(cost_estimate);
self
}
pub fn error_count(mut self, error_count: u32) -> Self {
self.error_count = error_count;
self
}
pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
self.metadata.insert(key, value);
self
}
pub fn build(self) -> Result<ExecutionMetadata, HeraldError> {
Ok(ExecutionMetadata {
execution_id: self.execution_id.ok_or_else(|| {
HeraldError::InvalidResult("execution_id is required".to_string())
})?,
start_time: self
.start_time
.ok_or_else(|| HeraldError::InvalidResult("start_time is required".to_string()))?,
end_time: self.end_time,
duration_ms: self.duration_ms,
model_used: self
.model_used
.ok_or_else(|| HeraldError::InvalidResult("model_used is required".to_string()))?,
token_usage: self
.token_usage
.ok_or_else(|| HeraldError::InvalidResult("token_usage is required".to_string()))?,
cost_estimate: self.cost_estimate,
error_count: self.error_count,
metadata: self.metadata,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockHerald;
impl Herald for MockHerald {
fn format_paladin_result(&self, result: &PaladinResult) -> Result<String, HeraldError> {
Ok(format!("MOCK: {}", result.output))
}
fn format_battalion_result(&self, result: &BattalionResult) -> Result<String, HeraldError> {
Ok(format!("MOCK BATTALION: {}", result.battalion_name))
}
fn format_stream_chunk(&self, chunk: &StreamChunk) -> Result<Option<String>, HeraldError> {
if chunk.is_final {
Ok(Some(chunk.content.clone()))
} else {
Ok(None)
}
}
fn finalize_stream(&self, metadata: &ExecutionMetadata) -> Result<String, HeraldError> {
Ok(format!(
"Execution time: {}ms",
metadata.duration_ms.unwrap_or(0)
))
}
fn format_error(&self, error: &PaladinError) -> String {
format!("ERROR: {}", error)
}
fn name(&self) -> &str {
"mock"
}
fn mime_type(&self) -> &str {
"text/plain"
}
}
#[test]
fn test_herald_trait_object() {
let herald: Box<dyn Herald> = Box::new(MockHerald);
assert_eq!(herald.name(), "mock");
assert_eq!(herald.mime_type(), "text/plain");
}
#[test]
fn test_format_paladin_result() {
use crate::platform::container::execution_result::StopReason;
let herald = MockHerald;
let result = PaladinResult {
output: "Test output".to_string(),
token_count: 100,
execution_time_ms: 1500,
loop_count: 1,
stop_reason: StopReason::Completed,
..Default::default()
};
let formatted = herald.format_paladin_result(&result).unwrap();
assert_eq!(formatted, "MOCK: Test output");
}
#[test]
fn test_format_battalion_result() {
use crate::platform::container::battalion::{BattalionStatus, BattalionStrategy};
use chrono::Utc;
use uuid::Uuid;
let herald = MockHerald;
let result = BattalionResult {
battalion_id: Uuid::new_v4(),
battalion_name: "TestBattalion".to_string(),
started_at: Utc::now(),
completed_at: Utc::now(),
final_output: "Combined output".to_string(),
paladin_results: vec![],
status: BattalionStatus::Completed,
strategy_used: BattalionStrategy::Formation,
strategy_selection_reasoning: None,
strategy_selection_time_ms: 0,
per_paladin_times: std::collections::HashMap::new(),
per_paladin_tokens: std::collections::HashMap::new(),
total_tokens: 0,
paladin_success_count: 0,
paladin_failure_count: 0,
};
let formatted = herald.format_battalion_result(&result).unwrap();
assert_eq!(formatted, "MOCK BATTALION: TestBattalion");
}
#[test]
fn test_format_stream_chunk() {
let herald = MockHerald;
let chunk = StreamChunk::builder()
.chunk_id(Uuid::new_v4())
.sequence_number(0)
.timestamp(Utc::now())
.content("partial output".to_string())
.is_final(false)
.build()
.unwrap();
let result = herald.format_stream_chunk(&chunk).unwrap();
assert!(result.is_none());
let final_chunk = StreamChunk::builder()
.chunk_id(Uuid::new_v4())
.sequence_number(1)
.timestamp(Utc::now())
.content("final output".to_string())
.is_final(true)
.build()
.unwrap();
let result = herald.format_stream_chunk(&final_chunk).unwrap();
assert_eq!(result, Some("final output".to_string()));
}
#[test]
fn test_finalize_stream() {
let herald = MockHerald;
let metadata = ExecutionMetadata::builder()
.execution_id(Uuid::new_v4())
.start_time(Utc::now())
.model_used("test-model".to_string())
.token_usage(TokenUsage {
prompt_tokens: 300,
completion_tokens: 200,
total_tokens: 500,
})
.duration_ms(1234)
.build()
.unwrap();
let formatted = herald.finalize_stream(&metadata).unwrap();
assert_eq!(formatted, "Execution time: 1234ms");
}
#[test]
fn test_format_error() {
let herald = MockHerald;
let error = PaladinError::ExecutionError("Something went wrong".to_string());
let formatted = herald.format_error(&error);
assert!(formatted.contains("ERROR"));
assert!(formatted.contains("Something went wrong"));
}
#[test]
fn test_herald_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<MockHerald>();
}
}