use smos_domain::{Embedding, Fact, FactId, MemoryKey, NewPendingRequest, SessionId};
use crate::errors::UseCaseError;
use crate::ports::{
Clock, Delay, EmbeddingProvider, FactRepository, LlmExtractor, SessionRepository,
};
use super::ExtractFactsFromResponse;
impl<'a, FR, SR, EP, LE, C, D> ExtractFactsFromResponse<'a, FR, SR, EP, LE, C, D>
where
FR: FactRepository,
SR: SessionRepository,
EP: EmbeddingProvider,
LE: LlmExtractor,
C: Clock,
D: Delay,
{
pub(crate) async fn persist_one_fact(
&self,
raw: &str,
vector: Vec<f32>,
memory_key: &MemoryKey,
session_id: &SessionId,
) -> Result<Option<FactId>, UseCaseError> {
let fact_id = FactId::from_content(raw);
if let Some(mut existing) = self.facts.get(&fact_id, memory_key).await? {
self.confirm_and_save(&mut existing, session_id).await?;
return Ok(None);
}
let similar = self
.facts
.search_for_dedup(vector.clone(), memory_key, 1)
.await?;
if let Some(hit) = similar.into_iter().next() {
match hit.metadata.distance {
Some(d) => {
let similarity = 1.0 - d;
if similarity >= self.extraction_cfg.dedup_cosine_threshold
&& let Some(mut fact) = self.facts.get(&hit.id, memory_key).await?
{
tracing::debug!(
raw = raw,
similarity = similarity,
matched_id = %hit.id,
"semantic dedup: rephrased fact matched an existing one"
);
self.confirm_and_save(&mut fact, session_id).await?;
return Ok(None);
}
}
None => {
tracing::warn!(
raw = raw,
matched_id = %hit.id,
"semantic dedup hit carried no distance; skipping Layer 2 \
(create new pending fact instead)"
);
}
}
}
let emb = Embedding::new(vector)?;
let fact = Fact::new_pending(NewPendingRequest {
content: raw,
memory_key: memory_key.clone(),
session: session_id.clone(),
embedding: emb,
extracted_at: self.clock.now(),
base_confidence: self.confidence_cfg.base,
})?;
self.facts.save(&fact).await?;
Ok(Some(fact_id))
}
pub(crate) async fn confirm_and_save(
&self,
fact: &mut Fact,
session_id: &SessionId,
) -> Result<(), UseCaseError> {
if fact.confirm_cross_session(session_id, self.confidence_cfg)? {
self.facts.save(fact).await?;
}
Ok(())
}
}