Skip to main content

turnframe_core/
understanding.rs

1//! What a turn was understood to say, assembled by code from small model tasks.
2//!
3//! An [`Understanding`] is the reducer's input: every act with an [`ActId`], its target,
4//! its parsed arguments and the words that state them, and the questions, constraints,
5//! card answer and disputes of the same message. Nothing in it is model-facing; the
6//! tasks that produced it have their own schemas.
7
8use std::collections::BTreeMap;
9use std::fmt;
10use std::str::FromStr;
11
12use serde::{Deserialize, Serialize};
13
14use crate::hash::{Digest, HashError, canonical_digest};
15use crate::ids::{OperationKey, OptionId, TargetToken, WorkflowKey};
16use crate::plan::AnswerBasis;
17
18/// One unit of a message: `u1`. The segmentation's units are numbered from 1 in message order,
19/// and units coverage adds follow them.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct UnitId(pub u16);
23
24impl fmt::Display for UnitId {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        write!(f, "u{}", self.0)
27    }
28}
29
30/// One act of a unit: `u2.a1`. Command ids, card keys and minted case ids derive from it.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub struct ActId {
33    /// The unit that asked for it.
34    pub unit: UnitId,
35    /// Its position among the unit's acts, from 1.
36    pub act: u16,
37}
38
39impl ActId {
40    /// The `act`-th act of `unit`.
41    #[must_use]
42    pub const fn new(unit: UnitId, act: u16) -> Self {
43        Self { unit, act }
44    }
45}
46
47impl fmt::Display for ActId {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        write!(f, "{}.a{}", self.unit, self.act)
50    }
51}
52
53/// An act identifier that is not of the form `u<n>.a<m>`.
54#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
55#[error("`{0}` is not an act identifier")]
56pub struct ActIdError(pub String);
57
58impl FromStr for ActId {
59    type Err = ActIdError;
60
61    fn from_str(text: &str) -> Result<Self, Self::Err> {
62        let invalid = || ActIdError(text.to_owned());
63        let (unit, act) = text.split_once(".a").ok_or_else(invalid)?;
64        let unit = unit.strip_prefix('u').ok_or_else(invalid)?;
65        Ok(Self {
66            unit: UnitId(unit.parse().map_err(|_| invalid())?),
67            act: act.parse().map_err(|_| invalid())?,
68        })
69    }
70}
71
72impl Serialize for ActId {
73    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
74        serializer.collect_str(self)
75    }
76}
77
78impl<'de> Deserialize<'de> for ActId {
79    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
80        let text = String::deserialize(deserializer)?;
81        text.parse().map_err(serde::de::Error::custom)
82    }
83}
84
85/// Which message some words are in: the turn's own, or one of the transcript window shown.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[serde(tag = "kind", rename_all = "snake_case")]
88pub enum MessageRef {
89    /// The message being understood.
90    Current,
91    /// The transcript window's message at this index, oldest first.
92    Earlier {
93        /// Index into the window.
94        index: usize,
95    },
96}
97
98/// Words of a message: word indices, inclusive, and the byte range they cover.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
100pub struct WordRange {
101    /// First word.
102    pub first: usize,
103    /// Last word.
104    pub last: usize,
105    /// Byte offset of the first word's start.
106    pub start: usize,
107    /// Byte offset just past the last word.
108    pub end: usize,
109}
110
111/// Words and the message they are in: the evidence for a value.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
113pub struct Excerpt {
114    /// The message.
115    pub message: MessageRef,
116    /// The words.
117    pub words: WordRange,
118}
119
120/// What a unit is.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123#[non_exhaustive]
124pub enum UnitKind {
125    /// Asks for something to be done.
126    Request,
127    /// Asks something.
128    Question,
129    /// A condition on the whole turn.
130    Constraint,
131    /// Changes something asked for earlier.
132    Correction,
133    /// Withdraws something asked for earlier.
134    Cancel,
135    /// Answers the card on screen.
136    CardAnswer,
137    /// Contests something the assistant reported doing.
138    Dispute,
139    /// Gives a value the assistant asked for.
140    ProvidesValue,
141    /// Greets, thanks, or says nothing that asks for anything.
142    Chitchat,
143}
144
145/// Which task found a unit.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum FoundBy {
149    /// The segmentation of the message.
150    Segment,
151    /// The check for requests the segmentation missed.
152    Coverage,
153    /// The check of the whole turn.
154    CrossCheck,
155}
156
157/// One unit of the message.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct Unit {
160    /// Its identifier.
161    pub id: UnitId,
162    /// What it is.
163    pub kind: UnitKind,
164    /// Its words in the current message.
165    pub words: WordRange,
166    /// The workflow it is about, when one was named or implied.
167    pub workflow: Option<WorkflowKey>,
168    /// Which task found it.
169    pub found_by: FoundBy,
170}
171
172/// What an act does.
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(tag = "kind", rename_all = "snake_case")]
175pub enum ActAction {
176    /// Applies an operation.
177    Apply {
178        /// The operation.
179        operation: OperationKey,
180    },
181    /// Starts a new case of a workflow.
182    Start {
183        /// The workflow.
184        workflow: WorkflowKey,
185    },
186}
187
188/// The record an act applies to.
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(tag = "kind", rename_all = "snake_case")]
191#[non_exhaustive]
192pub enum ActTarget {
193    /// A record in view, by its token.
194    Record {
195        /// The token.
196        token: TargetToken,
197    },
198    /// A record this act creates.
199    New {
200        /// Its workflow.
201        workflow: WorkflowKey,
202    },
203    /// The record an earlier act of the same turn creates.
204    SameTurn {
205        /// That act.
206        act: ActId,
207    },
208    /// The record of the card on screen.
209    Card,
210    /// A record the user named that is not in view; the application looks it up.
211    NotListed {
212        /// Its workflow.
213        workflow: WorkflowKey,
214        /// The words naming it, when the user used any.
215        words: Option<WordRange>,
216    },
217    /// Several records fit; the user is asked which.
218    Ambiguous {
219        /// The records that fit.
220        candidates: Vec<TargetToken>,
221    },
222    /// The operation applies to no record.
223    Nothing,
224}
225
226/// A record an argument names.
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(tag = "kind", rename_all = "snake_case")]
229pub enum RecordValue {
230    /// A record in view.
231    Record {
232        /// Its token.
233        token: TargetToken,
234    },
235    /// The record an earlier act of the same turn creates.
236    SameTurn {
237        /// That act.
238        act: ActId,
239    },
240    /// A record the user named that is not in view; the runtime looks it up.
241    Named {
242        /// The workflow it belongs to.
243        workflow: WorkflowKey,
244        /// The words that name it.
245        named: String,
246    },
247}
248
249/// An argument's value, parsed and evaluated by code.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
252pub enum ArgumentValue {
253    /// A value in the operation's own schema: text, a date, an amount, an enum value.
254    Json(serde_json::Value),
255    /// A record, resolved by the runtime into the operation's representation.
256    Record(RecordValue),
257}
258
259/// An argument and the words that state it.
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261pub struct UnderstoodArgument {
262    /// The value.
263    pub value: ArgumentValue,
264    /// The words it comes from; absent for a value carried over from an earlier turn.
265    pub excerpt: Option<Excerpt>,
266}
267
268/// Whether an act may proceed to the reducer as it is.
269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[serde(tag = "kind", rename_all = "snake_case")]
271#[non_exhaustive]
272pub enum ActStatus {
273    /// Complete and verified.
274    Ready,
275    /// Missing, unstated or rejected arguments; the user is asked, nothing is written.
276    NeedsValue {
277        /// The arguments to ask for.
278        arguments: Vec<String>,
279        /// The domain's explanation, when it rejected a value.
280        reason: Option<String>,
281    },
282    /// Another unit aimed at the same record was not understood.
283    Held {
284        /// That unit.
285        because: UnitId,
286    },
287}
288
289/// One act the turn asks for.
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291pub struct UnderstoodAct {
292    /// Its identifier.
293    pub id: ActId,
294    /// What it does.
295    pub action: ActAction,
296    /// What it applies to.
297    pub target: ActTarget,
298    /// Its arguments, by name.
299    pub arguments: BTreeMap<String, UnderstoodArgument>,
300    /// The words of the unit that asked for it.
301    pub words: WordRange,
302    /// Acts of the same turn it needs first.
303    pub depends_on: Vec<ActId>,
304    /// Whether it may proceed.
305    pub status: ActStatus,
306}
307
308impl UnderstoodAct {
309    /// The operation it applies, when it applies one.
310    #[must_use]
311    pub const fn operation(&self) -> Option<&OperationKey> {
312        match &self.action {
313            ActAction::Apply { operation } => Some(operation),
314            ActAction::Start { .. } => None,
315        }
316    }
317
318    /// Snake-case name of what it does: `apply_operation` or `start_workflow`.
319    #[must_use]
320    pub const fn kind_name(&self) -> &'static str {
321        match self.action {
322            ActAction::Apply { .. } => "apply_operation",
323            ActAction::Start { .. } => "start_workflow",
324        }
325    }
326}
327
328/// An act a later unit of the same message replaced or withdrew.
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330pub struct Superseded {
331    /// The act.
332    pub act: ActId,
333    /// What it asked for, kept because the act itself is gone.
334    pub action: ActAction,
335    /// The correction or cancellation.
336    pub by: UnitId,
337}
338
339/// A question the user asked.
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct UnderstoodQuestion {
342    /// Its unit.
343    pub unit: UnitId,
344    /// Its words.
345    pub words: WordRange,
346    /// The workflow it is about.
347    pub workflow: Option<WorkflowKey>,
348    /// The record it is about.
349    pub record: Option<TargetToken>,
350    /// The declared subjects it asks about.
351    pub subjects: Vec<String>,
352    /// Which state answers it.
353    pub basis: AnswerBasis,
354    /// What kind of thing it asks.
355    #[serde(default)]
356    pub topic: QuestionTopic,
357    /// Whether it follows up the assistant's last message.
358    pub continues_previous: bool,
359}
360
361/// What kind of thing a question asks.
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
363#[serde(rename_all = "snake_case")]
364#[non_exhaustive]
365pub enum QuestionTopic {
366    /// What a record holds, or where it stands.
367    #[default]
368    RecordState,
369    /// Which values a field accepts.
370    AcceptedValues,
371    /// What the user can do here, or whether something can be done.
372    Capabilities,
373    /// Anything else the domain knows.
374    Knowledge,
375}
376
377/// A condition the user placed on the whole turn.
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
379#[serde(rename_all = "snake_case")]
380#[non_exhaustive]
381pub enum ConstraintKind {
382    /// Submit nothing.
383    DoNotSubmit,
384    /// Delete nothing.
385    DoNotDelete,
386    /// Keep everything a draft.
387    DraftOnly,
388    /// Ask before applying anything.
389    AskBeforeApplying,
390    /// Apply only if a condition holds; its words are the condition.
391    ApplyOnlyIf,
392    /// No external effects.
393    NoExternalEffects,
394    /// Leave what its words name as it is: an act that would change it runs nothing.
395    KeepUnchanged,
396}
397
398/// A constraint and its words.
399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
400pub struct TurnConstraint {
401    /// Its unit.
402    pub unit: UnitId,
403    /// Which constraint.
404    pub kind: ConstraintKind,
405    /// Its words.
406    pub words: WordRange,
407}
408
409/// A typed answer to the card on screen.
410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411pub struct CardAnswer {
412    /// Its unit.
413    pub unit: UnitId,
414    /// The option chosen.
415    pub option: OptionId,
416    /// Its words.
417    pub words: WordRange,
418}
419
420/// Something the assistant reported that the user contests.
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub struct Dispute {
423    /// Its unit.
424    pub unit: UnitId,
425    /// Its words.
426    pub words: WordRange,
427    /// The receipt contested, by the key it was shown under.
428    pub receipt: Option<String>,
429}
430
431/// Why a unit was not understood.
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
433#[serde(tag = "kind", rename_all = "snake_case")]
434#[non_exhaustive]
435pub enum NotUnderstoodReason {
436    /// No operation on offer does what it asks.
437    NoOperation,
438    /// Its answers disagreed and the profile asks instead of choosing.
439    Unclear,
440    /// The verifier found the act was not asked for.
441    NotRequested,
442    /// It would change what a keep-unchanged constraint keeps.
443    KeptUnchanged {
444        /// The constraint's unit.
445        constraint: UnitId,
446    },
447    /// A task failed after its repairs and escalation.
448    TaskFailed {
449        /// The task.
450        task: String,
451        /// Its failure code.
452        code: String,
453    },
454}
455
456/// A unit that produced nothing to act on, and why.
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458pub struct NotUnderstood {
459    /// The unit.
460    pub unit: UnitId,
461    /// Its words.
462    pub words: WordRange,
463    /// Why.
464    pub reason: NotUnderstoodReason,
465}
466
467/// Why a whole message could not be read, so no act of it runs.
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469#[serde(tag = "kind", rename_all = "snake_case")]
470#[non_exhaustive]
471pub enum Unreadable {
472    /// The segmentation failed.
473    Segmentation {
474        /// Its failure code.
475        code: String,
476    },
477    /// A constraint was found by coverage, so its kind is unknown.
478    LostConstraint,
479}
480
481/// Everything a turn was understood to say.
482#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
483pub struct Understanding {
484    /// The units, in message order.
485    pub units: Vec<Unit>,
486    /// The acts, in message order.
487    pub acts: Vec<UnderstoodAct>,
488    /// Acts replaced or withdrawn by a later unit.
489    pub superseded: Vec<Superseded>,
490    /// The questions.
491    pub questions: Vec<UnderstoodQuestion>,
492    /// The constraints.
493    pub constraints: Vec<TurnConstraint>,
494    /// The typed answer to the card on screen.
495    pub card_answer: Option<CardAnswer>,
496    /// The disputes.
497    pub disputes: Vec<Dispute>,
498    /// Units that produced nothing to act on.
499    pub not_understood: Vec<NotUnderstood>,
500    /// Set when the message could not be read at all; then nothing else is.
501    pub unreadable: Option<Unreadable>,
502}
503
504impl Understanding {
505    /// A message that could not be read: no acts, no questions, only the reason.
506    #[must_use]
507    pub fn unreadable(reason: Unreadable) -> Self {
508        Self {
509            unreadable: Some(reason),
510            ..Self::default()
511        }
512    }
513
514    /// The act with this identifier.
515    #[must_use]
516    pub fn act(&self, id: ActId) -> Option<&UnderstoodAct> {
517        self.acts.iter().find(|act| act.id == id)
518    }
519
520    /// Canonical digest, so a replay can tell the same understanding from another.
521    ///
522    /// # Errors
523    ///
524    /// [`HashError`] when a value cannot be written as canonical JSON.
525    pub fn hash(&self) -> Result<Digest, HashError> {
526        canonical_digest(self)
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    #[test]
535    fn act_identifiers_read_and_parse_as_unit_and_position() {
536        let id = ActId::new(UnitId(2), 1);
537        assert_eq!(id.to_string(), "u2.a1");
538        assert_eq!("u2.a1".parse::<ActId>().unwrap(), id);
539        assert!("u2".parse::<ActId>().is_err());
540        assert!("x2.a1".parse::<ActId>().is_err());
541        let json = serde_json::to_value(id).unwrap();
542        assert_eq!(json, serde_json::json!("u2.a1"));
543        assert_eq!(serde_json::from_value::<ActId>(json).unwrap(), id);
544    }
545}