1#[cfg(not(target_family = "wasm"))] 2use std::fs; 3#[cfg(not(target_family = "wasm"))] 4use std::path::Path; 5 6use ::log::{debug, trace, warn}; 7#[cfg(not(target_family = "wasm"))] 8use ::log::info; 9 10#[cfg(not(target_family = "wasm"))] 11use crate::ports::LogError; 12use crate::ports::{Model, ModelError}; 13use crate::profile::Audience; 14use crate::types::{Entry, Event, Fallback, Outcome, PictureId, Speaker, Turn, Verdict}; 15 16#[cfg(not(target_family = "wasm"))]
The log read back from disk. A line that cannot be read (a crash mid-write) is counted rather than hiding the rest.
19pub fn read_log(path: &Path) -> Result<(Vec<Entry>, usize), LogError> { 20 // Lossy: a crash can cut a multi-byte character, and that must cost one line, not the whole log. 21 debug!("reading the log {}", path.display()); 22 let text = String::from_utf8_lossy(&fs::read(path).map_err(|e| { 23 warn!("cannot read the log {}: {e}", path.display()); 24 LogError(e.to_string()) 25 })?) 26 .into_owned(); 27 let mut entries = Vec::new(); 28 let mut unreadable = 0; 29 for line in text.lines().filter(|l| !l.trim().is_empty()) { 30 match serde_json::from_str::<Entry>(line) { 31 Ok(e) => entries.push(e), 32 Err(e) => { 33 unreadable += 1; 34 warn!("unreadable line in {} ({} bytes): {e}", path.display(), line.len()); 35 } 36 } 37 } 38 debug!("log {} read: {} entries, {unreadable} unreadable", path.display(), entries.len()); 39 Ok((entries, unreadable)) 40}
42#[cfg(not(target_family = "wasm"))]
The one log of everything said to Whiskers, wherever it was said. This device's own entries
are read from its own file, which is always complete for it; everyone else's come from the copy
of the service's log (replica, lines of {"device":..,"entry":..}) that sync keeps. Entries
are grouped by device because an exchange is a run of one device's entries; the exchanges
are laid out together by time. Returns the groups and how many lines could not be read.
48pub fn read_shared_log(own: &Path, replica: &Path, me: &str) -> Result<(Vec<Vec<Entry>>, usize), LogError> { 49 debug!("reading the shared log for device {me}"); 50 let (mine, mut unreadable) = read_log(own)?; 51 let mut groups = vec![mine]; 52 let mut others: std::collections::BTreeMap<String, Vec<Entry>> = std::collections::BTreeMap::new(); 53 if let Ok(bytes) = fs::read(replica) { 54 for line in String::from_utf8_lossy(&bytes).lines().filter(|l| !l.trim().is_empty()) { 55 #[derive(serde::Deserialize)] 56 struct Line { 57 device: String, 58 entry: Entry, 59 } 60 match serde_json::from_str::<Line>(line) { 61 Ok(l) if l.device != me => others.entry(l.device).or_default().push(l.entry), 62 Ok(_) => {} 63 Err(e) => { 64 unreadable += 1; 65 warn!("unreadable line in the replica log ({} bytes): {e}", line.len()); 66 } 67 } 68 } 69 } else { 70 debug!("no replica log at {}; only this device's entries", replica.display()); 71 } 72 groups.extend(others.into_values()); 73 info!("shared log: {} group(s), {unreadable} unreadable line(s)", groups.len()); 74 Ok((groups, unreadable)) 75}
One thing the child said and what became of it.
What the model wrote, including text she never heard.
What the parents see for a stretch of time (normally one day).
98impl Digest {
The exchanges that began in [from_ms, to_ms) across several devices' logs, in time order.
100 pub fn between_all(sources: &[Vec<Entry>], from_ms: u64, to_ms: u64) -> Self { 101 debug!("digest across {} source(s) for [{from_ms}, {to_ms})", sources.len()); 102 let mut out = Digest::default(); 103 for s in sources { 104 let d = Digest::between(s, from_ms, to_ms); 105 out.exchanges.extend(d.exchanges); 106 out.facts_learned.extend(d.facts_learned); 107 } 108 out.exchanges.sort_by_key(|e| e.at_ms); 109 out 110 }
The exchanges that began in [from_ms, to_ms).
113 pub fn between(entries: &[Entry], from_ms: u64, to_ms: u64) -> Self { 114 trace!("digest of {} entries for [{from_ms}, {to_ms})", entries.len()); 115 let mut out = Digest::default(); 116 let mut open: Option<Exchange> = None; 117 for e in entries { 118 match &e.event { 119 Event::Heard { text, pictures } => { 120 if let Some(unfinished) = open.take() { 121 out.exchanges.push(unfinished); 122 } 123 open = Some(Exchange { 124 at_ms: e.at_ms, 125 heard: text.clone(), 126 pictures: pictures.clone(), 127 model_wrote: None, 128 said: String::new(), 129 // A turn the log shows no end for was cut off; it is shown as unavailable. 130 outcome: Outcome::Fallback(Fallback::LogUnavailable), 131 notes: Vec::new(), 132 }); 133 } 134 Event::ModelWrote { text } => { 135 if let Some(d) = &mut open { 136 d.model_wrote = Some(text.clone()); 137 } 138 } 139 Event::Guarded { direction, verdict: Verdict::Refuse { reason, .. } } => { 140 if let Some(d) = &mut open { 141 d.notes.push(format!("guard stopped it ({direction:?}): {reason}")); 142 } 143 } 144 Event::PictureSeen { description } => { 145 if let Some(d) = &mut open { 146 d.notes.push(format!("the picture looked like: {description}")); 147 } 148 } 149 Event::Recalled { facts } => { 150 if let Some(d) = &mut open { 151 d.notes.push(format!("Whiskers remembered: {}", facts.join("; "))); 152 } 153 } 154 Event::Failed { stage, error } => { 155 if let Some(d) = &mut open { 156 d.notes.push(format!("{stage:?} failed: {error}")); 157 } 158 } 159 // A new session starting: whatever was open never finished. 160 Event::Greeted { .. } => { 161 if let Some(unfinished) = open.take() { 162 out.exchanges.push(unfinished); 163 } 164 } 165 Event::Said { text, outcome } => { 166 if let Some(mut d) = open.take() { 167 d.said = text.clone(); 168 d.outcome = *outcome; 169 out.exchanges.push(d); 170 } 171 } 172 Event::Remembered { fact } if (from_ms..to_ms).contains(&e.at_ms) => out.facts_learned.push(fact.clone()), 173 _ => {} 174 } 175 } 176 if let Some(unfinished) = open { 177 out.exchanges.push(unfinished); 178 } 179 out.exchanges.retain(|d| (from_ms..to_ms).contains(&d.at_ms)); 180 debug!("digest: {} exchange(s), {} fact(s) learned", out.exchanges.len(), out.facts_learned.len()); 181 out 182 }
Exchanges where she said something that means she needs a grown-up. These come first in the parents' view.
Counts for the top of the parents' view.
The transcript the summary is written from.
197 pub fn transcript(&self, audience: &Audience) -> String { 198 let mut s = String::new(); 199 for d in &self.exchanges { 200 s.push_str(&format!("{}: {}\n", audience.speaker_label(), if d.heard.is_empty() { "(showed a picture)" } else { &d.heard })); 201 if !d.pictures.is_empty() { 202 s.push_str(&format!(" [{} picture(s) shown]\n", d.pictures.len())); 203 } 204 s.push_str(&format!("Whiskers: {}\n", d.said)); 205 for n in &d.notes { 206 s.push_str(&format!(" [{n}]\n")); 207 } 208 s.push('\n'); 209 } 210 s 211 }
A short note for her parents, written by the model from the transcript. Never spoken to her, so it is not passed through the guard.
215 pub fn summarize(&self, model: &dyn Model, audience: &Audience) -> Result<String, ModelError> { 216 debug!("summarizing {} exchange(s)", self.exchanges.len()); 217 if self.exchanges.is_empty() { 218 return Ok(format!("{} did not talk with Whiskers in this time.", capitalised(audience.the_child()))); 219 } 220 let system = crate::persona::parents_note_prompt(audience); 221 let transcript = self.transcript(audience); 222 debug!("asking the model for the parents' note: transcript {} chars", transcript.len()); 223 model.complete(&system, &[Turn::said(Speaker::Child, transcript)]).inspect_err(|e| warn!("the parents' note could not be written: {}", e.reason)) 224 } 225}