meerkat-mobkit 0.6.52

Companion orchestration platform for the Meerkat multi-agent runtime
Documentation
//! Parameter parsing for memory RPC methods.

use super::*;

const MEMORY_SUPPORTED_STORES: [&str; 5] = [
    "knowledge_graph",
    "vector",
    "timeline",
    "todo",
    "top_of_mind",
];

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum MemoryParamsError {
    ParamsMustBeObject,
    EntityRequired,
    TopicRequired,
    StoreMustBeString,
    UnsupportedStore(String),
    FactMustBeString,
    MetadataMustBeJson,
    ConflictMustBeBoolean,
    ConflictReasonMustBeString,
    EntityMustBeString,
    TopicMustBeString,
    Index(MemoryIndexError),
}

impl MemoryParamsError {
    pub(super) fn backend_message(error: &ElephantMemoryStoreError) -> String {
        match error {
            ElephantMemoryStoreError::InvalidConfig(reason)
            | ElephantMemoryStoreError::Io(reason)
            | ElephantMemoryStoreError::Serialize(reason)
            | ElephantMemoryStoreError::InvalidStoreData(reason)
            | ElephantMemoryStoreError::ExternalCallFailed(reason) => reason.clone(),
        }
    }

    pub(super) fn message(&self) -> String {
        match self {
            MemoryParamsError::ParamsMustBeObject => "params must be a JSON object".to_string(),
            MemoryParamsError::EntityRequired => "entity must be a non-empty string".to_string(),
            MemoryParamsError::TopicRequired => "topic must be a non-empty string".to_string(),
            MemoryParamsError::StoreMustBeString => {
                "store must be a non-empty string when provided".to_string()
            }
            MemoryParamsError::UnsupportedStore(store) => format!(
                "store '{store}' is unsupported (allowed: knowledge_graph, vector, timeline, todo, top_of_mind)"
            ),
            MemoryParamsError::FactMustBeString => {
                "fact must be a non-empty string when provided".to_string()
            }
            MemoryParamsError::MetadataMustBeJson => {
                "metadata must be a JSON object when provided".to_string()
            }
            MemoryParamsError::ConflictMustBeBoolean => {
                "conflict must be a boolean when provided".to_string()
            }
            MemoryParamsError::ConflictReasonMustBeString => {
                "conflict_reason must be a string when provided".to_string()
            }
            MemoryParamsError::EntityMustBeString => "entity filter must be a string".to_string(),
            MemoryParamsError::TopicMustBeString => "topic filter must be a string".to_string(),
            MemoryParamsError::Index(MemoryIndexError::EntityRequired) => {
                "entity must be a non-empty string".to_string()
            }
            MemoryParamsError::Index(MemoryIndexError::TopicRequired) => {
                "topic must be a non-empty string".to_string()
            }
            MemoryParamsError::Index(MemoryIndexError::UnsupportedStore(store)) => format!(
                "store '{store}' is unsupported (allowed: knowledge_graph, vector, timeline, todo, top_of_mind)"
            ),
            MemoryParamsError::Index(MemoryIndexError::FactRequiredWhenConflictUnset) => {
                "fact is required unless conflict=true".to_string()
            }
            MemoryParamsError::Index(MemoryIndexError::BackendPersistFailed(error)) => {
                format!(
                    "memory backend persistence failed: {}",
                    Self::backend_message(error)
                )
            }
        }
    }
}

pub(super) fn parse_memory_stores_params(params: &Value) -> Result<(), MemoryParamsError> {
    if params.is_null() || params.is_object() {
        return Ok(());
    }
    Err(MemoryParamsError::ParamsMustBeObject)
}

fn parse_memory_store_field(value: &Value) -> Result<String, MemoryParamsError> {
    let store = value.as_str().ok_or(MemoryParamsError::StoreMustBeString)?;
    let canonical = store.trim().to_ascii_lowercase();
    if canonical.is_empty() {
        return Err(MemoryParamsError::StoreMustBeString);
    }
    if MEMORY_SUPPORTED_STORES.contains(&canonical.as_str()) {
        Ok(canonical)
    } else {
        Err(MemoryParamsError::UnsupportedStore(canonical))
    }
}

pub(super) fn parse_memory_index_params(
    params: &Value,
) -> Result<MemoryIndexRequest, MemoryParamsError> {
    let object = params
        .as_object()
        .ok_or(MemoryParamsError::ParamsMustBeObject)?;
    let entity = object
        .get("entity")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or(MemoryParamsError::EntityRequired)?;
    let topic = object
        .get("topic")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or(MemoryParamsError::TopicRequired)?;
    let store = match object.get("store") {
        None => None,
        Some(value) => Some(parse_memory_store_field(value)?),
    };
    let fact = match object.get("fact") {
        None => None,
        Some(value) => Some(
            value
                .as_str()
                .ok_or(MemoryParamsError::FactMustBeString)?
                .trim()
                .to_string(),
        ),
    };
    if fact.as_deref().is_some_and(str::is_empty) {
        return Err(MemoryParamsError::FactMustBeString);
    }
    let metadata = match object.get("metadata") {
        None => None,
        Some(value) => {
            if !value.is_object() {
                return Err(MemoryParamsError::MetadataMustBeJson);
            }
            Some(value.clone())
        }
    };
    let conflict = match object.get("conflict") {
        None => None,
        Some(value) => Some(
            value
                .as_bool()
                .ok_or(MemoryParamsError::ConflictMustBeBoolean)?,
        ),
    };
    let conflict_reason = match object.get("conflict_reason") {
        None => None,
        Some(value) => Some(
            value
                .as_str()
                .ok_or(MemoryParamsError::ConflictReasonMustBeString)?
                .to_string(),
        ),
    };

    Ok(MemoryIndexRequest {
        entity: entity.to_string(),
        topic: topic.to_string(),
        store,
        fact,
        metadata,
        conflict,
        conflict_reason,
    })
}

pub(super) fn parse_memory_query_params(
    params: &Value,
) -> Result<MemoryQueryRequest, MemoryParamsError> {
    if params.is_null() {
        return Ok(MemoryQueryRequest {
            entity: None,
            topic: None,
            store: None,
        });
    }
    let object = params
        .as_object()
        .ok_or(MemoryParamsError::ParamsMustBeObject)?;
    let entity = match object.get("entity") {
        None => None,
        Some(value) => Some(
            value
                .as_str()
                .ok_or(MemoryParamsError::EntityMustBeString)?
                .to_string(),
        ),
    };
    let topic = match object.get("topic") {
        None => None,
        Some(value) => Some(
            value
                .as_str()
                .ok_or(MemoryParamsError::TopicMustBeString)?
                .to_string(),
        ),
    };
    let store = match object.get("store") {
        None => None,
        Some(value) => Some(parse_memory_store_field(value)?),
    };
    Ok(MemoryQueryRequest {
        entity,
        topic,
        store,
    })
}