@@ -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 ( / \{ \{ S E T : ( [ ^ : { } ] + ) : ( [ ^ { } ] * ) \} \} / 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 ( / \{ \{ G E T : ( [ ^ { } ] + ) \} \} / g, ( match , varName ) => {
121+ return this . context [ varName ] || "" ;
122+ } ) ;
123+
124+ // Process BOT properties
125+ result = result . replace ( / \{ \{ B O T : ( [ ^ { } ] + ) \} \} / g, ( match , property ) => {
126+ return this . context [ property ] || this . context . botName || "" ;
127+ } ) ;
128+
129+ // Process STAR (wildcard captures)
130+ result = result . replace ( / \{ \{ S T A R : ( \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 ( / \{ T H A T \} / g, this . context . that || "" ) ;
137+
138+ // 2. Process transformation tags
139+
140+ // Process PERSON (pronoun transformation)
141+ result = result . replace ( / \{ \{ P E R S O N : ( [ ^ { } ] + ) \} \} / 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 ( / \{ \{ F O R M A L : ( [ ^ { } ] + ) \} \} / g, ( match , text ) => {
150+ return text . charAt ( 0 ) . toUpperCase ( ) + text . slice ( 1 ) . toLowerCase ( ) ;
151+ } ) ;
152+
153+ // Process UPPERCASE
154+ result = result . replace ( / \{ \{ U P P E R C A S E : ( [ ^ { } ] + ) \} \} / g, ( match , text ) => {
155+ return text . toUpperCase ( ) ;
156+ } ) ;
157+
158+ // Process LOWERCASE
159+ result = result . replace ( / \{ \{ L O W E R C A S E : ( [ ^ { } ] + ) \} \} / 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 ( / \{ \{ S R A I : ( [ ^ } ] + ) \} \} / g, ( match , srai ) => {
86- return this . srai ( srai ) ;
87- } ) ;
88-
89- // Process RANDOM
90- result = result . replace ( / \{ \{ R A N D O M : ( \[ .* ?\] ) \} \} / 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 ( / \{ \{ T H I N K : ( [ ^ { } ] * ) \} \} / 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 ( / \{ \{ R A N D O M : ( \[ [ ^ \] ] * \] ) \} \} / 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 ( / \{ \{ S R A I : ( [ ^ { } ] + ) \} \} / 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 ( / \{ \{ B O T : ( [ ^ } ] + ) \} \} / g, ( match , property ) => {
101- return this . context [ property ] || this . context . botName || "" ;
102- } ) ;
103-
104- // Process GET context variables
105- result = result . replace ( / \{ \{ G E T : ( [ ^ } ] + ) \} \} / g, ( match , varName ) => {
106- return this . context [ varName ] || "" ;
107- } ) ;
108-
109- // Process SET context variables
110- result = result . replace ( / \{ \{ S E T : ( [ ^ : ] + ) : ( [ ^ } ] + ) \} \} / g, ( match , varName , value ) => {
111- this . context [ varName ] = value ;
112- return value ;
113- } ) ;
114-
115- // Process PERSON (pronoun transformation)
116- result = result . replace ( / \{ \{ P E R S O N : ( [ ^ } ] + ) \} \} / 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 ( / \{ \{ T H I N K : ( [ ^ } ] + ) \} \} / g, ( match , content ) => {
125- this . processTemplate ( content , wildcards ) ;
126- return "" ;
127- } ) ;
128-
129- // Process STAR (wildcard captures)
130- result = result . replace ( / \{ \{ S T A R : ( \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 ( / \{ T H A T \} / g, this . context . that || "" ) ;
137-
138- // Process FORMAL (capitalize first letter)
139- result = result . replace ( / \{ \{ F O R M A L : ( [ ^ } ] + ) \} \} / g, ( match , text ) => {
140- return text . charAt ( 0 ) . toUpperCase ( ) + text . slice ( 1 ) . toLowerCase ( ) ;
141- } ) ;
142-
143- // Process UPPERCASE
144- result = result . replace ( / \{ \{ U P P E R C A S E : ( [ ^ } ] + ) \} \} / g, ( match , text ) => {
145- return text . toUpperCase ( ) ;
146- } ) ;
147-
148- // Process LOWERCASE
149- result = result . replace ( / \{ \{ L O W E R C A S E : ( [ ^ } ] + ) \} \} / 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