use std::sync::Arc;
use tracing::{debug, info, instrument, warn};
use crate::domain::entities::{
Claim, ClusterContext, EmbeddingId, EvidencePack, EvidenceRef, EvidenceRefType,
Interpretation, NeighborEvidence, RecordingMetadata, SequenceContext, SegmentId,
};
use crate::domain::repository::{ClusterRepository, EvidencePackRepository};
use crate::infrastructure::claim_generator::ClaimGenerator;
use crate::infrastructure::evidence_builder::EvidenceBuilder;
use crate::templates::InterpretationTemplates;
use crate::{Error, Result};
#[derive(Debug, Clone)]
pub struct InterpretationConfig {
pub max_neighbors: usize,
pub include_spectrograms: bool,
pub min_claim_confidence: f32,
pub max_claims: usize,
pub include_sequence_context: bool,
pub sequence_context_window: usize,
pub min_interpretation_confidence: f32,
}
impl Default for InterpretationConfig {
fn default() -> Self {
Self {
max_neighbors: 10,
include_spectrograms: true,
min_claim_confidence: 0.5,
max_claims: 10,
include_sequence_context: true,
sequence_context_window: 3,
min_interpretation_confidence: 0.3,
}
}
}
impl InterpretationConfig {
pub fn builder() -> InterpretationConfigBuilder {
InterpretationConfigBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct InterpretationConfigBuilder {
config: InterpretationConfig,
}
impl InterpretationConfigBuilder {
pub fn max_neighbors(mut self, n: usize) -> Self {
self.config.max_neighbors = n;
self
}
pub fn include_spectrograms(mut self, include: bool) -> Self {
self.config.include_spectrograms = include;
self
}
pub fn min_claim_confidence(mut self, confidence: f32) -> Self {
self.config.min_claim_confidence = confidence;
self
}
pub fn max_claims(mut self, n: usize) -> Self {
self.config.max_claims = n;
self
}
pub fn include_sequence_context(mut self, include: bool) -> Self {
self.config.include_sequence_context = include;
self
}
pub fn sequence_context_window(mut self, window: usize) -> Self {
self.config.sequence_context_window = window;
self
}
pub fn min_interpretation_confidence(mut self, confidence: f32) -> Self {
self.config.min_interpretation_confidence = confidence;
self
}
pub fn build(self) -> InterpretationConfig {
self.config
}
}
#[derive(Debug, Clone)]
pub struct Neighbor {
pub embedding_id: EmbeddingId,
pub distance: f32,
pub metadata: Option<serde_json::Value>,
}
impl Neighbor {
pub fn new(embedding_id: EmbeddingId, distance: f32) -> Self {
Self {
embedding_id,
distance,
metadata: None,
}
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
}
#[async_trait::async_trait]
pub trait VectorSpaceService: Send + Sync {
async fn find_neighbors(&self, embedding_id: &EmbeddingId, k: usize) -> Result<Vec<Neighbor>>;
async fn get_embedding(&self, embedding_id: &EmbeddingId) -> Result<Option<Vec<f32>>>;
async fn calculate_similarity(
&self,
embedding_id_a: &EmbeddingId,
embedding_id_b: &EmbeddingId,
) -> Result<f32>;
}
#[async_trait::async_trait]
pub trait SequenceService: Send + Sync {
async fn get_preceding_segments(
&self,
segment_id: &SegmentId,
count: usize,
) -> Result<Vec<SegmentId>>;
async fn get_following_segments(
&self,
segment_id: &SegmentId,
count: usize,
) -> Result<Vec<SegmentId>>;
async fn detect_motif(&self, segment_ids: &[SegmentId]) -> Result<Option<String>>;
}
#[async_trait::async_trait]
pub trait MetadataService: Send + Sync {
async fn get_recording_metadata(
&self,
embedding_id: &EmbeddingId,
) -> Result<RecordingMetadata>;
async fn get_spectrogram_url(&self, embedding_id: &EmbeddingId) -> Result<Option<String>>;
async fn get_segment_id(&self, embedding_id: &EmbeddingId) -> Result<Option<SegmentId>>;
}
pub struct InterpretationService {
vector_service: Arc<dyn VectorSpaceService>,
cluster_repo: Arc<dyn ClusterRepository>,
metadata_service: Arc<dyn MetadataService>,
sequence_service: Option<Arc<dyn SequenceService>>,
evidence_pack_repo: Option<Arc<dyn EvidencePackRepository>>,
evidence_builder: EvidenceBuilder,
claim_generator: ClaimGenerator,
config: InterpretationConfig,
}
impl InterpretationService {
pub fn new(
vector_service: Arc<dyn VectorSpaceService>,
cluster_repo: Arc<dyn ClusterRepository>,
metadata_service: Arc<dyn MetadataService>,
config: InterpretationConfig,
) -> Self {
let evidence_builder = EvidenceBuilder::new(&config);
let claim_generator = ClaimGenerator::new(&config);
Self {
vector_service,
cluster_repo,
metadata_service,
sequence_service: None,
evidence_pack_repo: None,
evidence_builder,
claim_generator,
config,
}
}
pub fn with_sequence_service(mut self, service: Arc<dyn SequenceService>) -> Self {
self.sequence_service = Some(service);
self
}
pub fn with_repository(mut self, repo: Arc<dyn EvidencePackRepository>) -> Self {
self.evidence_pack_repo = Some(repo);
self
}
#[instrument(skip(self), fields(query_id = %query_id))]
pub async fn build_evidence_pack(&self, query_id: &EmbeddingId) -> Result<EvidencePack> {
info!("Building evidence pack for query: {}", query_id);
let neighbors = self.vector_service
.find_neighbors(query_id, self.config.max_neighbors)
.await
.map_err(|e| Error::VectorServiceError(e.to_string()))?;
debug!("Found {} neighbors", neighbors.len());
let neighbor_evidence = self
.collect_neighbor_evidence(&neighbors)
.await?;
let cluster_context = self.build_cluster_context(query_id).await?;
debug!(
"Cluster context: assigned={}, confidence={}",
cluster_context.has_cluster(),
cluster_context.confidence
);
let sequence_context = if self.config.include_sequence_context {
self.build_sequence_context(query_id).await?
} else {
None
};
let interpretation = self
.generate_interpretation_internal(
query_id,
&neighbor_evidence,
&cluster_context,
&sequence_context,
)
.await?;
let evidence_pack = EvidencePack::new(
query_id.clone(),
neighbor_evidence,
cluster_context,
sequence_context,
interpretation,
);
info!(
"Built evidence pack {} with {} neighbors, confidence={}",
evidence_pack.id,
evidence_pack.neighbors.len(),
evidence_pack.overall_confidence()
);
if let Some(repo) = &self.evidence_pack_repo {
repo.save(&evidence_pack).await?;
debug!("Persisted evidence pack {}", evidence_pack.id);
}
Ok(evidence_pack)
}
#[instrument(skip(self, evidence))]
pub async fn generate_interpretation(
&self,
evidence: &EvidencePack,
) -> Result<Interpretation> {
self.generate_interpretation_internal(
&evidence.query_embedding_id,
&evidence.neighbors,
&evidence.cluster_context,
&evidence.sequence_context,
)
.await
}
#[instrument(skip(self, claims))]
pub async fn validate_claims(&self, claims: &[Claim]) -> Result<Vec<(Claim, bool)>> {
let mut results = Vec::with_capacity(claims.len());
for claim in claims {
let is_valid = self.validate_single_claim(claim).await?;
results.push((claim.clone(), is_valid));
}
let valid_count = results.iter().filter(|(_, v)| *v).count();
info!(
"Validated {} claims: {} valid, {} invalid",
claims.len(),
valid_count,
claims.len() - valid_count
);
Ok(results)
}
async fn collect_neighbor_evidence(
&self,
neighbors: &[Neighbor],
) -> Result<Vec<NeighborEvidence>> {
let mut evidence = Vec::with_capacity(neighbors.len());
for neighbor in neighbors {
let metadata = self
.metadata_service
.get_recording_metadata(&neighbor.embedding_id)
.await
.unwrap_or_else(|_| RecordingMetadata::new("unknown"));
let mut neighbor_ev = NeighborEvidence::new(
neighbor.embedding_id.clone(),
neighbor.distance,
metadata,
);
let cluster_ctx = self
.cluster_repo
.get_cluster_context(&neighbor.embedding_id)
.await
.unwrap_or_else(|_| ClusterContext::empty());
if let Some(cluster_id) = cluster_ctx.assigned_cluster {
neighbor_ev = neighbor_ev.with_cluster(cluster_id);
}
if self.config.include_spectrograms {
if let Ok(Some(url)) = self
.metadata_service
.get_spectrogram_url(&neighbor.embedding_id)
.await
{
neighbor_ev = neighbor_ev.with_spectrogram(url);
}
}
evidence.push(neighbor_ev);
}
Ok(evidence)
}
async fn build_cluster_context(&self, embedding_id: &EmbeddingId) -> Result<ClusterContext> {
self.cluster_repo
.get_cluster_context(embedding_id)
.await
.map_err(|e| Error::ClusterServiceError(e.to_string()))
}
async fn build_sequence_context(
&self,
embedding_id: &EmbeddingId,
) -> Result<Option<SequenceContext>> {
let sequence_service = match &self.sequence_service {
Some(s) => s,
None => return Ok(None),
};
let segment_id = match self.metadata_service.get_segment_id(embedding_id).await? {
Some(id) => id,
None => return Ok(None),
};
let window = self.config.sequence_context_window;
let preceding = sequence_service
.get_preceding_segments(&segment_id, window)
.await
.unwrap_or_default();
let following = sequence_service
.get_following_segments(&segment_id, window)
.await
.unwrap_or_default();
if preceding.is_empty() && following.is_empty() {
return Ok(None);
}
let mut all_segments = preceding.clone();
all_segments.push(segment_id);
all_segments.extend(following.clone());
let motif = sequence_service.detect_motif(&all_segments).await.ok().flatten();
let context = SequenceContext::new(preceding, following);
let context = if let Some(m) = motif {
context.with_motif(m)
} else {
context
};
Ok(Some(context))
}
async fn generate_interpretation_internal(
&self,
query_id: &EmbeddingId,
neighbors: &[NeighborEvidence],
cluster_context: &ClusterContext,
sequence_context: &Option<SequenceContext>,
) -> Result<Interpretation> {
let structural_description = self
.generate_structural_description(neighbors, cluster_context, sequence_context);
let claims = self
.claim_generator
.generate_claims(query_id, neighbors, cluster_context, sequence_context)
.await?;
let claims: Vec<Claim> = claims
.into_iter()
.filter(|c| c.confidence >= self.config.min_claim_confidence)
.take(self.config.max_claims)
.collect();
let confidence = if claims.is_empty() {
0.0
} else {
let sum: f32 = claims.iter().map(|c| c.confidence).sum();
sum / claims.len() as f32
};
Ok(Interpretation::new(structural_description, claims, confidence))
}
fn generate_structural_description(
&self,
neighbors: &[NeighborEvidence],
cluster_context: &ClusterContext,
sequence_context: &Option<SequenceContext>,
) -> String {
let templates = InterpretationTemplates::new();
let mut parts = Vec::new();
if !neighbors.is_empty() {
let avg_distance: f32 = neighbors.iter().map(|n| n.distance).sum::<f32>()
/ neighbors.len() as f32;
let similarity = 1.0 - avg_distance.min(1.0);
parts.push(templates.neighbor_description(neighbors.len(), similarity));
let taxa: Vec<&str> = neighbors
.iter()
.filter_map(|n| n.recording_metadata.taxon.as_deref())
.collect();
if !taxa.is_empty() {
parts.push(templates.taxon_description(&taxa));
}
}
if cluster_context.has_cluster() {
let label = cluster_context
.cluster_label
.as_deref()
.unwrap_or("unlabeled");
parts.push(templates.cluster_description(
label,
cluster_context.confidence,
cluster_context.exemplar_similarity,
));
}
if let Some(seq) = sequence_context {
if seq.has_temporal_context() {
parts.push(templates.sequence_description(
seq.sequence_length(),
seq.detected_motif.as_deref(),
));
}
}
if parts.is_empty() {
"Insufficient evidence for structural description.".to_string()
} else {
parts.join(" ")
}
}
async fn validate_single_claim(&self, claim: &Claim) -> Result<bool> {
if claim.evidence_refs.is_empty() {
warn!("Claim has no evidence references: {}", claim.statement);
return Ok(false);
}
if claim.confidence < self.config.min_claim_confidence {
debug!(
"Claim confidence {} below threshold {}: {}",
claim.confidence, self.config.min_claim_confidence, claim.statement
);
return Ok(false);
}
for evidence_ref in &claim.evidence_refs {
let exists = match evidence_ref.ref_type {
EvidenceRefType::Neighbor => {
let emb_id = EmbeddingId::new(&evidence_ref.ref_id);
self.vector_service
.get_embedding(&emb_id)
.await
.map(|e| e.is_some())
.unwrap_or(false)
}
EvidenceRefType::Cluster => {
let cluster_id = crate::domain::entities::ClusterId::new(&evidence_ref.ref_id);
self.cluster_repo
.get_cluster_label(&cluster_id)
.await
.is_ok()
}
EvidenceRefType::Sequence | EvidenceRefType::Taxon => {
true
}
};
if !exists {
warn!(
"Evidence reference not found: {} ({})",
evidence_ref.ref_id, evidence_ref.ref_type
);
return Ok(false);
}
}
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::repository::InMemoryClusterRepository;
use std::collections::HashMap;
use std::sync::RwLock;
struct MockVectorService {
neighbors: RwLock<HashMap<String, Vec<Neighbor>>>,
embeddings: RwLock<HashMap<String, Vec<f32>>>,
}
impl MockVectorService {
fn new() -> Self {
Self {
neighbors: RwLock::new(HashMap::new()),
embeddings: RwLock::new(HashMap::new()),
}
}
fn add_neighbor(&self, query_id: &str, neighbor: Neighbor) {
let mut neighbors = self.neighbors.write().unwrap();
neighbors
.entry(query_id.to_string())
.or_default()
.push(neighbor);
}
fn add_embedding(&self, id: &str, embedding: Vec<f32>) {
let mut embeddings = self.embeddings.write().unwrap();
embeddings.insert(id.to_string(), embedding);
}
}
#[async_trait::async_trait]
impl VectorSpaceService for MockVectorService {
async fn find_neighbors(&self, embedding_id: &EmbeddingId, k: usize) -> Result<Vec<Neighbor>> {
let neighbors = self.neighbors.read().unwrap();
let result = neighbors
.get(embedding_id.as_str())
.map(|n| n.iter().take(k).cloned().collect())
.unwrap_or_default();
Ok(result)
}
async fn get_embedding(&self, embedding_id: &EmbeddingId) -> Result<Option<Vec<f32>>> {
let embeddings = self.embeddings.read().unwrap();
Ok(embeddings.get(embedding_id.as_str()).cloned())
}
async fn calculate_similarity(
&self,
_embedding_id_a: &EmbeddingId,
_embedding_id_b: &EmbeddingId,
) -> Result<f32> {
Ok(0.85)
}
}
struct MockMetadataService;
#[async_trait::async_trait]
impl MetadataService for MockMetadataService {
async fn get_recording_metadata(
&self,
embedding_id: &EmbeddingId,
) -> Result<RecordingMetadata> {
Ok(RecordingMetadata::new(format!("recording-{}", embedding_id)))
}
async fn get_spectrogram_url(&self, embedding_id: &EmbeddingId) -> Result<Option<String>> {
Ok(Some(format!(
"https://spectrograms.example.com/{}",
embedding_id
)))
}
async fn get_segment_id(&self, _embedding_id: &EmbeddingId) -> Result<Option<SegmentId>> {
Ok(None)
}
}
#[tokio::test]
async fn test_interpretation_service_build_evidence_pack() {
let vector_service = Arc::new(MockVectorService::new());
let cluster_repo = Arc::new(InMemoryClusterRepository::new());
let metadata_service = Arc::new(MockMetadataService);
vector_service.add_neighbor(
"query-1",
Neighbor::new(EmbeddingId::new("neighbor-1"), 0.1),
);
vector_service.add_neighbor(
"query-1",
Neighbor::new(EmbeddingId::new("neighbor-2"), 0.2),
);
vector_service.add_embedding("neighbor-1", vec![0.1, 0.2, 0.3]);
vector_service.add_embedding("neighbor-2", vec![0.2, 0.3, 0.4]);
let config = InterpretationConfig::default();
let service = InterpretationService::new(
vector_service,
cluster_repo,
metadata_service,
config,
);
let query_id = EmbeddingId::new("query-1");
let result = service.build_evidence_pack(&query_id).await;
assert!(result.is_ok());
let pack = result.unwrap();
assert_eq!(pack.query_embedding_id, query_id);
assert_eq!(pack.neighbors.len(), 2);
}
#[tokio::test]
async fn test_validate_claims() {
let vector_service = Arc::new(MockVectorService::new());
let cluster_repo = Arc::new(InMemoryClusterRepository::new());
let metadata_service = Arc::new(MockMetadataService);
vector_service.add_embedding("evidence-1", vec![0.1, 0.2, 0.3]);
let config = InterpretationConfig::default();
let service = InterpretationService::new(
vector_service,
cluster_repo,
metadata_service,
config,
);
let valid_claim = Claim::new("Valid claim with evidence", 0.9)
.with_evidence(vec![EvidenceRef::neighbor(
&EmbeddingId::new("evidence-1"),
"Supporting evidence",
)]);
let invalid_claim = Claim::new("Invalid claim without evidence", 0.9);
let results = service
.validate_claims(&[valid_claim, invalid_claim])
.await
.unwrap();
assert_eq!(results.len(), 2);
assert!(results[0].1); assert!(!results[1].1); }
}