use crate::{Engine, EngineError, EngineResult, WriteMemoryInput, WriteMemoryOutput, WriteWarning};
use hippmem_core::hash::stable_hash64;
use hippmem_core::ids::{MemoryId, VectorId};
use hippmem_core::model::enums::ContentType;
use hippmem_core::model::links::SemanticSignature;
use hippmem_core::model::understanding::MemoryUnderstanding;
use hippmem_core::model::unit::{Language, MemoryContent, MemoryStage, MemoryUnit};
use hippmem_core::score::UnitScore;
use hippmem_core::time::{Clock, SystemClock};
use hippmem_model::deterministic::extract::DeterministicExtractor;
use hippmem_store::kv::InvertedIndex;
use hippmem_store::semantic::vector_index::BinaryIndex;
use hippmem_store::semantic::vector_index::VectorIndex;
use hippmem_write::edges::EdgeBuildParams;
use hippmem_write::keys::generate_keys;
use hippmem_write::staged::{raw_to_indexed, StagedWriteInput};
use hippmem_write::understanding::index_enriched_keys;
impl Engine {
pub fn write(&self, input: WriteMemoryInput) -> EngineResult<WriteMemoryOutput> {
let memory_id = MemoryId::generate();
write_internal(self, memory_id, input, false, None)
}
pub fn write_batch(
&self,
inputs: Vec<WriteMemoryInput>,
) -> EngineResult<Vec<WriteMemoryOutput>> {
if inputs.is_empty() {
return Ok(vec![]);
}
let n = inputs.len();
const CHUNK_SIZE: usize = 10;
let mut embeddings: Vec<Option<Vec<f32>>> = Vec::with_capacity(n);
let texts: Vec<String> = inputs.iter().map(|inp| inp.content.clone()).collect();
for chunk in texts.chunks(CHUNK_SIZE) {
match self.embedder.embed_sync(chunk) {
Ok(vectors) => {
for v in vectors {
embeddings.push(Some(v));
}
}
Err(_e) => {
embeddings.resize(embeddings.len() + chunk.len(), None);
}
}
}
let mut outputs = Vec::with_capacity(inputs.len());
for (input, embedding) in inputs.into_iter().zip(embeddings) {
let memory_id = MemoryId::generate();
let output = write_internal(self, memory_id, input, false, embedding)?;
outputs.push(output);
}
Ok(outputs)
}
}
pub(crate) fn write_internal(
engine: &Engine,
memory_id: MemoryId,
input: WriteMemoryInput,
skip_memory_log: bool,
precomputed_embedding: Option<Vec<f32>>,
) -> EngineResult<WriteMemoryOutput> {
let clock = SystemClock;
let _now = clock.now();
let content = MemoryContent {
raw: input.content.clone(),
summary: None,
normalized: None,
language: Language::Zh,
content_type: input.content_type.unwrap_or(ContentType::UserStatement),
};
let extractor = DeterministicExtractor;
let (understanding, mut warnings) = match extractor.extract_sync_immediate(&content) {
Ok(imm) => {
let u = MemoryUnderstanding {
entities: imm.entities,
topics: imm.topics,
causal_claims: imm.explicit_causals,
goals: vec![],
decisions: vec![],
preferences: vec![],
emotions: vec![],
events: vec![],
contradictions: vec![],
importance: input
.importance_hint
.map(UnitScore::new)
.unwrap_or(imm.importance),
confidence: UnitScore::new(0.5),
};
(u, vec![])
}
Err(_e) => (
MemoryUnderstanding {
entities: vec![],
topics: vec![],
causal_claims: vec![],
goals: vec![],
decisions: vec![],
preferences: vec![],
emotions: vec![],
events: vec![],
contradictions: vec![],
importance: UnitScore::new(0.0),
confidence: UnitScore::new(0.0),
},
vec![WriteWarning::ExtractorDegraded],
),
};
let mut semantic = build_semantic_signature(&input.content);
{
if let Some(vector) = precomputed_embedding {
let vector_id = memory_id.0;
let mut idx = engine.dense_vector_index.lock();
let _ = idx.insert(vector_id, &vector);
semantic.dense_embedding_ref = Some(VectorId(vector_id));
} else {
let texts = vec![input.content.clone()];
if let Ok(vectors) = engine.embedder.embed_sync(&texts) {
if let Some(vector) = vectors.first() {
let vector_id = memory_id.0;
let mut idx = engine.dense_vector_index.lock();
let _ = idx.insert(vector_id, vector);
semantic.dense_embedding_ref = Some(VectorId(vector_id));
}
}
}
}
if semantic.dense_embedding_ref.is_none() {
warnings.push(WriteWarning::EmbeddingDeferred);
}
{
let bc_bytes = binary_code_to_bytes(&semantic.binary_code);
let mut idx = engine.binary_code_index.lock();
let _ = idx.insert(memory_id.0, &bc_bytes);
}
let keys = generate_keys(&content, &understanding, &input.context, &semantic)
.map_err(|e| EngineError::Internal(format!("generate_keys: {}", e)))?;
let inverted = InvertedIndex::new(engine.store.db_arc());
let candidate_ids = discover_candidates(&keys, &inverted);
let existing_units = load_memory_units(&engine.store.db_arc(), &candidate_ids);
let staged_input = StagedWriteInput {
id: memory_id,
content: content.clone(),
understanding: understanding.clone(),
context: input.context.clone(),
semantic,
};
let algo = engine.params.read();
let edge_params = EdgeBuildParams {
strong_threshold: algo.strong_edge_threshold,
strong_max: algo.strong_edge_max as usize,
weak_max: algo.weak_edge_max as usize,
min_score: algo.edge_build_min_score,
observation_max: algo.observation_enter_max,
max_candidates: 30, };
let staged_output = raw_to_indexed(staged_input, &existing_units, &edge_params, &algo)
.map_err(|e| EngineError::Internal(format!("raw_to_indexed: {}", e)))?;
let unit = staged_output.unit;
let bincode_unit = bincode::serde::encode_to_vec(&unit, bincode::config::standard())
.map_err(|e| EngineError::Internal(e.to_string()))?;
let bincode_links =
bincode::serde::encode_to_vec(&staged_output.created_links, bincode::config::standard())
.map_err(|e| EngineError::Internal(e.to_string()))?;
hippmem_store::kv::persist_memory_unit(
engine.store.db_arc(),
memory_id.0,
&bincode_unit,
&bincode_links,
&keys.entity_keys,
&keys.topic_keys,
&keys.temporal_keys,
&keys.goal_keys,
&keys.event_keys,
&keys.causal_keys,
skip_memory_log,
)
.map_err(|e| EngineError::Store(e.to_string()))?;
{
let tokens = hippmem_core::hash::tokenize(&input.content, "zh");
let mut ft = engine.fulltext_index.lock();
ft.add_document_tokenized(memory_id.0, &tokens)
.map_err(|e| EngineError::Store(format!("Tantivy add_document: {}", e)))?;
}
warnings.push(WriteWarning::StrongDimsDeferred);
let mut enriched_unit = unit.clone();
crate::runtime::run_enrich_sync(&mut enriched_unit);
index_enriched_keys(&enriched_unit, &inverted, memory_id.0)
.map_err(|e| EngineError::Internal(format!("index_enriched_keys: {}", e)))?;
let re_bincode = bincode::serde::encode_to_vec(&enriched_unit, bincode::config::standard())
.map_err(|e| EngineError::Internal(e.to_string()))?;
hippmem_store::kv::KvStore::new(engine.store.db_arc())
.put(memory_id.0, &re_bincode)
.map_err(|e| EngineError::Store(e.to_string()))?;
Ok(WriteMemoryOutput {
memory_id,
stage_reached: MemoryStage::Indexed,
created_links: staged_output.created_links,
understanding,
warnings,
})
}
fn binary_code_to_bytes(bc: &[u64; 2]) -> [u8; 16] {
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&bc[0].to_le_bytes());
bytes[8..].copy_from_slice(&bc[1].to_le_bytes());
bytes
}
fn build_semantic_signature(text: &str) -> SemanticSignature {
let sim0 = stable_hash64(text);
let sim1 = stable_hash64(&format!("{}_1", text));
let sim2 = stable_hash64(&format!("{}_2", text));
let sim3 = stable_hash64(&format!("{}_3", text));
let bc0 = stable_hash64(&format!("bc_0_{}", text));
let bc1 = stable_hash64(&format!("bc_1_{}", text));
let mut minhash = [0u32; 16];
for (i, v) in minhash.iter_mut().enumerate() {
*v = stable_hash64(&format!("mh_{}_{}", i, text)) as u32;
}
SemanticSignature {
lexical_simhash: [sim0, sim1, sim2, sim3],
dense_embedding_ref: None,
binary_code: [bc0, bc1],
topic_minhash: minhash,
}
}
fn discover_candidates(
keys: &hippmem_core::model::links::AssociationKeys,
inverted: &InvertedIndex,
) -> Vec<MemoryId> {
let mut ids = std::collections::HashSet::new();
for ek in &keys.entity_keys {
if let Ok(hits) = inverted.get_entity(ek) {
for id in hits {
ids.insert(MemoryId(id));
}
}
}
for tk in &keys.topic_keys {
if let Ok(hits) = inverted.get_topic(tk) {
for id in hits {
ids.insert(MemoryId(id));
}
}
}
for tk in &keys.temporal_keys {
if let Ok(hits) = inverted.get_temporal(tk) {
for id in hits {
ids.insert(MemoryId(id));
}
}
}
ids.into_iter().collect()
}
fn load_memory_units(db: &std::sync::Arc<redb::Database>, ids: &[MemoryId]) -> Vec<MemoryUnit> {
let kv = hippmem_store::kv::KvStore::new(std::sync::Arc::clone(db));
ids.iter()
.filter_map(|mid| {
kv.get(&mid.0).ok().flatten().and_then(|data| {
bincode::serde::decode_from_slice::<MemoryUnit, _>(
&data,
bincode::config::standard(),
)
.ok()
.map(|(unit, _)| unit)
})
})
.collect()
}