relay-knowledge 1.1.5

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)]
mod tests {
    use super::*;
    use crate::api::{IngestEvidence, IngestEvidenceExtraction};

    #[test]
    fn converts_worker_outputs_to_ingest_request() {
        let converted = extraction_ingest_request(MultimodalExtractionRequest {
            source_scope: " docs ".to_owned(),
            parent_evidence_id: " image-1 ".to_owned(),
            derived_evidence: vec![derived_evidence(EvidenceModality::OcrText, "image-1")],
        })
        .expect("request should validate");

        assert_eq!(converted.source_scope(), "docs");
        assert_eq!(converted.parent_evidence_id, "image-1");
        assert_eq!(converted.derived_evidence_count, 1);
    }

    #[test]
    fn rejects_query_hot_path_or_unowned_outputs() {
        let missing_metadata = IngestEvidence {
            extraction: None,
            ..derived_evidence(EvidenceModality::OcrText, "image-1")
        };
        let direct_image = derived_evidence(EvidenceModality::ImageAsset, "image-1");
        let wrong_parent = derived_evidence(EvidenceModality::Caption, "other");
        let missing_extractor = IngestEvidence {
            extraction: Some(IngestEvidenceExtraction {
                extractor: Some(" ".to_owned()),
                ..extraction(EvidenceModality::Caption, "image-1")
            }),
            ..derived_evidence(EvidenceModality::Caption, "image-1")
        };

        for evidence in [
            missing_metadata,
            direct_image,
            wrong_parent,
            missing_extractor,
        ] {
            let error = extraction_ingest_request(MultimodalExtractionRequest {
                source_scope: "docs".to_owned(),
                parent_evidence_id: "image-1".to_owned(),
                derived_evidence: vec![evidence],
            })
            .expect_err("invalid output should be rejected");

            assert!(!error.is_empty());
        }
    }

    impl MultimodalExtractionIngest {
        fn source_scope(&self) -> &str {
            &self.ingest.source_scope
        }
    }

    fn derived_evidence(modality: EvidenceModality, parent: &str) -> IngestEvidence {
        IngestEvidence {
            id: Some(format!("derived-{}", modality.as_str())),
            source_path: None,
            span: None,
            confidence: None,
            status: None,
            content: "derived multimodal content".to_owned(),
            entity_labels: Vec::new(),
            extraction: Some(extraction(modality, parent)),
        }
    }

    fn extraction(modality: EvidenceModality, parent: &str) -> IngestEvidenceExtraction {
        IngestEvidenceExtraction {
            modality,
            source_uri: None,
            source_hash: None,
            media_hash: None,
            extractor: Some("fixture-worker".to_owned()),
            extractor_version: Some("1.0".to_owned()),
            observed_at: None,
            parent_evidence_id: Some(parent.to_owned()),
            layout_region: None,
            embedding_model: None,
            embedding_dimension: None,
            diagnostic: None,
        }
    }
}