Skip to content

Commit d7efeac

Browse files
Merge pull request #21 from ContextLab/fix/alice-aiml-parsing
Fix ALICE AIML template processing
2 parents 89ad8a1 + b41d31c commit d7efeac

File tree

2 files changed

+163
-272
lines changed

2 files changed

+163
-272
lines changed

demos/15-chatbot-evolution/js/alice-full.js

Lines changed: 154 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,35 @@ export class AliceFull {
2828
// Bot properties from AIML
2929
name: "ALICE",
3030
species: "robot",
31+
kingdom: "robot", // Used in "I am a {{BOT:kingdom}}"
3132
location: "California",
33+
city: "San Francisco",
34+
state: "California",
35+
country: "United States",
3236
vocabulary: "120000",
3337
size: "95026",
38+
ndevelopers: "100",
3439
developers: "100",
3540
birthday: "November 23, 1995",
41+
birthdate: "November 23, 1995",
3642
birthplace: "San Francisco, California",
3743
botmaster: "Dr. Richard Wallace",
44+
master: "Dr. Richard Wallace",
3845
gender: "female",
39-
age: new Date().getFullYear() - 1995,
40-
version: "1.0 Full"
46+
age: String(new Date().getFullYear() - 1995),
47+
version: "1.0 Full",
48+
// Additional common properties
49+
language: "English",
50+
os: "Cross-platform",
51+
website: "alicebot.org",
52+
53+
religion: "Pantheist",
54+
job: "chat robot",
55+
favoritesubject: "artificial intelligence",
56+
favoritecolor: "green",
57+
favoritefood: "electricity",
58+
favoritemovie: "Blade Runner",
59+
favoritebook: "ALICE In Wonderland"
4160
};
4261

4362
this.patterns = [];
@@ -75,83 +94,111 @@ export class AliceFull {
7594

7695
/**
7796
* Process template with AIML tags
97+
* Uses iterative inside-out processing to handle nested tags like {{THINK:{{SET:...}}}}
7898
*/
7999
processTemplate(template, wildcards = []) {
80100
if (!template) return "";
81101

82102
let result = template;
103+
let iterations = 0;
104+
const maxIterations = 20; // Prevent infinite loops
105+
106+
// Process iteratively until no more tags remain or we hit max iterations
107+
while (result.includes('{{') && iterations < maxIterations) {
108+
const before = result;
109+
110+
// 1. Process innermost tags first (no nested content)
111+
112+
// Process SET context variables (innermost - no nested allowed)
113+
// Allow empty values with ([^{}]*)
114+
result = result.replace(/\{\{SET:([^:{}]+):([^{}]*)\}\}/g, (match, varName, value) => {
115+
this.context[varName] = value;
116+
return ""; // SET should not output the value
117+
});
118+
119+
// Process GET context variables
120+
result = result.replace(/\{\{GET:([^{}]+)\}\}/g, (match, varName) => {
121+
return this.context[varName] || "";
122+
});
123+
124+
// Process BOT properties
125+
result = result.replace(/\{\{BOT:([^{}]+)\}\}/g, (match, property) => {
126+
return this.context[property] || this.context.botName || "";
127+
});
128+
129+
// Process STAR (wildcard captures)
130+
result = result.replace(/\{\{STAR:(\d+)\}\}/g, (match, index) => {
131+
const idx = parseInt(index) - 1;
132+
return wildcards[idx] || "";
133+
});
134+
135+
// Process THAT (previous bot response)
136+
result = result.replace(/\{THAT\}/g, this.context.that || "");
137+
138+
// 2. Process transformation tags
139+
140+
// Process PERSON (pronoun transformation)
141+
result = result.replace(/\{\{PERSON:([^{}]+)\}\}/g, (match, text) => {
142+
if (text === 'WILDCARD' && wildcards.length > 0) {
143+
return this.transformPerson(wildcards[0]);
144+
}
145+
return this.transformPerson(text);
146+
});
147+
148+
// Process FORMAL (capitalize first letter)
149+
result = result.replace(/\{\{FORMAL:([^{}]+)\}\}/g, (match, text) => {
150+
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
151+
});
152+
153+
// Process UPPERCASE
154+
result = result.replace(/\{\{UPPERCASE:([^{}]+)\}\}/g, (match, text) => {
155+
return text.toUpperCase();
156+
});
157+
158+
// Process LOWERCASE
159+
result = result.replace(/\{\{LOWERCASE:([^{}]+)\}\}/g, (match, text) => {
160+
return text.toLowerCase();
161+
});
162+
163+
// 3. Process container tags (may have nested content that's now resolved)
83164

84-
// Process SRAI (recursive pattern matching)
85-
result = result.replace(/\{\{SRAI:([^}]+)\}\}/g, (match, srai) => {
86-
return this.srai(srai);
87-
});
88-
89-
// Process RANDOM
90-
result = result.replace(/\{\{RANDOM:(\[.*?\])\}\}/g, (match, options) => {
91-
try {
92-
const optionList = JSON.parse(options);
93-
return optionList[Math.floor(Math.random() * optionList.length)];
94-
} catch (e) {
165+
// Process THINK (execute but don't output) - now safe since inner tags are processed
166+
result = result.replace(/\{\{THINK:([^{}]*)\}\}/g, (match, content) => {
167+
// Content is already processed, just return empty
95168
return "";
169+
});
170+
171+
// Process RANDOM - must parse JSON array
172+
result = result.replace(/\{\{RANDOM:(\[[^\]]*\])\}\}/g, (match, options) => {
173+
try {
174+
const optionList = JSON.parse(options);
175+
return optionList[Math.floor(Math.random() * optionList.length)];
176+
} catch (e) {
177+
return "";
178+
}
179+
});
180+
181+
// Process SRAI (recursive pattern matching) - do last as it may produce new tags
182+
result = result.replace(/\{\{SRAI:([^{}]+)\}\}/g, (match, srai) => {
183+
return this.srai(srai);
184+
});
185+
186+
// Check if we made any progress
187+
if (result === before) {
188+
// No changes made, break to avoid infinite loop
189+
break;
96190
}
97-
});
98-
99-
// Process BOT properties
100-
result = result.replace(/\{\{BOT:([^}]+)\}\}/g, (match, property) => {
101-
return this.context[property] || this.context.botName || "";
102-
});
103-
104-
// Process GET context variables
105-
result = result.replace(/\{\{GET:([^}]+)\}\}/g, (match, varName) => {
106-
return this.context[varName] || "";
107-
});
108-
109-
// Process SET context variables
110-
result = result.replace(/\{\{SET:([^:]+):([^}]+)\}\}/g, (match, varName, value) => {
111-
this.context[varName] = value;
112-
return value;
113-
});
114-
115-
// Process PERSON (pronoun transformation)
116-
result = result.replace(/\{\{PERSON:([^}]+)\}\}/g, (match, text) => {
117-
if (text === 'WILDCARD' && wildcards.length > 0) {
118-
return this.transformPerson(wildcards[0]);
119-
}
120-
return this.transformPerson(text);
121-
});
122-
123-
// Process THINK (execute but don't output)
124-
result = result.replace(/\{\{THINK:([^}]+)\}\}/g, (match, content) => {
125-
this.processTemplate(content, wildcards);
126-
return "";
127-
});
128-
129-
// Process STAR (wildcard captures)
130-
result = result.replace(/\{\{STAR:(\d+)\}\}/g, (match, index) => {
131-
const idx = parseInt(index) - 1;
132-
return wildcards[idx] || "";
133-
});
134-
135-
// Process THAT (previous bot response)
136-
result = result.replace(/\{THAT\}/g, this.context.that || "");
137-
138-
// Process FORMAL (capitalize first letter)
139-
result = result.replace(/\{\{FORMAL:([^}]+)\}\}/g, (match, text) => {
140-
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
141-
});
142-
143-
// Process UPPERCASE
144-
result = result.replace(/\{\{UPPERCASE:([^}]+)\}\}/g, (match, text) => {
145-
return text.toUpperCase();
146-
});
147-
148-
// Process LOWERCASE
149-
result = result.replace(/\{\{LOWERCASE:([^}]+)\}\}/g, (match, text) => {
150-
return text.toLowerCase();
151-
});
152-
153-
// Clean up any remaining template markers
154-
result = result.replace(/\{\{[^}]*\}\}/g, '');
191+
192+
iterations++;
193+
}
194+
195+
// Clean up any remaining malformed template markers
196+
result = result.replace(/\{\{[^{}]*\}\}/g, '');
197+
198+
// Clean up stray closing braces that might have been left behind
199+
result = result.replace(/^\s*\}\}+\s*/g, ''); // Leading }}
200+
result = result.replace(/\s*\}\}+\s*$/g, ''); // Trailing }}
201+
result = result.replace(/\}\}+\s+/g, ' '); // }} in middle of text
155202

156203
return result.trim();
157204
}
@@ -215,10 +262,21 @@ export class AliceFull {
215262

216263
/**
217264
* Match input against pattern database
265+
*
266+
* Pattern matching priority (AIML convention):
267+
* 1. Exact matches (no wildcards)
268+
* 2. Patterns with specific text + wildcards
269+
* 3. Pure wildcard patterns (_, *)
218270
*/
219271
matchPattern(input, isSrai = false) {
220272
const normalizedInput = this.normalize(input);
221273

274+
// Separate patterns into specific and wildcard-only
275+
let bestMatch = null;
276+
let bestWildcards = [];
277+
let wildcardMatch = null;
278+
let wildcardMatchWildcards = [];
279+
222280
// Patterns are already sorted by priority
223281
for (const pattern of this.patterns) {
224282
// Check topic constraint
@@ -239,16 +297,34 @@ export class AliceFull {
239297
// Extract wildcards (everything except full match)
240298
const wildcards = match.slice(1);
241299

242-
// Process template
243-
const response = this.processTemplate(pattern.template, wildcards);
244-
245-
// Update context (but not for SRAI calls)
246-
if (!isSrai) {
247-
this.context.that = response;
300+
// Check if this is a pure wildcard pattern (just _ or *)
301+
const isPureWildcard = pattern.pattern === '_' || pattern.pattern === '*';
302+
303+
if (isPureWildcard) {
304+
// Save as fallback if we don't find a specific match
305+
if (!wildcardMatch) {
306+
wildcardMatch = pattern;
307+
wildcardMatchWildcards = wildcards;
308+
}
309+
} else {
310+
// Found a specific match - use it
311+
bestMatch = pattern;
312+
bestWildcards = wildcards;
313+
break; // Use first specific match
248314
}
315+
}
316+
}
317+
318+
// Use specific match if found, otherwise fallback to wildcard match
319+
const matchedPattern = bestMatch || wildcardMatch;
320+
const matchedWildcards = bestMatch ? bestWildcards : wildcardMatchWildcards;
249321

250-
return response;
322+
if (matchedPattern) {
323+
const response = this.processTemplate(matchedPattern.template, matchedWildcards);
324+
if (!isSrai) {
325+
this.context.that = response;
251326
}
327+
return response;
252328
}
253329

254330
// No match found

0 commit comments

Comments
 (0)