use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::entities::{AnomalyType, ClusterId, EmbeddingId, RecordingId};
use super::value_objects::ClusteringMethod;
pub trait AnalysisEvent: Send + Sync {
fn event_id(&self) -> Uuid;
fn occurred_at(&self) -> DateTime<Utc>;
fn event_type(&self) -> &'static str;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClustersDiscovered {
pub event_id: Uuid,
pub occurred_at: DateTime<Utc>,
pub cluster_count: usize,
pub noise_count: usize,
pub method: ClusteringMethod,
pub silhouette_score: Option<f32>,
pub total_embeddings: usize,
}
impl ClustersDiscovered {
#[must_use]
pub fn new(
cluster_count: usize,
noise_count: usize,
method: ClusteringMethod,
total_embeddings: usize,
) -> Self {
Self {
event_id: Uuid::new_v4(),
occurred_at: Utc::now(),
cluster_count,
noise_count,
method,
silhouette_score: None,
total_embeddings,
}
}
#[must_use]
pub fn with_silhouette_score(mut self, score: f32) -> Self {
self.silhouette_score = Some(score);
self
}
}
impl AnalysisEvent for ClustersDiscovered {
fn event_id(&self) -> Uuid {
self.event_id
}
fn occurred_at(&self) -> DateTime<Utc> {
self.occurred_at
}
fn event_type(&self) -> &'static str {
"ClustersDiscovered"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterAssigned {
pub event_id: Uuid,
pub occurred_at: DateTime<Utc>,
pub embedding_id: EmbeddingId,
pub cluster_id: ClusterId,
pub confidence: f32,
pub distance_to_centroid: f32,
}
impl ClusterAssigned {
#[must_use]
pub fn new(
embedding_id: EmbeddingId,
cluster_id: ClusterId,
confidence: f32,
distance_to_centroid: f32,
) -> Self {
Self {
event_id: Uuid::new_v4(),
occurred_at: Utc::now(),
embedding_id,
cluster_id,
confidence,
distance_to_centroid,
}
}
}
impl AnalysisEvent for ClusterAssigned {
fn event_id(&self) -> Uuid {
self.event_id
}
fn occurred_at(&self) -> DateTime<Utc> {
self.occurred_at
}
fn event_type(&self) -> &'static str {
"ClusterAssigned"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MotifDetected {
pub event_id: Uuid,
pub occurred_at: DateTime<Utc>,
pub motif_id: String,
pub pattern: Vec<ClusterId>,
pub occurrences: usize,
pub confidence: f32,
pub avg_duration_ms: f64,
}
impl MotifDetected {
#[must_use]
pub fn new(
motif_id: String,
pattern: Vec<ClusterId>,
occurrences: usize,
confidence: f32,
avg_duration_ms: f64,
) -> Self {
Self {
event_id: Uuid::new_v4(),
occurred_at: Utc::now(),
motif_id,
pattern,
occurrences,
confidence,
avg_duration_ms,
}
}
}
impl AnalysisEvent for MotifDetected {
fn event_id(&self) -> Uuid {
self.event_id
}
fn occurred_at(&self) -> DateTime<Utc> {
self.occurred_at
}
fn event_type(&self) -> &'static str {
"MotifDetected"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SequenceAnalyzed {
pub event_id: Uuid,
pub occurred_at: DateTime<Utc>,
pub recording_id: RecordingId,
pub entropy: f32,
pub stereotypy_score: f32,
pub unique_clusters: usize,
pub unique_transitions: usize,
pub sequence_length: usize,
}
impl SequenceAnalyzed {
#[must_use]
pub fn new(
recording_id: RecordingId,
entropy: f32,
stereotypy_score: f32,
unique_clusters: usize,
unique_transitions: usize,
sequence_length: usize,
) -> Self {
Self {
event_id: Uuid::new_v4(),
occurred_at: Utc::now(),
recording_id,
entropy,
stereotypy_score,
unique_clusters,
unique_transitions,
sequence_length,
}
}
}
impl AnalysisEvent for SequenceAnalyzed {
fn event_id(&self) -> Uuid {
self.event_id
}
fn occurred_at(&self) -> DateTime<Utc> {
self.occurred_at
}
fn event_type(&self) -> &'static str {
"SequenceAnalyzed"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyDetected {
pub event_id: Uuid,
pub occurred_at: DateTime<Utc>,
pub embedding_id: EmbeddingId,
pub anomaly_score: f32,
pub anomaly_type: AnomalyType,
pub nearest_cluster: ClusterId,
pub distance_to_centroid: f32,
}
impl AnomalyDetected {
#[must_use]
pub fn new(
embedding_id: EmbeddingId,
anomaly_score: f32,
anomaly_type: AnomalyType,
nearest_cluster: ClusterId,
distance_to_centroid: f32,
) -> Self {
Self {
event_id: Uuid::new_v4(),
occurred_at: Utc::now(),
embedding_id,
anomaly_score,
anomaly_type,
nearest_cluster,
distance_to_centroid,
}
}
}
impl AnalysisEvent for AnomalyDetected {
fn event_id(&self) -> Uuid {
self.event_id
}
fn occurred_at(&self) -> DateTime<Utc> {
self.occurred_at
}
fn event_type(&self) -> &'static str {
"AnomalyDetected"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrototypesComputed {
pub event_id: Uuid,
pub occurred_at: DateTime<Utc>,
pub cluster_id: ClusterId,
pub prototype_count: usize,
pub best_exemplar_score: f32,
}
impl PrototypesComputed {
#[must_use]
pub fn new(cluster_id: ClusterId, prototype_count: usize, best_exemplar_score: f32) -> Self {
Self {
event_id: Uuid::new_v4(),
occurred_at: Utc::now(),
cluster_id,
prototype_count,
best_exemplar_score,
}
}
}
impl AnalysisEvent for PrototypesComputed {
fn event_id(&self) -> Uuid {
self.event_id
}
fn occurred_at(&self) -> DateTime<Utc> {
self.occurred_at
}
fn event_type(&self) -> &'static str {
"PrototypesComputed"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterLabeled {
pub event_id: Uuid,
pub occurred_at: DateTime<Utc>,
pub cluster_id: ClusterId,
pub label: Option<String>,
pub previous_label: Option<String>,
}
impl ClusterLabeled {
#[must_use]
pub fn new(
cluster_id: ClusterId,
label: Option<String>,
previous_label: Option<String>,
) -> Self {
Self {
event_id: Uuid::new_v4(),
occurred_at: Utc::now(),
cluster_id,
label,
previous_label,
}
}
}
impl AnalysisEvent for ClusterLabeled {
fn event_id(&self) -> Uuid {
self.event_id
}
fn occurred_at(&self) -> DateTime<Utc> {
self.occurred_at
}
fn event_type(&self) -> &'static str {
"ClusterLabeled"
}
}
#[async_trait::async_trait]
pub trait AnalysisEventPublisher: Send + Sync {
async fn publish<E: AnalysisEvent + Serialize + 'static>(
&self,
event: E,
) -> Result<(), EventPublishError>;
}
#[derive(Debug, thiserror::Error)]
pub enum EventPublishError {
#[error("Failed to serialize event: {0}")]
Serialization(String),
#[error("Failed to publish event: {0}")]
Transport(String),
#[error("Event channel closed")]
ChannelClosed,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clusters_discovered_event() {
let event = ClustersDiscovered::new(
10,
5,
ClusteringMethod::HDBSCAN,
100,
)
.with_silhouette_score(0.75);
assert_eq!(event.cluster_count, 10);
assert_eq!(event.noise_count, 5);
assert_eq!(event.silhouette_score, Some(0.75));
assert_eq!(event.event_type(), "ClustersDiscovered");
}
#[test]
fn test_cluster_assigned_event() {
let event = ClusterAssigned::new(
EmbeddingId::new(),
ClusterId::new(),
0.95,
0.1,
);
assert_eq!(event.confidence, 0.95);
assert_eq!(event.event_type(), "ClusterAssigned");
}
#[test]
fn test_motif_detected_event() {
let pattern = vec![ClusterId::new(), ClusterId::new()];
let event = MotifDetected::new(
"motif-1".to_string(),
pattern.clone(),
10,
0.85,
1500.0,
);
assert_eq!(event.pattern.len(), 2);
assert_eq!(event.occurrences, 10);
assert_eq!(event.event_type(), "MotifDetected");
}
#[test]
fn test_anomaly_detected_event() {
let event = AnomalyDetected::new(
EmbeddingId::new(),
0.9,
AnomalyType::Novel,
ClusterId::new(),
2.5,
);
assert_eq!(event.anomaly_type, AnomalyType::Novel);
assert_eq!(event.event_type(), "AnomalyDetected");
}
}