Skip to main content

turnframe_understand/tasks/
extract.rs

1//! `extract`: each argument's value as the user stated it, or `not_given`.
2//!
3//! A value is given in its argument's shape and always points at the words that state
4//! it. Code turns it into the operation's value: words become text, a date expression a
5//! date, an amount money, a handle a record ([`crate::values`]).
6
7use std::collections::BTreeMap;
8use std::fmt::Write as _;
9
10use serde::{Deserialize, Serialize};
11use serde_json::{Value, json};
12use turnframe_core::ids::OperationKey;
13use turnframe_core::operation::{ArgumentSpec, DateExpr, OperationSpec, ValueShape};
14use turnframe_core::understanding::RecordValue;
15use turnframe_provider::request::Message;
16use turnframe_tasks::{ModelTask, StructuralError, TaskKind};
17
18use crate::input::{RecordBrief, UnderstandingInput, WorkflowBrief};
19use crate::render;
20use crate::schema::{any_of, date_expression, index, object, one_of, variant};
21use crate::values;
22use crate::words::Span;
23
24/// The message name of the user's current message.
25pub const CURRENT: &str = "current";
26
27/// The record handle for a record the user names that is not listed.
28pub const BY_NAME: &str = "by_name";
29
30const BUILT_IN: &str = include_str!("../../prompts/understand/extract.md");
31
32/// The extraction task, over one turn.
33#[derive(Debug, Clone, Copy)]
34pub struct Extract<'a> {
35    turn: &'a UnderstandingInput,
36}
37
38impl<'a> Extract<'a> {
39    /// The task for `turn`.
40    #[must_use]
41    pub const fn new(turn: &'a UnderstandingInput) -> Self {
42        Self { turn }
43    }
44
45    /// The turn it runs over.
46    #[must_use]
47    pub const fn turn(&self) -> &'a UnderstandingInput {
48        self.turn
49    }
50}
51
52/// The record an act applies to, as extraction shows it.
53#[derive(Debug, Clone, Copy)]
54pub enum RecordContext<'a> {
55    /// A record in view.
56    Existing(&'a RecordBrief),
57    /// A record the act creates.
58    New,
59    /// A record an earlier act of this message creates.
60    SameTurn,
61    /// No record.
62    Nothing,
63}
64
65/// A record an argument may name, with the handle it is listed under.
66#[derive(Debug, Clone)]
67pub struct RecordChoice {
68    /// The handle.
69    pub handle: String,
70    /// What it resolves to.
71    pub value: RecordValue,
72    /// How it is shown.
73    pub label: String,
74}
75
76/// One act's arguments to extract, and their context.
77#[derive(Debug, Clone)]
78pub struct ExtractInput<'a> {
79    /// How the unit is shown.
80    pub label: &'static str,
81    /// Its words.
82    pub words: Span,
83    /// The operation.
84    pub spec: &'a OperationSpec,
85    /// Its workflow.
86    pub workflow: &'a WorkflowBrief,
87    /// The record it applies to.
88    pub record: RecordContext<'a>,
89    /// The arguments asked for, in declaration order.
90    pub arguments: Vec<&'a ArgumentSpec>,
91    /// The records each record-valued argument may name.
92    pub record_choices: BTreeMap<String, Vec<RecordChoice>>,
93    /// Words of this message the unit continues, such as the request a correction changes.
94    pub continues: Option<Span>,
95    /// Words of this message the other parts hold.
96    pub others: Vec<Span>,
97    /// Words of the other parts asking for this same operation: a value the segmentation
98    /// cut in two runs on into a neighbouring one.
99    pub kin: Vec<Span>,
100    /// The other operations the same request asks for.
101    pub also: Vec<OperationKey>,
102    /// How many earlier messages are shown.
103    pub transcript: usize,
104    /// What the whole-turn check found, when this call reads the act again.
105    pub note: Option<String>,
106    /// Which of the unit's acts of this operation to read, and how many it asks for.
107    pub occurrence: Option<(usize, usize)>,
108}
109
110impl ExtractInput<'_> {
111    /// The names a value's `message` may take.
112    #[must_use]
113    pub fn messages(&self, turn: &UnderstandingInput) -> Vec<String> {
114        let mut names = vec![CURRENT.to_owned()];
115        names.extend(render::shown_messages(turn, self.transcript));
116        names
117    }
118}
119
120/// Every argument's value, by name.
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct Extraction {
123    /// The values.
124    pub arguments: BTreeMap<String, Given>,
125}
126
127/// One argument's value as the model gave it.
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129#[serde(tag = "kind", rename_all = "snake_case")]
130#[allow(missing_docs)]
131pub enum Given {
132    /// The user did not state it.
133    NotGiven,
134    /// The user's words are the value. `text` copies them and narrows the pointer to
135    /// them; left empty, the pointer stands.
136    Words {
137        text: String,
138        message: String,
139        from: usize,
140        to: usize,
141    },
142    /// Text the model wrote from the words pointed at.
143    Written {
144        message: String,
145        from: usize,
146        to: usize,
147        text: String,
148    },
149    /// A value of a closed set, a number or a flag.
150    Value {
151        message: String,
152        from: usize,
153        to: usize,
154        value: Value,
155    },
156    /// A date as the user said it.
157    Date {
158        message: String,
159        from: usize,
160        to: usize,
161        date: DateExpr,
162    },
163    /// An amount of money.
164    Money {
165        message: String,
166        from: usize,
167        to: usize,
168        amount: String,
169        currency: String,
170    },
171    /// A record, by its handle; one not listed, by its name as written.
172    Record {
173        message: String,
174        from: usize,
175        to: usize,
176        record: String,
177        #[serde(default)]
178        name: String,
179    },
180}
181
182impl Given {
183    /// The message and words a given value points at.
184    #[must_use]
185    pub fn pointer(&self) -> Option<(&str, Span)> {
186        match self {
187            Self::NotGiven => None,
188            Self::Words {
189                message, from, to, ..
190            }
191            | Self::Written {
192                message, from, to, ..
193            }
194            | Self::Value {
195                message, from, to, ..
196            }
197            | Self::Date {
198                message, from, to, ..
199            }
200            | Self::Money {
201                message, from, to, ..
202            }
203            | Self::Record {
204                message, from, to, ..
205            } => Some((message, Span::from_shown(*from, *to))),
206        }
207    }
208}
209
210/// The kind of value an argument of this shape is given as.
211fn given_kind(shape: &ValueShape) -> &'static str {
212    match shape {
213        ValueShape::Text { written: false } => "words",
214        ValueShape::Text { written: true } => "written",
215        ValueShape::Date { .. } => "date",
216        ValueShape::Money => "money",
217        ValueShape::Record { .. } => "record",
218        _ => "value",
219    }
220}
221
222fn argument_schema(
223    argument: &ArgumentSpec,
224    spec: &OperationSpec,
225    messages: &[String],
226    records: Option<&Vec<RecordChoice>>,
227) -> Value {
228    let pointer = || {
229        vec![
230            ("message", one_of(messages.iter().cloned())),
231            ("from", index()),
232            ("to", index()),
233        ]
234    };
235    let mut fields = pointer();
236    let kind = given_kind(&argument.shape);
237    match &argument.shape {
238        ValueShape::Text { written: false } => fields.insert(
239            0,
240            (
241                "text",
242                json!({
243                    "type": "string",
244                    "description": "The value's own words, copied exactly as the user wrote them, without the punctuation that ends their sentence."
245                }),
246            ),
247        ),
248        ValueShape::Text { written: true } => fields.push(("text", json!({ "type": "string" }))),
249        ValueShape::Enum { values } => fields.push(("value", one_of(values.iter().cloned()))),
250        ValueShape::Integer => fields.push(("value", json!({ "type": "integer" }))),
251        ValueShape::Number => fields.push(("value", json!({ "type": "number" }))),
252        ValueShape::Bool => fields.push(("value", json!({ "type": "boolean" }))),
253        ValueShape::Date { .. } => fields.push(("date", date_expression())),
254        ValueShape::Money => {
255            fields.push(("amount", json!({ "type": "string" })));
256            fields.push(("currency", json!({ "type": "string" })));
257        }
258        ValueShape::Record { .. } => {
259            let mut handles: Vec<String> = records
260                .map(|choices| choices.iter().map(|c| c.handle.clone()).collect())
261                .unwrap_or_default();
262            handles.push(BY_NAME.to_owned());
263            // The name comes first, as a text value's words do: copying it anchors the
264            // choice, where a bare handle list reads as «nothing here» when it is short.
265            fields.insert(
266                0,
267                (
268                    "name",
269                    json!({
270                        "type": "string",
271                        "description": "The words naming the record, copied exactly as the user wrote them, without the punctuation that ends their sentence."
272                    }),
273                ),
274            );
275            fields.push(("record", one_of(handles)));
276        }
277        _ => fields.push(("value", structured(spec, &argument.name))),
278    }
279    any_of(vec![variant("not_given", vec![]), variant(kind, fields)])
280}
281
282/// The argument's own schema from the arguments type, with no references left.
283fn structured(spec: &OperationSpec, name: &str) -> Value {
284    let root = spec.arguments_schema.as_value();
285    let property = root
286        .pointer(&format!("/properties/{name}"))
287        .cloned()
288        .unwrap_or_else(|| json!({}));
289    let defs = root.get("$defs").cloned();
290    inline(property, defs.as_ref(), 0)
291}
292
293fn inline(value: Value, defs: Option<&Value>, depth: usize) -> Value {
294    match value {
295        Value::Object(map) if depth < 16 => {
296            if let Some(Value::String(reference)) = map.get("$ref") {
297                let name = reference.rsplit('/').next().unwrap_or_default();
298                let target = defs.and_then(|d| d.get(name)).cloned().unwrap_or(json!({}));
299                return inline(target, defs, depth + 1);
300            }
301            Value::Object(
302                map.into_iter()
303                    .map(|(key, child)| (key, inline(child, defs, depth + 1)))
304                    .collect(),
305            )
306        }
307        Value::Array(items) => Value::Array(
308            items
309                .into_iter()
310                .map(|item| inline(item, defs, depth + 1))
311                .collect(),
312        ),
313        other => other,
314    }
315}
316
317impl<'a> ModelTask for Extract<'a> {
318    type Input = ExtractInput<'a>;
319    type Output = Extraction;
320
321    fn kind(&self) -> TaskKind {
322        TaskKind::Extract
323    }
324
325    fn prompt_name(&self) -> &str {
326        "understand.extract"
327    }
328
329    fn instructions(&self) -> &str {
330        BUILT_IN
331    }
332
333    fn schema(&self, input: &ExtractInput<'a>) -> Value {
334        let messages = input.messages(self.turn);
335        let properties = input
336            .arguments
337            .iter()
338            .map(|argument| {
339                (
340                    argument.name.as_str(),
341                    argument_schema(
342                        argument,
343                        input.spec,
344                        &messages,
345                        input.record_choices.get(&argument.name),
346                    ),
347                )
348            })
349            .collect();
350        object(vec![("arguments", object(properties))])
351    }
352
353    fn render(&self, input: &ExtractInput<'a>) -> Vec<Message> {
354        let turn = self.turn;
355        let words = &turn.message;
356        let record = match input.record {
357            RecordContext::Existing(record) => {
358                Some(format!("Record: {}", render::record_line(record, false)))
359            }
360            RecordContext::New => Some(format!("Record: a new {} record", input.workflow.key)),
361            RecordContext::SameTurn => Some(format!(
362                "Record: the {} record this message creates",
363                input.workflow.key
364            )),
365            RecordContext::Nothing => None,
366        };
367        let mut choices = String::new();
368        for (name, listed) in &input.record_choices {
369            if !choices.is_empty() {
370                choices.push_str("\n\n");
371            }
372            if listed.is_empty() {
373                let _ = write!(
374                    choices,
375                    "No record is listed for {name}: one the user names is {BY_NAME}, with its name."
376                );
377                continue;
378            }
379            let _ = write!(choices, "Records {name} may name:");
380            for choice in listed {
381                let _ = write!(choices, "\n- {}: {}", choice.handle, choice.label);
382            }
383            let _ = write!(
384                choices,
385                "\n- {BY_NAME}: a record the user names that is not listed"
386            );
387        }
388        let asked: Vec<&str> = input.arguments.iter().map(|a| a.name.as_str()).collect();
389        let offered = input
390            .spec
391            .arguments
392            .iter()
393            .filter(|argument| render::model_given(&argument.source))
394            .count();
395        let only = (asked.len() < offered).then(|| format!("Give only: {}.", asked.join(", ")));
396        vec![Message::user(render::sections([
397            Some(format!(
398                "Operation: {}",
399                render::operation_line(input.spec, self.turn)
400            )),
401            input
402                .spec
403                .guidance
404                .as_ref()
405                .map(|g| format!("Guidance: {g}")),
406            render::arguments(input.spec, turn),
407            only,
408            render::examples(input.spec),
409            render::glossary(input.workflow),
410            record,
411            (!choices.is_empty()).then_some(choices),
412            Some(format!("Today: {}", turn.today.format("%A %-d %B %Y"))),
413            render::transcript(turn, input.transcript),
414            Some(render::titled_message(
415                &format!("Message ({CURRENT})"),
416                words,
417            )),
418            Some(render::unit(input.label, words, input.words)),
419            input
420                .continues
421                .map(|span| render::unit("It continues", words, span)),
422            (!input.also.is_empty()).then(|| {
423                let keys: Vec<&str> = input.also.iter().map(OperationKey::as_str).collect();
424                format!(
425                    "This request also asks for {}: the words of its values are that \
426                     operation's.",
427                    keys.join(", ")
428                )
429            }),
430            (!input.others.is_empty()).then(|| {
431                let spans: Vec<String> = input
432                    .others
433                    .iter()
434                    .map(|span| {
435                        let (from, to) = span.shown();
436                        format!("words {from} to {to}")
437                    })
438                    .collect();
439                format!(
440                    "Other parts of the message, each read on its own, give none of this \
441                     part's values: {}.",
442                    spans.join(", ")
443                )
444            }),
445            input.occurrence.map(|(number, of)| {
446                format!(
447                    "This request asks for {} {of} times: give only the values of occurrence \
448                     {number} of {of}, counting in the order the message says them.",
449                    input.spec.key
450                )
451            }),
452            input
453                .note
454                .as_ref()
455                .map(|note| format!("A check of the whole message found: {note}")),
456        ]))]
457    }
458
459    fn check(&self, input: &ExtractInput<'a>, output: &Extraction) -> Result<(), StructuralError> {
460        values::convert(self.turn, input, output).map(|_| ())
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn a_note_from_the_whole_turn_check_is_shown() {
470        let turn = UnderstandingInput::new("name Lisbon", "en-GB", chrono::NaiveDate::MIN);
471        let spec = OperationSpec::new("trip.set_name").summary("Name the trip.");
472        let workflow = WorkflowBrief::new("trip");
473        let input = ExtractInput {
474            label: "Request",
475            words: Span::new(0, 1),
476            spec: &spec,
477            workflow: &workflow,
478            record: RecordContext::Nothing,
479            arguments: Vec::new(),
480            record_choices: BTreeMap::new(),
481            continues: None,
482            others: Vec::new(),
483            kin: Vec::new(),
484            also: Vec::new(),
485            transcript: 0,
486            note: Some("the value of value is in «rent»".to_owned()),
487            occurrence: None,
488        };
489        let rendered = format!("{:?}", Extract::new(&turn).render(&input));
490        assert!(
491            rendered
492                .contains("A check of the whole message found: the value of value is in «rent»"),
493            "{rendered}"
494        );
495    }
496
497    #[test]
498    fn the_words_other_parts_hold_are_named() {
499        let turn =
500            UnderstandingInput::new("name Lisbon, fly tomorrow", "en-GB", chrono::NaiveDate::MIN);
501        let spec = OperationSpec::new("trip.set_name").summary("Name the trip.");
502        let workflow = WorkflowBrief::new("trip");
503        let input = ExtractInput {
504            label: "Request",
505            words: Span::new(0, 1),
506            spec: &spec,
507            workflow: &workflow,
508            record: RecordContext::Nothing,
509            arguments: Vec::new(),
510            record_choices: BTreeMap::new(),
511            continues: None,
512            others: vec![Span::new(2, 3)],
513            kin: Vec::new(),
514            also: Vec::new(),
515            transcript: 0,
516            note: None,
517            occurrence: None,
518        };
519        let rendered = format!("{:?}", Extract::new(&turn).render(&input));
520        assert!(
521            rendered.contains(
522                "Other parts of the message, each read on its own, give none of this part's \
523                 values: words 3 to 4."
524            ),
525            "{rendered}"
526        );
527    }
528
529    #[test]
530    fn the_other_operations_of_the_request_are_named() {
531        let turn = UnderstandingInput::new(
532            "name Lisbon and fly tomorrow",
533            "en-GB",
534            chrono::NaiveDate::MIN,
535        );
536        let spec = OperationSpec::new("trip.set_name").summary("Name the trip.");
537        let workflow = WorkflowBrief::new("trip");
538        let input = ExtractInput {
539            label: "Request",
540            words: Span::new(0, 4),
541            spec: &spec,
542            workflow: &workflow,
543            record: RecordContext::Nothing,
544            arguments: Vec::new(),
545            record_choices: BTreeMap::new(),
546            continues: None,
547            others: Vec::new(),
548            kin: Vec::new(),
549            also: vec!["trip.set_travel_date".into()],
550            transcript: 0,
551            note: None,
552            occurrence: None,
553        };
554        let rendered = format!("{:?}", Extract::new(&turn).render(&input));
555        assert!(
556            rendered.contains(
557                "This request also asks for trip.set_travel_date: the words of its values are \
558                 that operation's."
559            ),
560            "{rendered}"
561        );
562    }
563
564    #[test]
565    fn a_given_value_deserializes_by_its_kind() {
566        let given: Given = serde_json::from_value(json!({
567            "kind": "date", "message": "current", "from": 4, "to": 4,
568            "date": {"kind": "relative", "unit": "day", "amount": 1}
569        }))
570        .unwrap();
571        assert_eq!(
572            given.pointer(),
573            Some(("current", Span::new(3, 3))),
574            "a model counts words from 1"
575        );
576        let absent: Given = serde_json::from_value(json!({"kind": "not_given"})).unwrap();
577        assert_eq!(absent, Given::NotGiven);
578    }
579}