use super::error::{DeduplicationError, DeduplicationResult};
use super::id::EventId;
use llm_optimizer_types::events::{CloudEvent, FeedbackEvent};
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;
pub trait EventIdExtractor<T>: Send + Sync {
fn extract_id(&self, event: &T) -> DeduplicationResult<EventId>;
}
#[derive(Debug, Clone, Default)]
pub struct FeedbackEventUuidExtractor;
impl EventIdExtractor<FeedbackEvent> for FeedbackEventUuidExtractor {
fn extract_id(&self, event: &FeedbackEvent) -> DeduplicationResult<EventId> {
Ok(EventId::from(event.id))
}
}
#[derive(Debug, Clone, Default)]
pub struct CloudEventUuidExtractor;
impl EventIdExtractor<CloudEvent> for CloudEventUuidExtractor {
fn extract_id(&self, event: &CloudEvent) -> DeduplicationResult<EventId> {
Ok(EventId::from_string(event.id.clone()))
}
}
#[derive(Debug, Clone)]
pub struct ContentHashExtractor<H = Sha256> {
include_fields: Option<Vec<String>>,
exclude_fields: Vec<String>,
_phantom: PhantomData<H>,
}
impl ContentHashExtractor<Sha256> {
pub fn sha256() -> Self {
Self {
include_fields: None,
exclude_fields: vec![
"timestamp".to_string(),
"id".to_string(),
"processing_time".to_string(),
],
_phantom: PhantomData,
}
}
pub fn sha256_excluding(exclude_fields: Vec<String>) -> Self {
Self {
include_fields: None,
exclude_fields,
_phantom: PhantomData,
}
}
pub fn sha256_including(include_fields: Vec<String>) -> Self {
Self {
include_fields: Some(include_fields),
exclude_fields: Vec::new(),
_phantom: PhantomData,
}
}
fn filter_fields(&self, value: &serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let filtered: serde_json::Map<String, serde_json::Value> = map
.iter()
.filter(|(k, _)| {
if let Some(ref includes) = self.include_fields {
return includes.contains(&k.to_string());
}
!self.exclude_fields.contains(&k.to_string())
})
.map(|(k, v)| (k.clone(), self.filter_fields(v)))
.collect();
serde_json::Value::Object(filtered)
}
serde_json::Value::Array(arr) => {
let filtered: Vec<serde_json::Value> =
arr.iter().map(|v| self.filter_fields(v)).collect();
serde_json::Value::Array(filtered)
}
_ => value.clone(),
}
}
}
impl<T, H> EventIdExtractor<T> for ContentHashExtractor<H>
where
T: Serialize,
H: Digest,
{
fn extract_id(&self, event: &T) -> DeduplicationResult<EventId> {
let json = serde_json::to_value(event)?;
let filtered = self.filter_fields(&json);
let canonical = serde_json::to_string(&filtered)?;
let mut hasher = H::new();
hasher.update(canonical.as_bytes());
let hash = hasher.finalize();
Ok(EventId::from_bytes(&hash))
}
}
#[derive(Debug, Clone)]
pub enum FieldExtractor {
Field(String),
Metadata(String),
Path(Vec<String>),
Custom(Arc<dyn Fn(&serde_json::Value) -> Option<String> + Send + Sync>),
}
impl FieldExtractor {
fn extract(&self, value: &serde_json::Value) -> Option<String> {
match self {
FieldExtractor::Field(name) => value.get(name)?.as_str().map(String::from),
FieldExtractor::Metadata(key) => {
value.get("metadata")?.get(key)?.as_str().map(String::from)
}
FieldExtractor::Path(path) => {
let mut current = value;
for segment in path {
current = current.get(segment)?;
}
current.as_str().map(String::from)
}
FieldExtractor::Custom(f) => f(value),
}
}
}
#[derive(Debug, Clone)]
pub struct CompositeExtractor {
fields: Vec<FieldExtractor>,
separator: String,
}
impl CompositeExtractor {
pub fn new(fields: Vec<FieldExtractor>, separator: String) -> Self {
Self { fields, separator }
}
pub fn model_session() -> Self {
Self {
fields: vec![
FieldExtractor::Metadata("model_id".to_string()),
FieldExtractor::Metadata("session_id".to_string()),
],
separator: ":".to_string(),
}
}
pub fn user_session() -> Self {
Self {
fields: vec![
FieldExtractor::Metadata("user_id".to_string()),
FieldExtractor::Metadata("session_id".to_string()),
],
separator: ":".to_string(),
}
}
}
impl<T> EventIdExtractor<T> for CompositeExtractor
where
T: Serialize,
{
fn extract_id(&self, event: &T) -> DeduplicationResult<EventId> {
let json = serde_json::to_value(event)?;
let mut parts = Vec::new();
for field in &self.fields {
let value = field.extract(&json).ok_or_else(|| {
DeduplicationError::ExtractionFailed {
reason: format!("Failed to extract field: {:?}", field),
}
})?;
parts.push(value);
}
let composite_id = parts.join(&self.separator);
Ok(EventId::from_string(composite_id))
}
}
#[derive(Debug, Clone)]
pub struct TimeWindowHashExtractor {
base_extractor: ContentHashExtractor,
window_size_secs: i64,
}
impl TimeWindowHashExtractor {
pub fn new(window_size_secs: i64) -> Self {
Self {
base_extractor: ContentHashExtractor::sha256(),
window_size_secs,
}
}
pub fn hourly() -> Self {
Self::new(3600)
}
pub fn daily() -> Self {
Self::new(86400)
}
}
impl<T> EventIdExtractor<T> for TimeWindowHashExtractor
where
T: Serialize,
{
fn extract_id(&self, event: &T) -> DeduplicationResult<EventId> {
let base_id = self.base_extractor.extract_id(event)?;
let now = chrono::Utc::now();
let bucket = now.timestamp() / self.window_size_secs;
let windowed_id = format!("{}:{}", bucket, base_id.as_str());
Ok(EventId::from_string(windowed_id))
}
}
#[cfg(test)]
mod tests {
use super::*;
use llm_optimizer_types::events::{EventSource, EventType};
#[test]
fn test_feedback_event_uuid_extractor() {
let event = FeedbackEvent::new(
EventSource::Observatory,
EventType::Metric,
serde_json::json!({}),
);
let extractor = FeedbackEventUuidExtractor;
let id = extractor.extract_id(&event).unwrap();
assert_eq!(id.as_str(), event.id.to_string());
}
#[test]
fn test_cloud_event_uuid_extractor() {
let event = CloudEvent::new("test.event", "test-source", serde_json::json!({}));
let extractor = CloudEventUuidExtractor;
let id = extractor.extract_id(&event).unwrap();
assert_eq!(id.as_str(), event.id);
}
#[test]
fn test_content_hash_extractor() {
#[derive(Serialize)]
struct TestEvent {
id: String,
timestamp: i64,
data: String,
}
let event = TestEvent {
id: "test-1".to_string(),
timestamp: 123456,
data: "important-data".to_string(),
};
let extractor = ContentHashExtractor::sha256();
let id = extractor.extract_id(&event).unwrap();
let event2 = TestEvent {
id: "test-2".to_string(),
timestamp: 789012,
data: "important-data".to_string(),
};
let id2 = extractor.extract_id(&event2).unwrap();
assert_eq!(id, id2); }
#[test]
fn test_composite_extractor_model_session() {
let mut event = FeedbackEvent::new(
EventSource::Observatory,
EventType::Metric,
serde_json::json!({}),
);
event
.metadata
.insert("model_id".to_string(), "gpt-4".to_string());
event
.metadata
.insert("session_id".to_string(), "sess-123".to_string());
let extractor = CompositeExtractor::model_session();
let id = extractor.extract_id(&event).unwrap();
assert_eq!(id.as_str(), "gpt-4:sess-123");
}
#[test]
fn test_time_window_hash_extractor() {
#[derive(Serialize)]
struct TestEvent {
data: String,
}
let event = TestEvent {
data: "test".to_string(),
};
let extractor = TimeWindowHashExtractor::new(3600);
let id1 = extractor.extract_id(&event).unwrap();
assert!(id1.as_str().contains(':'));
let id2 = extractor.extract_id(&event).unwrap();
assert_eq!(id1, id2);
}
}