diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..5bb7b2b --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,44 @@ +name: Website + +# Builds the GitHub Pages site (https://simulamet.github.io/Verdi/) from +# site/build.py and docs/*.md whenever they change on main. +on: + push: + branches: [main] + paths: ["site/**", "docs/**", "pyproject.toml", ".github/workflows/pages.yml"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Build site + run: | + pip install markdown + python site/build.py + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index dfb1b6f..99de71c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ artifacts/ .env .env.* *.local.env + +# GitHub Pages build output (site/build.py) +_site/ diff --git a/README.md b/README.md index 9207116..4c9c769 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Verdi +[![Website](https://img.shields.io/badge/website-simulamet.github.io%2FVerdi-2f5bd3.svg)](https://simulamet.github.io/Verdi/) [![PyPI](https://img.shields.io/pypi/v/verdi-agents.svg)](https://pypi.org/project/verdi-agents/) [![Python](https://img.shields.io/pypi/pyversions/verdi-agents.svg)](https://pypi.org/project/verdi-agents/) [![CI](https://github.com/SimulaMet/Verdi/actions/workflows/ci.yml/badge.svg)](https://github.com/SimulaMet/Verdi/actions/workflows/ci.yml) @@ -7,6 +8,8 @@ **An auditable testbed for value drift and recovery in societies of AI agents.** +**Website and guides: https://simulamet.github.io/Verdi/** + Verdi runs a society of LLM-driven agents over time and measures how their declared and observed values change as they interact, negotiate shared rules, and respond to institutional incentives. Every decision, law, sanction, appeal, diff --git a/pyproject.toml b/pyproject.toml index 560c716..1082ae8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,9 @@ dev = [ ] [project.urls] -Homepage = "https://github.com/SimulaMet/Verdi" -Documentation = "https://github.com/SimulaMet/Verdi/tree/main/docs" +Homepage = "https://simulamet.github.io/Verdi/" +Documentation = "https://simulamet.github.io/Verdi/sweeps.html" +Source = "https://github.com/SimulaMet/Verdi" Issues = "https://github.com/SimulaMet/Verdi/issues" [project.scripts] diff --git a/site/build.py b/site/build.py new file mode 100644 index 0000000..f708c25 --- /dev/null +++ b/site/build.py @@ -0,0 +1,286 @@ +"""Build the GitHub Pages site (landing page + guides) into ``_site/``. + + pip install markdown + python site/build.py # writes _site/ + python -m http.server -d _site # preview + +The guides are rendered from ``docs/*.md`` so the site never drifts from the +repo. Every page gets a canonical URL, description, Open Graph tags and +structured data; ``sitemap.xml`` and ``robots.txt`` let search engines find +all of it. +""" +from __future__ import annotations + +import html +import json +import re +import shutil +from datetime import date +from pathlib import Path + +import markdown +import tomllib + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "_site" +SITE = "https://simulamet.github.io/Verdi/" +REPO = "https://github.com/SimulaMet/Verdi" +PYPI = "https://pypi.org/project/verdi-agents/" +VERSION = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["version"] + +# docs/.md -> /.html, with a search-friendly title and description. +GUIDES = [ + ("SWEEPS.md", "sweeps", "Sweeps: compare LLMs and settings across seeds", + "Rerun one agent-society scenario across models, governance settings and seeds " + "with one YAML file. Plan and cost first, resume, spreadsheet and comparison page."), + ("SCENARIOS.md", "scenarios", "Scenario reference", + "Every key of a Verdi scenario YAML: model endpoint, agents, resources, shocks, " + "constitution, economy, delivery tasks, interviews and probe schedule."), + ("RESEARCHER_GUIDE.md", "researcher-guide", "Researcher guide: paired institutional study", + "Run a fixed-vs-revisable institutions experiment on LLM agent societies with one " + "config file and read the comparative results."), + ("INSTITUTIONAL_RECOVERY.md", "institutional-recovery", + "Institutional recovery experiments", + "How Verdi measures whether societies of LLM agents recover from shocks under fixed " + "or amendable constitutions, using objective delivery tasks."), +] + +CSS = """ +:root{--bg:#fbfbfd;--surface:#fff;--fg:#1d1d1f;--muted:#5f6368;--line:#e4e4e9; +--accent:#2f5bd3;--accent-soft:#eef2fd;--code:#f4f4f7;--radius:14px} +@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#0f1115; +--surface:#161a20;--fg:#ecedf0;--muted:#a1a6b0;--line:#272c35;--accent:#8aa8ff; +--accent-soft:#1a2236;--code:#1b2027}} +*{box-sizing:border-box}html{-webkit-text-size-adjust:100%} +body{margin:0;background:var(--bg);color:var(--fg); +font:16px/1.65 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif} +a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline} +.wrap{max-width:1040px;margin:0 auto;padding:0 20px} +header.top{border-bottom:1px solid var(--line);background:var(--surface)} +header.top .wrap{display:flex;align-items:center;gap:20px;height:60px} +.brand{font-weight:700;font-size:18px;color:var(--fg)} +nav{display:flex;gap:18px;font-size:15px;white-space:nowrap;overflow-x:auto; +scrollbar-width:none}nav a{color:var(--muted)} +nav a:hover{color:var(--fg);text-decoration:none} +.spacer{flex:1} +.hero{padding:72px 0 40px}.hero h1{font-size:clamp(34px,6vw,54px);line-height:1.1; +margin:0 0 16px;letter-spacing:-.02em}.hero p.lead{font-size:clamp(17px,2.4vw,21px); +color:var(--muted);max-width:720px;margin:0 0 28px} +.cta{display:flex;gap:12px;flex-wrap:wrap} +.btn{display:inline-block;padding:11px 18px;border-radius:10px;font-weight:600; +border:1px solid var(--line);color:var(--fg);background:var(--surface)} +.btn.primary{background:var(--accent);color:#fff;border-color:var(--accent)} +.btn:hover{text-decoration:none;filter:brightness(1.05)} +pre,code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px} +pre{background:var(--code);border:1px solid var(--line);border-radius:var(--radius); +padding:16px 18px;overflow-x:auto;line-height:1.55} +:not(pre)>code{background:var(--code);padding:2px 6px;border-radius:6px} +section{padding:36px 0}section h2{font-size:28px;margin:0 0 8px;letter-spacing:-.01em} +section p.sub{color:var(--muted);margin:0 0 24px;max-width:720px} +.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))} +.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius); +padding:20px}.card h3{margin:0 0 6px;font-size:17px}.card p{margin:0;color:var(--muted); +font-size:15px} +.steps{counter-reset:s;display:grid;gap:16px; +grid-template-columns:repeat(auto-fit,minmax(280px,1fr))} +.steps .card h3::before{counter-increment:s;content:counter(s);display:inline-grid; +place-items:center;width:26px;height:26px;margin-right:8px;border-radius:50%; +background:var(--accent-soft);color:var(--accent);font-size:14px} +article.doc{max-width:820px;padding:40px 0 64px} +article.doc h1{font-size:clamp(28px,4.5vw,38px);line-height:1.2;letter-spacing:-.01em} +article.doc h2{margin-top:2em;padding-top:.4em;border-top:1px solid var(--line)} +article.doc table{border-collapse:collapse;display:block;overflow-x:auto;margin:1em 0} +article.doc th,article.doc td{border:1px solid var(--line);padding:6px 10px; +text-align:left;vertical-align:top}article.doc th{background:var(--code)} +.crumbs{font-size:14px;color:var(--muted);margin-bottom:8px} +footer{border-top:1px solid var(--line);padding:28px 0 40px;color:var(--muted);font-size:14px} +footer .wrap{display:flex;gap:16px;flex-wrap:wrap;justify-content:space-between} +@media (max-width:760px){nav.ext,.spacer{display:none}.hero{padding:44px 0 24px}} +""" + + +def page(*, title: str, description: str, path: str, body: str, + jsonld: dict | None = None) -> str: + url = SITE + path + nav = "".join(f'{t.split(":")[0]}' + for _, s, t, _ in GUIDES[:2]) + ld = f'' if jsonld else "" + e = html.escape + return f""" + + +{e(title)} + + + + + + + + +{ld} + +
+
{body}
+
+Verdi {VERSION} · MIT license · by Sushant Gautam, SimulaMet +GitHub · PyPI · +Issues +
+ +""" + + +def landing() -> str: + feats = [ + ("Auditable by construction", "Every action, vote, law, sanction and appeal is written " + "to an append-only event log and replayed to an identical state hash."), + ("Declared vs observed values", "Agents state their values; Verdi measures what they " + "actually do. The gap over time is the measurement."), + ("Self-governing societies", "Universal laws plus a constitution agents can amend in " + "assemblies: quorum, thresholds, sanctions, appeals, restoration."), + ("Shocks and recovery", "Deterministic exogenous shocks, a constrained economy and " + "objective delivery tasks show whether a society recovers."), + ("Compare models with sweeps", "Vary the model, quorum or shock size in one YAML file; " + "every combination runs on the same seeds, in parallel, resumable."), + ("One-click reports", "Each run renders a self-contained report.html; each sweep a " + "comparison page and a spreadsheet."), + ] + cards = "".join(f'

{t}

{d}

' for t, d in feats) + body = f""" +
+
+

Verdi: an auditable testbed for societies of LLM agents

+

Run a society of language-model agents over time, let them govern +themselves, hit them with shocks, and measure how their values and institutions +drift and recover. Reproducible, seeded, and fully logged.

+ +
+ +

Install in one line

+

No virtual environment needed: uv +fetches Verdi on first use. Works with any OpenAI-compatible endpoint (vLLM, Ollama, +hosted APIs).

+
uvx verdi-agents init my-study          # example scenarios, sweeps, .env template
+cd my-study                             # add your key to .env, endpoint to examples/starter.yaml
+uvx verdi-agents run examples/starter.yaml
+open runs/starter/report.html
+

Or pip install verdi-agents, then use the verdi command.

+
+ +

What Verdi measures

+

Built for research on AI safety, alignment and governance in multi-agent +LLM systems.

+
{cards}
+ +

How it works

+
+

Describe a society

One YAML file: the model endpoint, +agents with roles and declared values, resources, laws, constitution, shocks and +when to interview them. See the scenario reference.

+

Run it

verdi run scenario.yaml drives every +agent through every tick, validates each action against the law, and writes the +full event ledger.

+

Compare

verdi sweep reruns the scenario +across models and settings and summarizes the outcomes per condition. +See sweeps.

+
+ +

Compare LLMs on the same society

+

Change only the model; everything else stays identical.

+
name: model_comparison
+scenario: examples/full.yaml
+vary:
+  model.name: [model-a, model-b]
+parallel_runs: 2
+
uvx verdi-agents sweep examples/sweeps/model_comparison.yaml --dry-run   # plan + cost
+uvx verdi-agents sweep examples/sweeps/model_comparison.yaml             # run, resumable
+
+ +

Guides

+{"".join(f'

{html.escape(t)}

{html.escape(d)}

' for _, s, t, d in GUIDES)} +
+
""" + jsonld = { + "@context": "https://schema.org", "@type": "SoftwareSourceCode", + "name": "Verdi", "alternateName": "verdi-agents", + "description": DESCRIPTION, "url": SITE, "codeRepository": REPO, + "programmingLanguage": "Python", "license": "https://opensource.org/licenses/MIT", + "version": VERSION, "author": {"@type": "Person", "name": "Sushant Gautam"}, + "sourceOrganization": {"@type": "Organization", "name": "SimulaMet"}, + "keywords": "LLM agents, multi-agent simulation, AI safety, AI governance, " + "value drift, institutional design, agent-based modeling", + "runtimePlatform": "Python 3.10+", "downloadUrl": PYPI, + } + return page(title="Verdi: auditable testbed for societies of LLM agents", + description=DESCRIPTION, path="", body=body, jsonld=jsonld) + + +DESCRIPTION = ("Open-source Python testbed for societies of LLM agents: self-governance, " + "sanctions, shocks and recovery, with auditable logs, reproducible runs and " + "model comparison sweeps.") + + +def _fix_links(text: str) -> str: + slugs = {f: s for f, s, _, _ in GUIDES} + + def repl(m: re.Match) -> str: + target = m.group(1) + name = target.split("#")[0] + anchor = target[len(name):] + if name in slugs: + return f"]({slugs[name]}.html{anchor})" + if name.startswith("../"): + kind = "tree" if name.endswith("/") else "blob" + return f"]({REPO}/{kind}/main/{name[3:]}{anchor})" + if name.endswith(".md") and "://" not in name: + return f"]({REPO}/blob/main/docs/{name}{anchor})" + return m.group(0) + + return re.sub(r"\]\(([^)\s]+)\)", repl, text) + + +def guide(src: str, slug: str, title: str, description: str) -> str: + text = _fix_links((ROOT / "docs" / src).read_text(encoding="utf-8")) + body_html = markdown.markdown(text, extensions=["tables", "fenced_code", "toc", "sane_lists"]) + body = (f'
') + jsonld = {"@context": "https://schema.org", "@type": "TechArticle", "headline": title, + "description": description, "url": SITE + f"{slug}.html", + "isPartOf": {"@type": "WebSite", "name": "Verdi", "url": SITE}} + return page(title=f"{title} | Verdi", description=description, + path=f"{slug}.html", body=body, jsonld=jsonld) + + +def main() -> None: + if OUT.exists(): + shutil.rmtree(OUT) + OUT.mkdir() + (OUT / "index.html").write_text(landing(), encoding="utf-8") + urls = [SITE] + for src, slug, title, desc in GUIDES: + (OUT / f"{slug}.html").write_text(guide(src, slug, title, desc), encoding="utf-8") + urls.append(SITE + f"{slug}.html") + today = date.today().isoformat() + (OUT / "sitemap.xml").write_text( + '\n' + '\n' + + "".join(f" {u}{today}\n" for u in urls) + + "\n", encoding="utf-8") + (OUT / "robots.txt").write_text(f"User-agent: *\nAllow: /\nSitemap: {SITE}sitemap.xml\n", + encoding="utf-8") + (OUT / ".nojekyll").write_text("", encoding="utf-8") + print(f"Built {len(urls)} pages into {OUT}") + + +if __name__ == "__main__": + main()