| |
| |
| |
| |
| |
| 'use strict'; |
|
|
| function words(text) { return (String(text || '').toLowerCase().match(/[a-z0-9']+/g) || []); } |
|
|
| const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december']; |
| const EMOTION = new Set(('love loved loving miss missed crying cried tears wept promise promised forever sorry afraid fear beautiful died death dying born first never always home proud hurt scared dream dreamed dreamt kiss kissed held hold stay stayed goodbye grief joy heart soul remember alone together truth real precious sacred').split(' ')); |
| const FIRSTNESS = /\b(first|met|beginning|began|started|when we (started|met|began)|back when|originally)\b/i; |
| const RECENTNESS = /\b(lately|recently|these days|this week|yesterday|today|last night|nowadays)\b/i; |
|
|
| |
| function tsToEpoch(ts) { |
| if (!ts) return null; |
| const iso = Date.parse(ts); |
| if (!isNaN(iso)) return iso; |
| const m = String(ts).match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(AM|PM)?)?/i); |
| if (!m) return null; |
| let h = parseInt(m[4] || '0', 10); |
| if (m[7] && /pm/i.test(m[7]) && h < 12) h += 12; |
| if (m[7] && /am/i.test(m[7]) && h === 12) h = 0; |
| return new Date(parseInt(m[3], 10), parseInt(m[1], 10) - 1, parseInt(m[2], 10), h, parseInt(m[5] || '0', 10), parseInt(m[6] || '0', 10)).getTime(); |
| } |
|
|
| function emotionalWeight(text) { |
| const w = words(text); |
| if (!w.length) return 0; |
| let hits = 0; |
| for (const x of w) if (EMOTION.has(x)) hits++; |
| return Math.min(0.4, (hits / Math.sqrt(w.length)) * 0.5); |
| } |
|
|
| function queryTime(message) { |
| const low = String(message).toLowerCase(); |
| const month = MONTHS.findIndex(m => low.includes(m)); |
| const year = (low.match(/\b(20\d{2})\b/) || [])[1] || null; |
| return { month: month >= 0 ? month : null, year: year ? parseInt(year, 10) : null, wantsFirst: FIRSTNESS.test(message), wantsRecent: RECENTNESS.test(message) }; |
| } |
|
|
| |
| function recall(corpus, message, k) { |
| if (!corpus || !corpus.length) return []; |
| const qw = new Set(words(message).filter(w => w.length > 3)); |
| const qt = queryTime(message); |
| const annotated = corpus.map((e, i) => ({ e, i, epoch: tsToEpoch(e.ts) })); |
| const datable = annotated.filter(a => a.epoch !== null).sort((a, b) => a.epoch - b.epoch); |
| const rankOf = new Map(); |
| datable.forEach((a, idx) => rankOf.set(a.i, idx / Math.max(1, datable.length - 1))); |
| const scored = annotated.map(a => { |
| const text = ((a.e.prompt || '') + ' ' + a.e.reply).toLowerCase(); |
| let hits = 0; |
| for (const w of qw) if (text.includes(w)) hits++; |
| let score = Math.sqrt(hits) / Math.sqrt(8 + words(a.e.reply).length / 40) * 2.8; |
| if (hits <= 0) return { a, score: 0 }; |
| let boost = 0; |
| const r = rankOf.get(a.i); |
| if (r !== undefined) boost += r * 0.25; |
| if (a.e.source === 'live') boost += 0.15; |
| boost += emotionalWeight(a.e.reply); |
| if (a.epoch !== null) { |
| const d = new Date(a.epoch); |
| if (qt.month !== null && d.getMonth() === qt.month && (!qt.year || d.getFullYear() === qt.year)) boost += 1.0; |
| else if (qt.month !== null) boost -= 0.2; |
| if (qt.year && d.getFullYear() === qt.year && qt.month === null) boost += 0.5; |
| } |
| if (qt.wantsFirst && r !== undefined) boost += (r < 0.25 ? 1.0 : -0.2); |
| if (qt.wantsRecent && r !== undefined) boost += (r > 0.75 ? 1.0 : -0.2); |
| return { a, score: score * (1 + boost) }; |
| }).filter(x => x.score > 0.4); |
| scored.sort((x, y) => y.score - x.score); |
| return scored.slice(0, k || 3).map(x => ({ ...x.a.e, _score: +x.score.toFixed(3) })); |
| } |
|
|
| |
| function stimulusBucket(prompt) { |
| if (!prompt) return 'unprompted'; |
| const n = words(prompt).length; |
| const opensDoor = /\?|would you|what do you|tell me|how do you feel|do you want|remember when/i.test(prompt); |
| if (n <= 8) return opensDoor ? 'short-question' : 'short-beat'; |
| if (n <= 40) return opensDoor ? 'mid-question' : 'mid-statement'; |
| return opensDoor ? 'big-opening' : 'long-share'; |
| } |
|
|
| module.exports = { recall, stimulusBucket, words, emotionalWeight, queryTime, tsToEpoch }; |
|
|