use crate::metadata::{
KEY_ENTITIES, KEY_IMPORT_SOURCE_ID, KEY_TITLE, KEY_VALID_FROM, RESERVED_METADATA_KEYS,
};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use klieo_core::error::MemoryError;
use klieo_core::memory::{Fact, LongTermMemory, Scope};
use klieo_memory_graph::EntityRef;
use std::sync::Arc;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ImportDocument {
pub id: String,
pub title: String,
pub body: String,
pub metadata: serde_json::Value,
pub entity_hints: Vec<EntityRef>,
pub valid_from: Option<DateTime<Utc>>,
}
impl ImportDocument {
pub fn new(id: impl Into<String>, title: impl Into<String>, body: impl Into<String>) -> Self {
Self {
id: id.into(),
title: title.into(),
body: body.into(),
metadata: serde_json::Value::Null,
entity_hints: Vec::new(),
valid_from: None,
}
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
pub fn with_entity_hints(mut self, hints: Vec<EntityRef>) -> Self {
self.entity_hints = hints;
self
}
pub fn with_valid_from(mut self, valid_from: DateTime<Utc>) -> Self {
self.valid_from = Some(valid_from);
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ImportError {
#[error("import source fetch failed: {0}")]
Fetch(String),
#[error("import memory write failed: {0}")]
Memory(#[from] MemoryError),
}
#[async_trait]
pub trait ImportSource: Send + Sync {
async fn fetch(&self) -> Result<Vec<ImportDocument>, ImportError>;
}
pub struct KnowledgeIngestionPipeline {
source: Arc<dyn ImportSource>,
memory: Arc<dyn LongTermMemory>,
scope: Scope,
}
impl KnowledgeIngestionPipeline {
pub fn new(
source: Arc<dyn ImportSource>,
memory: Arc<dyn LongTermMemory>,
scope: Scope,
) -> Self {
Self {
source,
memory,
scope,
}
}
pub async fn run(&self) -> Result<usize, ImportError> {
let docs = self.source.fetch().await?;
let mut persisted = 0;
for doc in docs {
let fact = self.build_fact(doc)?;
self.memory.remember(self.scope.clone(), fact).await?;
persisted += 1;
}
Ok(persisted)
}
fn build_fact(&self, doc: ImportDocument) -> Result<Fact, ImportError> {
let mut map = match doc.metadata {
serde_json::Value::Object(m) => m,
_ => serde_json::Map::new(),
};
if let Some(vf) = doc.valid_from {
map.insert(
KEY_VALID_FROM.into(),
serde_json::Value::String(vf.to_rfc3339()),
);
}
if !doc.entity_hints.is_empty() {
let value = serde_json::to_value(&doc.entity_hints)
.map_err(|e| ImportError::Memory(MemoryError::Serialization(e.to_string())))?;
map.insert(KEY_ENTITIES.into(), value);
}
map.insert(
KEY_IMPORT_SOURCE_ID.into(),
serde_json::Value::String(doc.id),
);
map.insert(KEY_TITLE.into(), serde_json::Value::String(doc.title));
Ok(Fact::new(doc.body).with_metadata(serde_json::Value::Object(map)))
}
}
#[doc(hidden)]
pub fn reserved_metadata_keys() -> &'static [&'static str] {
RESERVED_METADATA_KEYS
}