Skip to main content

relay_knowledge/api/operations/
ingestion.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    api::ApiMetadata,
5    domain::{
6        CommitReceipt, ConfidenceScore, EvidenceExtractionMetadata, EvidenceModality, EvidenceSpan,
7        ExtractionDiagnostic, FactStatus, GraphVersionRange, IndexStatus, LayoutRegion,
8    },
9};
10
11/// Evidence item supplied to the ingest API.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct IngestEvidence {
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub id: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub source_path: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub span: Option<EvidenceSpan>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub confidence: Option<ConfidenceScore>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub status: Option<FactStatus>,
24    pub content: String,
25    #[serde(default)]
26    pub entity_labels: Vec<String>,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub extraction: Option<IngestEvidenceExtraction>,
29}
30
31/// Optional multimodal extraction metadata supplied with an evidence item.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct IngestEvidenceExtraction {
34    pub modality: EvidenceModality,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub source_uri: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub source_hash: Option<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub media_hash: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub extractor: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub extractor_version: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub observed_at: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub parent_evidence_id: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub layout_region: Option<LayoutRegion>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub embedding_model: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub embedding_dimension: Option<u16>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub diagnostic: Option<ExtractionDiagnostic>,
57}
58
59impl IngestEvidenceExtraction {
60    /// Converts API metadata into the domain extraction contract.
61    pub fn into_domain_metadata(self) -> EvidenceExtractionMetadata {
62        EvidenceExtractionMetadata {
63            modality: self.modality,
64            source_uri: self.source_uri,
65            source_hash: self.source_hash,
66            media_hash: self.media_hash,
67            extractor: self.extractor,
68            extractor_version: self.extractor_version,
69            observed_at: self.observed_at,
70            parent_evidence_id: self.parent_evidence_id,
71            layout_region: self.layout_region,
72            embedding_model: self.embedding_model,
73            embedding_dimension: self.embedding_dimension,
74            diagnostic: self
75                .diagnostic
76                .unwrap_or_else(|| EvidenceExtractionMetadata::text_span().diagnostic),
77        }
78    }
79}
80
81/// Structured relation supplied to the ingest API.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct IngestRelation {
84    pub id: String,
85    pub source_entity_label: String,
86    pub relation_type: String,
87    pub target_entity_label: String,
88    #[serde(default)]
89    pub evidence_ids: Vec<String>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub confidence: Option<ConfidenceScore>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub status: Option<FactStatus>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub version_range: Option<GraphVersionRange>,
96}
97
98/// Structured claim supplied to the ingest API.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct IngestClaim {
101    pub id: String,
102    pub subject_entity_label: String,
103    pub predicate: String,
104    pub object: String,
105    #[serde(default)]
106    pub evidence_ids: Vec<String>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub confidence: Option<ConfidenceScore>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub status: Option<FactStatus>,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub version_range: Option<GraphVersionRange>,
113}
114
115/// Structured event supplied to the ingest API.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct IngestEvent {
118    pub id: String,
119    pub event_type: String,
120    #[serde(default)]
121    pub entity_labels: Vec<String>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub occurred_at: Option<String>,
124    #[serde(default)]
125    pub evidence_ids: Vec<String>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub confidence: Option<ConfidenceScore>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub status: Option<FactStatus>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub version_range: Option<GraphVersionRange>,
132}
133
134/// Ingest request shared by CLI, Web, HTTP, and future agent adapters.
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct IngestRequest {
137    pub source_scope: String,
138    #[serde(default)]
139    pub evidence: Vec<IngestEvidence>,
140    #[serde(default)]
141    pub relations: Vec<IngestRelation>,
142    #[serde(default)]
143    pub claims: Vec<IngestClaim>,
144    #[serde(default)]
145    pub events: Vec<IngestEvent>,
146}
147
148/// Ingest response with committed graph and refreshed index versions.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct IngestResponse {
151    pub metadata: ApiMetadata,
152    pub receipt: CommitReceipt,
153    pub indexes: Vec<IndexStatus>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub index_refresh_error: Option<String>,
156}
157
158/// Maintenance-worker output for derived OCR, caption, table, layout, or image embeddings.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct MultimodalExtractionRequest {
161    pub source_scope: String,
162    pub parent_evidence_id: String,
163    pub derived_evidence: Vec<IngestEvidence>,
164}
165
166/// Commit result for a bounded multimodal maintenance batch.
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub struct MultimodalExtractionResponse {
169    pub metadata: ApiMetadata,
170    pub parent_evidence_id: String,
171    pub derived_evidence_count: usize,
172    pub receipt: CommitReceipt,
173    pub indexes: Vec<IndexStatus>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub index_refresh_error: Option<String>,
176}
177
178#[cfg(test)]
179#[path = "ingestion_tests.rs"]
180mod tests;