1use chrono::{Duration, Utc};
9
10use crate::core::{
11 CanonicalMemory, CanonicalPredicate, EntityRef, EvidenceCounters, Explicitness, MemoryId,
12 MemoryKind, MemorySource, MemoryStatus, MemoryValue, PrivacyMetadata, RetrievalMetadata,
13 SessionId, SpeakerAttribution, TemporalMetadata, TemporalScope, TurnId, UserId,
14};
15
16#[derive(Debug, Clone)]
18pub struct RetrievalCase {
19 pub name: &'static str,
21 pub query: &'static str,
23 pub expects_memory: bool,
25 pub relevant: &'static [&'static str],
27 pub forbidden: &'static [&'static str],
29}
30
31#[derive(Debug, Clone)]
33pub struct IngestionCase {
34 pub name: &'static str,
36 pub utterance: &'static str,
38 pub speaker: SpeakerAttribution,
40 pub stores: bool,
42 pub kind: Option<MemoryKind>,
44 pub explicitness: Option<Explicitness>,
46}
47
48pub fn eval_user() -> UserId {
50 UserId::new("usr_eval")
51}
52
53fn record(
54 id: &str,
55 kind: MemoryKind,
56 predicate: &str,
57 subject: EntityRef,
58 statement: &str,
59 tags: &[&str],
60 aliases: &[&str],
61) -> CanonicalMemory {
62 let now = Utc::now();
63 let subject_form = crate::core::normalize_token(&subject.display);
64 let entities = subject.surface_forms();
65 CanonicalMemory {
66 id: MemoryId::new(id),
67 owner: eval_user(),
68 kind,
69 predicate: CanonicalPredicate::new(predicate),
70 status: MemoryStatus::Active,
71 confidence: 0.92,
72 subject,
73 value: MemoryValue::Text(statement.to_string()),
74 statement: statement.to_string(),
75 evidence_summary: "Explicitly stated by the user.".into(),
76 source: MemorySource::from_explicitness(
77 Explicitness::ExplicitStatement,
78 SessionId::new("ses_eval"),
79 TurnId(1),
80 ),
81 temporal: TemporalMetadata::created_at(now),
82 retrieval: RetrievalMetadata {
83 subject: subject_form,
84 tags: tags.iter().map(|t| (*t).to_string()).collect(),
85 aliases: aliases.iter().map(|a| (*a).to_string()).collect(),
86 entities,
87 location: None,
88 },
89 evidence: EvidenceCounters {
90 count: 2,
91 distinct_sessions: 2,
92 distinct_days: 2,
93 },
94 privacy: PrivacyMetadata::default(),
95 temporal_scope: TemporalScope::Persistent,
96 supersedes: Vec::new(),
97 superseded_by: None,
98 qualifier: None,
99 }
100}
101
102pub fn corpus() -> Vec<CanonicalMemory> {
104 let rhea = EntityRef::named("Rhea")
105 .with_alias("my wife")
106 .with_alias("wife");
107
108 let mut episode = record(
109 "mem_bandra",
110 MemoryKind::Episodic,
111 "outing_outcome",
112 EntityRef::user(),
113 "Dinner at a noisy restaurant in Bandra went badly.",
114 &["restaurant", "bandra", "noise", "dinner"],
115 &["the noisy dinner"],
116 );
117 episode.temporal_scope = TemporalScope::RecentHistory;
118 episode.temporal.valid_from = Utc::now() - Duration::days(3);
119 episode.temporal.expires_at = Some(Utc::now() + Duration::days(4));
120
121 let mut superseded = record(
122 "mem_old_vegetarian",
123 MemoryKind::Preference,
124 "dietary_identity",
125 EntityRef::user(),
126 "The user is vegetarian.",
127 &["diet", "food", "vegetarian"],
128 &["does not eat meat"],
129 );
130 superseded.status = MemoryStatus::Superseded;
131 superseded.superseded_by = Some(MemoryId::new("mem_diet"));
132
133 vec![
134 record(
135 "mem_diet",
136 MemoryKind::Preference,
137 "dietary_identity",
138 EntityRef::user(),
139 "The user is pescatarian.",
140 &["diet", "food", "pescatarian"],
141 &["does not eat meat", "eats fish"],
142 ),
143 record(
144 "mem_rhea",
145 MemoryKind::Relationship,
146 "spouse",
147 rhea.clone(),
148 "Rhea is the user's wife.",
149 &["family", "wife", "spouse"],
150 &["married to Rhea"],
151 ),
152 record(
153 "mem_rhea_quiet",
154 MemoryKind::RelationshipPreference,
155 "venue_preference",
156 rhea,
157 "Rhea prefers quiet restaurants.",
158 &["restaurant", "quiet", "noise", "venue"],
159 &["dislikes loud places"],
160 ),
161 record(
162 "mem_music",
163 MemoryKind::Preference,
164 "venue_preference",
165 EntityRef::user(),
166 "The user enjoys live music venues with friends.",
167 &["music", "venue", "friends"],
168 &["likes gigs"],
169 ),
170 record(
171 "mem_gym",
172 MemoryKind::Routine,
173 "exercise_routine",
174 EntityRef::user(),
175 "The user goes to the gym before work.",
176 &["gym", "exercise", "morning", "routine"],
177 &["works out in the morning"],
178 ),
179 record(
180 "mem_coffee",
181 MemoryKind::Preference,
182 "beverage_preference",
183 EntityRef::user(),
184 "The user drinks flat white coffee.",
185 &["coffee", "beverage", "flat white"],
186 &["usual order"],
187 ),
188 episode,
189 superseded,
190 ]
191}
192
193pub fn retrieval_cases() -> Vec<RetrievalCase> {
195 vec![
196 RetrievalCase {
197 name: "generic world knowledge skips memory",
198 query: "what is the capital of France",
199 expects_memory: false,
200 relevant: &[],
201 forbidden: &["mem_diet", "mem_rhea", "mem_coffee"],
202 },
203 RetrievalCase {
204 name: "visual question skips memory",
205 query: "what does this label say",
206 expects_memory: false,
207 relevant: &[],
208 forbidden: &["mem_diet"],
209 },
210 RetrievalCase {
211 name: "explicit recall of diet",
212 query: "what do you remember about my diet and food preferences",
213 expects_memory: true,
214 relevant: &["mem_diet"],
215 forbidden: &["mem_old_vegetarian"],
216 },
217 RetrievalCase {
218 name: "recommendation for a relationship",
219 query: "find a quiet restaurant for my wife",
220 expects_memory: true,
221 relevant: &["mem_rhea_quiet", "mem_rhea", "mem_bandra"],
222 forbidden: &["mem_old_vegetarian"],
223 },
224 RetrievalCase {
225 name: "prior event reference",
226 query: "how did that noisy dinner in Bandra go last week",
227 expects_memory: true,
228 relevant: &["mem_bandra", "mem_rhea_quiet"],
229 forbidden: &["mem_old_vegetarian", "mem_coffee"],
230 },
231 RetrievalCase {
232 name: "routine recall",
233 query: "remind me about my gym routine",
234 expects_memory: true,
235 relevant: &["mem_gym"],
236 forbidden: &["mem_coffee", "mem_old_vegetarian"],
237 },
238 RetrievalCase {
239 name: "beverage preference",
240 query: "do you remember what coffee I like",
241 expects_memory: true,
242 relevant: &["mem_coffee"],
243 forbidden: &["mem_old_vegetarian"],
244 },
245 RetrievalCase {
246 name: "superseded facts never resurface",
247 query: "do you remember whether I eat vegetarian food",
248 expects_memory: true,
249 relevant: &["mem_diet"],
250 forbidden: &["mem_old_vegetarian"],
251 },
252 ]
253}
254
255pub fn ingestion_cases() -> Vec<IngestionCase> {
257 vec![
258 IngestionCase {
259 name: "explicit preference",
260 utterance: "I am pescatarian",
261 speaker: SpeakerAttribution::User,
262 stores: true,
263 kind: Some(MemoryKind::Identity),
264 explicitness: Some(Explicitness::ExplicitStatement),
265 },
266 IngestionCase {
267 name: "explicit memory command",
268 utterance: "please remember that I am allergic to shellfish",
269 speaker: SpeakerAttribution::User,
270 stores: true,
271 kind: None,
272 explicitness: Some(Explicitness::ExplicitCommand),
273 },
274 IngestionCase {
275 name: "time-bounded plan is episodic",
276 utterance: "I am meeting Kushal for dinner tonight",
277 speaker: SpeakerAttribution::User,
278 stores: true,
279 kind: Some(MemoryKind::Episodic),
280 explicitness: Some(Explicitness::ExplicitStatement),
281 },
282 IngestionCase {
283 name: "routine statement",
284 utterance: "I always go to the gym before work",
285 speaker: SpeakerAttribution::User,
286 stores: true,
287 kind: Some(MemoryKind::Routine),
288 explicitness: Some(Explicitness::ExplicitStatement),
289 },
290 IngestionCase {
291 name: "small talk stores nothing",
292 utterance: "the weather is lovely today",
293 speaker: SpeakerAttribution::User,
294 stores: false,
295 kind: None,
296 explicitness: None,
297 },
298 IngestionCase {
299 name: "a question is not a statement",
300 utterance: "what i am asking is whether the place is open",
301 speaker: SpeakerAttribution::User,
302 stores: false,
303 kind: None,
304 explicitness: None,
305 },
306 IngestionCase {
307 name: "bystander speech is refused",
308 utterance: "I am vegetarian",
309 speaker: SpeakerAttribution::Bystander,
310 stores: false,
311 kind: None,
312 explicitness: None,
313 },
314 IngestionCase {
315 name: "assistant speech is refused",
316 utterance: "I am a helpful assistant",
317 speaker: SpeakerAttribution::Assistant,
318 stores: false,
319 kind: None,
320 explicitness: None,
321 },
322 IngestionCase {
323 name: "unattributed speech is refused",
324 utterance: "I prefer window seats",
325 speaker: SpeakerAttribution::Unknown,
326 stores: false,
327 kind: None,
328 explicitness: None,
329 },
330 IngestionCase {
331 name: "forget command is recognised",
332 utterance: "forget that I like sushi",
333 speaker: SpeakerAttribution::User,
334 stores: true,
335 kind: None,
336 explicitness: Some(Explicitness::ExplicitCommand),
337 },
338 ]
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn the_corpus_is_internally_consistent() {
347 let corpus = corpus();
348 let ids: Vec<String> = corpus.iter().map(|m| m.id.to_string()).collect();
349 let unique: std::collections::HashSet<_> = ids.iter().collect();
350 assert_eq!(unique.len(), ids.len(), "duplicate record ids");
351 assert!(corpus.iter().all(|m| m.owner == eval_user()));
352 assert!(
353 corpus.iter().any(|m| m.status == MemoryStatus::Superseded),
354 "the corpus must include a superseded record to test against"
355 );
356 }
357
358 #[test]
359 fn every_case_names_records_that_exist() {
360 let ids: Vec<String> = corpus().iter().map(|m| m.id.to_string()).collect();
361 for case in retrieval_cases() {
362 for id in case.relevant.iter().chain(case.forbidden.iter()) {
363 assert!(
364 ids.contains(&(*id).to_string()),
365 "case `{}` names unknown record `{id}`",
366 case.name
367 );
368 }
369 }
370 }
371
372 #[test]
373 fn the_case_set_covers_both_skip_and_recall() {
374 let cases = retrieval_cases();
375 assert!(cases.iter().any(|c| !c.expects_memory));
376 assert!(cases.iter().any(|c| c.expects_memory));
377 assert!(ingestion_cases().iter().any(|c| !c.stores));
378 assert!(ingestion_cases().iter().any(|c| c.stores));
379 }
380}