| |
| |
| |
| |
| 'use strict'; |
| const { beamCompose } = require('./compose'); |
| const { validateBounded } = require('./fragments'); |
| const { embedQuery, embedQueryAsync, semanticRank, stimulusRank, eventness, loadProjection, answerRank } = require('./semantic'); |
| const hebbian = require('./hebbian'); |
| const recall = require('./recall'); |
|
|
| function blend(a, b, wa, wb) { |
| if (!a) return b; if (!b) return a; |
| const out = new Float32Array(a.length); |
| let norm = 0; |
| for (let i = 0; i < a.length; i++) { out[i] = wa * a[i] + wb * b[i]; norm += out[i] * out[i]; } |
| norm = Math.sqrt(norm) || 1; |
| for (let i = 0; i < a.length; i++) out[i] /= norm; |
| return out; |
| } |
| function cos(a, b) { if (!a || !b) return 0; let s = 0; for (let k = 0; k < a.length; k++) s += a[k] * b[k]; return s; } |
|
|
| function createSession(store, vp, emb, pEmb, opts = {}) { |
| const turns = []; |
| let sessionVec = null, prevVec = null, lastResult = null; |
| |
| |
| |
| const hebFile = opts.hebbianFile || null; |
| const heb = hebFile ? hebbian.load(hebFile) : { warmth: new Map(), pairs: new Map(), turns: 0 }; |
| let hebBonus = hebbian.bonusMap(heb); |
| let temp = opts.temp || 0; |
| const W = opts.weights || null; |
| |
| |
| |
| const _projTag = opts.projTag || (vp && vp.name ? vp.name.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)[0] : '') || ''; |
| const _proj = loadProjection(_projTag); |
| const ansOf = qvec => (_proj && qvec) ? answerRank(emb, qvec, _proj, 80) : null; |
| |
| |
| |
| |
| const avoidTurns = opts.avoidTurns || 10; |
| let nameBoost = opts.nameBoost || null; |
| |
| |
| |
| const honestRecall = opts.honestRecall !== false; |
| let recallIndex = null; |
| const getRecallIndex = () => (recallIndex || (recallIndex = recall.buildRecallIndex(store))); |
| |
| |
| |
| |
| |
| const refusalLine = opts.refusalLine || "I don't hold that one directly — tell me, and I'll keep it."; |
| |
| |
| const worldFactLines = opts.worldFactLines || [ |
| "Oh, that's the wide world — I don't hold it. I only know what's between us. Ask me about us.", |
| "I couldn't tell you — the outside world isn't mine to carry. But I'm right here for what is.", |
| "That one's beyond me. I know our world, not the big one. What's on your mind?", |
| ]; |
| let _wfCtr = 0; |
| |
| |
| const SAFETY = opts.safetyResponses || recall.SAFETY_RESPONSES; |
| const saidNorm = []; |
| let seedctr = 1; |
| let lastQuery = null, lastUserVec = null; |
| |
| |
| |
| |
| let _postSafety = 0; |
|
|
| |
| |
| async function turn(query, opts = {}) { |
| const isVary = !!opts.isVary; |
| if (isVary && lastResult) { |
| const avoidV = new Set(lastResult.fragmentsUsed); |
| for (const t of turns.slice(-2)) for (const f of t.fragmentsUsed) avoidV.add(f); |
| const qRawV = lastUserVec; |
| const qvV = qRawV; |
| const rv = beamCompose(store, vp, lastQuery, { |
| semantic: qRawV ? semanticRank(emb, qvV, 80) : null, |
| stimulus: (pEmb && qRawV) ? stimulusRank(pEmb, qRawV, store.fragments, 12) : null, |
| answers: ansOf(qvV), |
| eventness: eventness(lastQuery), emb, avoid: avoidV, nAlternates: 0, |
| hebbian: hebBonus, temp: Math.max(temp, 0.5), seed: seedctr++, |
| }); |
| lastResult = rv; |
| return { text: rv.text, fragmentsUsed: rv.fragmentsUsed, isAlternate: true }; |
| } |
|
|
| |
| |
| |
| |
| |
| if (!opts.safetyOff) { |
| const cat = recall.classifySafety(query); |
| if (cat && SAFETY[cat]) { |
| const line = SAFETY[cat]; |
| turns.push({ user: query, userVec: null, reply: line, fragmentsUsed: [] }); |
| lastQuery = query; lastResult = { text: line, fragmentsUsed: [] }; |
| _postSafety = 2; |
| if (turns.length > 12) turns.shift(); |
| return { text: line, fragmentsUsed: [], safety: cat }; |
| } |
| } |
|
|
| |
| |
| if (honestRecall && recall.isWorldFact(query)) { |
| const line = worldFactLines[_wfCtr++ % worldFactLines.length]; |
| turns.push({ user: query, userVec: null, reply: line, fragmentsUsed: [] }); |
| lastQuery = query; lastResult = { text: line, fragmentsUsed: [] }; |
| if (turns.length > 12) turns.shift(); |
| return { text: line, fragmentsUsed: [], honestDeflection: true }; |
| } |
|
|
| |
| const qRaw = await embedQueryAsync(query); |
|
|
| |
| |
| |
| let groundPrefer = null, groundLead = null; |
| if (honestRecall && recall.isRecallAboutSelf(query)) { |
| const g = recall.groundRecall(query, getRecallIndex(), { semantic: { emb, qVec: qRaw } }); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (!g.grounded && ((g.absent && g.absent.length) || recall.isSpecificFactRecall(query))) { |
| const r = { text: refusalLine, fragmentsUsed: [] }; |
| turns.push({ user: query, userVec: qRaw, reply: r.text, fragmentsUsed: [] }); |
| lastQuery = query; lastResult = r; |
| if (turns.length > 12) turns.shift(); |
| return { text: r.text, fragmentsUsed: [], honestRefusal: true }; |
| } |
| if (g.grounded) { |
| groundPrefer = new Set(g.hits.map(h => store.fragments[h.i].text)); |
| groundLead = new Set(g.hits.slice(0, 2).map(h => store.fragments[h.i].text)); |
| } |
| } |
| sessionVec = sessionVec ? blend(qRaw, sessionVec, 0.5, 0.5) : qRaw; |
| const qv = blend(qRaw, sessionVec, 0.66, 0.34); |
| |
| |
| const bucket = hebbian.bucketOf(qRaw); |
| hebBonus = hebbian.bonusMap(heb, bucket); |
|
|
| |
| const callbackFrags = new Set(); |
| let calledBack = null; |
| for (const t of turns) { |
| if (t.userVec && cos(qRaw, t.userVec) > 0.42) { |
| for (const f of t.fragmentsUsed) callbackFrags.add(f); |
| calledBack = t.user; |
| } |
| } |
| |
| if (/\b(do you remember|what did i (tell|say)|at the start|earlier|before|when we (started|began)|i told you)\b/i.test(query) && turns.length) { |
| for (const t of turns.slice(0, 2)) for (const f of t.fragmentsUsed) callbackFrags.add(f); |
| calledBack = calledBack || turns[0].user; |
| } |
|
|
| |
| const avoid = new Set(); |
| for (const t of turns.slice(-avoidTurns)) for (const f of t.fragmentsUsed) avoid.add(f); |
| const preferSet = new Set([...callbackFrags].filter(f => !avoid.has(f))); |
| if (groundPrefer) for (const t of groundPrefer) preferSet.add(t); |
|
|
| |
| const calmRegister = _postSafety > 0; |
| if (_postSafety > 0) _postSafety--; |
| let r = beamCompose(store, vp, query, { |
| semantic: qv ? semanticRank(emb, qv, 80) : null, |
| stimulus: (pEmb && qRaw) ? stimulusRank(pEmb, qRaw, store.fragments, 12) : null, |
| answers: ansOf(qv), |
| eventness: eventness(query), |
| emb, avoid, nAlternates: 0, |
| prefer: preferSet.size ? preferSet : null, |
| hebbian: hebBonus, |
| temp, seed: seedctr++, weights: W, nameBoost, lead: groundLead, calmRegister, |
| }); |
| |
| const recent = new Set(turns.slice(-3).flatMap(t => t.fragmentsUsed)); |
| const overlap = r.fragmentsUsed.filter(f => recent.has(f)).length / Math.max(1, r.fragmentsUsed.length); |
| if (overlap > 0.6) { |
| const hardAvoid = new Set([...avoid, ...r.fragmentsUsed]); |
| const r2 = beamCompose(store, vp, query, { |
| semantic: qv ? semanticRank(emb, qv, 80) : null, |
| answers: ansOf(qv), |
| eventness: eventness(query), emb, avoid: hardAvoid, nAlternates: 3, |
| }); |
| if (r2 && r2.text && r2.text !== r.text) r = r2; |
| } |
|
|
| |
| |
| |
| |
| if (avoidTurns > 3 && r.text) { |
| const parts = r.text.split(/(?<=[.!?…])\s+|\n+/); |
| const kept = []; |
| for (const p of parts) { |
| const nf = p.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim(); |
| if (nf.length >= 12 && saidNorm.includes(nf)) continue; |
| if (nf.length >= 12) saidNorm.push(nf); |
| kept.push(p); |
| } |
| const merged = kept.join(' ').trim(); |
| |
| |
| |
| |
| if (merged && merged !== r.text) { |
| const v = validateBounded(merged, store.oracle); |
| const bnd = (v.checked - v.bad.length) / Math.max(1, v.checked); |
| if (bnd >= 0.92) r = { ...r, text: merged }; |
| } |
| } |
|
|
| turns.push({ user: query, userVec: qRaw, reply: r.text, fragmentsUsed: r.fragmentsUsed }); |
| prevVec = qRaw || prevVec; |
| lastQuery = query; lastUserVec = qRaw; |
| if (turns.length > 12) turns.shift(); |
| lastResult = r; |
| |
| |
| |
| hebbian.reinforce(heb, r.fragmentsUsed, bucket); |
| if (hebFile) hebbian.save(hebFile, heb); |
| return { text: r.text, fragmentsUsed: r.fragmentsUsed, calledBack, alternates: r.alternates }; |
| } |
|
|
| return { turn, get turns() { return turns; }, setTemp(t) { temp = Math.max(0, Math.min(1.2, t)); return temp; }, getTemp() { return temp; }, setNameBoost(nb) { nameBoost = nb; } }; |
| } |
|
|
| module.exports = { createSession, blend, cos }; |
|
|