ShinyLive ❤️ Mesa Tutorial

POC of a Mesa tutorial running in the browser with ShinyLive for Python

This is my vibe coding guide on how I built a POC of concept for running Mesa app (their tutorial) in ShinyLive app so it can run in the browser without a front end.
Shinylive for python
Model thinking
Simulation
Author

Oren Bochman

Published

Thursday, October 2, 2025

Keywords

Shiny, Shinylive for python, Mesa, Agent-Based Modeling, POC, Wealth distribution, Altair, Pyodide

In this is the Mesa tutorial integrated into a shiny app and running via Pyodide in the browser from a static backend. I had some background with both shiny and mesa. However, it was a while since I coded with either and I realized that both had some major changes. I actually fixed some mesa bugs and made a custom version for this blog. But Mesa is getting lots of changes and so I needed something simple for the POC. I wanted to use a co-pilot for getting up to speed on the changes and for filling in the gaps about things like reactive programming which I also have some experience but less so in python. So In this post we will cover

  1. The challenges of adapting Mesa to work with the reactive architecture required by Shiny. Shiny apps are usually split into UI and Server parts. We want to be able to have good control of the mesa sim running in the server without exposing it directly to the UI.
  2. For a long time the Shiny-Quarto integration raised issues which turned out to be show stoppers. I cover how I faced and solved them some of these into notes.
  3. Vibe coding Working with a ai-copilot was a major requirement for this project. I find that the ai can help with a quick start but also may become a liability if not handled well. I realize, many people are facing this type of challenge and I am trying to also indicate how I used the copilot to overcome some of my own weaknesses from previous projects.
TL;DR: A POC for Getting Mesa Agent-Based Modeling to run in the browser via ShinyLive and Altair.

This is a proof of concept (POC) for running the Mesa tutorial in a Shiny app using ShinyLive for Python.

I’ve created hundreds of dashboards using BI tools but always considered Shiny as my goto solution for truly custom dashboards projects. Only I moved to python a decade or so back and Shiny was R only. Now with the new Shiny for Python, I can finally use Shiny with Python. Also Shiny needed a backend which is not ideal for lightweight projects like MVP and their POC. But with ShinyLive for Python it is possible to run on a static site with everything running in the browser.

On the way I also started using Mesa for ABM and Altair for very lightweight declarative widgets. But these tools did not play. Until now Mesa also had a clunky backend which required a server. But as this POC demonstrates we can do it all in python and run it in the server.

The POC

  • Chat GPT was able to write some small shiny and some mesa sims over the last three years. But getting shiny and mesa together was more that It could stomach when I asked for this. It would create a mesa lite clone from scratch instead when asked to create a hybrid solution.

  • So to make the POC I got started on the playground with shinylive.io. I quickly found out that since my last visit it was ok running MESA. Mesa had had since dropped some dependencies and Pyodide added support for scipy which Mesa depends on. (I actually made a version of mesa without this as only one function was used from scipy and I had created a tree line replacement.) This however required running everything locally which ended up a problematic proposition.

  • Moving on I was able to add the two main classes from the mesa tutorial, add the changes required and integrate with the shiny. I replaced the the plotting with altair using another demo. This required more changes like exposing the state as a dataframe. But I had something working in less than an hour.

  • I think that most of the time LLMs are glorified cut and paste solutions - they can rewrite bit and pieces early well but they need to be able to retrieve something that is close enough. Usually someone has coded something smarter and better than your next idea and the LLM has seen it in training. There is lots of mesa and shiny code out these. But probably nothing very helpful for chat GPT so it messed up. On the other hand making POCs for my idea has always been one of my core strength when it comes to coding. A second one is the ability to handle fairly complex systems. And one of my weakness is deviating from KISS.

  • Next steps were to get the initial demo fully functional. I had reactivity issues, extra code, no interactivity in the ui. The tutorial called for checking different setting of steps, agents and repetitions.

  • I added some ui for these as well a dashboard title. but it didn’t get picked up by the sim.

  • I was missing some annotation to make the sim function reactive.

  • I needed to access the UI sliders for the current values instead of using a global state

  • Once I had these in place it was more or less done.

  • I need to remove some extra bits of code and do the write up.

  • I had used GPT_5 as my copilot to get some minor fixes. It generally said great job just fix this line and gave a drop in replacement. In the next bit - the grid it would again be less helpful at coding and more as a troubleshooter.

Trouble shooting with a co-pilot
  • Java is well known for hundreds of line cascading error messages. This was exacerbated by an idiomatic/idiotic style of deeply nested anonymous constructs. I thought I liked Java for many years but felt that I was just waiting for the day I would design something soo much better to code in.
  • In this setting (quarto + filters + python + shinylive + mesa + altair + Pyodide) I quickly found that there were long cascading errors and some might be in the command line other in the shiny console with other yet in the browser’s console.
  • Instead of trying to decipher these issues I asked my co-pilot for help. This saved my eyes and sanity.
  • We come to a project with very limited human resources. The LLM can be either multiply and mentor or it it can become a demon that completely eat our limited capacity using complexity or via griefing ( not being helpful). It is up to us to figure out how to go with multiply and learn while avoiding any griefing process as soon as it starts.
  • Ask the co-pilot was able to help me identify these issues. If it can’t fix them this is a strong indicator indicator that you should step in. You are now in charge of the fix and you get to ask for advice on specific parts of your plan. This will save lots of time and get you more familiar with reading the code.
  • Always remember you are the boss, stakeholder etc and the co-pilot should be your side-kick.
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 600
#| components: [viewer] #[editor, viewer]

from shiny import reactive, render
from shiny.express import input, ui
from shinywidgets import render_altair

import numpy as np
import pandas as pd

import seaborn as sns
import altair as alt

import mesa


### UI for Agent Sim ##############################################

DEFAULT_RUNS = 100
DEFAULT_STEPS = 30
DEFAULT_AGENTS = 12

# Add page title and sidebar
ui.page_opts(title="Mesa Wealth Agents Tutorial in Shineylive for Python", fillable=True)


with ui.sidebar(open="desktop"):
    ui.input_slider("runs", "Runs", min=1, max=500, value=DEFAULT_RUNS)
    ui.input_slider("steps", "Steps per run", min=1, max=500, value=DEFAULT_STEPS)
    ui.input_slider("agents", "Agents", min=1, max=50, value=DEFAULT_AGENTS)
    ui.input_action_button("reset", "Reset")


### MESA MODEL ###################################################


class MoneyAgent(mesa.Agent):
    """An agent with fixed initial wealth."""

    def __init__(self, model):
        # Pass the parameters to the parent class.
        super().__init__(model)

        # Create the agent's variable and set the initial values.
        self.wealth = 1

    def exchange(self):
        # Verify agent has some wealth
        if self.wealth > 0:
            other_agent = self.random.choice(self.model.agents)
            if other_agent is not None:
                other_agent.wealth += 1
                self.wealth -= 1


class MoneyModel(mesa.Model):
    """A model with some number of agents."""

    def __init__(self, n):
        super().__init__()
        self.num_agents = n

        # Create agents
        MoneyAgent.create_agents(model=self, n=n)

    def step(self):
        """ Advance the model by one step."""
        # This function pseudo-randomly reorders the list of agent objects and
        # then iterates through calling the function passed in as the parameter
        self.agents.shuffle_do("exchange")


#ui.input_selectize("var", "Select variable", choices=["bill_length_mm", "body_mass_g"])


# ---------- RESET BEHAVIOR ----------
@reactive.effect
@reactive.event(input.reset)
def _reset_sliders():
    # These update_* helpers pick up the current session implicitly in Shiny Express
    ui.update_slider("runs", value=DEFAULT_RUNS)
    ui.update_slider("steps", value=DEFAULT_STEPS)
    ui.update_slider("agents", value=DEFAULT_AGENTS)

# ---------- SIMULATION (reactive) ----------
@reactive.calc
def sim_df():
    runs = input.runs()
    steps = input.steps()
    n = input.agents()

    all_wealth = []
    for _ in range(runs):
        model = MoneyModel(n)
        for _ in range(steps):
            model.step()
        # collect wealth after final step of this run
        all_wealth.extend(a.wealth for a in model.agents)

    return pd.DataFrame({"wealth": all_wealth})


# ---------- PLOT ----------
@render_altair
def hist():
    df = sim_df()  # depends on inputs via sim_df()
    return (
        alt.Chart(df, title="Wealth distribution")
        .mark_bar()
        .encode(x=alt.X("wealth:Q", bin=True, title="Wealth"),
                y=alt.Y("count()", title="Agents"))
        .properties(width="container", height=300)
        .interactive()
    )


## file: requirements.txt
shinyswatch
altair
anywidget
palmerpenguins
jsonschema
mesa
scipy
tqdm

About Wealth agents:

We run 1-500 simulations (runs) of 1-500 (steps) each with 1-50 (agents).

Each agent starts with one unit of wealth. At each step, every agent randomly selects another agent and gives them one unit of wealth if they have at least one unit to give.

After all runs are complete, we plot the distribution of wealth among all agents. Any change to the parameters re runs the simulation. Note that in ABM and RL we often run the simulation a number of times to avoid local view of the behavior. We then need to aggregate the data and if we do this well we get a deeper (more distributional view of the data.)


Linear Notes

  • I wanted this for a long time but couldn’t get it to work in quarto.
  • I recently asked GPT_5 for help buy it faked the mesa code. It worked ok but I felt cheated.
  • I knew I could do better. So I decided to try running MVP mesa model on the Shiny for Python website using the playground.
  • Shiny was ok installing mesa - a good sign
  • The tutorial worked more or less.
  • But it refused to render in my quarto project.
  • Getting it to work locally was 90% of the work
  • I had the Sine shiny app on my blog so I tried again there - it failed.
  • In fact this was just another failure in a long line of failures with shinylive.
  • I had many issues here is a summary from GPT_5 of the the main ones we troubleshooted together.

Moving parts:

  1. Quarto
  2. ShinyLive extension for Quarto
    1. install pip install shinylive --upgrade
    2. quarto add quarto-ext/shinylive
    3. modify _quarto.yml
    > filters:
    >  - shinylive    
    1. add to the quarto page frontmatter yml
    > filters:
    >  - shinylive
    1. add shinylive-python code blocks with the actual app
    2. to use more screen real estate wrap code with ::: {.column-screen-inset}
    3. Add dependency to requirements.txt in shinylive-python block
#| standalone: true
#| components: [editor, viewer]
## file: app.py
from shiny import App, ui render
from utils import square

# [App code here...]

## file: utils.py

def square(x, n):
    row = ui.div([x] * n)
    return ui.div([row] * n)

## file: requirements.txt
shinyswatch

## file: www/logo.png
## type: binary
iVBORw0KGgoAAAANSUhEUgAAACgAAA ...
  1. ShinyLive for Python (CLI, assets, Pyodide)
    1. install pip install shinylive shiny
    2. download assets tends to fail so I side-loaded it from shinylive repo
    3. It tends to time out so I used wget with retries and unpacked it.
    4. The shinylive expects it to be extracted.
cd ~/.cache/shinylive

wget -c -4 \
  --tries=0 \
  --waitretry=5 \
  --timeout=30 \
  --read-timeout=30 \
  --retry-connrefused \
  --show-progress \
  --server-response \
  'https://github.com/posit-dev/shinylive/releases/download/v0.10.5/shinylive-0.10.5.tar.gz'

tar -xzf shinylive-0.10.5.tar.gz
  1. Shiny for Python (behind the scenes)
  2. Altair for charts
    • add to requirements.txt per 1.
  3. Mesa for agent-based modeling (Heterogeneity)
  • The Mesa Tutorial gives us a layer for doing Agent Based Simulation of wealth distribution
  • I figured how to add it to the altair demo
  • I dropped some widgets.
  • I added new widgets per the tutorial request
  • I then needed to make things reactive - i.e. refresh when the simulation parameters change
    • Removed global vars for UI elements (replaced by default values)
    • Added annotations for the UI elements
    • Split chart rendering from simulation
    • Made these parts reactive as well.
    • Altair is great for small prototypes and quick visualizations. But crashes with large datasets. This is a known limitation but I have not fixed it in this version yet (todo: cap the number of row in the data frame, or aggregate so we don’t exceed the Altair limits)
  • The shiny app can also show an editor with all of the code. This is part of the meta data at the top of the code block. I have hidden it for now.

What Next Part 2 Adding the grid to Mesa

  1. In this part I quickly got the grid in place.
  2. I did not know how to handle a seaborn chart in shiny, it is actually simpler than altair which requires shinyextras
vibe coding - Roles switching

Vibe coding means being able to expediently switch roles between Project Manager, Lead dev, Trouble shooter etc. Expediency is primarily being on top of cost benefit of working with a co-pilot over even other devs.

The co-pilot is often an idiot-savant it knows a lot but can get stuck on tiny details. It is here that you can lose a lot of time. Rather than getting it to fix it you should ideally always be able to jump in and do the fix yourself. I used to work with the legendry “David Alouch” who said once “I never ask my workers to do anything I can’t do myself”.

If it knows a lot more than you do about some part of the project that is fine so long as you remember that you are the lead and you will quickly catch up.

  • It is good document what you want to do on two levels
    1. you need a plan,

    2. you need to remember many details

    3. you need to communicate what you are want to do next to your co-pilot.

    4. you want to split the MVP from other features

  • It smart to comment on the shortcoming of what has been done so far. You co-pilot can sometimes suggest quick or immediate fixes to things you don’t even want to think about because there are bigger issues.
  1. I want to be able to be able to switch between interactive mode on an off
    1. On - View the sim step by step
    2. Off run many sims. Turn of in
    3. See where we are
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 600
#| components: [viewer] #[editor, viewer]

_='''{.markdown} 
## Tasks:

1. ui: 
    1. [x] Sidebar toggle for Interactive mode 
    2. [x] Live counters: current_run, current_step, current_agent 
    3. [x] Grid and Heatmap updates after each step run reset 
2. backend 
    1. [x] Current State table updates live 
    2. [x] Current Wealth histogram (live) + Simulated Wealth histogram (by button)
    3. [x] Reset returns sliders to defaults and rebuilds model 
'''
import numpy as np
import pandas as pd
import seaborn as sns

from shiny.express import input, ui
from shinywidgets import render_altair
from shiny import reactive, render

from faicons import icon_svg as icon

# -------------------- Defaults --------------------
DEFAULT_RUNS = 30
DEFAULT_STEPS = 30
DEFAULT_AGENTS = 25
DEFAULT_GRID_W = 5
DEFAULT_GRID_H = 5

# -------------------- UI: page + sidebar --------------------
ui.page_opts(title="Mesa Wealth Agents Tutorial in ShinyLive (Python)", fillable=True)

with ui.sidebar(open="desktop"):
    ui.input_switch("interactive_mode", "Interactive mode", value=True)
    ui.help_text("Turn off to run in batch mode; turn on for step/run controls.")

    ui.input_slider("runs", "Runs", min=1, max=500, value=DEFAULT_RUNS)
    ui.input_slider("steps", "Steps per run", min=1, max=500, value=DEFAULT_STEPS)
    ui.input_slider("agents", "Agents", min=1, max=50, value=DEFAULT_AGENTS)
    ui.input_slider("grid_w", "Width", min=1, max=10, value=DEFAULT_GRID_W)
    ui.input_slider("grid_h", "Height", min=1, max=10, value=DEFAULT_GRID_H)

    ui.input_action_button("reset", "Reset")
    with ui.panel_conditional("input.interactive_mode"):
        ui.input_action_button("step_once", "Step once")
        ui.input_numeric("nsteps", "Run N steps", 10, min=1)
        ui.input_action_button("run_n", "Run N")
    ui.input_action_button("recompute_hist", "Recompute histogram")

# Reset sliders to defaults
@reactive.effect
@reactive.event(input.reset)
def _reset_sliders():
    ui.update_slider("runs", value=DEFAULT_RUNS)
    ui.update_slider("steps", value=DEFAULT_STEPS)
    ui.update_slider("agents", value=DEFAULT_AGENTS)
    ui.update_slider("grid_w", value=DEFAULT_GRID_W)
    ui.update_slider("grid_h", value=DEFAULT_GRID_H)

# -------------------- MESA model --------------------
import mesa
from mesa.discrete_space import CellAgent, OrthogonalMooreGrid

class MoneyAgent(CellAgent):
    def __init__(self, model, cell):
        super().__init__(model)
        self.cell = cell
        self.wealth = 1

    def move(self):
        new_cell = self.cell.neighborhood.select_random_cell()
        self.move_to(new_cell)

    def give_money(self):
        cellmates = [a for a in self.cell.agents if a is not self]
        if self.wealth > 0 and cellmates:
            other = self.random.choice(cellmates)
            other.wealth += 1
            self.wealth -= 1

class MoneyModel(mesa.Model):
    
    def __init__(self, n, width, height, seed=None):
        super().__init__(seed=seed)
        self.num_agents = n

        # Exposed counters state
        self.current_run = 0
        self.current_step = 0
        self.current_agent_id = None
        self.current_agent_index = None

        self.grid = OrthogonalMooreGrid(
            (width, height), torus=True, capacity=10, random=self.random
        )

        # Create agents (assumes CellAgent.create_agents helper exists in this env)
        _agents = MoneyAgent.create_agents(
            self,
            self.num_agents,
            self.random.choices(self.grid.all_cells.cells, k=self.num_agents),
        )

    def step(self):
        order = list(self.agents)
        self.random.shuffle(order)
        for idx, a in enumerate(order, start=1):
            self.current_agent_index = idx
            self.current_agent_id = a.unique_id
            a.move()
            a.give_money()
        self.current_step += 1

# -------------------- Reactive model helpers --------------------
model_val = reactive.Value(None)
step_tick = reactive.Value(0)  # global tick to force re-renders on step/run/reset


def _bump():
    step_tick.set(step_tick.get() + 1)


def build_model(n: int, w: int = 5, h: int = 5, seed=None):
    return MoneyModel(n, w, h, seed=seed)


def get_model():
    m = model_val.get()
    if m is None:
        m = build_model(input.agents(), input.grid_w(), input.grid_h())
        model_val.set(m)
    return m


@reactive.effect
@reactive.event(input.reset, input.agents)
def _recreate_model():
    model_val.set(build_model(input.agents(), input.grid_w(), input.grid_h()))
    _bump()


@reactive.effect
@reactive.event(input.step_once)
def _step_once():
    if not input.interactive_mode():
        return
    m = get_model()
    m.step()
    model_val.set(m)
    _bump()


@reactive.effect
@reactive.event(input.run_n)
def _run_n():
    if not input.interactive_mode():
        return
    m = get_model()
    for _ in range(int(input.nsteps())):
        m.step()
    m.current_run += 1
    model_val.set(m)
    _bump()

# -------------------- Simulation for histogram (by button) --------------------
@reactive.calc
def sim_df():
    reactive.event(input.recompute_hist)

    runs = input.runs()
    steps = input.steps()
    n = input.agents()

    all_wealth = []
    for _ in range(runs):
        model = MoneyModel(n, 10, 10)
        for _ in range(steps):
            model.step()
        all_wealth.extend(a.wealth for a in model.agents)

    return pd.DataFrame({"wealth": all_wealth})

# -------------------- Derived views from current model --------------------
@reactive.calc
def model_state_df():
    _ = step_tick.get()
    m = get_model()
    rows = []
    for a in m.agents:
        x, y = a.cell.coordinate
        rows.append({"id": a.unique_id, "x": x, "y": y, "wealth": a.wealth})
    return pd.DataFrame(rows)


@reactive.calc
def current_wealth_df():
    _ = step_tick.get()
    m = get_model()
    return pd.DataFrame({"wealth": [a.wealth for a in m.agents]})

# -------------------- Main content layout --------------------
with ui.layout_columns(col_widths=[4, 4, 4, 6, 6, 6,6]):

    # Value boxes
    with ui.value_box(showcase=icon("person-running")):
        "Current Run"
        @render.ui
        def current_run():
            _ = step_tick.get()
            return f"{get_model().current_run:,}"

    with ui.value_box(showcase=icon("shoe-prints")):
        "Current Step"
        @render.ui
        def current_step():
            _ = step_tick.get()
            return f"{get_model().current_step:,}"

    with ui.value_box(showcase=icon("person")):
        "Current Agent"
        @render.ui
        def current_agent():
            _ = step_tick.get()
            m = get_model()
            if m.current_agent_index is not None:
                return f"#{m.current_agent_index} (id={m.current_agent_id})"
            return "—"

    # Live wealth histogram
    with ui.card():
        ui.card_header("Live Wealth (Current Model)")

        @render_altair
        def current_hist():
            import altair as alt
            df = current_wealth_df()
            return (
                alt.Chart(df, title="Current Wealth Histogram")
                .mark_bar()
                .encode(
                    x=alt.X("wealth:Q", bin=alt.Bin(step=1), title="Wealth"),
                    y=alt.Y("count():Q", title="Agents"),
                )
                .properties(width="container", height=300)
                .interactive()
            )

    # Simulated wealth histogram (by button)
    with ui.card():
        ui.card_header("Wealth Distribution (Simulated)")

        @render_altair
        def hist_sim():
            import altair as alt
            df = sim_df()
            return (
                alt.Chart(df, title="Wealth Histogram (Simulated)")
                .mark_bar()
                .encode(
                    x=alt.X("wealth:Q", bin=alt.Bin(step=1), title="Wealth"),
                    y=alt.Y("count():Q", title="Agents"),
                )
                .properties(width="container", height=300)
                .interactive()
            )

    # Current state table
    with ui.card():
        ui.card_header("Current State")

        @render.data_frame
        def agents_table():
            _ = step_tick.get()
            return render.DataGrid(model_state_df(), height=300)


    # Grid heatmap
    with ui.card():
        ui.card_header("Grid view")

        @render.plot(alt="Number of agents on each cell")
        def space():
            _ = step_tick.get()
            m = get_model()
            W, H = m.grid.width, m.grid.height
            agent_counts = np.zeros((H, W), dtype=int)
            for cell in m.grid.all_cells.cells:
                x, y = cell.coordinate
                agent_counts[y, x] = len(cell.agents)  # [row=y, col=x]
            g = sns.heatmap(agent_counts, cmap="viridis", annot=True, cbar=False, square=True)
            g.figure.set_size_inches(5, 5)
            g.set(title="Agents per cell")



## file: requirements.txt
shinyswatch
altair
anywidget
palmerpenguins
jsonschema
mesa
scipy
tqdm

Here’s a summary of issues we fixed

What we fixed (recap)

  • Env mismatch → Pointed Quarto to your venv (execute.python: .venv/bin/python or QUARTO_PYTHON).
  • Wrong invocation → Use the shinylive CLI, not python -m shinylive.
  • Assets flakiness → Side-loaded GitHub release assets and unpacked them under ~/.cache/shinylive/shinylive-0.10.x (and, if needed, ~/.local/share/shinylive/), verified with shinylive assets info.
  • Clean builds → Use quarto render --clean (no quarto clean command).

Reporting

  • UI ?
  • saving replay buffer. ?
  • generating animations ? (replay?)

Future plans

So I’ve had a number of successful shiny and mesa apps over the last three years but now it’s time to integrate them into interactive versions that users can engage with in thier browsers.

Also I think that the ability to specify multiple files in one code block is ideal for vibe coding POC or MVPs.

  1. Adding space part of the tutorial on this page
  2. Sugarscape and other model thinking models I’ve developed.
  3. Demand from preferences Microeconomics model
  4. Demand curve simulator and forecasting
  5. Lewis Signaling Bayesian algs. and RL
  6. Sugarscape as a POMDP with RL or MARL.
  7. PYMC NDLM POC using ABM for simulating consumer behavior (in progress)
  8. Urn Models with ABM
  9. Lotka-Volterra equations population dynamics demos
  10. SIR models with ABM
  11. Bayesian MCMC algorithms demos
  12. BNP (Bayesian Nonparametric Models) demos
  13. Integrating with LLM support in Shiny.
  14. Price theory demos

Citation

BibTeX citation:
@online{bochman2025,
  author = {Bochman, Oren},
  title = {ShinyLive ❤️ {Mesa} {Tutorial}},
  date = {2025-10-02},
  url = {https://orenbochman.github.io/posts/2025/2025-10-02-Shiny-Mesa-Tutorial/},
  langid = {en}
}
For attribution, please cite this work as:
Bochman, Oren. 2025. “ShinyLive ❤️ Mesa Tutorial.” October 2, 2025. https://orenbochman.github.io/posts/2025/2025-10-02-Shiny-Mesa-Tutorial/.