* 2. Include this script: * 3. Create charts with minimal configuration - colors are auto-applied! */ (function() { 'use strict'; // ========================================================================== // READ COLORS FROM CSS CUSTOM PROPERTIES // This ensures chart colors stay in sync with the theme // ========================================================================== /** * Get a CSS custom property value from :root */ function getCSSVar(name, fallback = '') { if (typeof getComputedStyle === 'undefined') return fallback; const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return value || fallback; } /** * Build palette from CSS custom properties (with fallbacks) */ function buildPaletteFromCSS() { return { // Primary brand colors dartmouthGreen: getCSSVar('--dartmouth-green', '#00693e'), textPrimary: getCSSVar('--text-primary', '#0a2518'), textSecondary: getCSSVar('--text-secondary', '#0a3d23'), // Chart colors (from CSS --chart-color-N variables) chartColors: [ getCSSVar('--chart-color-1', '#00693e'), getCSSVar('--chart-color-2', '#267aba'), getCSSVar('--chart-color-3', '#ffa00f'), getCSSVar('--chart-color-4', '#9d162e'), getCSSVar('--chart-color-5', '#8a6996'), getCSSVar('--chart-color-6', '#a5d75f'), getCSSVar('--chart-color-7', '#003c73'), getCSSVar('--chart-color-8', '#d94415'), getCSSVar('--chart-color-9', '#643c20'), getCSSVar('--chart-color-10', '#c4dd88'), getCSSVar('--chart-color-11', '#f5dc69'), getCSSVar('--chart-color-12', '#424141'), ], // Background colors (semi-transparent versions) chartBgColors: [ getCSSVar('--chart-bg-1', 'rgba(0, 105, 62, 0.5)'), getCSSVar('--chart-bg-2', 'rgba(38, 122, 186, 0.5)'), getCSSVar('--chart-bg-3', 'rgba(255, 160, 15, 0.5)'), getCSSVar('--chart-bg-4', 'rgba(157, 22, 46, 0.5)'), getCSSVar('--chart-bg-5', 'rgba(138, 105, 150, 0.5)'), getCSSVar('--chart-bg-6', 'rgba(165, 215, 95, 0.5)'), ], // Semantic colors positive: getCSSVar('--chart-positive', '#00693e'), negative: getCSSVar('--chart-negative', '#9d162e'), neutral: getCSSVar('--chart-neutral', '#424141'), highlight: getCSSVar('--chart-highlight', '#ffa00f'), // Grid and axis colors gridLight: getCSSVar('--chart-grid-light', 'rgba(0, 105, 62, 0.1)'), gridMedium: getCSSVar('--chart-grid-medium', 'rgba(0, 105, 62, 0.15)'), gridDark: getCSSVar('--chart-grid-dark', 'rgba(0, 105, 62, 0.2)'), axisColor: getCSSVar('--chart-axis-color', '#0a2518'), // Font fontFamily: getCSSVar('--chart-font-family', "'Avenir LT Std', 'Avenir', 'Avenir Next', -apple-system, BlinkMacSystemFont, sans-serif"), }; } // Initialize palette (will be populated when DOM is ready) let CDL_PALETTE = null; // For convenience, expose primary chart colors array let CHART_COLORS = null; // ========================================================================== // FONT CONFIGURATION // Responsive font sizes based on typical Marp slide dimensions (1280x720) // ========================================================================== const FONT_CONFIG = { sizes: { title: 22, // Chart title subtitle: 18, // Subtitle legend: 16, // Legend labels axisTitle: 18, // Axis titles axisTicks: 16, // Axis tick labels tooltip: 14, // Tooltip text dataLabels: 14, // Data labels on charts }, weight: { normal: 400, medium: 500, bold: 600, }, }; // ========================================================================== // HELPER FUNCTIONS // ========================================================================== /** * Ensure palette is initialized */ function ensurePalette() { if (!CDL_PALETTE) { CDL_PALETTE = buildPaletteFromCSS(); CHART_COLORS = CDL_PALETTE.chartColors; } return CDL_PALETTE; } /** * Get color for a dataset at given index * Cycles through palette if more datasets than colors */ function getColor(index) { ensurePalette(); return CHART_COLORS[index % CHART_COLORS.length]; } /** * Get color with alpha transparency */ function getColorWithAlpha(color, alpha) { // Handle hex colors if (color.startsWith('#')) { const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } // Handle rgba colors if (color.startsWith('rgba')) { return color.replace(/[\d.]+\)$/, `${alpha})`); } return color; } /** * Generate colors for all datasets in chart data * Automatically assigns colors if not specified */ function autoAssignColors(data, chartType) { if (!data || !data.datasets) return data; data.datasets.forEach((dataset, index) => { const baseColor = getColor(index); // Only assign colors if not already specified switch (chartType) { case 'bar': case 'horizontalBar': if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } break; case 'line': if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (!dataset.backgroundColor) { dataset.backgroundColor = getColorWithAlpha(baseColor, 0.1); } if (dataset.borderWidth === undefined) { dataset.borderWidth = 3; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 6; } if (!dataset.pointBackgroundColor) { dataset.pointBackgroundColor = baseColor; } if (dataset.tension === undefined) { dataset.tension = 0.3; } break; case 'scatter': case 'bubble': if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 15; } if (dataset.pointHoverRadius === undefined) { dataset.pointHoverRadius = 18; } break; case 'pie': case 'doughnut': case 'polarArea': // For pie charts, we need multiple colors for one dataset if (!dataset.backgroundColor) { const numItems = dataset.data ? dataset.data.length : 6; dataset.backgroundColor = []; for (let i = 0; i < numItems; i++) { dataset.backgroundColor.push(getColor(i)); } } if (!dataset.borderColor) { dataset.borderColor = '#d8d8d8'; // Slide background } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } break; case 'radar': if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (!dataset.backgroundColor) { dataset.backgroundColor = getColorWithAlpha(baseColor, 0.2); } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 4; } if (!dataset.pointBackgroundColor) { dataset.pointBackgroundColor = baseColor; } break; default: // Generic color assignment if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } } }); return data; } // ========================================================================== // CHART.JS GLOBAL DEFAULTS // ========================================================================== function applyGlobalDefaults() { if (typeof Chart === 'undefined') { console.warn('Chart.js not loaded. chart-defaults.js requires Chart.js to be loaded first.'); return false; } // Ensure palette is loaded from CSS const palette = ensurePalette(); // Font defaults Chart.defaults.font.family = palette.fontFamily; Chart.defaults.font.size = FONT_CONFIG.sizes.axisTicks; Chart.defaults.color = palette.textPrimary; // Responsive defaults Chart.defaults.responsive = true; Chart.defaults.maintainAspectRatio = false; // Animation (subtle) Chart.defaults.animation.duration = 400; // Plugin defaults // Legend Chart.defaults.plugins.legend.labels.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.legend, weight: FONT_CONFIG.weight.normal, }; Chart.defaults.plugins.legend.labels.color = palette.textPrimary; Chart.defaults.plugins.legend.labels.usePointStyle = true; Chart.defaults.plugins.legend.labels.padding = 20; // Title Chart.defaults.plugins.title.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.title, weight: FONT_CONFIG.weight.medium, }; Chart.defaults.plugins.title.color = palette.textPrimary; // Tooltip Chart.defaults.plugins.tooltip.backgroundColor = palette.textPrimary; Chart.defaults.plugins.tooltip.titleFont = { family: palette.fontFamily, size: FONT_CONFIG.sizes.tooltip, weight: FONT_CONFIG.weight.medium, }; Chart.defaults.plugins.tooltip.bodyFont = { family: palette.fontFamily, size: FONT_CONFIG.sizes.tooltip, }; Chart.defaults.plugins.tooltip.cornerRadius = 4; Chart.defaults.plugins.tooltip.padding = 10; // Scale defaults (for cartesian charts) // These need to be applied per-scale type const scaleDefaults = { grid: { color: palette.gridLight, lineWidth: 1, }, border: { color: palette.gridDark, width: 1, }, ticks: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTicks, }, color: palette.textPrimary, }, title: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTitle, weight: FONT_CONFIG.weight.normal, }, color: palette.textPrimary, }, }; // Apply scale defaults to linear scale if (Chart.defaults.scales && Chart.defaults.scales.linear) { if (Chart.defaults.scales.linear.grid) Object.assign(Chart.defaults.scales.linear.grid, scaleDefaults.grid); if (Chart.defaults.scales.linear.border) Object.assign(Chart.defaults.scales.linear.border, scaleDefaults.border); if (Chart.defaults.scales.linear.ticks) Object.assign(Chart.defaults.scales.linear.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.linear.title) Object.assign(Chart.defaults.scales.linear.title, scaleDefaults.title); } // Apply scale defaults to category scale if (Chart.defaults.scales && Chart.defaults.scales.category) { if (Chart.defaults.scales.category.grid) Object.assign(Chart.defaults.scales.category.grid, scaleDefaults.grid); if (Chart.defaults.scales.category.border) Object.assign(Chart.defaults.scales.category.border, scaleDefaults.border); if (Chart.defaults.scales.category.ticks) Object.assign(Chart.defaults.scales.category.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.category.title) Object.assign(Chart.defaults.scales.category.title, scaleDefaults.title); } // Apply scale defaults to logarithmic scale if (Chart.defaults.scales && Chart.defaults.scales.logarithmic) { if (Chart.defaults.scales.logarithmic.grid) Object.assign(Chart.defaults.scales.logarithmic.grid, scaleDefaults.grid); if (Chart.defaults.scales.logarithmic.border) Object.assign(Chart.defaults.scales.logarithmic.border, scaleDefaults.border); if (Chart.defaults.scales.logarithmic.ticks) Object.assign(Chart.defaults.scales.logarithmic.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.logarithmic.title) Object.assign(Chart.defaults.scales.logarithmic.title, scaleDefaults.title); } // Apply scale defaults to radial scale (for radar charts) if (Chart.defaults.scales && Chart.defaults.scales.radialLinear) { if (Chart.defaults.scales.radialLinear.grid) Chart.defaults.scales.radialLinear.grid.color = palette.gridLight; if (Chart.defaults.scales.radialLinear.angleLines) Chart.defaults.scales.radialLinear.angleLines.color = palette.gridMedium; if (Chart.defaults.scales.radialLinear.pointLabels) { Chart.defaults.scales.radialLinear.pointLabels.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTicks, }; Chart.defaults.scales.radialLinear.pointLabels.color = palette.textPrimary; } } return true; } // ========================================================================== // CHART WRAPPER FOR AUTO-STYLING // ========================================================================== /** * Wrap the Chart constructor to automatically apply CDL styling */ function wrapChartConstructor() { if (typeof Chart === 'undefined') return; const OriginalChart = Chart; // Create a wrapper that auto-applies colors window.Chart = function(ctx, config) { // Auto-assign colors if not specified if (config && config.data) { config.data = autoAssignColors(config.data, config.type); } // Merge default options for specific chart types if (config && config.options) { config.options = applyChartTypeDefaults(config.type, config.options); } // Call original constructor return new OriginalChart(ctx, config); }; // Copy static properties and methods Object.setPrototypeOf(window.Chart, OriginalChart); Object.assign(window.Chart, OriginalChart); // Preserve the prototype chain window.Chart.prototype = OriginalChart.prototype; } /** * Apply chart-type specific defaults */ function applyChartTypeDefaults(chartType, userOptions) { const options = { ...userOptions }; switch (chartType) { case 'bar': case 'horizontalBar': // Bar chart defaults if (!options.scales) options.scales = {}; if (!options.scales.x) options.scales.x = {}; if (!options.scales.y) options.scales.y = {}; // Hide x-axis grid for cleaner look if (options.scales.x.grid === undefined) { options.scales.x.grid = { display: false }; } break; case 'line': // Line chart defaults if (!options.interaction) { options.interaction = { intersect: false, mode: 'index' }; } break; case 'pie': case 'doughnut': // Pie/doughnut defaults if (!options.plugins) options.plugins = {}; if (options.plugins.legend === undefined) { const palette = ensurePalette(); options.plugins.legend = { position: 'right', labels: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.legend, }, color: palette.textPrimary, padding: 15, }, }; } break; case 'radar': // Radar chart defaults - keep as-is, scale defaults applied globally break; case 'scatter': case 'bubble': // Scatter/bubble defaults if (!options.scales) options.scales = {}; if (!options.scales.x) options.scales.x = {}; if (!options.scales.y) options.scales.y = {}; break; } return options; } // ========================================================================== // CONVENIENCE FUNCTIONS FOR USERS // Exposed on window.CDLChart for easy access // ========================================================================== window.CDLChart = { // Color palette access (getters to ensure lazy initialization) get colors() { return ensurePalette().chartColors; }, get palette() { return ensurePalette(); }, // Get specific color by index getColor: getColor, // Get color with transparency getColorWithAlpha: getColorWithAlpha, // Get array of colors for a specific count getColors: function(count) { ensurePalette(); const result = []; for (let i = 0; i < count; i++) { result.push(getColor(i)); } return result; }, // Font configuration fonts: FONT_CONFIG, // Quick chart creation helpers // These create minimal config that auto-applies all styling /** * Create a simple bar chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - X-axis labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ bar: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'bar', data: { labels: labels, datasets: [{ data: data }], }, options: { plugins: { legend: { display: false } }, ...options, }, }); }, /** * Create a simple line chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - X-axis labels * @param {Array} datasets - Array of {label, data} objects * @param {object} options - Optional overrides */ line: function(canvasId, labels, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'line', data: { labels: labels, datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, fill: ds.fill !== undefined ? ds.fill : true, })), }, options: options, }); }, /** * Create a simple pie chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Slice labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ pie: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'pie', data: { labels: labels, datasets: [{ data: data }], }, options: options, }); }, /** * Create a simple scatter chart * @param {string} canvasId - Canvas element ID * @param {Array} datasets - Array of {label, data: [{x, y}]} objects * @param {object} options - Optional overrides */ scatter: function(canvasId, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'scatter', data: { datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, })), }, options: options, }); }, /** * Create a doughnut chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Slice labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ doughnut: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'doughnut', data: { labels: labels, datasets: [{ data: data }], }, options: options, }); }, /** * Create a radar chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Axis labels * @param {Array} datasets - Array of {label, data} objects * @param {object} options - Optional overrides */ radar: function(canvasId, labels, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'radar', data: { labels: labels, datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, })), }, options: options, }); }, }; // ========================================================================== // INITIALIZATION // ========================================================================== function initialize() { // Wait for Chart.js to be available if (typeof Chart !== 'undefined') { applyGlobalDefaults(); wrapChartConstructor(); console.log('CDL Chart defaults applied successfully.'); return true; } else { // Chart.js not yet loaded - wait and retry let retries = 0; const maxRetries = 50; // 5 seconds max wait const checkInterval = setInterval(function() { retries++; if (typeof Chart !== 'undefined') { clearInterval(checkInterval); applyGlobalDefaults(); wrapChartConstructor(); console.log('CDL Chart defaults applied successfully (after waiting for Chart.js).'); } else if (retries >= maxRetries) { clearInterval(checkInterval); console.warn('Chart.js not found after waiting. CDL Chart defaults not applied.'); } }, 100); return false; } } // Initialize IMMEDIATELY - this must run BEFORE any chart creation scripts // Chart.js CDN should be loaded before this script initialize(); })();

Lecture 4: Rules-based chatbots

PSYC 51.07: Models of language and communication

Jeremy R. Manning
Dartmouth College
Winter 2026

Assignment 1: Q & A

Let's take some time to address any questions about Assignment 1:

  • Implementation challenges
  • Pattern matching strategies
  • Configuration file format
  • Testing approaches

There are no bad questions. If you're confused about something, others probably are too!

Common issues and tips

  • Testing on only a few inputs and missing edge cases
  • Missing "special cases" like memory or goto statements
  • Handling whitespace and/or punctuation inconsistently
  • Matching keywords in the wrong order (instead of in descending order of rank)
  • Greedy vs. non-greedy pattern matching

Beyond ELIZA...

ELIZA (1966) was just the beginning! Weizenbaum's ideas inspired other researchers to test the limits of what rules-based systems could do.

  • PARRY (1972): a different kind of simulation
  • A.L.I.C.E. (1995): pattern matching at scale
  • Formal grammars: theoretical foundations of pattern matching
  • Fundamental limits of rules-based approaches

PARRY (1972)

  • Created by psychiatrist Kenneth Colby at Stanford
  • Simulated a patient with paranoid schizophrenia
  • Different goal than ELIZA: model a specific mental illness
  • Had internal state: beliefs, emotional level, goals

ELIZA reflects; PARRY models. ELIZA avoids commitment; PARRY has a coherent (if paranoid) worldview.

ELIZA versus PARRY

ELIZA (1966)

  • Non-directive therapy
  • Reflects user input back
  • No internal state or beliefs
  • Avoids making claims
  • Goal: keep user talking

PARRY (1972)

  • Intended to simulate "beliefs" about the world
  • Tracks emotional "state" across three dimensions: anger, fear, and mistrust
  • Uses internal state to select responses
  • Makes paranoid claims
  • Goal: behave like a paranoid schizophrenic patient

In 1973, Vint Cerf (one of the "fathers of the Internet") used ARPANET (the precursor to the Internet) to connect ELIZA and PARRY in a text-based conversation! You can read the transcript here.

The PARRY algorithm

User input Pattern match Update emotions Select response Output
PARRY's processing pipeline with emotional state

PARRY maintains a persistent emotional state across turns. This state influences which responses are selected, creating the illusion of coherent paranoid behavior over time.

Pattern matching: detect trigger keywords in user input

User input Pattern match Update emotions Select response Output

PARRY first scans the input for trigger keywords organized by topic. Each topic has associated emotional effects and response pools. This is much simpler than ELIZA's decomposition/reassembly mechanisms!

Category Keywords Emotional effect
Mafia/mob "mafia", "mob", "gangster" Fear +4, Mistrust +5
Police "police", "cop", "arrest" Mistrust +4, Anger +3
Trust "trust", "believe", "honest" Mistrust +3
Racetrack "horses", "racing", "track" Anger -1 (calming)

Emotional update: modify internal state

User input Pattern match Update emotions Select response Output

When a trigger pattern matches, PARRY adjusts its emotional variables. These values persist across the conversation.


1class Parry:
2    def __init__(self):
3        self.anger = 5       # 0-20 scale
4        self.fear = 8        # 0-20 scale
5        self.mistrust = 10   # 0-15 scale
6
7    def process_trigger(self, topic):
8        if topic == "mafia":
9            self.fear += 4
10            self.mistrust += 5
11            self.anger += 2

Response selection: choose based on emotional state

User input Pattern match Update emotions Select response Output

PARRY selects responses from different pools based on current emotional thresholds. Higher emotions trigger more paranoid responses.

Emotional level Response style Example
Low (calm) Cooperative "I used to gamble on horses."
Medium Guarded "I don't want to talk about that."
High (paranoid) Hostile "Are you one of THEM?"

Use the Chatbot Evolution Demo to interact with PARRY. The "Rule Breakdown" tab illustrates the internal state changes and how they affect responses.

The Turing Test, revisited

Colby, Hilf, Weber, & Kraemer (1972, Artificial Intelligence): Turing-like indistinguishability tests for the validation of a computer simulation of paranoid processes

In 1972, PARRY was tested via teletype against real patients and psychiatrists. Judges could not reliably distinguish PARRY from actual patients with paranoid schizophrenia.

  • This was one of the first informal "Turing tests"
  • Success? Or a comment on how we judge understanding?
  • Psychiatrists were looking for symptoms, not understanding

Does passing a specialized test mean the system understands anything? What does it mean that experts could be fooled?

A.L.I.C.E. (1995)

A.L.I.C.E. (Artificial Linguistic Internet Computer Entity) was created by Richard Wallace.

  • AIML (Artificial Intelligence Markup Language)
  • Over 40,000 patterns (vs ELIZA's ~200)
  • Won the Loebner Prize three times (2000, 2001, 2004)
  • Open source, widely studied and extended
  • Very similar to ELIZA, but scaled up to a much larger rule set
  • Explores the limits of pattern matching at scale

The A.L.I.C.E. algorithm

User input Normalize Pattern search Extract wildcards Process template Response
A.L.I.C.E.'s AIML processing pipeline

AIML supports recursive processing via <srai> (Symbolic Reduction AI), allowing patterns to trigger other patterns. This enables handling many input variations with fewer rules.

Normalization: prepare input for matching

User input Normalize Pattern search Extract wildcards Process template Response

First, A.L.I.C.E. normalizes input to uppercase and removes punctuation before pattern matching. This reduces the number of patterns needed.

Input Normalized
"Don't you think so?" "DO NOT YOU THINK SO"
"What's your name?" "WHAT IS YOUR NAME"
"I can't believe it!" "I CAN NOT BELIEVE IT"

Pattern search and wildcard extraction:

User input Normalize Pattern search Extract wildcards Process template Response

A.L.I.C.E. matches inputs to 40,000+ ranked patterns. Patterns use wildcards (*) to capture variable text.


1<category>
2  <pattern>MY NAME IS *</pattern>
3  <template>Nice to meet you, <star/>.</template>
4</category>
5
6<category>
7  <pattern>I AM FEELING *</pattern>
8  <template>Why are you feeling <star/>?</template>
9</category>
10...

This is uses essentially the same decomposition/reassembly approach as ELIZA, just in XML format with more extensive coverage.

SRAI: recursive pattern matching

User input Normalize Pattern search Extract wildcards Process template Response

The <srai> tag redirects processing to another pattern. This allows many input variations to map to a single response. It works like goto statements in ELIZA.


1<!-- These all redirect to the same base pattern -->
2<category>
3  <pattern>HI THERE</pattern>
4  <template><srai>HELLO</srai></template>
5</category>
6
7<category>
8  <pattern>HOWDY</pattern>
9  <template><srai>HELLO</srai></template>
10</category>
11
12<category>
13  <pattern>HELLO</pattern>
14  <template>Hello! How can I help you today?</template>
15</category>
16...

Live demo

Use the Chatbot Evolution Demo to interact with A.L.I.C.E.

  • Use the AIML Breakdown tab to see some additional details, like tracking the current topic, remembering the most recent response, and remembering the user's name.
  • How do PARRY and A.L.I.C.E. differ from ELIZA? How are they similar?
  • What do they do well?
  • Where do they break down?

Formal grammars: theory behind pattern matching

Chomsky (1956, IRE Transactions on Information Theory): Three Models for the Description of Language

In 1956, Noam Chomsky introduced a hierarchy of formal grammars that classify languages by the complexity of rules needed to generate them.

The Chomsky hierarchy tells us what kinds of patterns different computational systems can recognize—and what they cannot.

The Chomsky hierarchy

Type Grammar Recognizer Example
Type 3 Regular Finite automaton a*b+ (any number of a's followed by one or more b's)
Type 2 Context-free Pushdown automaton Palindromes: e.g., racecar
Type 1 Context-sensitive Linear-bounded automaton anbncna^n b^n c^n
Type 0 Unrestricted Turing machine Any computable language: e.g., is this a valid Python program?

Regular expressions (which ELIZA, PARRY, and A.L.I.C.E. use) are Type 3—the simplest class!

This isn't a theory of computation course; you don't need to follow all of the details here. The key takeaways are that (a) there are different levels of complexity in the kinds of patterns languages can have, and that (b) regular expressions are at the simplest level.

Type 3 grammars

A Type 3 grammar (regular grammar) has production rules of the form:

  • SaA  bBS \rightarrow aA~|~bB
  • AaBA \rightarrow aB
  • BbAB \rightarrow bA
  • AaA \rightarrow a
  • AεA \rightarrow \varepsilon

where AA and BB are non-terminal symbols, aa and bb are terminal symbols, and ε\varepsilon is the empty string (also a terminal symbol). A terminal symbol appears in the final output string, whereas a non-terminal symbol is a placeholder that can be replaced by other symbols according to the production rules. SS is a special non-terminal symbol called the start symbol; it represents the entire string generated by the grammar.

  • SaA  bBS \rightarrow aA~|~bB
  • AaS  aA \rightarrow aS~|~a
  • BbS  bB \rightarrow bS~|~b

Can you write down a Type 3 grammar that generates your first name, repeated 0 or more times?

Regular expressions are Type 3 grammars

Regular expressions and Type 3 (regular) grammars are provably equivalent—they recognize exactly the same class of languages. In other words, for any regular expression, there exists a Type 3 grammar that generates the same language, and vice versa.

Regular Expression Type 3 Grammar Description
(ab)*c+ S → A | C
A → abA | abC
C → c | cC
Matches: "c", "cc", "abc", "ababcc"
a(b|c)* S → aA
A → bA | cA | ε
Matches: "a", "ab", "ac", "abbc", "acbc"

Regular languages cannot match nested structures like palindromes or recursive syntax. This (among other reasons) is why rule-based chatbots struggle with complex language.

Stuff rule-based systems can't handle

  • Novel situations not covered by rules
  • Context that spans multiple turns
  • Contextually dependent patterns
  • Nested or recursive structure
  • Conceptual (semantic) similarity (beyond defining equivalent keywords)
  • Complex patterns (e.g., detecting whether something is a valid Python program)
  • ...and more!

If rules can't fully capture human language, where do we go from here?

Even with 40,000+ hand-crafted patterns, A.L.I.C.E. cannot:

  • Handle novel combinations of known concepts
  • Maintain context across a conversation
  • Understand implicit meaning or subtext
  • Generalize beyond its training examples

What if, instead of writing rules by hand, we could learn patterns automatically from massive amounts of text data?

Up next...

We're leaving hand-crafted rules behind! Next week we explore how to learn from data:

  • Lecture 5: Data cleaning and preprocessing
  • Lecture 6: Tokenization—breaking text into meaningful units
  • Lecture 7: Text classification
  • Lecture 8: POS tagging and sentiment analysis
Hand-crafted rules Learning from data
The paradigm shift that enables modern NLP

Instead of writing rules, we'll learn to extract patterns from large text corpora automatically.

Key takeaways

  1. PARRY adds emotional state: Pattern matching + persistent variables = coherent personality
  2. A.L.I.C.E. scales patterns: 40,000 rules with AIML and recursive SRAI processing
  3. Chomsky hierarchy: Regular expressions (Type 3) are the weakest class of grammars
  4. Fundamental limits: Rules capture syntax, not meaning—no amount of patterns can bridge this gap

Questions? Want to chat more?

📧 Email me
💬 Join our Discord
💁 Come to office hours

Start working on Assignment 1 now if you haven't already. Reach out early if you get stuck.