use serde_json::Value;
pub const AGENT_COLLECTIONS: &[&str] =
&["_semantic_memory", "_episodic_memory", "_procedural_memory"];
#[derive(Debug, Clone, PartialEq)]
pub struct RawFact {
pub id: u64,
pub payload: String,
pub source_vector: Vec<f32>,
}
pub fn enumerate_collection(
db: &velesdb_core::Database,
collection: &str,
page: usize,
) -> Result<Vec<RawFact>, crate::MemoryError> {
let mut out: Vec<RawFact> = Vec::new();
let mut offset = 0usize;
loop {
let batch = enumerate_page(db, collection, page, offset)?;
if batch.is_empty() {
break;
}
let returned = batch.len();
out.extend(batch);
if returned < page {
break;
}
offset += page;
}
Ok(out)
}
pub fn enumerate_page(
db: &velesdb_core::Database,
collection: &str,
page: usize,
offset: usize,
) -> Result<Vec<RawFact>, crate::MemoryError> {
let sql = format!("SELECT * FROM {collection} ORDER BY id LIMIT {page} OFFSET {offset}");
let query = velesdb_core::velesql::Parser::parse(&sql)
.map_err(|e| velesdb_core::Error::Query(e.to_string()))?;
let hits = db.execute_query(&query, &std::collections::HashMap::new())?;
Ok(hits
.into_iter()
.map(|hit| RawFact {
id: hit.point.id,
payload: hit
.point
.payload
.as_ref()
.map_or_else(|| Value::Null.to_string(), std::string::ToString::to_string),
source_vector: hit.point.vector,
})
.collect())
}
pub fn scroll_page(
db: &velesdb_core::Database,
collection: &str,
cursor: Option<u64>,
batch: usize,
) -> Result<(Vec<RawFact>, Option<u64>), crate::MemoryError> {
let any = db.get_any_collection(collection).ok_or_else(|| {
velesdb_core::Error::Query(format!("collection `{collection}` not found"))
})?;
let scrolled = match &any {
velesdb_core::AnyCollection::Vector(c) => c.scroll_batch(cursor, batch, None),
velesdb_core::AnyCollection::Graph(c) => c.scroll_batch(cursor, batch, None),
velesdb_core::AnyCollection::Metadata(c) => c.scroll_batch(cursor, batch, None),
_ => {
return Err(velesdb_core::Error::Query(format!(
"collection `{collection}` is of a kind that does not scroll"
))
.into())
}
}?;
let facts = scrolled
.points
.into_iter()
.map(|point| RawFact {
id: point.id,
payload: point
.payload
.as_ref()
.map_or_else(|| Value::Null.to_string(), std::string::ToString::to_string),
source_vector: point.vector,
})
.collect();
Ok((facts, scrolled.next_cursor))
}
pub fn enumerate_by_cursor(
db: &velesdb_core::Database,
collection: &str,
batch: usize,
) -> Result<Vec<RawFact>, crate::MemoryError> {
let mut out: Vec<RawFact> = Vec::new();
let mut cursor: Option<u64> = None;
loop {
let (facts, next) = scroll_page(db, collection, cursor, batch)?;
if facts.is_empty() {
break;
}
out.extend(facts);
match next {
Some(c) => cursor = Some(c),
None => break,
}
}
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum Reinsertion {
Inserted,
Collision {
existing: String,
},
}
pub fn reinsert(
db: &velesdb_core::Database,
collection: &str,
fact: &RawFact,
vector: &[f32],
) -> Result<Reinsertion, crate::MemoryError> {
let any = db.get_any_collection(collection).ok_or_else(|| {
velesdb_core::Error::Query(format!("collection `{collection}` not found"))
})?;
if let Some(Some(existing)) = any.get(&[fact.id]).into_iter().next() {
return Ok(Reinsertion::Collision {
existing: existing
.payload
.as_ref()
.map_or_else(|| Value::Null.to_string(), std::string::ToString::to_string),
});
}
let payload: Value = serde_json::from_str(&fact.payload).map_err(|e| {
velesdb_core::Error::Query(format!("fact {} carries unreadable payload: {e}", fact.id))
})?;
any.upsert(vec![velesdb_core::Point::new(
fact.id,
vector.to_vec(),
Some(payload),
)])?;
Ok(Reinsertion::Inserted)
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct BatchReinsertion {
pub inserted: u64,
pub collisions: Vec<u64>,
}
pub fn reinsert_batch(
db: &velesdb_core::Database,
collection: &str,
batch: &[(RawFact, Vec<f32>)],
) -> Result<BatchReinsertion, crate::MemoryError> {
let any = db.get_any_collection(collection).ok_or_else(|| {
velesdb_core::Error::Query(format!("collection `{collection}` not found"))
})?;
let ids: Vec<u64> = batch.iter().map(|(fact, _)| fact.id).collect();
let occupied: std::collections::HashSet<u64> = any
.get(&ids)
.into_iter()
.flatten()
.map(|point| point.id)
.collect();
let mut points = Vec::with_capacity(batch.len());
for (fact, vector) in batch {
if occupied.contains(&fact.id) {
continue;
}
let payload: Value = serde_json::from_str(&fact.payload).map_err(|e| {
velesdb_core::Error::Query(format!("fact {} carries unreadable payload: {e}", fact.id))
})?;
points.push(velesdb_core::Point::new(
fact.id,
vector.clone(),
Some(payload),
));
}
let inserted = u64::try_from(points.len()).unwrap_or(u64::MAX);
if !points.is_empty() {
any.upsert(points)?;
}
let mut collisions: Vec<u64> = occupied.into_iter().collect();
collisions.sort_unstable();
Ok(BatchReinsertion {
inserted,
collisions,
})
}