Skip to main content

turnframe_understand/
input.rs

1//! Everything one turn's understanding may see, built by the runtime from stores and views.
2
3use std::collections::BTreeMap;
4
5use chrono::NaiveDate;
6use turnframe_core::flow::StateField;
7use turnframe_core::ids::{OperationKey, OptionId, TargetToken, WorkflowKey};
8use turnframe_core::locale::Locale;
9use turnframe_core::operation::{GlossaryTerm, OperationSpec};
10use turnframe_core::understanding::UnderstoodArgument;
11
12use crate::words::Words;
13
14/// The input of one turn's understanding.
15#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub struct UnderstandingInput {
18    /// The user's message.
19    pub message: Words,
20    /// The turn's language.
21    pub locale: Locale,
22    /// Today, in the user's time zone: what relative dates count from.
23    pub today: NaiveDate,
24    /// The most recent messages before this one, oldest first.
25    pub transcript: Vec<TranscriptMessage>,
26    /// The workflows in view, in registration order.
27    pub workflows: Vec<WorkflowBrief>,
28    /// The card waiting for an answer.
29    pub card: Option<OpenCard>,
30    /// What the assistant asked for last turn.
31    pub expectation: Option<Expectation>,
32    /// Acts of earlier turns still waiting for a record the user named that did not exist.
33    pub waiting: Vec<PendingAct>,
34    /// Acts the last turn did, with the values they were given: a correction changes what
35    /// it says of one and keeps the rest.
36    pub done: Vec<PendingAct>,
37    /// What the assistant reported doing last turn, so a dispute can name it.
38    pub receipts: Vec<PreviousReceipt>,
39    /// The records the assistant's last message was about.
40    pub last_subjects: Vec<TargetToken>,
41    /// How this turn is run, over the understander's own settings.
42    pub settings: Option<crate::Settings>,
43    /// Whether a knowledge source can answer a question about the domain in general.
44    pub knowledge: bool,
45    /// The next steps the last reply offered, in order: a message is read first as taking
46    /// one up.
47    pub offers: Vec<OfferBrief>,
48}
49
50impl UnderstandingInput {
51    /// A turn saying `message` on `today`, with nothing else in view.
52    #[must_use]
53    pub fn new(message: &str, locale: impl Into<Locale>, today: NaiveDate) -> Self {
54        Self {
55            message: Words::split(message),
56            locale: locale.into(),
57            today,
58            transcript: Vec::new(),
59            workflows: Vec::new(),
60            card: None,
61            expectation: None,
62            waiting: Vec::new(),
63            done: Vec::new(),
64            receipts: Vec::new(),
65            last_subjects: Vec::new(),
66            settings: None,
67            knowledge: true,
68            offers: Vec::new(),
69        }
70    }
71
72    /// Adds a next step the last reply offered.
73    #[must_use]
74    pub fn with_offer(mut self, offer: OfferBrief) -> Self {
75        self.offers.push(offer);
76        self
77    }
78
79    /// Says whether a knowledge source can answer a question about the domain in general;
80    /// without one, no question is read as asking it.
81    #[must_use]
82    pub const fn with_knowledge(mut self, knowledge: bool) -> Self {
83        self.knowledge = knowledge;
84        self
85    }
86
87    /// Runs this turn under `settings` instead of the understander's.
88    #[must_use]
89    pub const fn with_settings(mut self, settings: crate::Settings) -> Self {
90        self.settings = Some(settings);
91        self
92    }
93
94    /// Adds a workflow.
95    #[must_use]
96    pub fn with_workflow(mut self, workflow: WorkflowBrief) -> Self {
97        self.workflows.push(workflow);
98        self
99    }
100
101    /// Adds an earlier message.
102    #[must_use]
103    pub fn with_earlier(mut self, speaker: Speaker, text: &str) -> Self {
104        self.transcript.push(TranscriptMessage {
105            speaker,
106            words: Words::split(text),
107        });
108        self
109    }
110
111    /// Sets the card on screen.
112    #[must_use]
113    pub fn with_card(mut self, card: OpenCard) -> Self {
114        self.card = Some(card);
115        self
116    }
117
118    /// Sets what the assistant asked for.
119    #[must_use]
120    pub fn with_expectation(mut self, expectation: Expectation) -> Self {
121        self.expectation = Some(expectation);
122        self
123    }
124
125    /// Adds an act of an earlier turn still waiting for a record the user named.
126    #[must_use]
127    pub fn with_waiting(mut self, act: PendingAct) -> Self {
128        self.waiting.push(act);
129        self
130    }
131
132    /// Adds an act the last turn did.
133    #[must_use]
134    pub fn with_done(mut self, act: PendingAct) -> Self {
135        self.done.push(act);
136        self
137    }
138
139    /// Adds a receipt of the last turn.
140    #[must_use]
141    pub fn with_receipt(mut self, receipt: PreviousReceipt) -> Self {
142        self.receipts.push(receipt);
143        self
144    }
145
146    /// Adds a record the assistant's last message was about.
147    #[must_use]
148    pub fn with_last_subject(mut self, record: TargetToken) -> Self {
149        if !self.last_subjects.contains(&record) {
150            self.last_subjects.push(record);
151        }
152        self
153    }
154
155    /// The workflow with this key.
156    #[must_use]
157    pub fn workflow(&self, key: &WorkflowKey) -> Option<&WorkflowBrief> {
158        self.workflows.iter().find(|workflow| &workflow.key == key)
159    }
160
161    /// The operation with this key, and its workflow.
162    #[must_use]
163    pub fn operation(&self, key: &OperationKey) -> Option<(&WorkflowBrief, &OperationSpec)> {
164        self.workflows.iter().find_map(|workflow| {
165            workflow
166                .operations
167                .iter()
168                .find(|spec| &spec.key == key)
169                .map(|spec| (workflow, spec))
170        })
171    }
172
173    /// The record with this token, and its workflow.
174    #[must_use]
175    pub fn record(&self, token: &TargetToken) -> Option<(&WorkflowBrief, &RecordBrief)> {
176        self.workflows.iter().find_map(|workflow| {
177            workflow
178                .records
179                .iter()
180                .find(|record| &record.token == token)
181                .map(|record| (workflow, record))
182        })
183    }
184}
185
186/// Who wrote an earlier message.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum Speaker {
189    /// The user.
190    User,
191    /// The assistant.
192    Assistant,
193}
194
195/// An earlier message, split into words so a value from it can be pointed at.
196#[derive(Debug, Clone)]
197pub struct TranscriptMessage {
198    /// Who wrote it.
199    pub speaker: Speaker,
200    /// What it said.
201    pub words: Words,
202}
203
204/// A workflow as understanding sees it.
205#[derive(Debug, Clone)]
206#[non_exhaustive]
207pub struct WorkflowBrief {
208    /// Its key.
209    pub key: WorkflowKey,
210    /// One line on what it is for.
211    pub summary: Option<String>,
212    /// What its words mean.
213    pub glossary: Vec<GlossaryTerm>,
214    /// Whether a new case of it may be started.
215    pub startable: bool,
216    /// Every operation offered on a record in view or on a new case, each once.
217    pub operations: Vec<OperationSpec>,
218    /// The records in view.
219    pub records: Vec<RecordBrief>,
220    /// The operations offered on a record that does not exist yet.
221    pub new_case: Vec<OperationKey>,
222    /// What a question about it may be about: its enumerations and stated fields.
223    pub subjects: Vec<String>,
224}
225
226impl WorkflowBrief {
227    /// A workflow with nothing in view.
228    #[must_use]
229    pub fn new(key: impl Into<WorkflowKey>) -> Self {
230        Self {
231            key: key.into(),
232            summary: None,
233            glossary: Vec::new(),
234            startable: false,
235            operations: Vec::new(),
236            records: Vec::new(),
237            new_case: Vec::new(),
238            subjects: Vec::new(),
239        }
240    }
241
242    /// Sets its summary.
243    #[must_use]
244    pub fn summary(mut self, summary: impl Into<String>) -> Self {
245        self.summary = Some(summary.into());
246        self
247    }
248
249    /// Adds a glossary term.
250    #[must_use]
251    pub fn term(mut self, term: GlossaryTerm) -> Self {
252        self.glossary.push(term);
253        self
254    }
255
256    /// Lets a new case be started.
257    #[must_use]
258    pub const fn startable(mut self) -> Self {
259        self.startable = true;
260        self
261    }
262
263    /// Adds an operation, stamped with this workflow.
264    #[must_use]
265    pub fn operation(mut self, mut spec: OperationSpec) -> Self {
266        spec.workflow = self.key.clone();
267        self.operations.push(spec);
268        self
269    }
270
271    /// Adds a record.
272    #[must_use]
273    pub fn record(mut self, record: RecordBrief) -> Self {
274        self.records.push(record);
275        self
276    }
277
278    /// Offers an operation on a new case.
279    #[must_use]
280    pub fn on_new_case(mut self, operation: impl Into<OperationKey>) -> Self {
281        self.new_case.push(operation.into());
282        self
283    }
284
285    /// Adds a subject questions may be about.
286    #[must_use]
287    pub fn subject(mut self, subject: impl Into<String>) -> Self {
288        self.subjects.push(subject.into());
289        self
290    }
291
292    /// The operation with this key.
293    #[must_use]
294    pub fn spec(&self, key: &OperationKey) -> Option<&OperationSpec> {
295        self.operations.iter().find(|spec| &spec.key == key)
296    }
297}
298
299/// A record in view.
300#[derive(Debug, Clone)]
301#[non_exhaustive]
302pub struct RecordBrief {
303    /// The token it is named by.
304    pub token: TargetToken,
305    /// Server-authored label.
306    pub label: String,
307    /// Its phase, as the workflow names it.
308    pub phase: String,
309    /// What it holds, as the workflow says it may be stated.
310    pub fields: Vec<StateField>,
311    /// What it still needs, as sentences.
312    pub obligations: Vec<String>,
313    /// The workflow's guidance for a turn about a record in this phase.
314    pub briefing: Option<String>,
315    /// The operations offered on it now.
316    pub operations: Vec<OperationKey>,
317}
318
319impl RecordBrief {
320    /// A record with its token, label and phase.
321    #[must_use]
322    pub fn new(
323        token: impl Into<TargetToken>,
324        label: impl Into<String>,
325        phase: impl Into<String>,
326    ) -> Self {
327        Self {
328            token: token.into(),
329            label: label.into(),
330            phase: phase.into(),
331            fields: Vec::new(),
332            obligations: Vec::new(),
333            briefing: None,
334            operations: Vec::new(),
335        }
336    }
337
338    /// Adds a field.
339    #[must_use]
340    pub fn field(mut self, field: StateField) -> Self {
341        self.fields.push(field);
342        self
343    }
344
345    /// Adds an open obligation.
346    #[must_use]
347    pub fn obligation(mut self, sentence: impl Into<String>) -> Self {
348        self.obligations.push(sentence.into());
349        self
350    }
351
352    /// Sets the phase guidance.
353    #[must_use]
354    pub fn briefing(mut self, briefing: impl Into<String>) -> Self {
355        self.briefing = Some(briefing.into());
356        self
357    }
358
359    /// Offers operations on it.
360    #[must_use]
361    pub fn offering<I, K>(mut self, operations: I) -> Self
362    where
363        I: IntoIterator<Item = K>,
364        K: Into<OperationKey>,
365    {
366        self.operations
367            .extend(operations.into_iter().map(Into::into));
368        self
369    }
370
371    /// Whether `operation` is offered on it.
372    #[must_use]
373    pub fn offers(&self, operation: &OperationKey) -> bool {
374        self.operations.contains(operation)
375    }
376}
377
378/// The card on screen, as its question and option labels.
379#[derive(Debug, Clone)]
380#[non_exhaustive]
381pub struct OpenCard {
382    /// The workflow it belongs to.
383    pub workflow: WorkflowKey,
384    /// The record it is about.
385    pub record: Option<TargetToken>,
386    /// Its question.
387    pub question: String,
388    /// Its options.
389    pub options: Vec<CardOption>,
390    /// Whether typed text may answer it at all.
391    pub accepts_typed_answer: bool,
392}
393
394impl OpenCard {
395    /// A card asking `question` about a record of `workflow`.
396    #[must_use]
397    pub fn new(workflow: impl Into<WorkflowKey>, question: impl Into<String>) -> Self {
398        Self {
399            workflow: workflow.into(),
400            record: None,
401            question: question.into(),
402            options: Vec::new(),
403            accepts_typed_answer: true,
404        }
405    }
406
407    /// Sets the record it is about.
408    #[must_use]
409    pub fn about(mut self, record: impl Into<TargetToken>) -> Self {
410        self.record = Some(record.into());
411        self
412    }
413
414    /// Adds an option.
415    #[must_use]
416    pub fn option(mut self, id: impl Into<OptionId>, label: impl Into<String>) -> Self {
417        self.options.push(CardOption {
418            id: id.into(),
419            label: label.into(),
420        });
421        self
422    }
423
424    /// Refuses typed answers: only a click resolves it.
425    #[must_use]
426    pub const fn click_only(mut self) -> Self {
427        self.accepts_typed_answer = false;
428        self
429    }
430}
431
432/// One option of a card.
433#[derive(Debug, Clone)]
434pub struct CardOption {
435    /// Its identifier.
436    pub id: OptionId,
437    /// Its label.
438    pub label: String,
439}
440
441/// What the assistant asked for last turn.
442#[derive(Debug, Clone)]
443#[non_exhaustive]
444pub enum Expectation {
445    /// Values an act needs before it can run.
446    Values(PendingAct),
447    /// An open obligation of a record.
448    Obligation {
449        /// The record.
450        record: TargetToken,
451        /// The obligation, as a sentence.
452        sentence: String,
453    },
454}
455
456impl Expectation {
457    /// The record the assistant asked about, when it named one.
458    #[must_use]
459    pub const fn record(&self) -> Option<&TargetToken> {
460        match self {
461            Self::Values(pending) => pending.record.as_ref(),
462            Self::Obligation { record, .. } => Some(record),
463        }
464    }
465}
466
467/// A next step the last reply offered: taken up, it is this act, on its record, with the
468/// values it already has; the words are those the reply offered it in.
469#[derive(Debug, Clone)]
470pub struct OfferBrief {
471    /// The words the reply offered it in.
472    pub words: String,
473    /// The act it runs.
474    pub act: PendingAct,
475}
476
477impl OfferBrief {
478    /// An offer of `act`, made in `words`.
479    #[must_use]
480    pub fn new(words: impl Into<String>, act: PendingAct) -> Self {
481        Self {
482            words: words.into(),
483            act,
484        }
485    }
486}
487
488/// An act of an earlier turn with the values it was given: one waiting for more, or one done.
489#[derive(Debug, Clone)]
490pub struct PendingAct {
491    /// The operation.
492    pub operation: OperationKey,
493    /// The record it applies to.
494    pub record: Option<TargetToken>,
495    /// The arguments already given.
496    pub given: BTreeMap<String, UnderstoodArgument>,
497    /// The arguments asked for.
498    pub missing: Vec<String>,
499}
500
501/// A change the assistant reported last turn.
502#[derive(Debug, Clone)]
503pub struct PreviousReceipt {
504    /// The key it is shown under, such as `r1`.
505    pub key: String,
506    /// What it said, as shown.
507    pub text: String,
508}
509
510impl PreviousReceipt {
511    /// A receipt shown under `key`.
512    #[must_use]
513    pub fn new(key: impl Into<String>, text: impl Into<String>) -> Self {
514        Self {
515            key: key.into(),
516            text: text.into(),
517        }
518    }
519}