Skip to content

Prakshal-Jain/learnbot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

124 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

LearnBot

Record a desktop task once. Replay it forever.

LearnBot watches you perform a task on a Linux desktop (via VNC), uses Claude to generalize the recording into a reusable task definition, and replays it autonomously using a multi-layer element location engine. It's desktop RPA that learns from demonstration instead of manual scripting.

Quick Start

# On EC2 (Ubuntu + XFCE + VNC)
cd learnbot && uv sync
uv run learnbot setup          # configure DISPLAY, AT-SPI, API key
uv run learnbot record -y      # perform your task, Ctrl+C to stop
uv run learnbot list           # see generated tasks
uv run learnbot play "My Task" # replay it

How It Works

Record (watch user) --> Analyze (Claude) --> Replay (execute autonomously)
     |                       |                       |
  Screenshots          TaskDefinition           Multi-layer
  + AT-SPI data        with steps               element location
  + Input events       (role, name, bbox)        + sanity checks

1. Record β€” Captures screenshots, accessibility tree snapshots (AT-SPI), and input events as you perform a task. Uses a producer-consumer architecture: AT-SPI listeners on the main thread feed events to a recording loop that streams frames to disk.

2. Analyze β€” Sends interleaved screenshots and event data to Claude Sonnet 4.5, which extracts the clean execution path: filtering noise (false starts, retries, exploratory navigation) while preserving every prerequisite step needed to replay from a clean desktop. Prefers keyboard shortcuts (Ctrl+L, Ctrl+V) over visual clicks for reliability.

3. Replay β€” Executes each step using a 7-layer element location fallback chain, with mandatory sanity checks that detect failures even when --no-verify is set.

Installation

Prerequisites

  • Ubuntu 22.04+ with XFCE desktop
  • TigerVNC or KasmVNC for remote access
  • Python 3.11+
  • uv package manager
  • An Anthropic API key (for Claude)

EC2 Deployment (Primary)

# System dependencies
sudo apt install python3-pyatspi xdotool at-spi2-core

# Clone and install
git clone https://github.com/Prakshal-Jain/learnbot.git
cd learnbot
uv sync

# Interactive setup (configures DISPLAY, DBUS, AT-SPI, API key)
uv run learnbot setup

learnbot setup handles:

  • Auto-detecting DISPLAY from /tmp/.X11-unix/ (prefers :1 for VNC)
  • Inheriting DBUS and AT-SPI session vars from xfce4-session
  • Setting GTK_MODULES=gail:atk-bridge (mandatory for GTK app accessibility)
  • Prompting for your ANTHROPIC_API_KEY
  • Symlinking system pyatspi and gi packages into the venv (no PyPI wheel exists)
  • Writing all vars to ~/.learnbot/env.sh (auto-loaded on every CLI invocation)

After setup, restart any running GTK apps so they pick up the accessibility bridge:

thunar --quit && GTK_MODULES=gail:atk-bridge thunar &

Local Development

cd learnbot
uv sync --group dev

# Tests mock all system deps β€” works on macOS/Linux without VNC
uv run python -m pytest tests/ -q --ignore=tests/test_ai_analyze.py

CLI Reference

learnbot setup

Interactive environment configuration. Must be run once on a fresh EC2 instance before any other command.

uv run learnbot setup

Validates: xdotool installed, pyatspi importable, AT-SPI daemon running, screenshots capturable, storage directory writable.

learnbot record

Record a desktop task. Start recording, perform the task in VNC, then press Ctrl+C to stop.

uv run learnbot record           # interactive mode (prompts for description)
uv run learnbot record -y        # non-interactive (auto-analyze, skip prompts)
Flag Description
-y Skip all interactive prompts. Auto-analyze after recording if API key is set.

What gets captured per frame:

  • Full-screen and window-cropped screenshots (PNG)
  • AT-SPI focused element + siblings (role, name, bounding box)
  • Input event (click coordinates, key pressed, scroll delta)
  • Window title and geometry
  • All coordinates stored as window-relative (screen-absolute minus window origin)

Keyboard handling:

  • Printable characters accumulate in a buffer
  • Buffer flushes after 300ms of silence or on a modifier key (Ctrl, Alt, Super)
  • Shift+char = uppercase letter (not a hotkey)
  • Modifier combos (Ctrl+S, Alt+F4) are captured as hotkey events

Recordings stream to ~/.learnbot/recordings/<session-id>/ as individual frame JSON files β€” no in-memory accumulation, safe against crashes.

learnbot analyze

Re-analyze an existing recording with Claude. Useful after prompt improvements or to re-generate a task definition.

uv run learnbot analyze <session-id>
uv run learnbot analyze <session-id> -y   # skip review prompt
Flag Description
-y Auto-save without prompting for review

For complex recordings (>10 events or multiple windows), prompts for a natural-language description and success criteria to give Claude better context.

Pipeline:

  1. Load frames from session directory
  2. Split at window-change boundaries into chunks
  3. Cap at 5 chunks (merge if rapid window switching)
  4. Send each chunk to Claude with screenshots + serialized events
  5. Consolidate per-chunk steps into a single TaskDefinition
  6. Validate against Pydantic schema (auto-retry on parse failure)

learnbot list

Display all saved tasks in a table.

uv run learnbot list

Shows: task name, creation date, last run date, run count, number of steps.

learnbot play

Replay a task step by step with a live Rich progress table.

uv run learnbot play "My Task"                    # by name
uv run learnbot play abc123-def456                 # by task ID
uv run learnbot play "My Task" --no-verify         # skip Claude verification
uv run learnbot play "My Task" --timeout 60        # 60s per step
uv run learnbot play "My Task" --confidence 0.9    # stricter image matching
Flag Default Description
--no-verify off Skip Claude Haiku verification and AT-SPI state polling. Mandatory sanity checks (screenshot diff, window title) still run.
--timeout 30 Per-step timeout in seconds, including retries
--confidence 0.85 Minimum confidence threshold for OpenCV image template matching
--no-evolve off (Planned) Disable task evolution β€” replay exactly as defined, no script graduation

Exit codes: 0 = all steps passed, 1 = any step failed.

What happens per step:

  1. Take pre-execution screenshot
  2. Execute the step (locate element via fallback chain + inject input)
  3. Run mandatory sanity checks (screenshot diff for clicks, window title for switches)
  4. If sanity fails: retry up to 2 times, then mark step FAILED
  5. If --verify (default): additionally run AT-SPI state polling + Claude Haiku screenshot check
  6. Record step outcome (passed/failed, location method used, details)

Architecture

Recording Pipeline

                    AT-SPI GLib Main Loop (main thread)
                              |
                    InputListener (producer)
                    - Mouse clicks (pyatspi mouse:button)
                    - Keyboard events (pyatspi key events)
                    - Scroll events
                    - Window change detection (polling)
                              |
                        queue.Queue
                              |
                    RecordingLoop (consumer thread)
                    - Screenshot capture (mss)
                    - AT-SPI snapshot (focused + siblings)
                    - Window geometry (xdotool)
                    - Stream frame JSON to disk

AT-SPI runs on the main thread because GLib's event loop requires it. The recording loop runs on a dedicated thread, consuming events from a queue and writing frames to disk immediately.

VNC keyboard events are captured through AT-SPI listeners, not pynput's X RECORD extension (which doesn't work over VNC).

AI Analysis

Claude receives interleaved screenshots and event text for each frame:

[Screenshot PNG β€” base64]
Frame 1: Action: click at (245, 130) button=left | target_role=push button | target_name=Save | window=document.txt - Mousepad

[Screenshot PNG β€” base64]
Frame 2: Action: keydown key=ctrl+s | window=document.txt - Mousepad

The system prompt instructs Claude to:

  1. Extract the clean path β€” filter mistakes, retries, and redundant actions
  2. Preserve every prerequisite β€” URL navigation, file opening, search queries. The test: "if you removed this step and replayed from scratch on a clean desktop, would later steps fail?"
  3. Prefer AT-SPI data β€” use target_role + target_name over coordinates for resilient targeting
  4. Prefer keyboard shortcuts β€” Ctrl+L for address bar, Ctrl+V for paste, Ctrl+S for save
  5. Include every window switch β€” each app transition must be an explicit window_change step with the application name in the target

For multi-window recordings, frames are split into chunks at window boundaries and analyzed separately, then consolidated into a single task.

What the Replay Engine Receives

The replay engine receives the TaskDefinition produced by analysis, plus access to the original recording for template extraction:

From the TaskDefinition (Claude's output):

Field Used By Purpose
input_type Runner Determines action type (click, type, key, window_change, etc.)
input_value Executor Text to type, key to press, "right" for right-click
target_role AT-SPI, Playwright Semantic element identity (e.g., "push button", "menu item")
target_name AT-SPI, Playwright, OCR Element name (e.g., "Save", "Copy link address")
target_bbox Coordinate fallback Window-relative [x, y, w, h] β€” last resort position
expected_state Verifier AT-SPI state to verify after action (e.g., "focused")
action Logging, UI Human-readable step description

From the original recording (via source_session_id):

Data Used By Purpose
Screenshots Image template layer Cropped at target_bbox to create per-step template PNGs for OpenCV matching

The analysis steps are the primary input. The recording screenshots are only used for image template extraction β€” the replay engine does not re-read the raw events or frame JSON files.

Element Location β€” 7-Layer Fallback

When replaying a step that requires finding a UI element, the executor tries each layer in order:

# Layer Confidence How It Works Best For
1 Playwright (CDP) High Connects to Chrome DevTools Protocol, queries DOM by ARIA role + name Chrome in-page elements (links, buttons, inputs)
2 AT-SPI High Walks the desktop accessibility tree, matches by role + name GTK/Qt native app elements (menus, buttons, text fields)
3 Computer Use (Claude) Medium Sends screenshot to Claude's Computer Use API, extracts click coordinates Any visible element when AT-SPI/Playwright fail
4 EasyOCR Medium Runs text detection on screenshot, matches target_name against detected text Text labels without accessibility info
5 Image Template (OpenCV) Low cv2.matchTemplate with TM_CCOEFF_NORMED against pre-recorded element crops Visually stable icons/buttons across runs
6 Coordinate Low Window-relative bbox from recording + current window origin Last resort when all semantic methods fail

For window_change steps, xdotool searches by window name. If the window isn't found, the executor auto-launches the application (Chrome, Terminal, Thunar, Mousepad, Firefox) and waits up to 30s for it to appear.

Location confidence is returned with each result:

  • High (playwright, atspi, xdotool): Element found by semantic identity β€” resilient to layout changes
  • Medium (computer_use, ocr): Element found by visual/text analysis β€” usually correct but not guaranteed
  • Low (image, coordinate): Element found by pixel matching or recorded position β€” fragile

When coordinate fallback is used for a step that has semantic targets (role/name), a warning is logged.

Input Dispatch

After locating the target element, the executor injects the appropriate input:

Input Type Action
click pyautogui.click(x, y) β€” or rightClick if input_value="right"
double_click pyautogui.doubleClick(x, y)
type Focus target (click or Ctrl+L for address bar) then pyautogui.write(text)
key pyautogui.press(key) or pyautogui.hotkey(*parts) for combos like ctrl+s
scroll pyautogui.scroll(amount) β€” positive=up, negative=down
window_change xdotool search --name <target> && windowactivate with auto-launch

Special handling:

  • Browser address bar: Ctrl+L instead of clicking (position varies with window size)
  • Text editors without a located target: click center-bottom of active window to ensure focus
  • XFCE submenu headers (role="menu"): hover instead of click (submenus open on hover in GTK)
  • Right-click context menus: coordinate fallback still enabled as last resort for menu items

Verification & Sanity Checks

                    Step Executed
                         |
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  MANDATORY (always)  β”‚
              β”‚                      β”‚
              β”‚  click/double_click: β”‚
              β”‚    screenshot diff   β”‚
              β”‚    (similarity check)β”‚
              β”‚                      β”‚
              β”‚  window_change:      β”‚
              β”‚    title match       β”‚
              β”‚    (xdotool verify)  β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                    Sanity OK?
                    /        \
                  No          Yes
                  |             |
               Retry       β”Œβ”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              (up to 2x)   β”‚ --verify only  β”‚
                           β”‚                β”‚
                           β”‚ AT-SPI state   β”‚
                           β”‚   polling      β”‚
                           β”‚                β”‚
                           β”‚ Claude Haiku   β”‚
                           β”‚   screenshot   β”‚
                           β”‚   check        β”‚
                           β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                          Step Passed

Mandatory sanity checks run on every step, regardless of --no-verify:

  • Screenshot diff (click/double_click): Compares pre and post screenshots using OpenCV connected component analysis on the grayscale diff. Small noise (cursor blink, clock updates) is filtered by requiring changed regions to be at least 500px in area. Similarity thresholds are tiered by location confidence: 0.999 for high-confidence locations (AT-SPI, Playwright), 0.998 for low-confidence (coordinate fallback). If similarity exceeds the threshold, the click had no visible effect β€” retry up to 2 times, then mark the step as FAILED.
  • Window title verification (window_change): Runs xdotool getactivewindow getwindowname and checks if the expected window name appears in the active title. Handles split parts (e.g., "ubuntu - Thunar" matches "Thunar").

Optional verification (enabled by default, disabled with --no-verify):

  • AT-SPI state polling: If the step has an expected_state (e.g., "focused", "checked"), polls the accessibility tree with a timeout. Used for confirming state transitions after clicks.
  • Claude Haiku screenshot check: Sends the current screenshot to Claude Haiku and asks whether the step's action appears to have succeeded. Returns pass/fail/retry verdict. On "retry", re-executes the step (up to 2 retries).
Check --verify (default) --no-verify
Screenshot diff after click YES YES
Window title after switch YES YES
AT-SPI state polling YES NO
Claude Haiku screenshot YES NO

Task Evolution (Planned)

Over repeated successful runs, LearnBot will detect step sequences that can be replaced with direct script commands and graduate them from UI automation to deterministic execution.

Example: The step sequence "open terminal β†’ type curl https://api.example.com β†’ press Enter" becomes a single subprocess.run(["curl", "https://api.example.com"]) call β€” faster, cheaper, and more reliable than clicking through the UI.

Scriptable patterns:

  • Terminal commands (type + Enter) β†’ subprocess.run()
  • URL navigation (address bar + type URL + Enter) β†’ google-chrome "https://..."
  • File operations (Thunar navigation) β†’ direct filesystem calls
  • Clipboard operations β†’ xclip / xsel

--no-evolve flag: Disables all task evolution. The task replays exactly as originally defined β€” no script graduation, no step replacements, no modifications to the TaskDefinition. Use this when deterministic, unchanged replay matters more than optimization.

Evolution history will be tracked in SQLite with rollback capability β€” graduated steps retain both their UI automation path and script path.

Auto-Launch

When a window_change step targets an application that isn't running, the executor auto-launches it:

Keyword in target_name Launch Command
terminal xfce4-terminal --default-working-directory $HOME
chrome, google chrome google-chrome --no-first-run --force-renderer-accessibility --password-store=basic --remote-debugging-port=9222
thunar, file manager thunar
mousepad, text editor mousepad
firefox firefox
chromium chromium-browser

The executor waits up to 30s for browser windows or 15s for other apps, polling via xdotool search. Stale/zombie window IDs are tracked and excluded from matching.

Chrome is launched with --force-renderer-accessibility (required for AT-SPI to see web page elements) and --remote-debugging-port=9222 (required for Playwright CDP connection).

Data Models

TaskDefinition

The output of AI analysis β€” a reusable, replayable task.

TaskDefinition(
    task_id="a1b2c3d4-...",             # UUID, auto-generated
    name="Paste YouTube URL into editor", # short imperative phrase
    description="Copy a video link...",   # 1-2 sentence summary
    source_session_id="e5f6g7h8-...",     # recording this came from
    success_criteria="...",               # user-defined validation text
    steps=[...],                          # ordered TaskStep list
    run_count=5,                          # number of times replayed
    last_run_at=1710500000.0,             # timestamp of last replay
)

TaskStep

A single action in a task.

TaskStep(
    step_id="...",                          # UUID
    order=1,                                # 1-based sequence number
    action="Click the Save button",         # human-readable description
    target_role="push button",              # AT-SPI role (semantic identifier)
    target_name="Save",                     # AT-SPI accessible name
    target_bbox=(120, 45, 80, 30),          # window-relative (x, y, width, height)
    input_type="click",                     # click|double_click|type|key|scroll|window_change
    input_value=None,                       # "right" for right-click, text for type, key name
    expected_state="focused",               # AT-SPI state to verify after action
    verified=False,                         # whether step has been deterministically verified
    copilot_intervention=None,              # human annotation (planned)
)

input_type values:

Type input_value target needed? Description
click None (left) or "right" Yes Single click at target position
double_click None Yes Double click at target position
type Text to type Optional Type text (focus target first if available)
key Key name, e.g. "Return", "ctrl+s" Optional Press key or hotkey combo
scroll Amount as string, e.g. "-3" Optional Scroll at target or current position
window_change None No (uses target_name) Switch to or launch application

RunResult

The outcome of a replay.

RunResult(
    run_id="...",              # UUID
    task_id="...",             # which task was replayed
    status="passed",           # passed|failed|running
    started_at=1710500000.0,
    ended_at=1710500012.3,
    total_duration=12.3,       # seconds
    step_outcomes=[
        {
            "step_id": "...",
            "order": 1,
            "action": "Activate Chrome",
            "status": "passed",
            "location_method": "xdotool",
            "details": "step completed successfully",
        },
        {
            "step_id": "...",
            "order": 2,
            "action": "Right-click video thumbnail",
            "status": "failed",
            "location_method": "coordinate",
            "details": "sanity checks failed after retries",
        },
    ],
)

RecordingFrame

A single captured moment during recording.

RecordingFrame(
    frame_id="...",                        # UUID
    timestamp=1710500000.123,              # capture time
    trigger="click",                       # what triggered this frame
    full_screenshot_path="/path/to/full.png",
    window_screenshot_path="/path/to/window.png",
    window_title="ubuntu - Thunar",
    window_geometry=(100, 50, 800, 600),   # x, y, width, height
    input_event=InputEvent(...),           # the user action
    a11y_focused=A11yNode(...),            # AT-SPI focused element
    a11y_siblings=[A11yNode(...)],         # sibling elements
)

Storage Layout

~/.learnbot/
  env.sh                          # Auto-loaded environment variables
  db/
    learnbot.db                   # SQLite database
  recordings/
    <session-id>/
      session.json                # RecordingSession metadata
      frame_0.json ... frame_N.json   # Per-frame capture data
      screenshot_0.png ...        # Full screenshots
      window_0.png ...            # Window-cropped screenshots
  tasks/
    <task-id>.json                # TaskDefinition (from analysis)
  templates/
    <task-id>/
      <step-id>.png               # Cropped element images for template matching

SQLite tables:

  • recording_sessions β€” session metadata (ID, name, created_at, frame/event counts)
  • task_definitions β€” task metadata + JSON file path pointer
  • run_results β€” run outcomes with step_outcomes JSON blob

Known Limitations

  • AT-SPI requires GTK_MODULES: Without GTK_MODULES=gail:atk-bridge, GTK apps (Thunar, Mousepad) expose no accessibility tree. learnbot setup configures this automatically.
  • Chrome needs special flags: --force-renderer-accessibility for AT-SPI visibility, --remote-debugging-port=9222 for Playwright CDP. The auto-launcher includes both.
  • XFCE submenus open on hover: If AT-SPI matches a "menu" role (not "menu item"), the executor hovers instead of clicking to avoid dismissing the parent menu.
  • super+d is a toggle: In XFCE, pressing super+d in show-desktop mode restores windows. The executor minimizes all visible windows first to avoid this.
  • AT-SPI offscreen coordinates: Collapsed submenu items report coords at -2147483648. Rejected via bounds check before any click.
  • Auto-launch opens clean state: Apps launch without file arguments (e.g., mousepad not mousepad file.txt). Tasks requiring a specific file must include the file-opening steps.
  • VNC keyboard capture: AT-SPI listeners capture VNC keyboard input. pynput's X RECORD extension does not work over VNC.
  • pyatspi is system-only: No PyPI wheel exists β€” installed via apt and symlinked into the venv by learnbot setup.
  • Coordinate fallback is fragile: When all semantic locators fail, the executor falls back to recorded coordinates. These break if window position or size changes. The sanity check catches this (no visible effect = retry + fail).

Why Not Just Use Claude Computer Use?

LearnBot uses Claude's Computer Use API as one layer in the fallback chain (Layer 3), but not as the primary execution engine. Here's why:

LearnBot (AT-SPI + pyautogui) Pure Computer Use
Cost per step Free (AT-SPI/Playwright) or ~$0.01 (vision fallback) ~$0.01-0.05 per action
Latency per step Milliseconds (AT-SPI) 2-5 seconds (API round-trip)
Determinism AT-SPI match = same result every time May click slightly different spots
11-step task ~2s total (AT-SPI path) ~30-60s (API time alone)
Offline capable Yes (AT-SPI/Playwright/OCR/image/coordinate) No

The recording gives the system something Computer Use lacks: a concrete step-by-step plan derived from watching a human, with semantic element identifiers (AT-SPI role + name) that survive UI layout changes.

Computer Use is the fallback brain β€” it handles steps that AT-SPI and Playwright can't locate. The planned self-healing system will use it more deeply, sending failure context to Claude for adaptive recovery.

Competitive Moat

The architecture (record -> AI analyze -> replay) is straightforward to replicate. The moat develops from:

  • Accumulated edge-case knowledge: Months of real-world debugging β€” keysym mapping, XFCE submenu hover behavior, Chrome AT-SPI quirks, xfdesktop activation, stale window tracking, coordinate overflow prevention. A competitor starts at zero.
  • Data flywheel: Every replay failure teaches the system what goes wrong. Recordings + failure modes across diverse desktops and applications make the analysis and self-healing dramatically better over time.
  • Robustness compounding: Each bug fix makes the system more reliable. Reliability begets trust begets usage begets more edge cases found and fixed.
  • Task evolution: Tasks that start as UI automation gradually evolve into direct script execution β€” faster, cheaper, and more reliable with each successful run. This optimization loop is hard to replicate without the underlying recording + analysis infrastructure.
  • Domain-specific task libraries: Pre-built, tested task definitions for common workflows become a network effect when shared across users.

The defensibility isn't in the architecture β€” it's in the accumulated knowledge of how desktop automation breaks in the real world, and the data loop that continuously improves it.

Development

# Run all tests (217 tests, ~16s)
cd learnbot
uv run python -m pytest tests/ -q --ignore=tests/test_ai_analyze.py

# Run specific test files
uv run python -m pytest tests/test_runner.py tests/test_executor.py -v

# Deploy to EC2
git subtree push --prefix=learnbot origin main
ssh ubuntu@<EC2-IP> "cd ~/learnbot && git pull && uv sync"

Pre-commit hooks enforce ruff-format and ruff linting on every commit.

Dependencies

Core: pydantic, pillow, click, rich, mss, anthropic, pyautogui, opencv-python-headless, python-xlib, pynput

Optional: playwright (Chrome CDP), easyocr (text detection)

Dev: pytest, ruff, pre-commit

Project Structure

src/learnbot/
  ai/           # Claude API client, analysis prompts, cost tracking
  capture/      # Screenshot (mss), AT-SPI snapshots, window geometry
  cli/          # Click CLI: setup, record, analyze, list, play (lazy-loaded)
  models/       # Pydantic v2: RecordingSession, TaskDefinition, RunResult
  platform/     # AT-SPI availability checks
  recording/    # Producer-consumer: InputListener + RecordingLoop
  replay/       # Executor (7-layer location), Runner (retry + sanity), Verifier
  storage/      # SQLite + filesystem CRUD for recordings, tasks, runs
tests/          # 217 pytest tests (mock all system deps β€” run anywhere)
scripts/        # E2E test suite, utility scripts

About

Learn by seeing.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors