louped

Plugins

Add pages, tabs, blocks, routes, a command or agent tools to louped from your own project.

Most additions need no plugin. A new chart is a view, and views.vega draws any Vega-Lite chart. A plugin is for the rest: a page of your own in the app, a tab on every run or experiment, the API routes they call, a command, or tools for your agent. You don't need a louped release for any of it.

A plugin is a folder under plugins/ in your project:

plugins/verdicts/
  plugin.toml       # required
  plugin.py         # optional: routes, a command, agent tools
  panel/
    index.html        # optional: a page of its own, at its sidebar entry
    run.html          # optional: a tab on every run's page
    experiment.html   # optional: a tab on every experiment's page
plugin.toml
title = "Verdicts"              # its sidebar entry
section = "behavior"            # workspace, behavior or efficiency
description = "Judge each example."
plugin.py
from fastapi import APIRouter

router = APIRouter()            # served at /api/x/verdicts


@router.get("/left")
def left() -> dict:
    return {"left": 3}


def main(argv: list[str]) -> None:      # louped verdicts ...
    print("verdicts", argv)


def tools(mcp, api) -> None:            # tools in louped mcp
    @mcp.tool()
    async def verdicts_left() -> dict:
        """How many examples are left to judge."""
        return (await api.get("/x/verdicts/left")).json()

The pages are served from /x/verdicts/:

  • index.html shows at the plugin's sidebar entry.
  • run.html is a tab, named after the plugin, on every run's page. It opens with ?run=<run id>.
  • experiment.html is a tab on every experiment's page. It opens with ?experiment=<name>.

Any other page in panel/, such as card.html, is a block a layout can place anywhere, sized to what it draws.

The kit

Build pages from louped's kit, so they look like the rest of the app in both modes:

panel/run.html
<link rel="stylesheet" href="/kit/louped.css" />
<script type="module">
  import { api, run, stats, table } from "/kit/louped.js";

  const r = await api(`/runs/${run}`);
  document.body.append(stats(Object.entries(r.metrics)));
  document.body.append(table(await api(`/x/verdicts/rows?run=${run}`)));
</script>
From the kitWhat it is
run, experimentThe run or experiment the page is on
api(path, init)/api<path> as JSON; a failure throws the server's message
stats([[label, value, note]])The app's metric cards
table(rows, columns)The app's table, numbers right-aligned
h(tag, props, ...children), emptyAn element; an empty state
l-card, l-button, l-input, l-badge, l-muted, l-monoClasses for the rest

Its scripts call the same API the app does, such as fetch("/api/runs") or fetch("/api/x/verdicts/left"). The app hands every page its theme: the colours as CSS variables, such as var(--foreground) and var(--border), and the fonts as var(--font-geist-sans) and var(--font-geist-mono).

A plugin that fails to load shows its error on its page. Plugins run your project's code in louped's process. They load only on your own machine, never with louped serve --expose or in louped publish.

On this page