turnframe_understand/tasks/
respects.rs1use 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#[derive(Debug, Clone, Copy)]
17pub struct Respects<'a> {
18 turn: &'a UnderstandingInput,
19}
20
21impl<'a> Respects<'a> {
22 #[must_use]
24 pub const fn new(turn: &'a UnderstandingInput) -> Self {
25 Self { turn }
26 }
27}
28
29#[derive(Debug, Clone)]
31pub struct RespectsInput {
32 pub constraint: Span,
34 pub act: String,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Respect {
41 pub reason: String,
43 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}