Skip to content

Commit 40da018

Browse files
jeremymanningclaude
andcommitted
fix(eliza): Complete rules from official instructions.txt source
- Add missing 'xnone' keyword (critical fallback rule) - Generate correct rules from official Weizenbaum instructions.txt - All 36 keywords now present, matching Python eliza_solution.ipynb - Fix array substitution handling (i'm → i am, you're → you are) - Add punctuation handling methods matching Python implementation - Use xnone keyword for fallback instead of random fallback array - Add scripts to compare and generate rules from official source Fixes #26, #28 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 97bc41a commit 40da018

File tree

5 files changed

+480
-26
lines changed

5 files changed

+480
-26
lines changed

demos/01-eliza/data/eliza-rules.json

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@
1313
"machine": "computer",
1414
"computers": "computer",
1515
"were": "was",
16-
"you're": "you are",
17-
"i'm": "i am",
16+
"you're": [
17+
"you",
18+
"are"
19+
],
20+
"i'm": [
21+
"i",
22+
"am"
23+
],
1824
"same": "alike"
1925
},
2026
"postSubstitutions": {
@@ -26,7 +32,10 @@
2632
"i": "you",
2733
"you": "me",
2834
"my": "your",
29-
"i'm": "you are"
35+
"i'm": [
36+
"you",
37+
"are"
38+
]
3039
},
3140
"synonyms": {
3241
"belief": [
@@ -89,6 +98,20 @@
8998
"That will be $200."
9099
],
91100
"rules": [
101+
{
102+
"keyword": "xnone",
103+
"patterns": [
104+
{
105+
"pattern": "*",
106+
"responses": [
107+
"I'm not sure I understand you fully.",
108+
"Please go on.",
109+
"What does that suggest to you ?",
110+
"Do you feel strongly about discussing such things ?"
111+
]
112+
}
113+
]
114+
},
92115
{
93116
"keyword": "sorry",
94117
"patterns": [
@@ -586,13 +609,14 @@
586609
]
587610
},
588611
{
589-
"pattern": "$ * my *",
612+
"pattern": "* my *",
590613
"responses": [
591614
"Your (2) .",
592615
"Let's discuss your (2) further .",
593616
"What does your (2) mean to you ?",
594617
"Tell me more about your (2) ."
595-
]
618+
],
619+
"save": true
596620
},
597621
{
598622
"pattern": "* my *",
@@ -761,8 +785,7 @@
761785
"Really, always ?"
762786
]
763787
}
764-
],
765-
"rank": 1
788+
]
766789
},
767790
{
768791
"keyword": "alike",

demos/01-eliza/js/eliza-engine.js

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export class ElizaEngine {
185185
} else {
186186
// No match found
187187

188-
// Try to use memory if available
188+
// Step 7: Try to use memory if available (matching Python solution)
189189
if (this.memoryStack.length > 0) {
190190
const memory = this.memoryStack.pop();
191191
// Recursively call getResponse with the memory, forcing saveOverride=true
@@ -199,17 +199,42 @@ export class ElizaEngine {
199199
return memoryResponse;
200200
}
201201

202-
// Use fallback if no memory available
203-
response = this.fallbacks[Math.floor(Math.random() * this.fallbacks.length)];
204-
// Convert to uppercase to match original ELIZA behavior
205-
response = response.toUpperCase();
206-
matchInfo = {
207-
keyword: 'fallback',
208-
pattern: 'none',
209-
rank: 0,
210-
template: response,
211-
captures: []
212-
};
202+
// Step 8: Use xnone keyword as fallback (matching Python solution exactly)
203+
// The Python solution does: words = self.respond('xnone')
204+
const xnoneRule = this.rules.find(r => r.keyword === 'xnone');
205+
if (xnoneRule && xnoneRule.patterns && xnoneRule.patterns.length > 0) {
206+
const xnonePattern = xnoneRule.patterns[0];
207+
const patternKey = 'xnone:*';
208+
209+
if (!this.responseIndices[patternKey]) {
210+
this.responseIndices[patternKey] = 0;
211+
}
212+
213+
const responseIndex = this.responseIndices[patternKey];
214+
response = xnonePattern.responses[responseIndex];
215+
216+
// Cycle to next response
217+
this.responseIndices[patternKey] =
218+
(responseIndex + 1) % xnonePattern.responses.length;
219+
220+
matchInfo = {
221+
keyword: 'xnone',
222+
pattern: '*',
223+
rank: 0,
224+
template: response,
225+
captures: []
226+
};
227+
} else {
228+
// Ultimate fallback if xnone rule is missing
229+
response = this.fallbacks[Math.floor(Math.random() * this.fallbacks.length)];
230+
matchInfo = {
231+
keyword: 'fallback',
232+
pattern: 'none',
233+
rank: 0,
234+
template: response,
235+
captures: []
236+
};
237+
}
213238
}
214239

215240
// Add to conversation history

demos/01-eliza/js/pattern-matcher.js

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,40 @@
55
export class PatternMatcher {
66
constructor() {
77
this.debugMode = false;
8+
// Punctuation that splits input - matches Python solution exactly
9+
// 'but' is treated as a clause separator (like the Python solution)
10+
this.punctuation = [',', ':', ';', '!', '.', '?', 'but'];
11+
}
12+
13+
/**
14+
* Strip punctuation characters from a word
15+
*/
16+
stripPunctuation(word) {
17+
return word.replace(/[,:;!.?]/g, '');
18+
}
19+
20+
/**
21+
* Parse punctuation: truncate at first punctuation occurrence
22+
* Following the Python solution's parse_punctuation method
23+
*/
24+
parsePunctuation(words) {
25+
const result = [];
26+
for (const word of words) {
27+
// Check if word is a punctuation separator (like 'but')
28+
if (this.punctuation.includes(word.toLowerCase())) {
29+
break;
30+
}
31+
// Check if word contains punctuation - strip it and potentially stop
32+
const stripped = this.stripPunctuation(word);
33+
if (stripped) {
34+
result.push(stripped);
35+
}
36+
// If word ended with punctuation, stop processing
37+
if (word !== stripped && /[,:;!.?]$/.test(word)) {
38+
break;
39+
}
40+
}
41+
return result;
842
}
943

1044
/**
@@ -18,8 +52,10 @@ export class PatternMatcher {
1852
const regex = new RegExp('\\b' + from + '\\b', 'gi');
1953
if (regex.test(result)) {
2054
const oldResult = result;
21-
result = result.replace(regex, to);
22-
steps.push({ from, to, before: oldResult, after: result });
55+
// Handle array replacements (e.g., "i'm" → ["i", "am"])
56+
const replacement = Array.isArray(to) ? to.join(' ') : to;
57+
result = result.replace(regex, replacement);
58+
steps.push({ from, to: replacement, before: oldResult, after: result });
2359
}
2460
}
2561

@@ -47,9 +83,11 @@ export class PatternMatcher {
4783
const oldResult = result;
4884
// Use a unique placeholder that won't match any substitution pattern
4985
const placeholder = `__POSTSUB_${placeholderIndex++}__`;
50-
placeholders.set(placeholder, to);
86+
// Handle array replacements (e.g., "i'm" → ["you", "are"])
87+
const replacement = Array.isArray(to) ? to.join(' ') : to;
88+
placeholders.set(placeholder, replacement);
5189
result = result.replace(regex, placeholder);
52-
steps.push({ from, to, before: oldResult.trim(), after: result.trim() });
90+
steps.push({ from, to: replacement, before: oldResult.trim(), after: result.trim() });
5391
}
5492
}
5593

@@ -98,12 +136,15 @@ export class PatternMatcher {
98136
* Returns: { matched: true, captures: [[], [], ['unhappy'], []] }
99137
*/
100138
matchPattern(input, pattern, synonyms) {
101-
// Split input into words, stripping punctuation
102-
const inputWords = input.toLowerCase()
103-
.replace(/[.,!?;:]/g, ' ')
139+
// Split input into words, preserving punctuation for now
140+
const rawWords = input.toLowerCase()
104141
.split(/\s+/)
105142
.filter(w => w.length > 0);
106143

144+
// Strip punctuation from each word for matching purposes
145+
const inputWords = rawWords.map(w => this.stripPunctuation(w))
146+
.filter(w => w.length > 0);
147+
107148
// Split pattern into parts
108149
const patternParts = pattern.toLowerCase()
109150
.split(/\s+/)

0 commit comments

Comments
 (0)