* 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 3: ELIZA implementation

PSYC 51.07: Models of language and communication

Jeremy R. Manning
Dartmouth College
Winter 2026

Today's agenda

  1. Algorithm: how ELIZA actually works, broken down step-by-step
  2. Demo: playing with a reference implementation to see the algorithm in action
  3. Assignment 1: tips and tricks for getting started

Leave today knowing how to build your own implementation of ELIZA.

The ELIZA algorithm

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response
The complete ELIZA processing pipeline

Each step is simple string manipulation. The illusion of "intelligence" comes from three things:

  • The user's text, which provides structured content
  • Hand-crafted patterns and templates that guide the response
  • Hard-to-shake human tendencies to anthropomorphize

ELIZA's complete instruction set comprises ~200 simple rules.

Pre-substitutions: normalize input before pattern matching

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

Normalizing common variations helps patterns match more reliably, allowing fewer rules to effectively cover more inputs. This step also handles common misspellings, simplifies phrasing, and helps to set the right tone in the final response.

  • "dont" -> "don't"
  • "cant" -> "can't"
  • "recollect" -> "remember"
  • "how" -> "what"
  • "machine" -> "computer"

Pattern matching: detect keywords, synonyms, and patterns in the input

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

We need to identify which rule to apply based on the user's input. ELIZA uses basic regular expressions to define patterns that can capture important keywords and structure in the input text.

  • Keywords: look for specific words (e.g., "mother", "father")
  • Synonyms: equate different words with similar meanings
  • Sequences: identify when words appear in a certain order

Pattern matching: detect keywords, synonyms, and patterns in the input

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

ELIZA uses a ranked list of keywords to check for. Each keyword is associated with one or more patterns based on regular expressions.

  • computer (rank 50)
  • dreamed (rank 4)
  • everyone (rank 2)
    ...
  • xnone (rank 0): fallback when no other patterns match

Pattern matching: detect keywords, synonyms, and patterns in the input

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

Some keywords have synonyms— alternative words with similar meanings. ELIZA treats these as equivalent when matching patterns.

  • belief feel think believe wish
  • family mother mom father dad sister brother wife children child
  • desire want need
  • sad unhappy depressed sick
  • happy elated glad better

Pattern matching: detect keywords, synonyms, and patterns in the input

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

Patterns are defined using simple regular expressions. They can include specific words or wildcards (*). Synonyms are denoted by "@".

keyword patterns (selected) matching text
am * am *; * I am really excited to build a chatbot
are * are you *; * are * Why are you asking me that?
i * i @desire *; * i am * @happy *; ... I am so glad to hear that!
yes * Yes, I guess that's true but I never really thought about it before.

Pattern matching: memory

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

When patterns start with "$", ELIZA saves the matched input to a buffer for later use. When the xnone keyword is triggered, ELIZA can pull from this buffer to generate a response to a previous input.

In the original rule set, there is only one memory pattern: $ * my * (e.g., "I have been thinking a lot about my family"). If no other patterns match later, ELIZA can respond with "Did you come to me to talk about your family?" (or other * my * responses) using the saved input.

Pattern matching: rankings

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

Text is matched against patterns in decreasing order of keyword rank. The first pattern that matches is selected for further processing.

Since "computer" has a rank of 50, whereas "dreamed" has a rank of 4, the text "I dreamed about my computer" would be tested against patterns associated with "computer" first. However, if no patterns for higher-ranked keywords match, ELIZA continues down the list until a match is found. The "xnone" keyword (rank 0) provides a fallback pattern (*) when no other patterns match.

Decomposition: extract relevant parts of the input

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

Once text is matched to a pattern, ELIZA breaks down the input into capture groups that can be used to craft a response. Each synonym and wildcard (*) in the pattern corresponds to a capture group. Capture groups are numbered in order of appearance, starting from 1.

Pattern Input Captured groups
* i am * @sad "I am feeling unhappy today" 1: "", 2: "feeling", 3: "unhappy", 4: "today"
* my * @family * "My mother and father are kind" 1: "", 2: "", 3: "mother", 4: "and father are kind"
* "I am worried about my dream" 1: "I am worried about my dream"

Reassembly: generate a response using templates

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

ELIZA's responses "reflect back" parts of the user's input using reassembly templates. The templates define how to construct a response— often using capture groups from the decomposition step.

Input Captured groups Reassembly template Result
"I can't trust people" 1: "", 2: "can't", 3: "trust people" "Perhaps you could (3) now." "Perhaps you could trust people now."
"I don't remember why I said that" 1: "", 2: "remember why I said that" "Do you wish to be able to (2)?" "Do you wish to be able to remember why you said that?"
"I suppose it wasn't that long ago" 1: "I suppose it wasn't that long ago" "Do you say (1) for some special reason?" "Do you say I suppose it wasn't that long ago for some special reason?"

Post-substitutions: reflect first and second person

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

To ensure the response sounds natural, ELIZA applies post-substitutions to adjust pronouns and verb forms. This step flips first-person references to second-person and vice versa. It works similarly to pre-substitutions; it's a simple string replacement based on a predefined dictionary.

  • "am" -> "are"
  • "your" -> "my"
  • "me" -> "you"
  • "myself" -> "yourself"
  • "i'm" -> "you are"

It's....demo time!

User input Pre-subs Pattern match Decompose Reassemble Post-subs Response

With this complete pipeline in mind, let's revisit our ELIZA demo to see how it all fits together. Use the "Rule Breakdown" tab to trace each step of the pipeline for different inputs.

Take a look at the complete list of rules, or view them in the "Live Rule Editor" tab of the demo. Pick out a few patterns and see if you can get ELIZA to pick up on them. Also try to find some edge cases where the pipeline breaks down! For a special challenge, try creating your own rules to see if you can "patch up" the edge cases you find.

How should you approach Assignment 1?

  1. First, make sure you fully understand the ELIZA algorithm we covered today. Review the slides and/or demo as needed.
  2. Use vibe coding to accelerate your development. Some strategies are in the Assignment 1 instructions.
  3. You must carefully test your implementation. Write functions for each step of the pipeline, and test them one-at-a-time. Then integrate them.
  4. Consider edge cases and failure modes as you test. What happens if the user input is empty? Or very long? Or contains special characters? What if new rules were added— would your code still work?
  5. Chat with your implementation by setting up a simple conversation loop. Make sure it behaves as expected; look out for strange or nonsensical responses. Compare to responses generated by the demo implementation.

Debugging tips


1def find_match(text, rules):
2    for rule in rules:
3        match = re.search(rule["pattern"], text)
4        if match:
5            print(f"Matched: {rule['pattern']}")
6            print(f"Groups: {match.groups()}")
7            return rule, match
8    print("No match found!")
9    return None, None

Print what patterns match and why. Remove prints when done.

Assignment 1 overview

A complete ELIZA implementation that:

  • Reads rules from instructions.txt
  • Handles pre/post substitutions
  • Matches patterns and generates responses
  • Maintains a conversation loop

Friday, January 16 (end of Week 2)

Key takeaways

  1. ELIZA is simple: Just string manipulation and pattern matching
  2. Pre/post subs: Normalize input, fix output pronouns
  3. Pattern priority: Check specific patterns before general ones
  4. Vibe coding: Use AI tools to accelerate development
  5. Test incrementally: Build and test piece by piece

The "magic" isn't in the algorithm. It's in how humans interpret the output!

Questions? Want to chat more?

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

Start Assignment 1 early. Ask questions, talk to each other, and come to office hours if you get stuck!