Skip to main content

turnframe_understand/tasks/
respects.rs

1//! `respects`: whether an act changes what a keep-unchanged constraint keeps.
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use turnframe_provider::request::Message;
6use turnframe_tasks::{ModelTask, StructuralError, TaskKind};
7
8use crate::input::UnderstandingInput;
9use crate::render;
10use crate::schema::{object, text};
11use crate::words::Span;
12
13const BUILT_IN: &str = include_str!("../../prompts/understand/respects.md");
14
15/// The check of one act against one constraint, over one turn.
16#[derive(Debug, Clone, Copy)]
17pub struct Respects<'a> {
18    turn: &'a UnderstandingInput,
19}
20
21impl<'a> Respects<'a> {
22    /// The task for `turn`.
23    #[must_use]
24    pub const fn new(turn: &'a UnderstandingInput) -> Self {
25        Self { turn }
26    }
27}
28
29/// What the check is shown.
30#[derive(Debug, Clone)]
31pub struct RespectsInput {
32    /// The constraint's words.
33    pub constraint: Span,
34    /// The act: its operation, record and values, each value with the words it came from.
35    pub act: String,
36}
37
38/// The check's answer.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Respect {
41    /// Why, in one sentence.
42    pub reason: String,
43    /// Whether the act changes what the constraint keeps.
44    pub changes: bool,
45}
46
47impl ModelTask for Respects<'_> {
48    type Input = RespectsInput;
49    type Output = Respect;
50
51    fn kind(&self) -> TaskKind {
52        TaskKind::Respects
53    }
54
55    fn prompt_name(&self) -> &str {
56        "understand.respects"
57    }
58
59    fn instructions(&self) -> &str {
60        BUILT_IN
61    }
62
63    fn schema(&self, _input: &RespectsInput) -> Value {
64        object(vec![
65            ("reason", text("Why, in one sentence.")),
66            ("changes", json!({"type": "boolean"})),
67        ])
68    }
69
70    fn render(&self, input: &RespectsInput) -> Vec<Message> {
71        vec![Message::user(render::sections([
72            Some(render::message(&self.turn.message)),
73            Some(render::unit(
74                "Keep as it is",
75                &self.turn.message,
76                input.constraint,
77            )),
78            Some(format!("Act: {}", input.act)),
79        ]))]
80    }
81
82    fn check(&self, _input: &RespectsInput, output: &Respect) -> Result<(), StructuralError> {
83        if output.reason.trim().is_empty() {
84            return Err(StructuralError::new(
85                "empty_reason",
86                "`reason` is empty; say in one sentence why",
87            ));
88        }
89        Ok(())
90    }
91
92    fn agree(&self, left: &Respect, right: &Respect) -> bool {
93        left.changes == right.changes
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn the_check_is_shown_the_words_to_keep_and_the_act() {
103        let turn = UnderstandingInput::new(
104            "rebook the first leg, don't touch the return",
105            "en-GB",
106            chrono::NaiveDate::MIN,
107        );
108        let input = RespectsInput {
109            constraint: Span::new(4, 7),
110            act: "u1.a1 trip.request_rebooking on Trip 1: leg 1".to_owned(),
111        };
112        let messages = Respects::new(&turn).render(&input);
113        let shown = format!("{messages:?}");
114        assert!(
115            shown.contains("Keep as it is: words 5 to 8, «don't touch the return»"),
116            "{shown}"
117        );
118        assert!(
119            shown.contains("Act: u1.a1 trip.request_rebooking"),
120            "{shown}"
121        );
122    }
123}