From b10ebe3367514665a98b5b5b37cbebc4582c522c Mon Sep 17 00:00:00 2001 From: Sushant Gautam Date: Tue, 3 Nov 2026 23:43:37 +0100 Subject: [PATCH] site: GitHub Pages website with SEO, link it everywhere site/build.py renders a landing page plus the guides (docs/*.md) into _site/, each page with canonical URL, description, Open Graph and schema.org data, plus sitemap.xml and robots.txt. pages.yml deploys it on changes to main. README, pyproject URLs and the repo homepage point to https://simulamet.github.io/Verdi/. --- .github/workflows/pages.yml | 44 ++++++ .gitignore | 3 + README.md | 3 + pyproject.toml | 5 +- site/build.py | 286 ++++++++++++++++++++++++++++++++++++ 5 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 site/build.py 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()