{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ediza-000",
   "metadata": {},
   "source": [
    "**Run the following cell before doing anything else** and then click the blue bar to the left of the cell to hide it, if it isn't already. Do **not** edit it!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-001",
   "metadata": {
    "deletable": false,
    "editable": false,
    "init_cell": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input",
     "init_cell",
     "tutorial-setup"
    ]
   },
   "outputs": [],
   "source": [
    "from IPython import get_ipython\n",
    "from IPython.display import HTML, Javascript, clear_output, display\n",
    "import hashlib\n",
    "import hmac\n",
    "import html\n",
    "import json\n",
    "\n",
    "\n",
    "_TUTORIAL_CHECK_ORDER = ['1.0.1',\n",
    " '1.1.1',\n",
    " '2.1.1',\n",
    " '2.2.1',\n",
    " '2.3.1',\n",
    " '2.4.1',\n",
    " '2.5.1',\n",
    " '3.1.1',\n",
    " '3.2.1',\n",
    " '3.3.1',\n",
    " '4.0.1',\n",
    " '4.1.1',\n",
    " '4.1.2']\n",
    "\n",
    "_TUTORIAL_EXACT_CHECKS = {'2.1.1': ('dce84fdacd0d35070da50ecb8c533f9c8fd018d71c60006a43ea795b4ced4820',\n",
    "           'Run ediza.py and copy only its greeting.'),\n",
    " '2.2.1': ('15afe38664ce992974ead2f69538e94ee16053cc3aca6f2a3ecb498071ca3da8',\n",
    "           \"Use three separate runs: BYE, I like Arthur's Seat, then an empty input. Copy \"\n",
    "           'only the reply from each, beginning with EDIZA: Goodbye!. Store the result of '\n",
    "           '.lower(), and compare strings using ==.'),\n",
    " '2.3.1': ('f22a7cecab84a46436e528b7e3adb8ad2ad1808a93603a55132f6077a41d7fbf',\n",
    "           'Keep input() and the conditional inside the while loop. Only the bye branch should '\n",
    "           'break. Paste the three replies from one conversation, without the greeting or '\n",
    "           'You: lines.'),\n",
    " '2.4.1': ('9caf8f7fe41418dd1bdbf46a4e790d089d936dcbefb4cdab8a51f26e43990b60',\n",
    "           'Restart before the test. Initialise response_index outside while, advance it only '\n",
    "           'for a general reply, and wrap to zero before the next list lookup would be out of '\n",
    "           'range.'),\n",
    " '3.1.1': ('65974d973ab532f51c1862f326d3b8eb17c26b49a702da14711f38c2128ca961',\n",
    "           'Copy only the three lists for HELLO, Edinburgh?!, my   dog likes cats., and '\n",
    "           'hobbit. Append and reset word when a space ends a nonempty word; add each '\n",
    "           'non-space character otherwise. Keep the extra space at the end of the loop '\n",
    "           'input.'),\n",
    " '3.2.1': ('05331757f9b3c2a060eecfd1487e263e468690532698866b17dbd792587fa16a',\n",
    "           'Reset reply to an empty string on every turn. Search the words for dictionary '\n",
    "           'keys and break on a match. Use and advance the general-response list only if no '\n",
    "           'keyword matched.'),\n",
    " '3.3.1': ('05331757f9b3c2a060eecfd1487e263e468690532698866b17dbd792587fa16a',\n",
    "           'The first keyword in each message should win, so break after the first match. '\n",
    "           'smother is a different word from mother and should use the first general response '\n",
    "           'in this fresh run.'),\n",
    " '4.0.1': ('32752c08c665939d217ffcc16d35881921e12f1718d790d95c3f53360c11ca66',\n",
    "           'Use a fresh run and enter all eight messages in order. Edinburgh, university, and '\n",
    "           'father should get keyword replies. Only general replies advance response_index. '\n",
    "           'Reset reply and words on every turn, and clean punctuation before checking for bye.')}\n",
    "\n",
    "_TUTORIAL_PROGRESS_IDS = {}\n",
    "\n",
    "_TUTORIAL_CHECK_BRANCHES = {}\n",
    "\n",
    "_TUTORIAL_TEXT_CHECKS = {'1.0.1', '1.1.1', '2.5.1', '4.1.1', '4.1.2'}\n",
    "\n",
    "_TUTORIAL_OPTIONAL_NOTES = {'5.1.1', '5.2.1', '5.3.1'}\n",
    "\n",
    "_TUTORIAL_SUCCESS_MESSAGES = {'1.0.1': ['Got it.', \"Let's go and meet EDIZA's ancestor!\"],\n",
    " '1.1.1': ['Notes recorded.', \"Let's see whether your intuition was correct.\"],\n",
    " '2.1.1': ['Correct!', ''],\n",
    " '2.2.1': ['Correct!', ''],\n",
    " '2.3.1': ['Correct!', 'You now have a working conversation loop.'],\n",
    " '2.4.1': ['Correct!', ''],\n",
    " '2.5.1': ['Notes recorded.', ''],\n",
    " '3.1.1': ['Correct!', ''],\n",
    " '3.2.1': ['Correct!', ''],\n",
    " '3.3.1': ['Correct!', ''],\n",
    " '4.0.1': ['Correct!', 'Well done, you have built EDIZA!'],\n",
    " '4.1.1': ['Notes recorded.', ''],\n",
    " '4.1.2': ['Notes recorded.',\n",
    "           'You have completed the core tutorial! Choose any bonus task if there is time.'],\n",
    " '5.1.1': ['Notes recorded.', ''],\n",
    " '5.2.1': ['Notes recorded.', ''],\n",
    " '5.3.1': ['Notes recorded.', '']}\n",
    "\n",
    "_TUTORIAL_STARTER_ANSWERS = {'1.0.1': 'WRITE NAMES HERE',\n",
    " '1.1.1': 'WRITE NOTES HERE',\n",
    " '2.1.1': 'PASTE OUTPUT HERE',\n",
    " '2.2.1': 'PASTE OUTPUT HERE',\n",
    " '2.3.1': 'PASTE OUTPUT HERE',\n",
    " '2.4.1': 'PASTE OUTPUT HERE',\n",
    " '2.5.1': 'WRITE NOTES HERE',\n",
    " '3.1.1': 'PASTE OUTPUT HERE',\n",
    " '3.2.1': 'PASTE OUTPUT HERE',\n",
    " '3.3.1': 'PASTE OUTPUT HERE',\n",
    " '4.0.1': 'PASTE OUTPUT HERE',\n",
    " '4.1.1': 'WRITE NOTES HERE',\n",
    " '4.1.2': 'WRITE NOTES HERE',\n",
    " '5.1.1': 'WRITE NOTES HERE',\n",
    " '5.2.1': 'WRITE NOTES HERE',\n",
    " '5.3.1': 'WRITE NOTES HERE'}\n",
    "\n",
    "_previous_completed = set(globals().get(\"_TUTORIAL_COMPLETED\", set()))\n",
    "_TUTORIAL_COMPLETED = set()\n",
    "for _check_id in _TUTORIAL_CHECK_ORDER:\n",
    "    if _check_id not in _previous_completed:\n",
    "        break\n",
    "    _TUTORIAL_COMPLETED.add(_check_id)\n",
    "_TUTORIAL_COMPLETED.update(_previous_completed & _TUTORIAL_OPTIONAL_NOTES)\n",
    "\n",
    "_TUTORIAL_SELECTIONS = {}\n",
    "\n",
    "\n",
    "def _tutorial_progress_id(check_id):\n",
    "    return _TUTORIAL_PROGRESS_IDS.get(check_id, check_id)\n",
    "\n",
    "\n",
    "def _tutorial_reset_progress():\n",
    "    _TUTORIAL_COMPLETED.clear()\n",
    "    _TUTORIAL_SELECTIONS.clear()\n",
    "\n",
    "\n",
    "def _tutorial_normalize(value):\n",
    "    return \"\\n\".join(line.rstrip() for line in value.strip().splitlines())\n",
    "\n",
    "\n",
    "def _tutorial_digest(value):\n",
    "    return hashlib.sha256(value.encode(\"utf-8\")).hexdigest()\n",
    "\n",
    "\n",
    "def _tutorial_message(kind, title, message):\n",
    "    colours = {\n",
    "        \"correct\": (\"#166534\", \"#dcfce7\", \"#86efac\"),\n",
    "        \"incorrect\": (\"#991b1b\", \"#fef2f2\", \"#fca5a5\"),\n",
    "    }\n",
    "    text, background, border = colours[kind]\n",
    "    display(HTML(\n",
    "        f'<div style=\"margin:8px 0;padding:10px 12px;border-left:4px solid {border};'\n",
    "        f'background:{background};color:{text}\">'\n",
    "        f'<strong>{html.escape(title)}</strong> {html.escape(message)}</div>'\n",
    "    ))\n",
    "\n",
    "\n",
    "def _tutorial_check(line, cell):\n",
    "    clear_output(wait=True)\n",
    "    check_id = line.strip()\n",
    "    progress_id = _tutorial_progress_id(check_id)\n",
    "    answer = _tutorial_normalize(cell)\n",
    "    known_checks = set(_TUTORIAL_CHECK_ORDER) | _TUTORIAL_OPTIONAL_NOTES\n",
    "\n",
    "    if check_id not in known_checks:\n",
    "        _tutorial_message(\"incorrect\", \"This check could not run.\", \"Please ask your tutor for help.\")\n",
    "        return\n",
    "\n",
    "    placeholders = tuple(value.upper() for value in _TUTORIAL_STARTER_ANSWERS.values())\n",
    "    if not answer or any(placeholder in answer.upper() for placeholder in placeholders):\n",
    "        _tutorial_message(\"incorrect\", \"No answer yet.\", \"Replace the placeholder, then run the cell again.\")\n",
    "        return\n",
    "\n",
    "    if progress_id in _TUTORIAL_CHECK_ORDER:\n",
    "        position = _TUTORIAL_CHECK_ORDER.index(progress_id)\n",
    "        missing = [\n",
    "            earlier for earlier in _TUTORIAL_CHECK_ORDER[:position]\n",
    "            if earlier not in _TUTORIAL_COMPLETED\n",
    "        ]\n",
    "        if missing:\n",
    "            _tutorial_message(\"incorrect\", \"Complete the previous step first.\", \"Return to the last visible check above.\")\n",
    "            return\n",
    "\n",
    "    correct = False\n",
    "    hint = \"Check your answer carefully and try again.\"\n",
    "    completion_details = {}\n",
    "\n",
    "    if check_id in _TUTORIAL_EXACT_CHECKS:\n",
    "        expected_digest, hint = _TUTORIAL_EXACT_CHECKS[check_id]\n",
    "        correct = hmac.compare_digest(_tutorial_digest(answer), expected_digest)\n",
    "    elif check_id in _TUTORIAL_TEXT_CHECKS or check_id in _TUTORIAL_OPTIONAL_NOTES:\n",
    "        correct = bool(answer)\n",
    "        hint = \"Write something before running the cell.\"\n",
    "\n",
    "    if not correct:\n",
    "        _tutorial_message(\"incorrect\", \"Not quite.\", hint)\n",
    "        return\n",
    "\n",
    "    already_completed = progress_id in _TUTORIAL_COMPLETED\n",
    "    if progress_id in _TUTORIAL_CHECK_ORDER and not already_completed:\n",
    "        position = _TUTORIAL_CHECK_ORDER.index(progress_id)\n",
    "        for later_id in _TUTORIAL_CHECK_ORDER[position + 1:]:\n",
    "            _TUTORIAL_COMPLETED.discard(later_id)\n",
    "        completion_details[\"resetFollowing\"] = True\n",
    "\n",
    "    _TUTORIAL_COMPLETED.add(progress_id)\n",
    "\n",
    "    if check_id in _TUTORIAL_OPTIONAL_NOTES:\n",
    "        _tutorial_message(\"correct\", \"Notes recorded.\", \"You can continue with any bonus task.\")\n",
    "        return\n",
    "\n",
    "    title, message = _TUTORIAL_SUCCESS_MESSAGES.get(check_id, (\"Correct!\", \"\"))\n",
    "    _tutorial_message(\"correct\", title, message)\n",
    "    display(Javascript(\n",
    "        \"window.__tutorialProgress && window.__tutorialProgress.complete(\"\n",
    "        + json.dumps(check_id)\n",
    "        + \", \"\n",
    "        + json.dumps(completion_details)\n",
    "        + \");\"\n",
    "    ))\n",
    "\n",
    "\n",
    "_tutorial_shell = get_ipython()\n",
    "if _tutorial_shell is None:\n",
    "    raise RuntimeError(\"Run this cell with a Python Jupyter kernel.\")\n",
    "_tutorial_shell.register_magic_function(_tutorial_check, \"cell\", \"check\")\n",
    "_tutorial_message(\"correct\", \"Setup complete.\", \"The tutorial checks are ready.\")\n",
    "\n",
    "\n",
    "_tutorial_progress_javascript = r\"\"\"\n",
    "(() => {\n",
    "  const checkOrder = __CHECK_ORDER__;\n",
    "  const progressIds = __PROGRESS_IDS__;\n",
    "  const checkBranches = __CHECK_BRANCHES__;\n",
    "  const optionalNotes = __OPTIONAL_NOTES__;\n",
    "  const completedFromKernel = __COMPLETED__;\n",
    "  const selectionsFromKernel = __SELECTIONS__;\n",
    "  const hiddenClass = \"tutorial-progress-hidden\";\n",
    "  const styleId = \"tutorial-progress-style\";\n",
    "  const starterAnswers = __STARTER_ANSWERS__;\n",
    "\n",
    "  if (!document.getElementById(styleId)) {\n",
    "    const style = document.createElement(\"style\");\n",
    "    style.id = styleId;\n",
    "    style.textContent = `.${hiddenClass} { display: none !important; }`;\n",
    "    document.head.appendChild(style);\n",
    "  }\n",
    "\n",
    "  const previous = window.__tutorialProgress;\n",
    "  if (previous && previous.observer) previous.observer.disconnect();\n",
    "\n",
    "  const state = {\n",
    "    completed: new Set(completedFromKernel),\n",
    "    selections: { ...selectionsFromKernel },\n",
    "    observer: null,\n",
    "    scheduled: false,\n",
    "  };\n",
    "\n",
    "  function progressId(id) {\n",
    "    return progressIds[id] || id;\n",
    "  }\n",
    "\n",
    "  function visibleNotebook() {\n",
    "    const scriptNotebook = document.currentScript &&\n",
    "      document.currentScript.closest(\".jp-Notebook, #notebook-container\");\n",
    "    if (scriptNotebook) return scriptNotebook;\n",
    "    const candidates = [...document.querySelectorAll(\".jp-Notebook, #notebook-container\")];\n",
    "    return candidates.find((node) => node.offsetParent !== null) || candidates[0] || null;\n",
    "  }\n",
    "\n",
    "  function currentNotebookPanel() {\n",
    "    const apps = [\n",
    "      window.jupyterapp,\n",
    "      window.jupyterlab,\n",
    "      window.jupyterLab,\n",
    "      window.jupyterFrontEnd,\n",
    "      window.jupyterApp,\n",
    "    ];\n",
    "    for (const app of apps) {\n",
    "      const widget = app && app.shell && app.shell.currentWidget;\n",
    "      if (widget && widget.content && widget.content.model && widget.context) return widget;\n",
    "    }\n",
    "    return null;\n",
    "  }\n",
    "\n",
    "  state.notebook = visibleNotebook();\n",
    "\n",
    "  if (!state.notebook) {\n",
    "    const warning = document.createElement(\"div\");\n",
    "    warning.style.cssText = \"margin:8px 0;padding:10px 12px;border-left:4px solid #f59e0b;background:#fffbeb;color:#92400e;font-family:system-ui,sans-serif\";\n",
    "    warning.textContent = \"The checks are ready, but this notebook frontend does not allow automatic section reveal. The checks will still mark answers, but all sections may remain visible.\";\n",
    "    document.body.appendChild(warning);\n",
    "    return;\n",
    "  }\n",
    "\n",
    "  function cells() {\n",
    "    const selector = state.notebook.id === \"notebook-container\"\n",
    "      ? \":scope > .cell\"\n",
    "      : \".jp-Notebook-cell\";\n",
    "    return [...state.notebook.querySelectorAll(selector)].filter(\n",
    "      (cell) => !cell.closest(\".jp-Notebook\") || cell.closest(\".jp-Notebook\") === state.notebook\n",
    "    );\n",
    "  }\n",
    "\n",
    "  function source(cell) {\n",
    "    const classicEditor = cell.querySelector(\".CodeMirror\");\n",
    "    if (classicEditor && classicEditor.CodeMirror) return classicEditor.CodeMirror.getValue();\n",
    "    const modernEditor = cell.querySelector(\".cm-content\");\n",
    "    if (modernEditor) return modernEditor.innerText || modernEditor.textContent || \"\";\n",
    "    const input = cell.querySelector(\".input_area, .jp-InputArea-editor\");\n",
    "    return input ? (input.innerText || input.textContent || \"\") : \"\";\n",
    "  }\n",
    "\n",
    "  function checkId(cell) {\n",
    "    const match = source(cell).match(/^%%check\\s+([a-z0-9_.-]+)/m);\n",
    "    return match ? match[1] : null;\n",
    "  }\n",
    "\n",
    "  function renderedMarker(cell) {\n",
    "    return cell.querySelector(\"[data-tutorial-section]\");\n",
    "  }\n",
    "\n",
    "  function explicitSectionNumber(cell) {\n",
    "    const sectionMarker = renderedMarker(cell);\n",
    "    if (sectionMarker) return Number(sectionMarker.dataset.tutorialSection);\n",
    "    const markerMatch = source(cell).match(/data-tutorial-section=[\\\"'](\\d+)[\\\"']/);\n",
    "    if (markerMatch) return Number(markerMatch[1]);\n",
    "    const id = checkId(cell);\n",
    "    const progress = id ? progressId(id) : null;\n",
    "    const checkIndex = progress ? checkOrder.indexOf(progress) : -1;\n",
    "    if (checkIndex >= 0) return checkIndex;\n",
    "    if (id && optionalNotes.includes(id)) return checkOrder.length;\n",
    "    return null;\n",
    "  }\n",
    "\n",
    "  function sectionNumber(cell, notebookCells) {\n",
    "    const ownSection = explicitSectionNumber(cell);\n",
    "    if (ownSection !== null) return ownSection;\n",
    "    const index = notebookCells.indexOf(cell);\n",
    "    for (let cursor = index - 1; cursor >= 0; cursor -= 1) {\n",
    "      const inheritedSection = explicitSectionNumber(notebookCells[cursor]);\n",
    "      if (inheritedSection !== null) return inheritedSection;\n",
    "    }\n",
    "    return 0;\n",
    "  }\n",
    "\n",
    "  function branch(cell) {\n",
    "    const sectionMarker = renderedMarker(cell);\n",
    "    if (sectionMarker && sectionMarker.dataset.tutorialOs) {\n",
    "      return {\n",
    "        software: sectionMarker.dataset.tutorialSoftware,\n",
    "        os: sectionMarker.dataset.tutorialOs,\n",
    "      };\n",
    "    }\n",
    "    const id = checkId(cell);\n",
    "    if (id && checkBranches[id]) return checkBranches[id];\n",
    "    const cellSource = source(cell);\n",
    "    const software = cellSource.match(/data-tutorial-software=[\\\"']([^\\\"']+)[\\\"']/);\n",
    "    const os = cellSource.match(/data-tutorial-os=[\\\"']([^\\\"']+)[\\\"']/);\n",
    "    return software && os ? { software: software[1], os: os[1] } : null;\n",
    "  }\n",
    "\n",
    "  function refresh() {\n",
    "    state.scheduled = false;\n",
    "    let unlockedSection = 0;\n",
    "    for (const id of checkOrder) {\n",
    "      if (!state.completed.has(id)) break;\n",
    "      unlockedSection += 1;\n",
    "    }\n",
    "    const notebookCells = cells();\n",
    "    for (const cell of notebookCells) {\n",
    "      const requiredBranch = branch(cell);\n",
    "      const wrongBranch = requiredBranch &&\n",
    "        state.selections[requiredBranch.software] !== requiredBranch.os;\n",
    "      const locked = sectionNumber(cell, notebookCells) > unlockedSection;\n",
    "      cell.classList.toggle(hiddenClass, Boolean(locked || wrongBranch));\n",
    "    }\n",
    "    bindResetButtons();\n",
    "    window.dispatchEvent(new Event(\"resize\"));\n",
    "  }\n",
    "\n",
    "  function scheduleRefresh() {\n",
    "    if (state.scheduled) return;\n",
    "    state.scheduled = true;\n",
    "    requestAnimationFrame(refresh);\n",
    "  }\n",
    "\n",
    "  state.complete = (id, details = {}) => {\n",
    "    const progress = progressId(id);\n",
    "    if (!checkOrder.includes(progress)) return;\n",
    "    const position = checkOrder.indexOf(progress);\n",
    "    if (details.resetFollowing) {\n",
    "      for (const laterId of checkOrder.slice(position + 1)) state.completed.delete(laterId);\n",
    "    }\n",
    "    state.completed.add(progress);\n",
    "    if (details.software && details.os) state.selections[details.software] = details.os;\n",
    "    refresh();\n",
    "  };\n",
    "\n",
    "\n",
    "  function starterSource(id) {\n",
    "    return `%%check ${id}\\n${starterAnswers[id]}`;\n",
    "  }\n",
    "\n",
    "  function modelSource(cell) {\n",
    "    if (cell.sharedModel && cell.sharedModel.getSource) return cell.sharedModel.getSource();\n",
    "    if (cell.value && typeof cell.value.text === \"string\") return cell.value.text;\n",
    "    return \"\";\n",
    "  }\n",
    "\n",
    "  function clearDomOutput(cellNode) {\n",
    "    for (const output of cellNode.querySelectorAll(\".output, .output_wrapper, .jp-OutputArea, .jp-Cell-outputArea\")) {\n",
    "      output.replaceChildren();\n",
    "    }\n",
    "  }\n",
    "\n",
    "  function resetDomCheckCells() {\n",
    "    let resetCount = 0;\n",
    "    for (const cellNode of cells()) {\n",
    "      const id = checkId(cellNode);\n",
    "      if (!id || !starterAnswers[id]) continue;\n",
    "      const sourceText = starterSource(id);\n",
    "      const classicEditor = cellNode.querySelector(\".CodeMirror\");\n",
    "      if (classicEditor && classicEditor.CodeMirror) {\n",
    "        classicEditor.CodeMirror.setValue(sourceText);\n",
    "        clearDomOutput(cellNode);\n",
    "        resetCount += 1;\n",
    "      }\n",
    "    }\n",
    "    return resetCount;\n",
    "  }\n",
    "\n",
    "  async function resetNotebookCells() {\n",
    "    const classicNotebook = window.Jupyter && window.Jupyter.notebook;\n",
    "    if (classicNotebook && classicNotebook.get_cells) {\n",
    "      for (const cell of classicNotebook.get_cells()) {\n",
    "        const match = cell.get_text().match(/^%%check\\s+([a-z0-9_.-]+)/m);\n",
    "        if (!match || !starterAnswers[match[1]]) continue;\n",
    "        cell.set_text(starterSource(match[1]));\n",
    "        if (cell.clear_output) cell.clear_output();\n",
    "        if (cell.set_input_prompt) cell.set_input_prompt(null);\n",
    "      }\n",
    "      classicNotebook.set_dirty(true);\n",
    "      await classicNotebook.save_notebook();\n",
    "      return;\n",
    "    }\n",
    "\n",
    "    const panel = currentNotebookPanel();\n",
    "    const notebookModel = panel && panel.content && panel.content.model;\n",
    "    if (notebookModel && notebookModel.cells) {\n",
    "      for (let index = 0; index < notebookModel.cells.length; index += 1) {\n",
    "        const cell = notebookModel.cells.get(index);\n",
    "        const match = modelSource(cell).match(/^%%check\\s+([a-z0-9_.-]+)/m);\n",
    "        if (!match || !starterAnswers[match[1]]) continue;\n",
    "        if (cell.sharedModel && cell.sharedModel.setSource) {\n",
    "          cell.sharedModel.setSource(starterSource(match[1]));\n",
    "        } else if (cell.value && typeof cell.value.text === \"string\") {\n",
    "          cell.value.text = starterSource(match[1]);\n",
    "        }\n",
    "        if (cell.outputs && cell.outputs.clear) cell.outputs.clear();\n",
    "        if (\"executionCount\" in cell) cell.executionCount = null;\n",
    "      }\n",
    "      await panel.context.save();\n",
    "      return;\n",
    "    }\n",
    "\n",
    "    if (resetDomCheckCells() > 0) return;\n",
    "\n",
    "    throw new Error(\"This notebook frontend does not expose a writable notebook model.\");\n",
    "  }\n",
    "\n",
    "  async function resetKernelProgress() {\n",
    "    const code = \"_tutorial_reset_progress()\";\n",
    "    try {\n",
    "      const classicKernel = window.Jupyter && window.Jupyter.notebook && window.Jupyter.notebook.kernel;\n",
    "      if (classicKernel && classicKernel.execute) {\n",
    "        classicKernel.execute(code, {}, { silent: true, store_history: false });\n",
    "        return true;\n",
    "      }\n",
    "\n",
    "      const panel = currentNotebookPanel();\n",
    "      const kernel = panel && panel.sessionContext && panel.sessionContext.session &&\n",
    "        panel.sessionContext.session.kernel;\n",
    "      if (kernel && kernel.requestExecute) {\n",
    "        const future = kernel.requestExecute({ code, silent: true, store_history: false });\n",
    "        await future.done;\n",
    "        return true;\n",
    "      }\n",
    "    } catch (error) {\n",
    "      console.error(\"Could not reset tutorial kernel state\", error);\n",
    "    }\n",
    "    return false;\n",
    "  }\n",
    "\n",
    "  async function resetProgress(button) {\n",
    "    const confirmed = window.confirm(\n",
    "      \"Reset all tutorial progress? Every answer and check result will be cleared.\"\n",
    "    );\n",
    "    if (!confirmed) return;\n",
    "\n",
    "    button.disabled = true;\n",
    "    button.textContent = \"Resetting…\";\n",
    "    try {\n",
    "      await resetNotebookCells();\n",
    "      const kernelWasReset = await resetKernelProgress();\n",
    "      state.completed.clear();\n",
    "      state.selections = {};\n",
    "      refresh();\n",
    "      state.notebook.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n",
    "      button.disabled = false;\n",
    "      button.textContent = \"Reset all tutorial progress\";\n",
    "      if (kernelWasReset) {\n",
    "        window.alert(\"Tutorial progress has been reset.\");\n",
    "      } else {\n",
    "        window.alert(\n",
    "          \"Answers were reset. Restart the kernel before beginning again to clear its saved progress too.\"\n",
    "        );\n",
    "      }\n",
    "    } catch (error) {\n",
    "      console.error(\"Could not reset tutorial progress\", error);\n",
    "      window.alert(`The tutorial could not be reset: ${error.message}`);\n",
    "      button.disabled = false;\n",
    "      button.textContent = \"Reset all tutorial progress\";\n",
    "    }\n",
    "  }\n",
    "\n",
    "  function bindResetButtons() {\n",
    "    for (const button of state.notebook.querySelectorAll(\"[data-tutorial-reset]\")) {\n",
    "      if (button.dataset.tutorialResetBound) continue;\n",
    "      button.dataset.tutorialResetBound = \"true\";\n",
    "      button.addEventListener(\"click\", () => window.__tutorialProgress.reset(button));\n",
    "    }\n",
    "  }\n",
    "\n",
    "  state.reset = resetProgress;\n",
    "  state.observer = new MutationObserver(scheduleRefresh);\n",
    "  state.observer.observe(state.notebook, { childList: true, subtree: true });\n",
    "  window.__tutorialProgress = state;\n",
    "  refresh();\n",
    "  setTimeout(refresh, 100);\n",
    "  setTimeout(refresh, 500);\n",
    "  setTimeout(refresh, 1500);\n",
    "})();\n",
    "\"\"\".replace(\"__STARTER_ANSWERS__\", json.dumps(_TUTORIAL_STARTER_ANSWERS)).replace(\"__CHECK_ORDER__\", json.dumps(_TUTORIAL_CHECK_ORDER)).replace(\n",
    "    \"__PROGRESS_IDS__\", json.dumps(_TUTORIAL_PROGRESS_IDS)\n",
    ").replace(\n",
    "    \"__CHECK_BRANCHES__\", json.dumps(_TUTORIAL_CHECK_BRANCHES)\n",
    ").replace(\n",
    "    \"__OPTIONAL_NOTES__\", json.dumps(sorted(_TUTORIAL_OPTIONAL_NOTES))\n",
    ").replace(\n",
    "    \"__COMPLETED__\", json.dumps(sorted(_TUTORIAL_COMPLETED))\n",
    ").replace(\n",
    "    \"__SELECTIONS__\", json.dumps(_TUTORIAL_SELECTIONS)\n",
    ")\n",
    "\n",
    "display(Javascript(_tutorial_progress_javascript))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-002",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"0\" hidden></span>\n",
    "\n",
    "# Tutorial 2: EDIZA\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "Week 3 | Author: Rayo Verweij (<code>rayo.verweij@ed.ac.uk</code>)<br>\n",
    "<em>Adapted from materials from the course <a href=\"https://guzdial.engin.umich.edu/compfor-302-ling-394-alien-anatomy-how-chatgpt-works/\" target=\"_blank\">Alien Anatomy: How ChatGPT Works</a> by Steve Abney and Mark Guzdial at the University of Michigan</em>\n",
    "</div>\n",
    "\n",
    "Welcome back! Last time, you set up a Python development environment on your own laptop. Today, it's time to put it to work by building a chatbot.\n",
    "\n",
    "As discussed in the opening lecture, AI is having a massive impact on the field of programming and one of the aims of this course is to allow you to hone your judgment on whether and when to use AI tools. That includes getting an introduction to the history of the field, with which we're getting started in this tutorial. We'll meet one of the earliest chatbots, **ELIZA**, and explore an early approach to AI known as **symbolic AI**. Then you'll build your own version of it, using the material from lectures 1-5: variables, conditionals, loops, lists, and dictionaries, which is already enough to make something you can have a conversation with.\n",
    "\n",
    "Whether you'll have a *good* conversation... is another matter entirely.\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-003",
   "metadata": {},
   "source": [
    "## 1 Introductions\n",
    "\n",
    "Find yourself a partner! Try to work with someone at about the **same skill level** as you. Both of you should write and run the program on your own laptops, although you only need to fill out one copy of this tutorial sheet.\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "<b>ℹ️ Checking your work</b>\n",
    "\n",
    "This tutorial uses interactive cells to allow you to check your work as you go along. In the cell below, replace the text `WRITE NAMES HERE` with your names, and run the cell using the ▶️ button or by pressing Ctrl+Enter while in the cell and the next section of the tutorial will appear.\n",
    "\n",
    "Leave each cell's `%%check` line in place; this is how the notebook identifies individual cells.\n",
    "\n",
    "While this cell will accept any text input, some of the later ones will only accept correct answers. If you get a cell wrong, use the hint provided to find out where you went wrong or ask your tutor to help you out!\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-004",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 1.0.1\n",
    "WRITE NAMES HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-005",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"1\" hidden></span>\n",
    "\n",
    "### 1.1 Meet ELIZA\n",
    "\n",
    "In 1966, Joseph Weizenbaum, working at MIT, published a description of a chatbot he called **ELIZA**. It came with several scripts, but the best-known one, *DOCTOR*, imitated aspects of a Rogerian psychotherapist: it often turned the user's own words back into questions. This let it keep a conversation going without needing to know very much about the world.\n",
    "\n",
    "Several online recreations of ELIZA still exist today! Spend a couple of minutes [talking to this online version of ELIZA](https://www.masswerk.at/elizabot/) or, alternatively, [this one](https://sites.google.com/view/elizaarchaeology/try-eliza).\n",
    "\n",
    "Here are some conversation ideas to try out:\n",
    "\n",
    "1. Start with something ordinary, such as moving to a new city. (No need to share anything personal, feel free to invent situations.)\n",
    "2. Try a sentence about your family, then a sentence beginning with `I am`.\n",
    "3. Change the subject abruptly, or ask a factual question.\n",
    "\n",
    "Find one reply that seems surprisingly appropriate and one that gives the game away. In pseudocode (or simply your own words), how do you think these responses were coded? What _rules_ might ELIZA be following that explain the replies?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-006",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 1.1.1\n",
    "WRITE NOTES HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-007",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"2\" hidden></span>\n",
    "\n",
    "## 2 A chatbot of our own\n",
    "\n",
    "While we won't be implementing _every_ feature of ELIZA in this tutorial, you might be surprised to learn that the programming concepts encountered in the course so far are actually enough to get most of the way. We're going to be building our own version of it, and because we're in Edinburgh, we'll call it **EDIZA**.\n",
    "\n",
    "### 2.1 Set up your working folder\n",
    "\n",
    "1. In the `Intro to Programming` folder you created last time, make a new folder called `tutorial-2-ediza`. If you cannot find the original folder, create it somewhere easy to find, such as Documents.\n",
    "2. In VS Code, choose **File > Open Folder...** and open `tutorial-2-ediza`.\n",
    "3. Create a new file called `ediza.py`.\n",
    "4. In the file, add this greeting:\n",
    "\n",
    "```python\n",
    "print(\"EDIZA: Hello! I'm EDIZA. Tell me what's on your mind.\")\n",
    "```\n",
    "\n",
    "5. Save the file and run it using **Run Python File** (the ▶️ button), or one of the other methods you explored last time.\n",
    "\n",
    "The output should appear in VS Code's integrated terminal. If your setup from tutorial 1 is not working, or you need help remembering how to navigate VS Code, ask your tutor!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-008",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 2.1.1\n",
    "PASTE OUTPUT HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-009",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"3\" hidden></span>\n",
    "\n",
    "### 2.2 Something to say\n",
    "\n",
    "To allow the user to respond, add the following lines after the greeting:\n",
    "\n",
    "```python\n",
    "message = input(\"You: \")\n",
    "message = message.lower()\n",
    "message = message.strip()\n",
    "```\n",
    "\n",
    "Remember `.strip()` from lecture 1 and `.lower()` from lecture 2? They remove surrounding whitespace and convert letters to lowercase, respectively. Because strings are _immutable_, we store the returned string in `message` each time.\n",
    "\n",
    "Next, use an `if` statement to make EDIZA print a reply according to this table:\n",
    "\n",
    "| If the cleaned message is... | Print exactly... |\n",
    "| --- | --- |\n",
    "| `bye` | `EDIZA: Goodbye!` |\n",
    "| The empty string (`\"\"`) | `EDIZA: Even a chatbot needs something to go on.` |\n",
    "| Anything else | `EDIZA: Please tell me more.` |\n",
    "\n",
    "Refer back to lecture 3 if you need a reminder for `if` statements and remember to be careful with the difference between `=` and `==`.\n",
    "\n",
    "These explicit rules are an example of **symbolic AI**, in which a programmer represents information using _symbols_ and specifies _rules_ for manipulating them. While our `if` statement doesn't know what a farewell *means*, it appears that it does because we've given it a rule to say goodbye when the symbol `bye` appears.\n",
    "\n",
    "To test your program, run it **three times**, entering these inputs in order:\n",
    "\n",
    "1. `BYE` (in capitals)\n",
    "2. `I like Arthur's Seat`\n",
    "3. Nothing (just press Enter)\n",
    "\n",
    "Paste the **three replies**, one per line, below. Leave out the opening greetings and the `You:` lines."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-010",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 2.2.1\n",
    "PASTE OUTPUT HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-011",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"4\" hidden></span>\n",
    "\n",
    "### 2.3 Keep the conversation going\n",
    "\n",
    "EDIZA has a rather short attention span at the moment. Let's fix that using a **loop**.\n",
    "\n",
    "1. Keep the greeting at the top of your file, so it runs only once.\n",
    "2. After it, add `while True:`.\n",
    "3. Move the input, the two string-cleaning lines, and your entire conditional **inside** that loop. **Tip:** select all the lines in VS Code and press Tab to indent them together.\n",
    "4. Immediately after printing `EDIZA: Goodbye!`, add `break` at the same indentation level as that `print`. It should run only in the `bye` branch.\n",
    "\n",
    "Why `while True`? The condition stays true, so the loop keeps asking for input until `break` ends it. Make sure the `input` line is inside the loop too, or EDIZA will keep answering the same message forever.\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "<b>ℹ️ Stopping a runaway program</b>\n",
    "\n",
    "If you make a mistake, it's possible that you end up in an infinite loop and EDIZA will not stop. If this happens, click in the VS Code terminal and press <code>Ctrl+C</code> (also on macOS) to end the program. Check your indentation and the position of <code>break</code> before running it again.\n",
    "</div>\n",
    "\n",
    "Start the program once. In that same conversation, enter `It is raining`, then an empty input, then `BYE`. Check that:\n",
    "\n",
    "- the greeting appears once;\n",
    "- each input gets exactly one reply;\n",
    "- after `BYE`, the program ends and the terminal's command prompt returns.\n",
    "\n",
    "Paste the three replies below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-012",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 2.3.1\n",
    "PASTE OUTPUT HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-013",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"5\" hidden></span>\n",
    "\n",
    "### 2.4 A little variety\n",
    "\n",
    "EDIZA is very keen for us to tell it more. Fair enough, but can it say anything else?\n",
    "\n",
    "Add a **list** before the `while` loop called `general_responses`, with the items `\"Please tell me more.\"`, `\"How does that make you feel?\"`, and `\"What makes you say that?\"`. These are our **general responses**: replies that might fit lots of different messages. We'll work through the list in order, making it appear that EDIZA has a different response each time.\n",
    "\n",
    "After the list, add a variable called `response_index` and set it to `0`. Then, replace the code in your final `else` branch so that it:\n",
    "\n",
    "1. Looks up the list item at `response_index` and assigns it to a variable called `reply`.\n",
    "2. Prints it, prefixed with `EDIZA:` like the other responses.\n",
    "3. Increases `response_index` by one.\n",
    "4. Sets `response_index` back to `0` if it has reached `len(general_responses)`.\n",
    "\n",
    "Remember that list indices start at zero. Alternatively, you can also use the remainder operator `%` to return the index to the start if you prefer.\n",
    "\n",
    "Empty input and `bye` should keep their existing replies.\n",
    "\n",
    "Restart EDIZA and enter the following in one conversation:\n",
    "\n",
    "1. `The tram is purple`\n",
    "2. `I bought a sandwich`\n",
    "3. An empty input.\n",
    "4. `It is raining`\n",
    "5. `Penguins run the library`\n",
    "6. `bye`\n",
    "\n",
    "Paste all six replies below. If your output checks out, you have correctly implemented all three general responses as well as the algorithm that cycles through them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-014",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 2.4.1\n",
    "PASTE OUTPUT HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-015",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"6\" hidden></span>\n",
    "\n",
    "### 2.5 That sounds just like me!\n",
    "\n",
    "Consider this statement:\n",
    "\n",
    "> You sometimes doubt whether you have made the right decision, but you also have abilities that other people haven't fully recognised.\n",
    "\n",
    "Does it sound as though it might describe you? Your partner? Almost everyone in the room?\n",
    "\n",
    "The [**Barnum effect**](https://dictionary.apa.org/barnum-effect), also called the Forer effect after its discoverer, is our tendency to treat broad descriptions as if they were specifically accurate descriptions of ourselves. Psychologist **Bertram Forer** demonstrated this in 1949. In his study, students rated supposedly individual personality feedback highly, even though they had all received the same description.\n",
    "\n",
    "EDIZA's general questions are not personality descriptions, so this is not _quite_ the same situation. But they can similarly invite us to supply a personal meaning that the program has never worked out. While often an absurd response, `How does that make you feel?` might sometimes seem surprisingly thoughtful.\n",
    "\n",
    "Record one example of input that fits the response and one that does not fit the response at all. At this point in our coding, what information does EDIZA actually use to select its replies?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-016",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 2.5.1\n",
    "WRITE NOTES HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-017",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"7\" hidden></span>\n",
    "\n",
    "## 3 Finding the words\n",
    "\n",
    "While we have a conversation loop, EDIZA still ignores almost everything we say. We'll try to fix that by giving it some **keywords** to look for so it actually can tailor its response based on what's in a message.\n",
    "\n",
    "### 3.1 Tokenisation\n",
    "\n",
    "If we want to find keywords, we will need to break up our input into individual words. Turning text into smaller pieces for a program to work with is called **tokenisation** and is an important step in natural language processing systems to this day, including large language model-based systems like ChatGPT.\n",
    "\n",
    "Before we can break up our input, we'll need to clean up any punctuation marks. For example, we want `Edinburgh`, `EDINBURGH`, and `Edinburgh!` to lead to the same word, so we can perform keyword detection regardless of the punctuation marks used.\n",
    "\n",
    "Before your `while` loop, define a new list of punctuation marks. Write at least `[\".\", \",\", \"!\", \"?\"]` but feel free to add any others you'd like to include.\n",
    "\n",
    "Then, in between the lines where you convert the input message to lowercase and strip it of whitespace, add a new loop that iterates over the list and, for each punctuation mark, uses `.replace()` to replace it with a space. (We replace them with spaces rather than an empty string, as otherwise `hello,Edinburgh` would become `helloedinburgh`).\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "<b>ℹ️ Remember decomposition</b>\n",
    "\n",
    "In this section, we're making quite a few edits in different parts of the code. If you're not entirely sure that what you are writing is correct, it might help to create a temporary file in which you can test parts of your code individually. For the above algorithm, in your temporary file, copy over the list of punctuation marks and the loop that you've written and test it with a few different strings until you are sure all punctuation marks are correctly replaced with spaces.\n",
    "</div>\n",
    "\n",
    "Our input is now properly cleaned. Next, we'll break the input up into its individual words. We'll do this inside the final `else` statement, as it's only needed at this point.\n",
    "\n",
    "Inside the `else` block, _before the existing general-response code_, paste the following code scaffold. It is shown without the surrounding indentation, so indent the whole block to sit inside `else`, keeping its internal indentation.\n",
    "\n",
    "Read carefully to see how this code works. First, it initialises an empty list `words` that will store each individual word in the input. Then, it initialises an accumulator variable `word` to an empty string. Over the course of a `for` loop, we're going over the input message _character by character_. If the character is not a space, we add it to the `word` that we are building. If the character _is_ a space, we've finished a word, so we'll add it to the list and reset the accumulator variable to start building our next word.\n",
    "\n",
    "Finish the loop by replacing each `pass` with the code described by the comment next to it.\n",
    "\n",
    "```python\n",
    "words = []\n",
    "word = \"\"\n",
    "for character in message + \" \":\n",
    "    if character == \" \":\n",
    "        if word != \"\":\n",
    "            pass  # Append the finished word to words\n",
    "            pass  # Set word back to an empty string\n",
    "    else:\n",
    "        pass  # Add character to the end of word\n",
    "\n",
    "print(words)  # Temporary output to see what we have collected, for easier testing\n",
    "```\n",
    "\n",
    "Still not sure how it works? Say the input given is the string `hi  ed` (with two spaces). We can trace the code as follows:\n",
    "\n",
    "| Character just processed | `word` afterwards | `words` afterwards |\n",
    "| --- | --- | --- |\n",
    "| `h` | `\"h\"` | `[]` |\n",
    "| `i` | `\"hi\"` | `[]` |\n",
    "| First space | `\"\"` | `[\"hi\"]` |\n",
    "| Second space | `\"\"` | `[\"hi\"]` |\n",
    "| `e` | `\"e\"` | `[\"hi\"]` |\n",
    "| `d` | `\"ed\"` | `[\"hi\"]` |\n",
    "| The extra space from `message + \" \"` | `\"\"` | `[\"hi\", \"ed\"]` |\n",
    "\n",
    "The extra space we add when writing `message + \" \"` makes the loop store the _last word_ too, as our algorithm assumes each word is followed by a space. The inner conditional prevents consecutive spaces from adding empty strings to the list.\n",
    "\n",
    "Start a conversation and enter these three messages, followed by `bye`:\n",
    "\n",
    "1. <code>&nbsp;&nbsp;&nbsp;HELLO, Edinburgh?!&nbsp;&nbsp;</code> (with spaces around it).\n",
    "2. <code>my&nbsp;&nbsp;&nbsp;dog likes cats.</code> (with several spaces after `my`).\n",
    "3. `hobbit`\n",
    "\n",
    "Paste the three printed lists from the temporary output line below, in order. Leave out the EDIZA replies this time. If you're stuck, trace your own loop on paper beside the table, or ask your tutor to trace it with you."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-018",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 3.1.1\n",
    "PASTE OUTPUT HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-019",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"8\" hidden></span>\n",
    "\n",
    "### 3.2 Keywords and dictionaries\n",
    "\n",
    "Great—we can now recognise individual words. Remove the temporary `print(words)` line to clean up what is printed to the terminal.\n",
    "\n",
    "With this newfound capability, we can now check if any particular **keywords** are printed in the user's input, and tailor our responses to them. Before the `while` loop, create a dictionary named `keyword_responses` using these keys and values:\n",
    "\n",
    "| Key | Value |\n",
    "| --- | --- |\n",
    "| `mother` | `Tell me more about your family.` |\n",
    "| `father` | `Tell me more about your family.` |\n",
    "| `edinburgh` | `What is it like living in Edinburgh?` |\n",
    "| `university` | `What do you think about your studies?` |\n",
    "\n",
    "All keys and values are strings. The values should _not_ include `EDIZA:`; we'll add that when printing, just as we do for the general responses.\n",
    "\n",
    "After collecting `words`, EDIZA should now do the following:\n",
    "\n",
    "1. Start with `reply = \"\"`, ready to choose a new reply **on every turn**.\n",
    "2. Loop over `words` in order. If a word is a key in `keyword_responses`, assign its dictionary value to `reply` and stop searching with `break`.\n",
    "3. If `reply` is still empty after the search, choose the next general response and update `response_index`, using the code you wrote earlier.\n",
    "4. Print the chosen reply once, after deciding which kind to use.\n",
    "\n",
    "Here's a structure to work from. Replace the three `pass` lines and move your general-response code into the indicated place. Remember to remove the `print` line from that old block as the final `print` below now handles both kinds of reply.\n",
    "\n",
    "```python\n",
    "reply = \"\"\n",
    "for word in words:\n",
    "    if word in keyword_responses:\n",
    "        pass  # Look up this word's reply in the dictionary\n",
    "        pass  # Stop searching now that we have a reply\n",
    "\n",
    "if reply == \"\":\n",
    "    pass  # Choose a general reply and advance its index here (using your code from section 2.4)\n",
    "\n",
    "print(\"EDIZA:\", reply)\n",
    "```\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "<b>ℹ️ Note the code</b>\n",
    "\n",
    "There are two interesting things about this code that are worth highlighting. First, note the difference in how the `in` keyword works between lists and dictionaries. `if word in keyword_responses` checks whether the dictionary has that key, whereas `for word in words` visits each item in the list.\n",
    "\n",
    "Secondly, you now have loops inside a loop. `break` only ends one loop at a time, the innermost one, so you don't have to worry about accidentally ending your `while True:` loop early.\n",
    "</div>\n",
    "\n",
    "Restart EDIZA and enter these messages in one conversation:\n",
    "\n",
    "1. `My MOTHER visits.`\n",
    "2. `I study at university!`\n",
    "3. `The tram is purple`\n",
    "4. `bye`\n",
    "\n",
    "Paste the four replies from EDIZA below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-020",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 3.2.1\n",
    "PASTE OUTPUT HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-021",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"9\" hidden></span>\n",
    "\n",
    "### 3.3 Who gets the first word?\n",
    "\n",
    "What if a message contains _two_ keywords?\n",
    "\n",
    "Our rule is that the **first matching word in the user's message wins**. While the original ELIZA had a more elaborate system of keyword priorities and sentence patterns, this current system is complicated enough for our version.\n",
    "\n",
    "Restart the program and try, in order:\n",
    "\n",
    "1. `mother university`\n",
    "2. `university mother`\n",
    "3. `smother`\n",
    "4. `bye`\n",
    "\n",
    "Predict all four replies with your partner before running them and then paste the actual replies below.\n",
    "\n",
    "If the first two messages give the same reply, check where you put `break`. If `smother` gets a reply about family, check that you are matching the whole word against a dictionary key."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-022",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 3.3.1\n",
    "PASTE OUTPUT HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-023",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"10\" hidden></span>\n",
    "\n",
    "## 4 Put EDIZA to the test\n",
    "\n",
    "Time to check that all the pieces work together. Restart EDIZA and enter the following in **one conversation**, in this order:\n",
    "\n",
    "1. `Hello, EDINBURGH!`\n",
    "2. `It's not bad, I go to university here.`\n",
    "3. `?!?!?!?!`\n",
    "4. `I wish my father would come to help me.`\n",
    "5. `They're all right though sometimes they smother me a bit.`\n",
    "6. `Annoyed, I guess?`\n",
    "7. `They're just a lot.`\n",
    "8. `BYE!`\n",
    "\n",
    "Paste the eight replies below. Check for yourself that the greeting appeared once and the program ended after the last reply. This conversation exercises punctuation, capital letters, empty input, keyword priority, whole words, and the return to the start of the general-response list."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-024",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 4.0.1\n",
    "PASTE OUTPUT HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-025",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"11\" hidden></span>\n",
    "\n",
    "### 4.1 Does it understand us?\n",
    "\n",
    "Now try `I have no father`, or invent another sentence whose meaning doesn't fit EDIZA's reply. What information would it need to give a more appropriate answer?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-4-1-first-discussion",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 4.1.1\n",
    "WRITE NOTES HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-4-1-eliza-effect",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"12\" hidden></span>\n",
    "\n",
    "Already back in the day, Weizenbaum was struck by how readily people treated ELIZA as more than a program. The **ELIZA effect** describes our tendency to attribute understanding to a computer on the basis of its responses. Unlike the Barnum effect's broad personal descriptions, this is about the abilities we read into the machine itself.\n",
    "\n",
    "You know every rule in EDIZA. Even so, did any reply feel thoughtful? Record one example where its rule failed and explain why the code chose that reply. Then add a sentence about what made a reply seem understanding, if any did."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-026",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 4.1.2\n",
    "WRITE NOTES HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-027",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"13\" hidden></span>\n",
    "\n",
    "## 5 Bonus Tasks\n",
    "\n",
    "Well done, that was the core part of the tutorial! At the end of this tutorial, your tutor will lead a group discussion on what you found. If there's still time left, choose whichever of the following tasks you find most interesting. You can do them in any order.\n",
    "\n",
    "### 5.1 Give EDIZA something to reflect on\n",
    "\n",
    "ELIZA could build a reply from parts of the user's sentence, instead of only choosing a fixed response. Let's give EDIZA a small version of that ability.\n",
    "\n",
    "For messages with at least two words whose first word is `i`, make EDIZA turn the sentence into a question. For example:\n",
    "\n",
    "```text\n",
    "You: I am worried about my timetable\n",
    "EDIZA: Why do you say you are worried about your timetable?\n",
    "```\n",
    "\n",
    "Here are some hints to get you started:\n",
    "\n",
    "1. Make a dictionary mapping `i` to `you`, `am` to `are`, `my` to `your`, and `me` to `you`.\n",
    "2. After collecting the words, check their length and their first item. Give this new rule priority over the keyword rules.\n",
    "3. Start an empty string and loop through the words. Look up replacements in your dictionary when a key matches and keep other words as they are.\n",
    "4. Build the reflected sentence using string concatenation and spaces. Use `.strip()` to remove any leftover whitespace.\n",
    "5. Put <code>Why do you say&nbsp;</code> before it and `?` after it before printing the full reply.\n",
    "\n",
    "Test an example of a sentence starting with _I_ as well as examples of other sentences to verify your previous code still works as intended. Then find a sentence where these mechanical substitutions produce awkward grammar or a silly question. Record your changes and tests below. Can you improve the rule without spoiling a case that already worked?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-028",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 5.1.1\n",
    "WRITE NOTES HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-029",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"13\" hidden></span>\n",
    "\n",
    "### 5.2 Whatever happened to symbolic AI?\n",
    "\n",
    "The popularity of ELIZA garnered much excitement: if a small collection of rules can produce something that resembles a conversation, can we create a system that has so many rules that it becomes a \"true\" AI?\n",
    "\n",
    "Alas, building and maintaining large collections of rules proved more difficult than expected, and after the initial hype, symbolic AI lost prominence in many areas.\n",
    "\n",
    "Find out:\n",
    "\n",
    "- Why was creating symbolic AI systems so difficult?\n",
    "- Where are symbolic approaches still used today?\n",
    "\n",
    "The [Stanford Encyclopedia of Philosophy's article on artificial intelligence](https://plato.stanford.edu/entries/artificial-intelligence/) is a good starting point. Find at least one additional source, note when each was written, and write your findings below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-030",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 5.2.1\n",
    "WRITE NOTES HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-031",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"13\" hidden></span>\n",
    "\n",
    "### 5.3 A listener, or the appearance of one?\n",
    "\n",
    "Weizenbaum described people becoming absorbed in ELIZA conversations, including [asking for privacy while talking to it](https://spectrum.ieee.org/why-people-demanded-privacy-to-confide-in-the-worlds-first-chatbot). Take a minute to read the (short) article.\n",
    "\n",
    "Imagine someone wants to put EDIZA on a university website as a place for students to talk when they are lonely or worried. Discuss:\n",
    "\n",
    "- Why might someone feel listened to, even if they know how the program works? How do the ELIZA effect and the Barnum effect help explain different parts of that experience?\n",
    "- What might be useful about that interaction, and what psychological or ethical problems could arise if someone relied on it?\n",
    "- What should a user be told about the program's abilities and about who can see or retain their messages?\n",
    "\n",
    "Propose **two concrete design features** for the imagined website and explain what each would help with."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ediza-032",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%check 5.3.1\n",
    "WRITE NOTES HERE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ediza-033",
   "metadata": {},
   "source": [
    "<span class=\"tutorial-section-marker\" data-tutorial-section=\"13\" hidden></span>\n",
    "\n",
    "## 6 Summary\n",
    "\n",
    "Well done! You have built a working chatbot in a Python file, tested it in VS Code, and explored why a program with a small collection of rules can sometimes seem more capable than it is.\n",
    "\n",
    "In doing so, we've combined almost all material from lectures 1 through 5. Quite complexly so, actually: at its deepest, your program might contain an if statement inside of an if statement inside of a for loop inside of an if statement inside of a while loop! Did you manage to keep all the indentation straight?\n",
    "\n",
    "Some key terms to take away:\n",
    "\n",
    "<dl>\n",
    "    <dt><b>Symbolic AI</b></dt>\n",
    "    <dd>An approach to AI that represents information explicitly using symbols and rules for manipulating them.</dd>\n",
    "    <dt><b>ELIZA</b></dt>\n",
    "    <dd>Joseph Weizenbaum's early chatbot, described in 1966. Its DOCTOR script used patterns and response rules to imitate aspects of a psychotherapist's conversation.</dd>\n",
    "    <dt><b>Tokenisation</b></dt>\n",
    "    <dd>Breaking text into smaller pieces, called tokens, for a program to process. EDIZA uses words as tokens; other systems may use smaller pieces.</dd>\n",
    "    <dt><b>Barnum effect</b></dt>\n",
    "    <dd>Treating a broad personal description as if it were specifically accurate about you.</dd>\n",
    "    <dt><b>ELIZA effect</b></dt>\n",
    "    <dd>Attributing understanding to a computer because of the responses it gives.</dd>\n",
    "</dl>\n",
    "\n",
    "Once again, these terms will not be assessed on the class tests or assignments. They're good to be aware of, however. Despite the massive improvements in the underlying technology, the rise of large language model-driven AI has meant that the Barnum and ELIZA effects are more relevant than ever.\n",
    "\n",
    "### Sources and further reading\n",
    "\n",
    "The teaching sequence draws on Mark Guzdial's [Classical AI and Chatbots slides](https://docs.google.com/presentation/d/1XljSt48WsV5stfJVs-AndCeWj_urQVrJPBckgNSPMW0/edit?slide=id.p#slide=id.p). Shout-out to Mark for making his teaching materials available online (and buying me dinner in Madrid).\n",
    "\n",
    "- [Stanford Encyclopedia of Philosophy: Artificial Intelligence](https://plato.stanford.edu/entries/artificial-intelligence/).\n",
    "- Joseph Weizenbaum (1966), [ELIZA—a computer program for the study of natural language communication between man and machine](https://doi.org/10.1145/365153.365168).\n",
    "- [ELIZA Archaeology](https://sites.google.com/view/elizaarchaeology/), for more on the original program and its reconstruction.\n",
    "- [ELIZA on mass:werk](https://www.masswerk.at/elizabot/), an online recreation to try.\n",
    "- Bertram R. Forer (1949), [The fallacy of personal validation: A classroom demonstration of gullibility](https://doi.org/10.1037/h0059240).\n",
    "- [APA Dictionary of Psychology: Barnum effect](https://dictionary.apa.org/barnum-effect).\n",
    "- Oscar Schwartz (2019), [Why People Demanded Privacy to Confide in the World’s First Chatbot](https://spectrum.ieee.org/why-people-demanded-privacy-to-confide-in-the-worlds-first-chatbot).\n",
    "\n",
    "---\n",
    "\n",
    "### Start over\n",
    "\n",
    "Use this button to erase every answer and begin the tutorial again.\n",
    "\n",
    "<button type=\"button\" data-tutorial-reset style=\"padding:8px 14px;border:1px solid #b91c1c;border-radius:6px;background:#dc2626;color:white;font-weight:600;cursor:pointer\">Reset all tutorial progress</button>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
