eliza-core
Version:
A rendition of ELIZA program engine by Weizenbaum sharable for all javascript environments
375 lines (374 loc) • 15 kB
JavaScript
import * as tslib_1 from "tslib";
import { of, concat } from 'rxjs';
import { scan, tap, concatMap, filter } from 'rxjs/operators';
import { Decomp } from './decompo';
import { Reassemble } from './Reassemble';
import * as estring from './estring';
import * as mention from './mention-router';
import { Key } from './key';
import { GotoKey } from './key-goto';
import { buildKeyStack } from './key-stack';
import * as PrePostUtil from './pre-post-utils';
import * as printers from './printers';
import { notEmpty } from './utils';
import { UnexpectedNumberException, UnknownRuleException, GotoLostException, ScriptInterpretingError, DuplicateAnnotateException, InvalidStringException, } from './exceptions';
const printSynonyms = true;
const printKeys = true;
const PRINT_PRE_POST = true;
const PRINT_INITIAL_FINAL = true;
export function loadEliza(script$, keyFilter) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const eliza = new ElizaImpl(keyFilter || (key => true));
yield eliza.readScript(script$).toPromise();
return eliza;
});
}
export function loadElizaInEnglish(script$) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const eliza = new ElizaImpl(key => true, sentence => sentence.split(' '));
yield eliza.readScript(script$).toPromise();
return eliza;
});
}
class ElizaImpl {
constructor(keyFilter, tokenizer) {
this.keyFilter = keyFilter;
this.tokenizer = tokenizer;
this.mem = [];
this.keys = [];
this.synonyms = [];
this.preList = [];
this.postList = [];
this.annotates = [];
this.tweakList = [];
this.initialStr = 'Hello.';
this.finalStr = 'Goodbye.';
this.quitList = [];
this.lastDecomp = [];
this.lastReasemb = [];
this.finished = false;
}
getInitialStr() {
return this.initialStr;
}
isFinished() {
return this.finished;
}
collect(s) {
const matchedPattern = [
{
tryMatch: (testStr) => estring.match(testStr, '*reasmb: *'),
onMatched: (matchedParts) => {
if (!this.lastReasemb) {
throw new ScriptInterpretingError('A Reasmb rule missing decomp rule');
}
this.lastReasemb.push(new Reassemble(matchedParts[1], this.lastDecomp[this.lastDecomp.length - 1]));
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*decomp: *'),
onMatched: (matchedParts) => {
if (!this.lastDecomp) {
throw new ScriptInterpretingError('A Decomp rule missing Key Initialization');
}
this.lastReasemb = [];
const temp = matchedParts[1];
const newMatch = estring.match(temp, '$ *');
this.lastDecomp.push(new Decomp(newMatch ? newMatch[0] : temp, newMatch ? true : false, this.lastReasemb));
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*key: * #*'),
onMatched: (matchedParts) => {
this.lastDecomp = [];
this.lastReasemb = null;
let n = 0;
if (matchedParts[2].length > 0) {
n = parseInt(matchedParts[2], 10);
if (isNaN(n)) {
throw new UnexpectedNumberException(matchedParts[2], 'key');
}
}
this.keys.push(new Key(matchedParts[1], n, this.lastDecomp));
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*key: *'),
onMatched: (matchedParts) => {
this.lastDecomp = [];
this.lastReasemb = null;
this.keys.push(new Key(matchedParts[1], 0, this.lastDecomp));
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*mention: *@* *'),
onMatched: (matchedParts) => {
const words = JSON.parse(`[${matchedParts[3]}]`);
this.synonyms.push({
tag: matchedParts[2],
words: words.slice(1),
});
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*annotate: *'),
onMatched: (matchedParts) => {
const tag = matchedParts[1].trim();
if (tag.length < 1 || this.annotates.find(w => w === tag)) {
throw new DuplicateAnnotateException(tag);
}
this.annotates.push(tag);
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*pre: * => *'),
onMatched: (matchedParts) => {
this.preList.push({ src: JSON.parse(matchedParts[1]), dest: JSON.parse(matchedParts[2]) });
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*post: * => *'),
onMatched: (matchedParts) => {
this.postList.push({ src: JSON.parse(matchedParts[1]), dest: JSON.parse(matchedParts[2]) });
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*tweak: * => *'),
onMatched: (matchedParts) => {
this.tweakList.push({ src: JSON.parse(matchedParts[1]), dest: JSON.parse(matchedParts[2]) });
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*initial: *'),
onMatched: (matchedParts) => {
this.initialStr = matchedParts[1];
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*final: *'),
onMatched: (matchedParts) => {
this.finalStr = matchedParts[1];
},
},
{
tryMatch: (testStr) => estring.match(testStr, '*quit: *'),
onMatched: (matchedParts) => {
this.quitList.push(` ${matchedParts[1]} `);
},
},
{
tryMatch: (testStr) => {
const parts = estring.match(testStr, '*: *');
if (parts && this.annotates.indexOf(estring.trim(parts[0])) > -1) {
return parts;
}
return null;
},
onMatched: (matchedParts) => {
const instruction = estring.trim(matchedParts[0]);
if (!this.lastReasemb) {
throw new ScriptInterpretingError('An annotated Reasmb rule missing decomp rule');
}
this.lastReasemb.push(new Reassemble(matchedParts[1], this.lastDecomp[this.lastDecomp.length - 1], instruction));
},
},
].find(({ tryMatch, onMatched }) => {
const matchedParts = tryMatch(s);
if (matchedParts) {
onMatched(matchedParts);
return true;
}
return false;
});
if (!matchedPattern) {
throw new UnknownRuleException(s);
}
}
toJson() {
const toPrint = {};
if (printKeys) {
toPrint.keys = this.keys.map(k => printers.snapshotKey(k));
}
if (printSynonyms) {
toPrint.mentionRoutes = this.synonyms;
}
if (PRINT_PRE_POST) {
toPrint.preList = this.preList;
toPrint.postList = this.postList;
}
if (PRINT_INITIAL_FINAL) {
toPrint.initial = this.initialStr;
toPrint.final = this.finalStr;
toPrint.quitList = this.quitList.join(' ');
}
return toPrint;
}
processHyperInput(s) {
let matchedParts = estring.match(s, '*.*');
while (matchedParts) {
const reply = this.sentence(matchedParts[0]);
if (reply) {
return reply;
}
s = estring.trim(matchedParts[1]);
matchedParts = estring.match(s, '*.*');
}
if (s.length > 0) {
const reply = this.sentence(s);
if (reply) {
return reply;
}
}
const m = this.mem.shift();
if (m) {
return { assembled: { reassembled: m } };
}
const key = this.keys.find(k => k.getKey() === 'xnone');
if (key) {
const context = this.fullyDecompose(key, s);
if (context && context.assembled) {
return context;
}
}
return null;
}
processInput(s) {
if (typeof s !== 'string') {
throw new InvalidStringException(s);
}
s = estring.replaceAll(s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
s = estring.replaceAll(s, '@#$%^&*()_-+=~`{[}]|:;<>\\"', ' ');
s = estring.replaceAll(s, ',?!', '...');
s = estring.compress(s);
return this.processHyperInput(s);
}
readScript(script$) {
return concat(script$, of('\n')).pipe(scan(({ buffer }, chunk) => {
const split = (buffer + chunk).split('\n');
const rest = split.pop();
return { buffer: rest || '', lines: split };
}, { buffer: '', lines: [''] }), concatMap(pack => pack.lines), filter(line => line.trim().length > 0 && !line.startsWith('%')), tap(line => this.collect(line)));
}
sentence(s) {
s = PrePostUtil.translate(this.preList, s).trim();
s = estring.pad(s);
if (this.quitList.indexOf(s) >= 0) {
this.finished = true;
return { assembled: { reassembled: this.finalStr } };
}
if (this.tokenizer) {
let result = null;
return buildKeyStack(this.keys.filter(k => this.keyFilter(k.getKey())), this.tokenizer(s))
.find(key => {
const ctx = this.fullyDecompose(key, s);
if (ctx) {
result = ctx;
return true;
}
return false;
}) ? result : null;
}
const sortedReplyContexts = this.keys.filter(k => this.keyFilter(k.getKey()))
.map(key => this.fullyDecompose(key, s))
.filter(notEmpty).sort((ctxA, ctxB) => (ctxA.matches ||
{ slottedTokens: [{ token: s, scopes: {} }] })
.slottedTokens.filter(t => Object.keys(t.scopes).length < 1)
.map(t => t.token).join('').length
- (ctxB.matches ||
{ slottedTokens: [{ token: s, scopes: {} }] })
.slottedTokens.filter(t => Object.keys(t.scopes).length < 1)
.map(t => t.token).join('').length);
return sortedReplyContexts[0] || null;
}
decompose(key, s) {
const ASSEMBLE_FUNC = this.assemble;
return (key.getDecomp() || []).map(decomposition => {
const matches = mention.matchDecomposition(this.synonyms, s, decomposition.getPattern());
if (!matches) {
return null;
}
return {
decomposition, matches,
assembled: null,
};
}).filter(notEmpty).sort((ctxA, ctxB) => (ctxA.matches ||
{ slottedTokens: [{ token: s, scopes: {} }] })
.slottedTokens.filter(t => Object.keys(t.scopes).length < 1)
.map(t => t.token).join('').length
- (ctxB.matches ||
{ slottedTokens: [{ token: s, scopes: {} }] })
.slottedTokens.filter(t => Object.keys(t.scopes).length < 1)
.map(t => t.token).join('').length)
.find(ctx => {
ctx.assembled = this.assemble(ctx.decomposition, ctx.matches.slottedTokens);
if (!ctx.assembled) {
return false;
}
if (ctx.assembled instanceof GotoKey) {
if (ctx.assembled.getKey()) {
return true;
}
}
else {
return true;
}
return false;
}) || null;
}
fullyDecompose(key, s) {
let decomposeCtx = this.decompose(key, s);
while (decomposeCtx) {
if (decomposeCtx.assembled instanceof GotoKey) {
const gotoKey = decomposeCtx.assembled;
decomposeCtx = this.decompose(gotoKey, s);
continue;
}
if (!decomposeCtx.assembled) {
return null;
}
const assembledResult = decomposeCtx.assembled;
assembledResult.reassembled = PrePostUtil
.translate(this.tweakList, assembledResult.reassembled);
Object.keys(assembledResult.annotations).forEach(k => {
assembledResult.annotations[k] = PrePostUtil
.translate(this.tweakList, assembledResult.annotations[k]);
});
return {
decomposition: decomposeCtx.decomposition,
matches: decomposeCtx.matches,
assembled: assembledResult,
};
}
return null;
}
assemble(d, decomposedSlots) {
const decomposedTokens = decomposedSlots.map(s => s.token);
const rule = d.nextRule();
const assembledResult = rule.assemble(decomposedTokens.map(token => PrePostUtil.translate(this.postList, token).trim()));
if (assembledResult.gotoKey && assembledResult.gotoKey.length > 0) {
const gotoKey = this.keys.find(k => k.getKey() === assembledResult.gotoKey);
if (gotoKey && gotoKey.getKey()) {
return new GotoKey(gotoKey);
}
throw new GotoLostException(rule.getTemplate());
}
if (!assembledResult.result) {
return null;
}
if (d.isMemoryKey()) {
this.mem.push(assembledResult.result);
return null;
}
return {
reassembled: assembledResult.result,
annotations: d.getAnnotates()
.map(annotate => annotate.assemble(decomposedTokens))
.reduce((reduced, current) => {
if (current.annotation) {
reduced[current.annotation] = current.result + '';
}
return reduced;
}, {}),
};
}
}