1use std::collections::{BTreeMap, BTreeSet};
4
5use rustev_contract::canonical::canonical_bytes;
6use rustev_contract::definition::{ItemSource, RequiredKind, StepBody};
7use rustev_contract::descriptor::{OutputKind, TextStructure};
8use rustev_contract::evidence::EvidenceRecord;
9use rustev_contract::generation::*;
10use rustev_contract::generation_result::*;
11use rustev_contract::ids::ContentDigest;
12use rustev_contract::judgment::{Derivation, Judgment, Lineage, OutValue, Outcome, Unresolved};
13use rustev_contract::output::{RawOutput, TextValue};
14use rustev_contract::plan::{Normalization, PlanExecution, PlanStepDetail};
15use rustev_contract::request::RequestDoc;
16use rustev_contract::schema;
17use rustev_contract::scope::Scope;
18
19use crate::Compiled;
20use crate::evaluate::{NotReady, RequestIdentityError, Supplied, SupplyError};
21use crate::generation::Checked;
22use crate::template as t;
23use crate::value::Value;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum GenerationError {
27 NotReady(NotReady),
28 InvalidState(String),
30}
31impl std::fmt::Display for GenerationError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(f, "generation: {self:?}")
34 }
35}
36impl std::error::Error for GenerationError {}
37fn bad(s: impl ToString) -> GenerationError {
38 GenerationError::InvalidState(s.to_string())
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct GenerationRequest {
44 pub backend_id: String,
45 pub document: GenerateRequest,
46}
47struct Item {
48 step: String,
49 declaration: Generate,
50 result: TextItem,
51 backstops: BTreeMap<String, TextField>,
52 pending: Option<GenerationRequest>,
53 round: u64,
54}
55
56pub struct GenerationEvaluation<'c> {
59 compiled: &'c Compiled,
60 judgment: Judgment,
61 evidence: EvidenceRecord,
62 steps: Vec<TextStep>,
63 items: Vec<Item>,
64 notices: Vec<TextNotice>,
65 remaining: u64,
66}
67impl<'c> GenerationEvaluation<'c> {
68 pub(crate) fn start(
69 compiled: &'c Compiled,
70 judgment: Judgment,
71 evidence: EvidenceRecord,
72 values: (BTreeMap<String, Value>, BTreeMap<String, Value>),
73 lineage: BTreeMap<String, Lineage>,
74 issued: u64,
75 ) -> Result<Self, GenerationError> {
76 let (inputs, exact) = values;
77 let mut stage = Self {
78 compiled,
79 judgment,
80 evidence,
81 steps: vec![],
82 items: vec![],
83 notices: vec![],
84 remaining: compiled
85 .plan
86 .definition
87 .limits
88 .max_semantic_requests
89 .saturating_sub(issued),
90 };
91 let mut roots = BTreeMap::from([
92 ("input".into(), Value::Record(inputs)),
93 ("step".into(), Value::Record(exact)),
94 ]);
95 if let Outcome::Propose { params, .. } = &stage.judgment.outcome {
96 roots.insert(
97 "proposal".into(),
98 Value::Record(
99 params
100 .iter()
101 .map(|(n, v)| Ok((n.clone(), from_out(v)?)))
102 .collect::<Result<_, GenerationError>>()?,
103 ),
104 );
105 }
106 for step in &compiled.plan.definition.steps {
107 let StepBody::Generate(g) = &step.body else {
108 continue;
109 };
110 let checked = compiled
111 .info
112 .generation
113 .get(&step.id)
114 .ok_or_else(|| bad("missing compiled templates"))?;
115 let matches = match &stage.judgment.outcome {
116 Outcome::Propose { action, .. } => match &g.when {
117 GenerateWhen::AnyPropose => true,
118 GenerateWhen::Actions(actions) => actions.contains(action),
119 },
120 _ => false,
121 };
122 let mut result = TextStep {
123 step: step.id.clone(),
124 state: GenerationState::Ran,
125 lineage: stage.judgment.lineage.clone(),
126 items: vec![],
127 text_origin: TextOrigin::None,
128 counts: TextCounts::default(),
129 };
130 if !matches {
131 result.state = GenerationState::Skipped {
132 reason: GenerationSkipReason::OutcomeNotMatched,
133 };
134 } else {
135 match make_items(&step.id, g, checked, &roots, &lineage) {
136 Ok(mut items) => {
137 for item in &mut items {
138 if item.pending.is_some() {
139 if stage.remaining == 0 {
140 complete(item, TextReason::BudgetExhausted);
141 } else {
142 stage.remaining -= 1;
143 }
144 }
145 }
146 stage.items.extend(items);
147 }
148 Err(u) => {
149 result.state = GenerationState::Skipped {
150 reason: GenerationSkipReason::SourceUnresolved(u),
151 }
152 }
153 }
154 }
155 stage.steps.push(result);
156 }
157 Ok(stage)
158 }
159
160 pub fn judgment(&self) -> &Judgment {
162 &self.judgment
163 }
164
165 pub fn pending(&self) -> Vec<GenerationRequest> {
167 self.items
168 .iter()
169 .filter_map(|i| i.pending.clone())
170 .collect()
171 }
172
173 pub fn request_document(
174 &self,
175 step: &str,
176 instance: &[String],
177 target: u32,
178 scope: &Scope,
179 ) -> Result<RequestDoc, RequestIdentityError> {
180 let missing = || RequestIdentityError::NotPending {
181 step: step.into(),
182 instance: instance.to_vec(),
183 };
184 let item = self.find(step, instance).ok_or_else(missing)?;
185 let request = item.pending.as_ref().ok_or_else(missing)?;
186 let binding = self
187 .compiled
188 .plan
189 .steps
190 .iter()
191 .find_map(|s| match &s.detail {
192 PlanStepDetail::Generate(b) if s.id == step => Some(b),
193 _ => None,
194 })
195 .ok_or_else(missing)?;
196 let unknown = || RequestIdentityError::UnknownTarget {
197 step: step.into(),
198 target,
199 };
200 let (backend_id, artifact, descriptor) = if target == 0 {
201 (&binding.backend_id, &binding.artifact, &binding.descriptor)
202 } else {
203 let PlanExecution::Declared(e) = &self.compiled.plan.execution else {
204 return Err(unknown());
205 };
206 let f = e
207 .fallbacks
208 .iter()
209 .find(|f| f.step == step && f.target == target)
210 .ok_or_else(unknown)?;
211 (&f.backend_id, &f.artifact, &f.descriptor)
212 };
213 let envelope = serde_json::json!({"operation":"generate", "plan_id":self.compiled.id, "request":request.document});
216 let projection = canonical_bytes(&envelope)
217 .ok()
218 .and_then(|b| String::from_utf8(b).ok())
219 .ok_or_else(|| RequestIdentityError::ProjectionNotUtf8 { step: step.into() })?;
220 let max_output_bytes = request
221 .document
222 .output
223 .fields
224 .iter()
225 .map(|f| match &f.ty {
226 GeneratedType::Text { max_bytes, .. } => *max_bytes,
227 GeneratedType::List {
228 vocabulary,
229 max_items,
230 } => {
231 max_items.saturating_mul(
234 binding
235 .vocabularies
236 .get(vocabulary)
237 .into_iter()
238 .flatten()
239 .map(|s| s.len() as u64)
240 .max()
241 .unwrap_or(0),
242 )
243 }
244 })
245 .fold(0u64, u64::saturating_add);
246 Ok(RequestDoc {
247 schema: schema::REQUEST.into(),
248 scope: scope.clone(),
249 backend_id: backend_id.clone(),
250 artifact: artifact.clone(),
251 descriptor: descriptor.clone(),
252 projection,
253 output: OutputKind::TextRecord {
254 structure: TextStructure::JsonSchema,
255 max_output_bytes,
256 languages: vec![request.document.language.clone()],
257 streaming: false,
258 },
259 requires: RequiredKind::TextRecord,
260 normalization: Normalization::None,
261 })
262 }
263
264 fn find(&self, step: &str, instance: &[String]) -> Option<&Item> {
265 self.items.iter().find(|i| {
266 i.step == step
267 && i.pending
268 .as_ref()
269 .is_some_and(|r| r.document.instance == instance)
270 })
271 }
272
273 pub fn check_output(
276 &self,
277 step: &str,
278 instance: &[String],
279 raw: &RawOutput,
280 ) -> Result<Result<(), Unresolved>, SupplyError> {
281 let item = self
282 .find(step, instance)
283 .ok_or_else(|| SupplyError::NotPending {
284 step: step.into(),
285 instance: instance.to_vec(),
286 })?;
287 Ok(record_check(item, raw))
288 }
289
290 pub fn supply(
292 &mut self,
293 step: &str,
294 instance: &[String],
295 supplied: Supplied,
296 ) -> Result<(), GenerationError> {
297 self.supply_attempt(
298 step,
299 instance,
300 supplied,
301 &format!("{step}:{instance:?}:1"),
302 1,
303 )
304 }
305
306 pub fn supply_attempt(
309 &mut self,
310 step: &str,
311 instance: &[String],
312 supplied: Supplied,
313 attempt: &str,
314 attempts: u32,
315 ) -> Result<(), GenerationError> {
316 if attempt.is_empty() || attempts == 0 {
317 return Err(bad("attempt identity and count required"));
318 }
319 let failure = if let Supplied::Failed(u) = &supplied {
320 Some(failure_reason(u, attempts)?)
321 } else {
322 None
323 };
324 let item = self
325 .items
326 .iter_mut()
327 .find(|i| {
328 i.step == step
329 && i.pending
330 .as_ref()
331 .is_some_and(|r| r.document.instance == instance)
332 })
333 .ok_or_else(|| bad("request is not pending"))?;
334 let request = item
335 .pending
336 .as_ref()
337 .ok_or_else(|| bad("request is not pending"))?
338 .clone();
339 if let Some(reason) = failure {
340 complete(item, reason);
341 return Ok(());
342 }
343 let Supplied::Output(raw) = supplied else {
344 return Err(bad("missing supply"));
345 };
346 if record_check(item, &raw).is_err() {
347 if let RawOutput::TextRecord(fields) = &raw {
348 for name in fields
349 .keys()
350 .filter(|n| !item.declaration.output.fields.iter().any(|f| &f.name == *n))
351 {
352 self.notices.push(TextNotice {
353 kind: TextNoticeKind::OwnershipViolation,
354 step: step.into(),
355 instance: item.result.id.clone(),
356 field: name.clone(),
357 });
358 }
359 }
360 complete(item, TextReason::InvalidOutput);
361 return Ok(());
362 }
363 let RawOutput::TextRecord(fields) = raw else {
364 return Err(bad("record check disagreed"));
365 };
366 let mut invalid = Vec::new();
367 for field in &item.declaration.output.fields {
368 if item.result.generated.contains_key(&field.name) {
369 continue;
370 }
371 match validate_field(
372 field,
373 fields.get(&field.name),
374 &request.document.vocabularies,
375 ) {
376 Ok(value) => {
377 item.result.generated.insert(
378 field.name.clone(),
379 TextField {
380 origin: if item.round == 0 {
381 FieldOrigin::Generated
382 } else {
383 FieldOrigin::Repaired
384 },
385 truncated: false,
386 source: TextSource::Fresh,
387 value: Some(value.clone()),
388 derivation: Some(Derivation::ModelDerived),
389 lineage: Some(TextLineage::Model {
390 step: step.into(),
391 template: request.document.template_id.clone(),
392 projection: item.declaration.projection.clone(),
393 attempt: attempt.into(),
394 }),
395 },
396 );
397 }
398 Err(reason) => invalid.push(RepairField {
399 name: field.name.clone(),
400 reason,
401 }),
402 }
403 }
404 if invalid.is_empty() {
405 item.pending = None;
406 } else if item.round < item.declaration.repair.max_attempts {
407 if self.remaining == 0 {
408 complete(item, TextReason::BudgetExhausted);
409 return Ok(());
410 }
411 self.remaining -= 1;
412 item.round += 1;
413 let mut next = request;
414 next.document.instance = item.result.id.clone();
415 next.document
416 .instance
417 .push(format!("repair:{}", item.round));
418 next.document.repair = RepairRequest::Round {
421 round: item.round,
422 fields: invalid.clone(),
423 };
424 next.document.output = item.declaration.output.clone();
425 if item.declaration.repair.only_missing_fields {
426 next.document
427 .output
428 .fields
429 .retain(|f| invalid.iter().any(|i| i.name == f.name));
430 }
431 item.pending = Some(next);
432 } else {
433 complete(item, TextReason::InvalidOutput);
434 }
435 Ok(())
436 }
437
438 pub fn withhold(
441 &mut self,
442 step: &str,
443 instance: &[String],
444 reason: TextReason,
445 ) -> Result<(), GenerationError> {
446 let item = self
447 .items
448 .iter_mut()
449 .find(|i| {
450 i.step == step
451 && i.pending
452 .as_ref()
453 .is_some_and(|r| r.document.instance == instance)
454 })
455 .ok_or_else(|| bad("request is not pending"))?;
456 complete(item, reason);
457 Ok(())
458 }
459
460 pub fn verify_ownership(&self, judgment: &mut Judgment, text: &mut TextResult) {
463 if judgment != &self.judgment {
464 *judgment = self.judgment.clone();
465 text.notices.push(TextNotice {
466 kind: TextNoticeKind::OwnershipRestored,
467 step: String::new(),
468 instance: vec![],
469 field: "judgment".into(),
470 });
471 }
472 for step in &mut text.steps {
473 for item in &mut step.items {
474 if let Some(source) = self
475 .items
476 .iter()
477 .find(|i| i.step == step.step && i.result.id == item.id)
478 {
479 let names: BTreeSet<_> = item
480 .context
481 .keys()
482 .chain(source.result.context.keys())
483 .cloned()
484 .collect();
485 for name in names {
486 if item.context.get(&name) != source.result.context.get(&name) {
487 text.notices.push(TextNotice {
488 kind: TextNoticeKind::OwnershipRestored,
489 step: step.step.clone(),
490 instance: item.id.clone(),
491 field: name,
492 });
493 }
494 }
495 item.context = source.result.context.clone();
496 }
497 }
498 }
499 }
500
501 pub fn finish(
502 &self,
503 ) -> Result<(Judgment, EvidenceRecord, Option<TextResult>), GenerationError> {
504 let pending = self.pending().len();
505 if pending != 0 {
506 return Err(GenerationError::NotReady(NotReady { pending }));
507 }
508 if self.steps.is_empty() {
509 return Ok((self.judgment.clone(), self.evidence.clone(), None));
510 }
511 let mut text = TextResult {
512 schema: schema::TEXT_RESULT.into(),
513 decision_id: self.evidence.decision_id.clone(),
514 plan_id: self.compiled.id.clone(),
515 steps: self.steps.clone(),
516 text_origin: TextOrigin::None,
517 counts: TextCounts::default(),
518 notices: self.notices.clone(),
519 };
520 for step in &mut text.steps {
521 step.items = self
522 .items
523 .iter()
524 .filter(|i| i.step == step.step)
525 .map(|i| i.result.clone())
526 .collect();
527 for item in &step.items {
528 for (name, field) in &item.generated {
529 if field.truncated {
530 text.notices.push(TextNotice {
531 kind: TextNoticeKind::BackstopTruncated,
532 step: step.step.clone(),
533 instance: item.id.clone(),
534 field: name.clone(),
535 });
536 }
537 }
538 }
539 (step.text_origin, step.counts) =
540 summarize(step.items.iter().flat_map(|i| i.generated.values()));
541 }
542 (text.text_origin, text.counts) = summarize(
543 text.steps
544 .iter()
545 .flat_map(|s| s.items.iter().flat_map(|i| i.generated.values())),
546 );
547 let mut judgment = self.judgment.clone();
548 self.verify_ownership(&mut judgment, &mut text);
549 let mut evidence_record = self.evidence.clone();
550 evidence_record.generation = Some(
551 text.steps
552 .iter()
553 .map(|s| evidence(s, &text.notices))
554 .collect::<Result<_, _>>()?,
555 );
556 Ok((judgment, evidence_record, Some(text)))
557 }
558}
559
560fn failure_reason(u: &Unresolved, attempts: u32) -> Result<TextReason, GenerationError> {
561 Ok(match u {
562 Unresolved::InvalidBackendOutput { .. } => TextReason::InvalidOutput,
563 Unresolved::DeadlineExceeded => TextReason::Deadline,
564 Unresolved::BudgetExhausted { .. } => TextReason::BudgetExhausted,
565 Unresolved::BackendUnavailable { .. } if attempts >= 2 => TextReason::RetriesExhausted,
566 Unresolved::BackendUnavailable { .. } => TextReason::BackendUnavailable,
567 _ => return Err(bad("not a runtime failure")),
568 })
569}
570fn record_check(item: &Item, raw: &RawOutput) -> Result<(), Unresolved> {
571 let RawOutput::TextRecord(fields) = raw else {
572 return Err(Unresolved::InvalidBackendOutput {
573 detail: "expected text_record".into(),
574 });
575 };
576 let Some(request) = &item.pending else {
577 return Err(Unresolved::InvalidBackendOutput {
578 detail: "no pending request".into(),
579 });
580 };
581 if fields
582 .keys()
583 .any(|n| !request.document.output.fields.iter().any(|f| &f.name == n))
584 {
585 return Err(Unresolved::InvalidBackendOutput {
586 detail: "undeclared or unrequested member".into(),
587 });
588 }
589 Ok(())
590}
591pub fn validate_field<'a>(
593 field: &GeneratedField,
594 value: Option<&'a TextValue>,
595 vocabularies: &BTreeMap<String, Vec<String>>,
596) -> Result<&'a TextValue, FieldInvalid> {
597 let value = value.ok_or(FieldInvalid::Missing)?;
598 match (&field.ty, value) {
599 (GeneratedType::Text { max_bytes, lines }, TextValue::Text(s)) => {
600 if s.is_empty() {
601 return Err(FieldInvalid::Empty);
602 }
603 if s.len() as u64 > *max_bytes {
604 return Err(FieldInvalid::TooLong);
605 }
606 if s.chars()
607 .any(|c| c.is_control() && !(c == '\n' && *lines == Lines::Multi))
608 {
609 return Err(FieldInvalid::ControlCharacter);
610 }
611 }
612 (
613 GeneratedType::List {
614 vocabulary,
615 max_items,
616 },
617 TextValue::List(v),
618 ) => {
619 if field.required && v.is_empty() {
620 return Err(FieldInvalid::Empty);
621 }
622 if v.len() as u64 > *max_items {
623 return Err(FieldInvalid::TooManyItems);
624 }
625 let options = vocabularies
626 .get(vocabulary)
627 .ok_or(FieldInvalid::NotInVocabulary)?;
628 if v.iter().any(|s| !options.contains(s)) {
629 return Err(FieldInvalid::NotInVocabulary);
630 }
631 if v.iter().collect::<BTreeSet<_>>().len() != v.len() {
632 return Err(FieldInvalid::DuplicateItem);
633 }
634 }
635 _ => return Err(FieldInvalid::WrongType),
636 }
637 Ok(value)
638}
639fn complete(item: &mut Item, reason: TextReason) {
640 for field in &item.declaration.output.fields {
641 item.result
642 .generated
643 .entry(field.name.clone())
644 .or_insert_with(|| {
645 let mut slot = item
646 .backstops
647 .get(&field.name)
648 .cloned()
649 .unwrap_or(TextField {
650 origin: FieldOrigin::Absent { reason },
651 truncated: false,
652 source: TextSource::Fresh,
653 value: None,
654 derivation: None,
655 lineage: None,
656 });
657 if slot.value.is_some() {
658 slot.origin = FieldOrigin::Backstop { reason };
659 }
660 slot
661 });
662 }
663 item.pending = None;
664}
665fn summarize<'a>(fields: impl Iterator<Item = &'a TextField>) -> (TextOrigin, TextCounts) {
666 let mut c = TextCounts::default();
667 for f in fields {
668 match f.origin {
669 FieldOrigin::Generated => c.generated += 1,
670 FieldOrigin::Repaired => c.repaired += 1,
671 FieldOrigin::Backstop { reason } => {
672 c.backstop += 1;
673 *c.reasons.entry(reason).or_default() += 1;
674 }
675 FieldOrigin::Absent { reason } => {
676 c.absent += 1;
677 *c.reasons.entry(reason).or_default() += 1;
678 }
679 }
680 }
681 let origin = if c.generated + c.repaired + c.backstop == 0 {
682 TextOrigin::None
683 } else if c.backstop + c.absent == 0 {
684 TextOrigin::Generated
685 } else if c.generated + c.repaired == 0 {
686 TextOrigin::Backstop
687 } else {
688 TextOrigin::Partial
689 };
690 (origin, c)
691}
692fn digest<T: serde::Serialize>(value: &T) -> Result<ContentDigest, GenerationError> {
693 rustev_contract::generation_result::generation_value_digest(value).map_err(bad)
694}
695fn evidence(
696 step: &TextStep,
697 notices: &[TextNotice],
698) -> Result<GenerationEvidence, GenerationError> {
699 Ok(GenerationEvidence {
700 step: step.step.clone(),
701 state: step.state.clone(),
702 text_origin: step.text_origin,
703 counts: step.counts.clone(),
704 notices: notices
705 .iter()
706 .filter(|n| n.step == step.step)
709 .cloned()
710 .collect(),
711 items: step
712 .items
713 .iter()
714 .map(|i| {
715 Ok(GenerationItemEvidence {
716 id: i.id.clone(),
717 context: i
718 .context
719 .iter()
720 .map(|(n, v)| Ok((n.clone(), digest(&v.value)?)))
721 .collect::<Result<_, GenerationError>>()?,
722 generated: i
723 .generated
724 .iter()
725 .map(|(n, f)| {
726 Ok((
727 n.clone(),
728 TextFieldEvidence {
729 origin: f.origin.clone(),
730 truncated: f.truncated,
731 source: f.source.clone(),
732 value_digest: f.value.as_ref().map(digest).transpose()?,
733 byte_length: f.value.as_ref().map(|v| match v {
734 TextValue::Text(s) => s.len() as u64,
735 TextValue::List(v) => {
736 v.iter().map(|s| s.len() as u64).sum()
737 }
738 }),
739 },
740 ))
741 })
742 .collect::<Result<_, GenerationError>>()?,
743 })
744 })
745 .collect::<Result<_, GenerationError>>()?,
746 })
747}
748
749fn reference<'a>(r: &str, roots: &'a BTreeMap<String, Value>) -> Result<&'a Value, Unresolved> {
750 let missing = || Unresolved::missing([r.to_string()]);
751 let (root, rest) = r.split_once(':').ok_or_else(missing)?;
752 let mut parts = rest.split('/');
753 let mut value = if root == "bind" {
754 roots
755 .get(parts.next().ok_or_else(missing)?)
756 .ok_or_else(missing)?
757 } else {
758 roots.get(root).ok_or_else(missing)?
759 };
760 for part in parts {
761 let Value::Record(fields) = value else {
762 return Err(missing());
763 };
764 value = fields.get(part).ok_or_else(missing)?;
765 }
766 if matches!(value, Value::Null | Value::Absent(_)) {
767 return Err(missing());
768 }
769 Ok(value)
770}
771fn from_out(v: &OutValue) -> Result<Value, GenerationError> {
772 Ok(match v {
773 OutValue::Bool(v) => Value::Bool(*v),
774 OutValue::Integer(v) => Value::Integer(*v),
775 OutValue::Decimal(v) => Value::Decimal(*v),
776 OutValue::Text(v) => Value::Text(v.clone()),
777 OutValue::Enum(v) => Value::Enum(v.clone()),
778 OutValue::Timestamp(v) => Value::Timestamp(*v),
779 OutValue::Duration(v) => Value::Duration(*v),
780 OutValue::None => Value::Maybe(None),
781 OutValue::List(v) => Value::List(v.iter().map(from_out).collect::<Result<_, _>>()?),
782 OutValue::Record(v) => Value::Record(
783 v.iter()
784 .map(|(n, v)| Ok((n.clone(), from_out(v)?)))
785 .collect::<Result<_, GenerationError>>()?,
786 ),
787 OutValue::Filtered(v) => Value::Filtered(v.clone()),
788 OutValue::Shortlist(v) => Value::Shortlist(v.clone()),
789 OutValue::Ranking(v) => Value::Ranking(v.clone()),
790 })
791}
792fn template_value(v: &Value) -> Option<t::Value> {
795 Some(match v {
796 Value::Bool(v) => t::Value::Bool(*v),
797 Value::Integer(v) => t::Value::Integer(*v),
798 Value::Decimal(v) => t::Value::Decimal(*v),
799 Value::Text(v) => t::Value::Text(v.clone()),
800 Value::Enum(v) => t::Value::Enum(v.clone()),
801 Value::Timestamp(v) => t::Value::Timestamp(*v),
802 Value::Maybe(v) => t::Value::Maybe(match v {
803 Some(v) => Some(Box::new(template_value(v)?)),
804 None => None,
805 }),
806 Value::Null | Value::Absent(_) => t::Value::Maybe(None),
807 Value::List(v) => t::Value::List(v.iter().map(template_value).collect::<Option<_>>()?),
808 Value::Record(v) => t::Value::Record(
809 v.iter()
810 .filter_map(|(n, v)| template_value(v).map(|v| (n.clone(), v)))
811 .collect(),
812 ),
813 Value::Filtered(_) | Value::Shortlist(_) => {
814 t::Value::List(v.ids()?.into_iter().map(t::Value::Text).collect())
815 }
816 _ => return None,
817 })
818}
819fn make_items(
820 id: &str,
821 g: &Generate,
822 checked: &Checked,
823 roots: &BTreeMap<String, Value>,
824 lineage: &BTreeMap<String, Lineage>,
825) -> Result<Vec<Item>, Unresolved> {
826 let invalid = || Unresolved::invalid([], "generation source cannot be rendered");
827 let instances = match &g.for_each {
828 ModelForEach::None => vec![vec![]],
829 ModelForEach::List(f) => reference(&f.over, roots)?
830 .ids()
831 .ok_or_else(invalid)?
832 .into_iter()
833 .map(|id| vec![id])
834 .collect(),
835 };
836 let mut items = Vec::new();
837 for (index, instance) in instances.into_iter().enumerate() {
838 let mut roots = roots.clone();
839 if let ModelForEach::List(f) = &g.for_each {
840 let item_id = instance.first().ok_or_else(invalid)?;
841 let value = match &f.items {
842 ItemSource::None => Value::Text(item_id.clone()),
843 ItemSource::List { list, id_field } => {
844 let Value::List(values) = reference(list, &roots)? else {
845 return Err(invalid());
846 };
847 values.iter().find(|v| matches!(v, Value::Record(m) if matches!(m.get(id_field), Some(Value::Text(s) | Value::Enum(s)) if s == item_id))).cloned().ok_or_else(invalid)?
848 }
849 };
850 roots.insert(f.bind.clone(), value);
851 }
852 let mut params = BTreeMap::new();
853 for (name, binding) in &g.template.params {
854 let value = match binding {
855 TemplateBinding::Reference(r) => reference(r, &roots)?.clone(),
856 TemplateBinding::Literal { literal } => Value::from_literal(literal),
857 TemplateBinding::Parameter(_) => return Err(invalid()),
858 };
859 params.insert(name.clone(), template_value(&value).ok_or_else(invalid)?);
860 }
861 let instructions = checked
862 .prompt
863 .as_ref()
864 .ok_or_else(invalid)?
865 .render(&BTreeMap::from([(
866 "param".into(),
867 t::Value::Record(params),
868 )]))
869 .map_err(|_| invalid())?
870 .text;
871 let values = roots
872 .iter()
873 .filter_map(|(n, v)| template_value(v).map(|v| (n.clone(), v)))
874 .collect();
875 let mut backstops = BTreeMap::new();
876 for (name, backstop) in &g.backstop.fields {
877 let (value, truncated, paths) = match backstop {
878 BackstopField::Text(_) => {
879 let template = checked.backstops.get(name).ok_or_else(invalid)?;
880 let rendered = template.render(&values).map_err(|_| invalid())?;
881 (
882 TextValue::Text(rendered.text),
883 rendered.truncated,
884 rendered.reads.into_iter().collect(),
885 )
886 }
887 BackstopField::List(v) => (TextValue::List(v.clone()), false, vec![]),
888 };
889 backstops.insert(
890 name.clone(),
891 TextField {
892 origin: FieldOrigin::Backstop {
893 reason: TextReason::NotAttempted,
894 },
895 value: Some(value),
896 truncated,
897 source: TextSource::Fresh,
898 derivation: Some(Derivation::ExactDerived),
899 lineage: Some(TextLineage::Backstop { paths }),
900 },
901 );
902 }
903 let mut context = BTreeMap::new();
904 for r in &g.carry {
905 let value = reference(r, &roots)?;
906 let source = if r.starts_with("bind:") {
907 match &g.for_each {
908 ModelForEach::List(f) => match &f.items {
909 ItemSource::List { list, .. } => list.as_str(),
910 ItemSource::None => f.over.as_str(),
911 },
912 ModelForEach::None => r.as_str(),
913 }
914 } else {
915 r.as_str()
916 };
917 let base = crate::expr::base_ref_v2(source);
918 let l = lineage.get(&base).cloned().unwrap_or_else(|| Lineage {
919 inputs: base
920 .strip_prefix("input:")
921 .map(String::from)
922 .into_iter()
923 .collect(),
924 steps: base
925 .strip_prefix("step:")
926 .map(String::from)
927 .into_iter()
928 .collect(),
929 });
930 let name = r.rsplit(['/', ':']).next().ok_or_else(invalid)?;
931 context.insert(
932 name.into(),
933 ContextValue {
934 value: value.to_out(),
935 derivation: Derivation::ExactDerived,
936 lineage: l,
937 },
938 );
939 }
940 let projection = g
941 .projection
942 .iter()
943 .map(|r| Ok((r.clone(), reference(r, &roots)?.to_json())))
944 .collect::<Result<_, Unresolved>>()?;
945 let Language::Literal(language) = &g.language else {
946 return Err(invalid());
947 };
948 let mut item = Item {
949 step: id.into(),
950 declaration: g.clone(),
951 result: TextItem {
952 id: instance.clone(),
953 context,
954 generated: BTreeMap::new(),
955 },
956 backstops,
957 round: 0,
958 pending: Some(GenerationRequest {
959 backend_id: checked.binding.backend_id.clone(),
960 document: GenerateRequest {
961 schema: schema::GENERATE_REQUEST.into(),
962 step: id.into(),
963 instance,
964 template: checked.binding.template.id.clone(),
965 template_id: checked.binding.template_id.clone(),
966 instructions,
967 language: language.clone(),
968 projection,
969 output: g.output.clone(),
970 vocabularies: checked.binding.vocabularies.clone(),
971 repair: RepairRequest::None,
972 },
973 }),
974 };
975 if index as u64 >= checked.binding.max_items {
976 complete(&mut item, TextReason::NotAttempted);
977 }
978 items.push(item);
979 }
980 Ok(items)
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986
987 fn fixture() -> (
988 rustev_contract::plan::Plan,
989 rustev_contract::descriptor::BackendDescriptor,
990 ) {
991 use rustev_contract::definition::{Determinism, Operation};
992 use rustev_contract::descriptor::{
993 BackendDescriptor, InputExcess, InputLimit, OperationSupport,
994 };
995 let plan = serde_json::from_str(include_str!("testdata/generation.plan.json")).unwrap();
996 let backend = BackendDescriptor {
997 schema: schema::BACKEND.into(),
998 backend_id: "synthetic-linear-head".into(),
999 artifact: rustev_contract::ids::ArtifactId::parse(&format!(
1000 "sha256:{}",
1001 "a".repeat(64)
1002 ))
1003 .unwrap(),
1004 operations: vec![OperationSupport {
1005 isolation: None,
1006 screen: None,
1007 operation: Operation::Generate,
1008 output: OutputKind::TextRecord {
1009 structure: TextStructure::JsonSchema,
1010 max_output_bytes: 4096,
1011 languages: vec!["en-GB".into()],
1012 streaming: false,
1013 },
1014 max_options: 32,
1015 }],
1016 input_limit: InputLimit {
1017 max_bytes: 65_536,
1018 on_excess: InputExcess::Refuse,
1019 },
1020 determinism: Determinism::Tolerance,
1021 };
1022 (plan, backend)
1023 }
1024
1025 #[test]
1026 fn ranking_and_duration_proposal_template_reads_are_category_23() {
1027 use crate::{expr::StepTy, value::Ty};
1028 use rustev_contract::definition::{OutcomeDecl, ParamDecl, ParamSource};
1029 for ty in [Ty::Integer, Ty::Ranking { max: 4 }, Ty::Duration] {
1030 for prompt_binding in [false, true] {
1031 let (mut plan, _) = fixture();
1032 let PlanStepDetail::Generate(binding) = &plan.steps[0].detail else {
1033 panic!()
1034 };
1035 let mut prompt = binding.template.clone();
1036 let OutcomeDecl::Propose { params, .. } = &mut plan.definition.policy.rules[0].then
1037 else {
1038 panic!()
1039 };
1040 params.push(ParamDecl {
1041 name: "value".into(),
1042 value: ParamSource::Ref("step:source".into()),
1043 });
1044 let mut source = plan.definition.steps[0].clone();
1045 source.id = "source".into();
1046 let StepBody::Generate(g) = &mut plan.definition.steps[0].body else {
1047 panic!()
1048 };
1049 if prompt_binding {
1050 prompt.params = vec![PromptParam {
1051 name: "value".into(),
1052 ty: PromptParamType::Integer,
1053 }];
1054 prompt.instructions = "Value: {{param.value}}".into();
1055 g.template.params.insert(
1056 "value".into(),
1057 TemplateBinding::Reference("proposal:value".into()),
1058 );
1059 } else {
1060 g.backstop.fields.insert(
1061 "headline".into(),
1062 BackstopField::Text("Value: {{proposal.value}}".into()),
1063 );
1064 }
1065 source.body = StepBody::Exact(rustev_contract::definition::ExactDecl {
1067 op: "count".into(),
1068 version: 1,
1069 args: serde_json::json!({"list":"input:items"}),
1070 });
1071 plan.definition.steps.push(source);
1072 let result = crate::generation::type_check(
1073 &plan.definition,
1074 &BTreeMap::from([("item_text".into(), prompt)]),
1075 &BTreeMap::new(),
1076 &BTreeMap::from([("source".into(), StepTy::Exact(ty.clone()))]),
1077 &BTreeMap::from([("source".into(), Derivation::ExactDerived)]),
1078 &BTreeMap::new(),
1079 );
1080 if ty == Ty::Integer {
1081 assert!(result.is_ok(), "integer control: {:?}", result.err());
1082 } else {
1083 let Err(error) = result else {
1084 panic!("unsupported template read must fail")
1085 };
1086 assert_eq!(error.category, crate::Category::Template);
1087 assert_eq!(error.subject, "step:item_text");
1088 }
1089 }
1090 }
1091 }
1092
1093 #[test]
1094 fn repair_output_budget_covers_only_requested_fields() {
1095 use rustev_contract::scope::Handle;
1096 use rustev_contract::time::Timestamp;
1097 for only_missing in [true, false] {
1098 let (mut plan, backend) = fixture();
1099 let PlanStepDetail::Generate(binding) = &plan.steps[0].detail else {
1100 panic!()
1101 };
1102 let prompt = binding.template.clone();
1103 let StepBody::Generate(g) = &mut plan.definition.steps[0].body else {
1104 panic!()
1105 };
1106 g.repair.only_missing_fields = only_missing;
1107 let compiled = crate::compile_with_templates(
1108 &plan.definition,
1109 &[backend],
1110 &[],
1111 &[],
1112 None,
1113 &[prompt],
1114 )
1115 .unwrap();
1116 let snapshot = rustev_contract::snapshot::Snapshot {
1117 schema: schema::SNAPSHOT.into(),
1118 entries: vec![],
1119 };
1120 let mut stage =
1121 crate::Evaluation::start(&compiled, &snapshot, Timestamp::from_ms(0).unwrap())
1122 .unwrap()
1123 .begin_generation("decision")
1124 .unwrap();
1125 let scope = Scope::tenant_only(
1126 Handle::new("tenant").unwrap(),
1127 Handle::new("revision").unwrap(),
1128 );
1129 let initial = stage.request_document("item_text", &[], 0, &scope).unwrap();
1130 assert!(matches!(
1131 initial.output,
1132 OutputKind::TextRecord {
1133 max_output_bytes: 548,
1134 ..
1135 }
1136 ));
1137 stage
1138 .supply(
1139 "item_text",
1140 &[],
1141 Supplied::Output(RawOutput::TextRecord(BTreeMap::from([
1142 ("headline".into(), TextValue::Text("Keep".into())),
1143 ("why".into(), TextValue::Text(String::new())),
1144 ("tags".into(), TextValue::List(vec!["quiet".into()])),
1145 ]))),
1146 )
1147 .unwrap();
1148 let pending = stage.pending().remove(0);
1149 assert_eq!(pending.document.instance, vec!["repair:1"]);
1150 assert_eq!(
1151 pending.document.output.fields.len(),
1152 if only_missing { 1 } else { 3 }
1153 );
1154 let repair = stage
1155 .request_document("item_text", &pending.document.instance, 0, &scope)
1156 .unwrap();
1157 let OutputKind::TextRecord {
1158 max_output_bytes, ..
1159 } = repair.output
1160 else {
1161 panic!()
1162 };
1163 assert_eq!(max_output_bytes, if only_missing { 400 } else { 548 });
1164 }
1165 }
1166
1167 #[test]
1168 fn evidence_scopes_notices_to_their_generate_step() {
1169 let notices = vec![
1170 TextNotice {
1171 kind: TextNoticeKind::OwnershipRestored,
1172 step: String::new(),
1173 instance: vec![],
1174 field: "judgment".into(),
1175 },
1176 TextNotice {
1177 kind: TextNoticeKind::OwnershipViolation,
1178 step: "first".into(),
1179 instance: vec![],
1180 field: "judgment".into(),
1181 },
1182 ];
1183 for id in ["first", "second"] {
1184 let step = TextStep {
1185 step: id.into(),
1186 state: GenerationState::Ran,
1187 lineage: Lineage::default(),
1188 items: vec![],
1189 text_origin: TextOrigin::None,
1190 counts: TextCounts::default(),
1191 };
1192 let actual = evidence(&step, ¬ices).unwrap();
1193 let expected = if id == "first" {
1194 vec![notices[1].clone()]
1195 } else {
1196 vec![]
1197 };
1198 assert_eq!(actual.notices, expected);
1199 }
1200 }
1201}