1use std::collections::BTreeMap;
3
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::{
7 extraction::{
8 AbsenceReason, Distributions, ExtractionLimits, ExtractionSchema, ExtractionType, Fill,
9 RequestFailure,
10 },
11 extraction_documents::ExtractRules,
12 generation::{PromptTemplate, TemplateRef},
13 ids::{ArtifactId, CalibrationId, ContentDigest, DescriptorId, PromptTemplateId},
14 judgment::{Derivation, Unresolved},
15 snapshot::Producer,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
19#[serde(deny_unknown_fields)]
20pub struct ExtractBinding {
21 pub template_id: PromptTemplateId,
22 pub template: PromptTemplate,
23 pub template_binding: TemplateRef,
24 pub projection: Vec<String>,
25 pub schema: ExtractionSchema,
26 pub schema_digest: ContentDigest,
27 pub vocabularies: BTreeMap<String, ResolvedVocabulary>,
28 pub distributions: Distributions,
29 pub limits: ExtractionLimits,
30 pub backend_id: String,
31 pub artifact: ArtifactId,
32 pub descriptor: DescriptorId,
33 pub output: crate::descriptor::OutputKind,
34 pub worst_case_request_bytes: u64,
35 pub fallback: BoundExtractionFallback,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
39#[serde(rename_all = "snake_case", deny_unknown_fields)]
40pub enum ResolvedVocabulary {
41 Options(Vec<String>),
42 Input(String),
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
46#[serde(rename_all = "snake_case", deny_unknown_fields)]
47pub enum BoundExtractionFallback {
48 None,
49 Bound {
50 identity: ContentDigest,
51 program: ExtractRules,
52 fill: Fill,
53 },
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
57#[serde(deny_unknown_fields)]
58pub struct ExtractionEvidence {
59 pub step: String,
60 pub template_id: PromptTemplateId,
61 pub schema_digest: ContentDigest,
62 pub artifact: ArtifactId,
63 pub fallback: FallbackIdentity,
64 pub request_outcome: ExtractionRequestOutcome,
65 pub fields: Vec<ExtractionFieldEvidence>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
69#[serde(rename_all = "snake_case", deny_unknown_fields)]
70pub enum FallbackIdentity {
71 None,
72 Identity(ContentDigest),
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
76#[serde(rename_all = "snake_case", deny_unknown_fields)]
77pub enum ExtractionRequestOutcome {
78 Succeeded,
79 Failed { reason: RequestFailure },
80 Withheld { reason: AbsenceReason },
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
84#[serde(rename_all = "snake_case", deny_unknown_fields)]
85pub enum ExtractionOrigin {
86 Extracted,
87 Fallback,
88 Absent { reason: AbsenceReason },
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
94#[serde(deny_unknown_fields)]
95pub struct ExtractionFieldEvidence {
96 pub name: String,
97 pub origin: ExtractionOrigin,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub derivation: Option<Derivation>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub subfield: Option<String>,
103 pub rejected: Vec<RejectedExtractionItem>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub value_digest: Option<ContentDigest>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 value: Option<serde_json::Value>,
108 items: Vec<ExtractionItemEvidence>,
110 pub distribution: FieldDistribution,
111 pub calibration: ExtractionCalibration,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub provider_reported: Option<String>,
114}
115
116impl ExtractionFieldEvidence {
117 pub fn new(
122 name: String,
123 ty: &ExtractionType,
124 origin: ExtractionOrigin,
125 derivation: Option<Derivation>,
126 value_digest: Option<ContentDigest>,
127 value: Option<serde_json::Value>,
128 mut items: Vec<ExtractionItemEvidence>,
129 ) -> Self {
130 let value_digest =
131 value_digest.filter(|_| !matches!(origin, ExtractionOrigin::Absent { .. }));
132 for item in &mut items {
133 item.value = item.value.take().filter(|value| match ty {
134 ExtractionType::List { item, .. } => retained_value(item, value),
135 _ => false,
136 });
137 }
138 Self {
139 name,
140 origin,
141 derivation,
142 subfield: None,
143 rejected: Vec::new(),
144 value_digest,
145 value: value.filter(|value| retained_value(ty, value)),
146 items,
147 distribution: FieldDistribution::Absent {
148 reason: DistributionAbsence::NotReturned,
149 },
150 calibration: ExtractionCalibration::None,
151 provider_reported: None,
152 }
153 }
154
155 pub fn value(&self) -> Option<&serde_json::Value> {
156 self.value.as_ref()
157 }
158
159 pub fn items(&self) -> &[ExtractionItemEvidence] {
160 &self.items
161 }
162}
163
164impl<'de> Deserialize<'de> for ExtractionFieldEvidence {
165 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
166 #[derive(Deserialize)]
167 #[serde(remote = "ExtractionFieldEvidence", deny_unknown_fields)]
168 struct Wire {
169 name: String,
170 origin: ExtractionOrigin,
171 #[serde(default, deserialize_with = "crate::screening::present")]
172 derivation: Option<Derivation>,
173 #[serde(default, deserialize_with = "crate::screening::present")]
174 subfield: Option<String>,
175 rejected: Vec<RejectedExtractionItem>,
176 #[serde(default, deserialize_with = "crate::screening::present")]
177 value_digest: Option<ContentDigest>,
178 #[serde(default, deserialize_with = "crate::screening::present")]
179 value: Option<serde_json::Value>,
180 items: Vec<ExtractionItemEvidence>,
181 distribution: FieldDistribution,
182 calibration: ExtractionCalibration,
183 #[serde(default, deserialize_with = "crate::screening::present")]
184 provider_reported: Option<String>,
185 }
186 let v = Wire::deserialize(d)?;
187 let valid_origin = match v.origin {
188 ExtractionOrigin::Absent { .. } => {
189 v.derivation.is_none()
190 && v.value.is_none()
191 && v.items.is_empty()
192 && v.value_digest.is_none()
193 }
194 ExtractionOrigin::Extracted => {
195 v.derivation == Some(Derivation::ModelDerived) && v.value_digest.is_some()
196 }
197 ExtractionOrigin::Fallback => {
198 matches!(
199 v.derivation,
200 Some(Derivation::ExactDerived | Derivation::MixedDerived)
201 ) && v.value_digest.is_some()
202 }
203 };
204 if !valid_origin || v.provider_reported.as_ref().is_some_and(|s| s.len() > 256) {
205 return Err(serde::de::Error::custom(
206 "invalid extraction origin, derivation or provider report",
207 ));
208 }
209 Ok(v)
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
214#[serde(deny_unknown_fields)]
215pub struct RejectedExtractionItem {
216 pub index: u64,
217 pub reason: AbsenceReason,
218 pub value_digest: ContentDigest,
219}
220
221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
222#[serde(deny_unknown_fields)]
223pub struct ExtractionItemEvidence {
224 pub index: u64,
225 pub value_digest: ContentDigest,
226 #[serde(
227 default,
228 skip_serializing_if = "Option::is_none",
229 deserialize_with = "crate::screening::present"
230 )]
231 pub value: Option<serde_json::Value>,
232 pub distribution: FieldDistribution,
233 pub calibration: ExtractionCalibration,
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
237#[serde(rename_all = "snake_case", deny_unknown_fields)]
238pub enum FieldDistribution {
239 Masses(#[serde(deserialize_with = "masses")] Vec<f64>),
240 Absent { reason: DistributionAbsence },
241}
242
243fn masses<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<f64>, D::Error> {
244 let v = Vec::<f64>::deserialize(d)?;
245 if v.is_empty() || v.iter().any(|n| !n.is_finite() || !(0.0..=1.0).contains(n)) {
246 return Err(serde::de::Error::custom("invalid distribution masses"));
247 }
248 Ok(v)
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
252#[serde(rename_all = "snake_case")]
253pub enum DistributionAbsence {
254 IncompleteLogprobs,
255 NotReturned,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
259#[serde(rename_all = "snake_case", deny_unknown_fields)]
260pub enum ExtractionCalibration {
261 None,
262 Applied(CalibrationId),
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
266#[serde(rename_all = "snake_case", deny_unknown_fields)]
267pub enum AcceptedOrigin {
268 Supplied,
269 Extracted,
270 Fallback,
271 HostExtracted {
272 #[serde(
273 default,
274 skip_serializing_if = "Option::is_none",
275 deserialize_with = "crate::screening::present"
276 )]
277 producer: Option<Producer>,
278 },
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
282#[serde(deny_unknown_fields)]
283pub struct AcceptanceEvidence {
284 pub step: String,
285 pub field: String,
286 #[serde(
288 default,
289 skip_serializing_if = "Option::is_none",
290 deserialize_with = "crate::screening::present"
291 )]
292 pub value_read: Option<AcceptanceRead>,
293 #[serde(
294 default,
295 skip_serializing_if = "Option::is_none",
296 deserialize_with = "crate::screening::present"
297 )]
298 pub origin: Option<AcceptedOrigin>,
299 pub outcome: AcceptanceOutcome,
300 pub validation: AcceptanceValidation,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
304#[serde(rename_all = "snake_case", deny_unknown_fields)]
305pub enum AcceptanceOutcome {
306 Accepted,
307 PassedThrough,
308 Dropped { cause: AcceptanceFailure },
309 Unresolved,
310}
311
312#[derive(
313 Debug,
314 Clone,
315 Copy,
316 PartialEq,
317 Eq,
318 PartialOrd,
319 Ord,
320 Hash,
321 Serialize,
322 Deserialize,
323 schemars::JsonSchema,
324)]
325#[serde(rename_all = "snake_case", deny_unknown_fields)]
326pub enum AcceptanceFailure {
327 Absence(AbsenceReason),
328 ValidationFailed,
329 ValidationFault,
330 MinMassNotMet,
331 DistributionAbsent,
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
335#[serde(rename_all = "snake_case", deny_unknown_fields)]
336pub enum AcceptanceValidation {
337 NotApplied,
338 Passed,
339 Failed,
340 Fault(Unresolved),
341}
342
343pub type Producers = BTreeMap<String, Producer>;
345
346fn retained_value(ty: &ExtractionType, value: &serde_json::Value) -> bool {
347 match ty {
348 ExtractionType::Bool {} => value.is_boolean(),
349 ExtractionType::Integer { .. } => value.as_i64().is_some(),
350 ExtractionType::Decimal(_) | ExtractionType::Date {} | ExtractionType::Enum { .. } => {
351 value.is_string()
352 }
353 ExtractionType::List { item, .. } if matches!(**item, ExtractionType::Enum { .. }) => value
354 .as_array()
355 .is_some_and(|a| a.iter().all(serde_json::Value::is_string)),
356 _ => false,
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Serialize, schemars::JsonSchema)]
362#[serde(deny_unknown_fields)]
363pub struct AcceptanceRead {
364 pub value_digest: ContentDigest,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
366 value: Option<serde_json::Value>,
367}
368impl AcceptanceRead {
369 pub fn new(
372 ty: &ExtractionType,
373 value_digest: ContentDigest,
374 value: Option<serde_json::Value>,
375 ) -> Self {
376 Self {
377 value_digest,
378 value: value.filter(|value| retained_value(ty, value)),
379 }
380 }
381
382 pub fn value(&self) -> Option<&serde_json::Value> {
383 self.value.as_ref()
384 }
385}
386
387impl<'de> Deserialize<'de> for AcceptanceRead {
388 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
389 #[derive(Deserialize)]
390 #[serde(remote = "AcceptanceRead", deny_unknown_fields)]
391 struct Wire {
392 value_digest: ContentDigest,
393 #[serde(default, deserialize_with = "crate::screening::present")]
394 value: Option<serde_json::Value>,
395 }
396 Wire::deserialize(d)
397 }
398}