relay-knowledge 1.1.14

Graph-database-based knowledge graph project.
Documentation
use crate::{
    api::{IngestEvidence, IngestRequest, MultimodalExtractionRequest},
    domain::{EvidenceModality, SourceScope},
};

const MAX_MULTIMODAL_EXTRACTION_ITEMS: usize = 64;

#[derive(Debug)]
pub(in crate::application) struct MultimodalExtractionIngest {
    pub parent_evidence_id: String,
    pub derived_evidence_count: usize,
    pub ingest: IngestRequest,
}

pub(in crate::application) fn extraction_ingest_request(
    request: MultimodalExtractionRequest,
) -> Result<MultimodalExtractionIngest, String> {
    let source_scope = SourceScope::parse(request.source_scope)
        .map(String::from)
        .map_err(|error| error.to_string())?;
    let parent_evidence_id = required_text("parent_evidence_id", request.parent_evidence_id)?;
    validate_batch_size(request.derived_evidence.len())?;
    for evidence in &request.derived_evidence {
        validate_derived_evidence(evidence, &parent_evidence_id)?;
    }

    Ok(MultimodalExtractionIngest {
        parent_evidence_id,
        derived_evidence_count: request.derived_evidence.len(),
        ingest: IngestRequest {
            source_scope,
            evidence: request.derived_evidence,
            relations: Vec::new(),
            claims: Vec::new(),
            events: Vec::new(),
        },
    })
}

fn validate_batch_size(count: usize) -> Result<(), String> {
    if count == 0 {
        return Err("multimodal extraction batch must include derived evidence".to_owned());
    }
    if count > MAX_MULTIMODAL_EXTRACTION_ITEMS {
        return Err(format!(
            "multimodal extraction batch limit is {MAX_MULTIMODAL_EXTRACTION_ITEMS} items"
        ));
    }

    Ok(())
}

fn validate_derived_evidence(
    evidence: &IngestEvidence,
    parent_evidence_id: &str,
) -> Result<(), String> {
    let extraction = evidence
        .extraction
        .as_ref()
        .ok_or_else(|| "derived multimodal evidence requires extraction metadata".to_owned())?;
    if !maintenance_modality(extraction.modality) {
        return Err(format!(
            "modality '{}' is not produced by multimodal maintenance",
            extraction.modality.as_str()
        ));
    }
    if extraction.parent_evidence_id.as_deref().map(str::trim) != Some(parent_evidence_id) {
        return Err(format!(
            "derived evidence must reference parent evidence '{parent_evidence_id}'"
        ));
    }
    if extraction
        .extractor
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .is_none()
    {
        return Err("derived multimodal evidence requires extractor identity".to_owned());
    }

    Ok(())
}

fn maintenance_modality(modality: EvidenceModality) -> bool {
    matches!(
        modality,
        EvidenceModality::OcrText
            | EvidenceModality::Caption
            | EvidenceModality::ImageEmbedding
            | EvidenceModality::Table
            | EvidenceModality::LayoutRegion
    )
}

fn required_text(field: &'static str, value: String) -> Result<String, String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return Err(format!("{field} must not be empty"));
    }

    Ok(trimmed.to_owned())
}

#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;