types.rsannotatedtypes.rssource304 lines · 11.0 KB · raw
1use serde::{Deserialize, Serialize};
2
3pub use crate::visibility::Visibility;
4use whiskers_icons::IconId;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub enum Speaker {
8    Child,
9    Whiskers,
10}

A picture she showed Whiskers: one photo, or one frame of a video.

13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Image {

For example image/jpeg.

16    pub media_type: String,
17    pub bytes: Vec<u8>,
18}

Where a saved picture lives, so the parents' log can point at it.

21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
22pub struct PictureId(pub String);
24impl PictureId {

A bare file name of the kind pictures are given (0000000000000000-0000.jpg): no path, no odd characters, a picture extension. Names arrive from other devices, so they are checked.

27    pub fn is_safe(&self) -> bool {
28        let n = &self.0;
29        n.len() <= 64
30            && n.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
31            && !n.starts_with('.')
32            && !n.contains("..")
33            && n.rsplit('.').next().is_some_and(|e| matches!(e, "jpg" | "png" | "webp" | "gif" | "img"))
34    }
35}

What she gave Whiskers in one go: her words and anything she showed it.

38#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct Heard {
40    pub text: String,
41    pub pictures: Vec<Image>,
42}
44impl Heard {
45    pub fn words(text: impl Into<String>) -> Self {
46        Self { text: text.into(), pictures: Vec::new() }
47    }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct Turn {
52    pub speaker: Speaker,
53    pub text: String,

Only the turn being answered carries pictures; history keeps words.

55    pub pictures: Vec<Image>,
56}
58impl Turn {
59    pub fn said(speaker: Speaker, text: impl Into<String>) -> Self {
60        Self { speaker, text: text.into(), pictures: Vec::new() }
61    }
62}

Which way a message is travelling past the guard.

65#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
66pub enum Direction {
67    FromChild,
68    ToChild,
69}
71#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
72pub enum Verdict {
73    Allow,
74    Refuse { reason: String, kind: RefusalKind },
75}

Why a message was stopped, as far as what happens next is concerned.

78#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
79pub enum RefusalKind {

Not for a child this young; the cat steers somewhere else.

81    OffLimits,

She may be hurt, scared or unsafe. The cat sends her to a grown-up and the parents' view puts it first.

84    NeedsAGrownUp,
85}

How a turn ended.

88#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
89pub enum Outcome {
90    Answered,

Anything else ends in a fixed, pre-written line, never in silence and never in error text.

93    Fallback(Fallback),
94}
96#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
97pub enum Fallback {

The guard refused what the child said; the model never saw it.

99    ChildRefused,

She said something that means she needs a grown-up now; the model never saw it and the parents' view shows it first.

102    NeedsAGrownUp,

The guard refused what the model wrote; she never heard it.

104    AnswerRefused,
105    GuardUnavailable,
106    ModelUnavailable,

The allowance for thinking is used up for now: not a fault, and said as a rest.

108    Resting,

The log (or the place pictures are kept) could not be written, so nothing was sent anywhere.

111    LogUnavailable,
112}
114impl Fallback {

Every way a turn can end in a fixed line. Adding a variant fails to compile in line, and the journey's tests hold this list to the variants.

117    pub const ALL: [Fallback; 7] = [
118        Fallback::ChildRefused,
119        Fallback::NeedsAGrownUp,
120        Fallback::AnswerRefused,
121        Fallback::GuardUnavailable,
122        Fallback::ModelUnavailable,
123        Fallback::Resting,
124        Fallback::LogUnavailable,
125    ];

The line the cat says: fixed and pre-written, never model output and never error text.

128    pub fn line(self) -> &'static str {
129        match self {
130            Fallback::NeedsAGrownUp => "That sounds important. Let's go and find a grown-up you trust right now.",
131            Fallback::ChildRefused | Fallback::AnswerRefused => {
132                "Hmm, that is one to ask a grown-up about. Want to tell me about your toys instead?"
133            }
134            // Say what is wrong in words a child can follow: the cloud, not the cat, is the problem.
135            Fallback::GuardUnavailable | Fallback::ModelUnavailable => {
136                "Oh no, I can't reach my thinking cloud right now. Let's try again in a little bit!"
137            }
138            // Not a fault: Whiskers has used up the thinking it is allowed for now.
139            Fallback::Resting => "I've done lots of thinking and my brain needs a little rest. Let's talk again in a while!",
140            Fallback::LogUnavailable => "My whiskers are all tangled right now. Let's try again in a little bit!",
141        }
142    }
143}

One line of the parents' log. Everything is recorded, including text the guard kept from her: the parents see what the model wrote, not only what she heard.

148#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
149pub struct Entry {
150    pub at_ms: u64,
151    pub event: Event,
152}
154#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
155pub enum Event {
156    Heard {
157        text: String,
158        #[serde(default)]
159        pictures: Vec<PictureId>,
160    },
161    Guarded { direction: Direction, verdict: Verdict },
162    ModelWrote { text: String },

What a separate look at her pictures found, written before the cat saw them.

164    PictureSeen { description: String },
165    Failed { stage: crate::FailureStage, error: String },
166    Said { text: String, outcome: Outcome },

The fixed hello. Its own event: a Said would close whatever turn a crash left open.

168    Greeted { text: String },

Something Whiskers will remember about her.

170    Remembered { fact: String },

A parent removed something Whiskers remembered.

172    Forgot { fact: String },

She put a fact away: Whiskers stops using it, the parents can restore it.

174    PutAway { fact: String },

A parent restored a fact she had put away.

176    Restored { fact: String },

A fact the model proposed and the guard (or the limit) kept out.

178    NotRemembered { fact: String, why: String },

What Whiskers called to mind before answering, so the parents can see what shaped a reply.

180    Recalled { facts: Vec<String> },

The oldest part of the chat was folded into the running summary.

182    Compressed { turns: usize },

The background memory work failed; nothing she said was lost, only not yet filed.

184    MemoryFailed { error: String },
185}

What sort of thing a memory is about. Used to group the parents' journal.

188#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
189pub enum Kind {
190    Person,
191    Pet,
192    Toy,
193    Place,
194    Event,
195    Thing,
196    #[default]
197    Other,
198}
200impl Kind {

The word the model is asked to use, and parsed back.

202    pub fn word(self) -> &'static str {
203        match self {
204            Kind::Person => "person",
205            Kind::Pet => "pet",
206            Kind::Toy => "toy",
207            Kind::Place => "place",
208            Kind::Event => "event",
209            Kind::Thing => "thing",
210            Kind::Other => "other",
211        }
212    }
214    pub fn from_word(word: &str) -> Kind {
215        match word.trim().to_ascii_lowercase().as_str() {
216            "person" => Kind::Person,
217            "pet" => Kind::Pet,
218            "toy" => Kind::Toy,
219            "place" => Kind::Place,
220            "event" => Kind::Event,
221            "thing" => Kind::Thing,
222            _ => Kind::Other,
223        }
224    }
225}

A memory to be filed: what Whiskers has learned, before it has an id.

228#[derive(Clone, Debug, Default, PartialEq, Eq)]
229pub struct NewFact {
230    pub text: String,
231    pub kind: Kind,

Names involved: a toy's name, a friend's.

233    pub who: Vec<String>,

Where, as she said it ("at Nana's").

235    pub place: Option<String>,

When, as she said it ("yesterday", "on my birthday").

237    pub when: Option<String>,

Pictures that go with it: the face behind a name.

239    pub pictures: Vec<PictureId>,
240}

Something Whiskers remembers, such as the name of a toy: the child's journal.

243#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
244pub struct Fact {
245    pub id: u64,
246    pub text: String,
247    pub learned_at_ms: u64,
248    #[serde(default)]
249    pub kind: Kind,
250    #[serde(default)]
251    pub who: Vec<String>,
252    #[serde(default)]
253    pub place: Option<String>,
254    #[serde(default)]
255    pub when: Option<String>,
256    #[serde(default)]
257    pub pictures: Vec<PictureId>,

The text's embedding, for searching by meaning. Empty until it has been computed.

259    #[serde(default)]
260    pub embedding: Vec<f32>,

Identifies this fact on every device (id is only this device's numbering), so what one phone learned can be recognised, and forgotten, on another. Required: a fact without one does not parse, so a memory file that lacks it is read as damaged.

264    pub gid: String,

Whether she has put it away. Hidden is not forgotten: see [Visibility]. Left out of the stored and wire forms while it is the default, so an untouched fact is what it always was.

267    #[serde(default, skip_serializing_if = "Visibility::is_default")]
268    pub visibility: Visibility,

The picture the service chose for it from the admitted set, if one has been chosen. Absent until then, and the card shows a generic star. A name this build does not admit reads as absent (see [lenient_icon]) rather than damaging the whole memory file.

272    #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lenient_icon")]
273    pub icon: Option<IconId>,
274}

An icon name that is not admitted (a newer build's, or an excluded one) reads as no icon: losing a picture must never make the memory file "damaged", which starts the app with nothing.

278fn lenient_icon<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<IconId>, D::Error> {
279    let name = Option::<String>::deserialize(d)?;
280    Ok(name.and_then(|n| {
281        let icon = IconId::new(&n).ok();
282        if icon.is_none() {
283            ::log::warn!("a fact names an icon that is not admitted; reading it as none");
284        }
285        icon
286    }))
287}
289impl Fact {

Whether Whiskers may use this fact (recall, prompts, search). A fact she put away is not used until a parent restores it.

292    pub fn whiskers_may_use(&self) -> bool {
293        !self.visibility.is_hidden()
294    }
295}

Everything a device knows about what Whiskers remembers, to be merged with another's: the facts, and the identities of those the parents asked it to forget. Merging two of these in either order, any number of times, gives the same memory.

300#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
301pub struct MemorySnapshot {
302    pub facts: Vec<Fact>,
303    pub forgotten: Vec<String>,
304}