Spaces:
Running on Zero
Running on Zero
for demo
Browse files- HANDOFF.md +99 -0
- README.md +32 -8
- app.py +409 -0
- index.html +57 -0
- pyproject.toml +11 -0
- requirements.txt +3 -0
- uv.lock +0 -0
HANDOFF.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Council AI — Theorem Explorer: Handoff Notes
|
| 2 |
+
|
| 3 |
+
This is a Gradio app implementing Theorem 1 (deliberation convergence) and
|
| 4 |
+
Theorem 2 (multi-expert safety bound) from the Council AI paper, styled to
|
| 5 |
+
match Nexenda's brand (Instrument Sans / Noto Serif, dark palette, gold accent).
|
| 6 |
+
|
| 7 |
+
**Status:** Math and theorem logic are locked, verified against the paper's
|
| 8 |
+
worked examples. UI/branding/layout is the open area for refinement.
|
| 9 |
+
|
| 10 |
+
**Constraints to keep in mind while editing:** no Bamzi product names, no
|
| 11 |
+
"Mixture of Councils" / "MoC" naming, anywhere in the app or its outputs.
|
| 12 |
+
Both are withheld until later papers. See the Council AI paper draft for
|
| 13 |
+
the full naming context if needed.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 1. Run it locally
|
| 18 |
+
|
| 19 |
+
Requires Python 3.9+ (check with `python3 --version`; on a fresh Mac this is
|
| 20 |
+
often older, in which case `brew install python@3.12` first).
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
cd council-ai-theorem-explorer
|
| 24 |
+
python3 -m venv venv
|
| 25 |
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
python app.py
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
Opens at `http://127.0.0.1:7860`. Every slider updates the plot live.
|
| 31 |
+
|
| 32 |
+
**Note:** `venv` must be reactivated (`source venv/bin/activate`) every time
|
| 33 |
+
you open a new terminal session, it doesn't persist automatically.
|
| 34 |
+
|
| 35 |
+
**Faster iteration loop:** run `gradio app.py` instead of `python app.py`
|
| 36 |
+
(same package, different entry point), it auto-reloads on file save so you
|
| 37 |
+
don't have to stop/restart manually while tweaking the UI.
|
| 38 |
+
|
| 39 |
+
**Quick temporary share link, no deploy needed:** in `app.py`, change the
|
| 40 |
+
final `demo.launch(...)` call to add `share=True`. Gradio prints a public
|
| 41 |
+
`https://xxxxx.gradio.live` URL valid for 72 hours, useful for a fast look
|
| 42 |
+
before anything's pushed anywhere. It only stays live while your machine
|
| 43 |
+
and terminal session are running.
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## 2. Push to GitHub
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
git init
|
| 51 |
+
git add .
|
| 52 |
+
git commit -m "Initial theorem explorer"
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
Create a private repo under the Nexenda org on github.com (don't
|
| 56 |
+
initialize it with a README, this project already has one), then:
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
git remote add origin https://github.com/nexendahq/<repo-name>.git
|
| 60 |
+
git branch -M main
|
| 61 |
+
git push -u origin main
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
Add teammates under the repo's **Settings → Collaborators and teams**.
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## 3. Deploy to a Hugging Face Space (persistent live link)
|
| 69 |
+
|
| 70 |
+
1. Create a new Space at huggingface.co, SDK: **Gradio**. Set visibility
|
| 71 |
+
to **Private** initially, since this is pre-launch.
|
| 72 |
+
2. The Space is a git repo too. Add it as a second remote on the same
|
| 73 |
+
local project:
|
| 74 |
+
```bash
|
| 75 |
+
git remote add space https://huggingface.co/spaces/nexendahq/<space-name>
|
| 76 |
+
git push space main
|
| 77 |
+
```
|
| 78 |
+
3. The Space rebuilds automatically on every push. `README.md`'s YAML
|
| 79 |
+
frontmatter at the top of the file already configures the title, SDK,
|
| 80 |
+
and app entry point, nothing extra to set in the HF UI.
|
| 81 |
+
4. Once it's live, that URL works for anyone you invite, no one needs to
|
| 82 |
+
run anything locally to view or interact with it.
|
| 83 |
+
|
| 84 |
+
Two placeholders in `README.md`'s frontmatter worth reviewing before
|
| 85 |
+
going public: the `license` field (currently `apache-2.0`) and the
|
| 86 |
+
"Read the paper" link (currently a placeholder pending Paper 1's
|
| 87 |
+
publication).
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 4. Files in this folder
|
| 92 |
+
|
| 93 |
+
| File | Purpose |
|
| 94 |
+
|---|---|
|
| 95 |
+
| `app.py` | The full Gradio app, both theorem panels, Nexenda theming |
|
| 96 |
+
| `requirements.txt` | Pinned dependencies |
|
| 97 |
+
| `README.md` | HF Space config (YAML frontmatter) + public-facing description |
|
| 98 |
+
| `.gitignore` | Standard Python/Gradio ignores |
|
| 99 |
+
| `HANDOFF.md` | This file |
|
README.md
CHANGED
|
@@ -1,14 +1,38 @@
|
|
| 1 |
---
|
| 2 |
-
title: Council
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
-
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Council AI Theorem Explorer
|
| 3 |
+
emoji: 🪶
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.24.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Council AI — Theorem Explorer
|
| 14 |
+
|
| 15 |
+
An interactive companion to *Council AI: Toward Deliberative Artificial Intelligence*
|
| 16 |
+
(Michael Jidael, Udochukwu Nwokoma, Benjamin Ogbonna, et al., Nexenda, 2026).
|
| 17 |
+
|
| 18 |
+
This Space implements exactly two things, both taken directly from the paper:
|
| 19 |
+
|
| 20 |
+
- **Theorem 1 (Deliberation Convergence)** — the council state sequence converges
|
| 21 |
+
exponentially to a fixed point under a contractive deliberation operator.
|
| 22 |
+
- **Theorem 2 (Multi-Expert Safety Bound)** — the probability of a catastrophic
|
| 23 |
+
error is bounded by `p·ρ^(k−1)` (or `p^k` in the alternate regime), where `p` is
|
| 24 |
+
individual expert error rate, `ρ` is inter-expert error correlation, and `k` is
|
| 25 |
+
the number of experts required for high-confidence agreement.
|
| 26 |
+
|
| 27 |
+
Every number in the app is computed live from these formulas. Nothing here runs a
|
| 28 |
+
trained model, and the app does not reference any product name, expert-cluster
|
| 29 |
+
name, or internal architecture name beyond what appears in the published paper.
|
| 30 |
+
|
| 31 |
+
Read the paper: *(link once published)*
|
| 32 |
+
|
| 33 |
+
## Running locally
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
pip install -r requirements.txt
|
| 37 |
+
python app.py
|
| 38 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Council AI — Theorem Explorer
|
| 3 |
+
------------------------------
|
| 4 |
+
Interactive companion to "Council AI: Toward Deliberative Artificial Intelligence"
|
| 5 |
+
(Michael Jidael, Udochukwu Nwokoma, Benjamin Ogbonna, et al., Nexenda, 2026).
|
| 6 |
+
|
| 7 |
+
Implements exactly two things, both taken directly from the paper's proofs:
|
| 8 |
+
|
| 9 |
+
Theorem 1 (Deliberation Convergence):
|
| 10 |
+
||H(t) - H*||_F <= gamma^t * ||H(0) - H*||_F
|
| 11 |
+
|
| 12 |
+
Theorem 2 (Multi-Expert Safety Bound):
|
| 13 |
+
P(catastrophic error) <= p * rho^(k-1) if p <= rho
|
| 14 |
+
P(catastrophic error) <= p^k if p > rho
|
| 15 |
+
|
| 16 |
+
No proprietary architecture names, expert-cluster names, or product names
|
| 17 |
+
appear anywhere in this app or its outputs. Consistent with the naming
|
| 18 |
+
constraints on Paper 1.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
import matplotlib.pyplot as plt
|
| 23 |
+
import gradio as gr
|
| 24 |
+
|
| 25 |
+
# ---------------------------------------------------------------------------
|
| 26 |
+
# Palettes — cool, not warm. Two full sets so theme toggling actually
|
| 27 |
+
# re-renders the plots server-side, not just the CSS chrome.
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
def get_palette(is_dark: bool) -> dict:
|
| 30 |
+
if is_dark:
|
| 31 |
+
return dict(
|
| 32 |
+
bg="#0F1416", panel="#171D20", hairline="#232B2E",
|
| 33 |
+
text="#EAF2F2", muted="#93A6A8", faint="#5C6C6E",
|
| 34 |
+
accent="#45C9C2", green="#7FBF9E", red="#D97B67",
|
| 35 |
+
)
|
| 36 |
+
return dict(
|
| 37 |
+
bg="#F5F7F7", panel="#FFFFFF", hairline="#DCE4E4",
|
| 38 |
+
text="#14181A", muted="#4B5B5D", faint="#7C8B8D",
|
| 39 |
+
accent="#1E958D", green="#3E8A63", red="#B85440",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def style_axes(fig, ax, pal):
|
| 44 |
+
"""Apply a palette to a matplotlib figure/axes explicitly (never touches
|
| 45 |
+
global rcParams, so concurrent viewers on different themes don't clobber
|
| 46 |
+
each other's renders on a shared server)."""
|
| 47 |
+
fig.patch.set_facecolor(pal["bg"])
|
| 48 |
+
ax.set_facecolor(pal["panel"])
|
| 49 |
+
for spine in ax.spines.values():
|
| 50 |
+
spine.set_color(pal["hairline"])
|
| 51 |
+
ax.tick_params(colors=pal["faint"], labelsize=9)
|
| 52 |
+
ax.xaxis.label.set_color(pal["muted"])
|
| 53 |
+
ax.yaxis.label.set_color(pal["muted"])
|
| 54 |
+
ax.grid(alpha=0.35, color=pal["hairline"])
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
# Theorem 1 — Deliberation Convergence
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
def theorem1(gamma: float, rounds: int, d0: float, tau: float, is_dark: bool):
|
| 61 |
+
pal = get_palette(is_dark)
|
| 62 |
+
t = np.arange(0, rounds + 1)
|
| 63 |
+
dist = d0 * (gamma ** t)
|
| 64 |
+
agreement = np.clip(1 - dist / d0, 0, 1)
|
| 65 |
+
|
| 66 |
+
reached = np.argmax(agreement >= tau) if np.any(agreement >= tau) else None
|
| 67 |
+
consensus_round = int(reached) if reached is not None and agreement[reached] >= tau else None
|
| 68 |
+
|
| 69 |
+
fig, ax = plt.subplots(figsize=(7.5, 4.2))
|
| 70 |
+
style_axes(fig, ax, pal)
|
| 71 |
+
ax.axhline(tau, color=pal["red"], linestyle="--", linewidth=1.6, label="Consensus threshold τ")
|
| 72 |
+
ax.plot(t, agreement, color=pal["green"], linewidth=2.4, label="Agreement (1 − normalized distance)")
|
| 73 |
+
ax.plot(t, dist / d0, color=pal["accent"], linewidth=2.0, label="‖H(t)−H*‖ / ‖H(0)−H*‖ (Theorem 1)")
|
| 74 |
+
|
| 75 |
+
ax.set_xlabel("Deliberation round t")
|
| 76 |
+
ax.set_ylabel("Normalized value")
|
| 77 |
+
ax.set_ylim(-0.02, 1.05)
|
| 78 |
+
leg = ax.legend(loc="upper right", fontsize=8.5, framealpha=0.0)
|
| 79 |
+
for txt in leg.get_texts():
|
| 80 |
+
txt.set_color(pal["muted"])
|
| 81 |
+
fig.tight_layout()
|
| 82 |
+
|
| 83 |
+
stats = (
|
| 84 |
+
f"**Round consensus reached:** {'t = ' + str(consensus_round) if consensus_round is not None else 'not reached in given rounds'}\n\n"
|
| 85 |
+
f"**Residual disagreement at round T:** {dist[-1]:.4f}\n\n"
|
| 86 |
+
f"**Convergence rate:** γ = {gamma:.2f} (lower γ → faster convergence)"
|
| 87 |
+
)
|
| 88 |
+
return fig, stats
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
DEFAULTS_T1 = (0.55, 12, 1.2, 0.95)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def reset_theorem1(is_dark: bool):
|
| 95 |
+
fig, stats = theorem1(*DEFAULTS_T1, is_dark)
|
| 96 |
+
return (*DEFAULTS_T1, fig, stats)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ---------------------------------------------------------------------------
|
| 100 |
+
# Theorem 2 — Multi-Expert Safety Bound
|
| 101 |
+
# ---------------------------------------------------------------------------
|
| 102 |
+
def safety_bound(p: float, rho: float, k: int) -> float:
|
| 103 |
+
if p <= rho:
|
| 104 |
+
return p * (rho ** (k - 1))
|
| 105 |
+
return p ** k
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def theorem2(p: float, rho: float, n: int, k: int, is_dark: bool):
|
| 109 |
+
pal = get_palette(is_dark)
|
| 110 |
+
k = min(k, n)
|
| 111 |
+
regime = "p ≤ ρ → bound = p·ρ^(k−1)" if p <= rho else "p > ρ → tighter bound = p^k"
|
| 112 |
+
|
| 113 |
+
ks = np.arange(1, n + 1)
|
| 114 |
+
bounds = np.array([safety_bound(p, rho, kk) for kk in ks])
|
| 115 |
+
current_bound = safety_bound(p, rho, k)
|
| 116 |
+
|
| 117 |
+
fig, ax = plt.subplots(figsize=(7.5, 4.2))
|
| 118 |
+
style_axes(fig, ax, pal)
|
| 119 |
+
ax.axhline(p, color=pal["red"], linestyle="--", linewidth=1.6, label=f"Single-model error rate (p = {p:.4f})")
|
| 120 |
+
ax.plot(ks, bounds, color=pal["accent"], linewidth=2.4, marker="o", markersize=3.5,
|
| 121 |
+
label="Council bound as k increases (Theorem 2)")
|
| 122 |
+
ax.scatter([k], [current_bound], color=pal["accent"], s=70, zorder=5,
|
| 123 |
+
edgecolor=pal["text"], linewidth=1)
|
| 124 |
+
|
| 125 |
+
ax.set_yscale("log")
|
| 126 |
+
ax.set_xlabel("Experts required for high-confidence agreement (k)")
|
| 127 |
+
ax.set_ylabel("P(catastrophic error), log scale")
|
| 128 |
+
leg = ax.legend(loc="upper right", fontsize=8.5, framealpha=0.0)
|
| 129 |
+
for txt in leg.get_texts():
|
| 130 |
+
txt.set_color(pal["muted"])
|
| 131 |
+
fig.tight_layout()
|
| 132 |
+
|
| 133 |
+
reduction = p / current_bound if current_bound > 0 else float("inf")
|
| 134 |
+
stats = (
|
| 135 |
+
f"**Regime:** {regime}\n\n"
|
| 136 |
+
f"**Single-model error rate (p):** {p:.4g}\n\n"
|
| 137 |
+
f"**Council bound at k={k}:** {current_bound:.4g}\n\n"
|
| 138 |
+
f"**Reduction factor:** {reduction:,.1f}×"
|
| 139 |
+
)
|
| 140 |
+
return fig, stats
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
DEFAULTS_T2 = (0.01, 0.30, 10, 5)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def reset_theorem2(is_dark: bool):
|
| 147 |
+
fig, stats = theorem2(*DEFAULTS_T2, is_dark)
|
| 148 |
+
return (*DEFAULTS_T2, fig, stats)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# ---------------------------------------------------------------------------
|
| 152 |
+
# Theme toggle — flips a shared state, restyles both plots server-side,
|
| 153 |
+
# and (via the paired js= call below) flips the CSS chrome client-side
|
| 154 |
+
# in the same click, so nothing waits on a round trip to feel instant.
|
| 155 |
+
# ---------------------------------------------------------------------------
|
| 156 |
+
def toggle_theme(is_dark, g, r, d0, tau, p, rho, n, k):
|
| 157 |
+
new_dark = not is_dark
|
| 158 |
+
fig1, s1 = theorem1(g, r, d0, tau, new_dark)
|
| 159 |
+
fig2, s2 = theorem2(p, rho, n, k, new_dark)
|
| 160 |
+
label = "🌙 Dark" if new_dark else "☀️ Light"
|
| 161 |
+
return new_dark, fig1, s1, fig2, s2, label
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
TOGGLE_JS = """
|
| 165 |
+
() => { document.body.classList.toggle('light-theme'); }
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
# CSS — cool palette (teal accent, not gold), richer button treatment,
|
| 170 |
+
# and a light-theme override block driven by a body class toggled in JS.
|
| 171 |
+
# ---------------------------------------------------------------------------
|
| 172 |
+
CUSTOM_CSS = """
|
| 173 |
+
@import url('https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400;0,500;0,600;0,700&family=Noto+Serif:ital,wght@0,500;1,400&display=swap');
|
| 174 |
+
|
| 175 |
+
:root {
|
| 176 |
+
--bg: #0F1416;
|
| 177 |
+
--panel: #171D20;
|
| 178 |
+
--hairline: #232B2E;
|
| 179 |
+
--hairline-strong: #2E383B;
|
| 180 |
+
--text: #EAF2F2;
|
| 181 |
+
--muted: #93A6A8;
|
| 182 |
+
--faint: #5C6C6E;
|
| 183 |
+
--accent: #45C9C2;
|
| 184 |
+
--accent-soft: rgba(69,201,194,0.14);
|
| 185 |
+
--accent-dim: rgba(69,201,194,0.35);
|
| 186 |
+
}
|
| 187 |
+
body.light-theme {
|
| 188 |
+
--bg: #F5F7F7;
|
| 189 |
+
--panel: #FFFFFF;
|
| 190 |
+
--hairline: #DCE4E4;
|
| 191 |
+
--hairline-strong: #C7D1D1;
|
| 192 |
+
--text: #14181A;
|
| 193 |
+
--muted: #4B5B5D;
|
| 194 |
+
--faint: #7C8B8D;
|
| 195 |
+
--accent: #1E958D;
|
| 196 |
+
--accent-soft: rgba(30,149,141,0.10);
|
| 197 |
+
--accent-dim: rgba(30,149,141,0.30);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.gradio-container {
|
| 201 |
+
background-color: var(--bg) !important;
|
| 202 |
+
font-family: 'Instrument Sans', sans-serif !important;
|
| 203 |
+
transition: background-color 0.25s ease;
|
| 204 |
+
}
|
| 205 |
+
.gradio-container h1, .gradio-container h2, .gradio-container h3 {
|
| 206 |
+
font-family: 'Noto Serif', serif !important;
|
| 207 |
+
font-weight: 500 !important;
|
| 208 |
+
color: var(--text) !important;
|
| 209 |
+
}
|
| 210 |
+
.gradio-container, .gradio-container p, .gradio-container span, .gradio-container label {
|
| 211 |
+
color: var(--muted) !important;
|
| 212 |
+
}
|
| 213 |
+
.gradio-container blockquote {
|
| 214 |
+
font-family: 'Noto Serif', serif !important;
|
| 215 |
+
font-style: italic !important;
|
| 216 |
+
color: var(--text) !important;
|
| 217 |
+
border-left: 2px solid var(--accent) !important;
|
| 218 |
+
padding-left: 14px !important;
|
| 219 |
+
opacity: 0.92;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.gradio-container .block, .gradio-container .form {
|
| 223 |
+
background-color: var(--panel) !important;
|
| 224 |
+
border: 1px solid var(--hairline) !important;
|
| 225 |
+
border-radius: 8px !important;
|
| 226 |
+
transition: background-color 0.25s ease, border-color 0.25s ease;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.gradio-container .tab-nav {
|
| 230 |
+
border-bottom: 1px solid var(--hairline) !important;
|
| 231 |
+
}
|
| 232 |
+
.gradio-container .tab-nav button {
|
| 233 |
+
font-family: 'Instrument Sans', sans-serif !important;
|
| 234 |
+
color: var(--muted) !important;
|
| 235 |
+
transition: color 0.2s ease;
|
| 236 |
+
}
|
| 237 |
+
.gradio-container .tab-nav button.selected {
|
| 238 |
+
color: var(--accent) !important;
|
| 239 |
+
border-bottom-color: var(--accent) !important;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.gradio-container input[type=range] {
|
| 243 |
+
accent-color: var(--accent) !important;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.gradio-container .plot-container {
|
| 247 |
+
background-color: var(--panel) !important;
|
| 248 |
+
border-radius: 8px !important;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
/* ---- richer buttons ---- */
|
| 252 |
+
.gradio-container button.lg, .gradio-container button.sm, .gradio-container .gr-button {
|
| 253 |
+
border-radius: 8px !important;
|
| 254 |
+
font-weight: 600 !important;
|
| 255 |
+
letter-spacing: 0.01em;
|
| 256 |
+
transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.15s ease, background-color 0.15s ease;
|
| 257 |
+
border-width: 1px !important;
|
| 258 |
+
}
|
| 259 |
+
.gradio-container button:hover {
|
| 260 |
+
transform: translateY(-1px);
|
| 261 |
+
box-shadow: 0 4px 14px rgba(0,0,0,0.18);
|
| 262 |
+
}
|
| 263 |
+
.gradio-container button:active {
|
| 264 |
+
transform: translateY(0);
|
| 265 |
+
box-shadow: inset 0 1px 4px rgba(0,0,0,0.25);
|
| 266 |
+
}
|
| 267 |
+
.gradio-container button:focus-visible {
|
| 268 |
+
outline: 2px solid var(--accent) !important;
|
| 269 |
+
outline-offset: 2px;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.gradio-container .secondary {
|
| 273 |
+
background-color: var(--panel) !important;
|
| 274 |
+
border: 1px solid var(--hairline-strong) !important;
|
| 275 |
+
color: var(--text) !important;
|
| 276 |
+
}
|
| 277 |
+
.gradio-container .secondary:hover {
|
| 278 |
+
border-color: var(--accent-dim) !important;
|
| 279 |
+
background-color: var(--accent-soft) !important;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
#theme-toggle-btn {
|
| 283 |
+
background-color: var(--panel) !important;
|
| 284 |
+
border: 1px solid var(--hairline-strong) !important;
|
| 285 |
+
color: var(--text) !important;
|
| 286 |
+
border-radius: 999px !important;
|
| 287 |
+
padding: 6px 18px !important;
|
| 288 |
+
min-width: 0 !important;
|
| 289 |
+
}
|
| 290 |
+
#theme-toggle-btn:hover {
|
| 291 |
+
border-color: var(--accent) !important;
|
| 292 |
+
box-shadow: 0 0 0 3px var(--accent-soft) !important;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.gradio-container footer {
|
| 296 |
+
display: none !important;
|
| 297 |
+
}
|
| 298 |
+
"""
|
| 299 |
+
|
| 300 |
+
HEAD_HTML = """
|
| 301 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 302 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 303 |
+
"""
|
| 304 |
+
|
| 305 |
+
NEXENDA_THEME = gr.themes.Base(
|
| 306 |
+
primary_hue=gr.themes.colors.teal,
|
| 307 |
+
neutral_hue=gr.themes.colors.slate,
|
| 308 |
+
font=gr.themes.GoogleFont("Instrument Sans"),
|
| 309 |
+
).set(
|
| 310 |
+
body_background_fill="#0F1416",
|
| 311 |
+
body_text_color="#93A6A8",
|
| 312 |
+
background_fill_primary="#171D20",
|
| 313 |
+
background_fill_secondary="#171D20",
|
| 314 |
+
border_color_primary="#2E383B",
|
| 315 |
+
block_background_fill="#171D20",
|
| 316 |
+
block_border_color="#232B2E",
|
| 317 |
+
block_title_text_color="#EAF2F2",
|
| 318 |
+
block_label_text_color="#93A6A8",
|
| 319 |
+
button_primary_background_fill="#45C9C2",
|
| 320 |
+
button_primary_background_fill_hover="#5AD6CF",
|
| 321 |
+
button_primary_text_color="#0F1416",
|
| 322 |
+
slider_color="#45C9C2",
|
| 323 |
+
color_accent="#45C9C2",
|
| 324 |
+
body_text_color_subdued="#5C6C6E",
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
# ---------------------------------------------------------------------------
|
| 328 |
+
# UI
|
| 329 |
+
# ---------------------------------------------------------------------------
|
| 330 |
+
with gr.Blocks(title="Council AI — Theorem Explorer") as demo:
|
| 331 |
+
is_dark_state = gr.State(True)
|
| 332 |
+
|
| 333 |
+
with gr.Row():
|
| 334 |
+
with gr.Column(scale=5):
|
| 335 |
+
gr.Markdown(
|
| 336 |
+
"## Council AI — Theorem Explorer\n"
|
| 337 |
+
"Interactive companion to *Council AI: Toward Deliberative Artificial Intelligence* "
|
| 338 |
+
"(Nexenda, 2026). Both panels compute the paper's actual theorem statements live."
|
| 339 |
+
)
|
| 340 |
+
with gr.Column(scale=1, min_width=140):
|
| 341 |
+
theme_btn = gr.Button("🌙 Dark", elem_id="theme-toggle-btn", size="sm")
|
| 342 |
+
|
| 343 |
+
with gr.Tab("Theorem 1 — Deliberation Convergence"):
|
| 344 |
+
gr.Markdown(
|
| 345 |
+
"> *If the deliberation operator Φ is contractive with constant γ < 1, "
|
| 346 |
+
"the council state sequence converges exponentially to a unique fixed point: "
|
| 347 |
+
"‖H(t) − H\\*‖ ≤ γᵗ‖H(0) − H\\*‖.*"
|
| 348 |
+
)
|
| 349 |
+
with gr.Row():
|
| 350 |
+
with gr.Column(scale=1):
|
| 351 |
+
gamma_s = gr.Slider(0.1, 0.9, value=DEFAULTS_T1[0], step=0.01, label="Contraction rate γ")
|
| 352 |
+
rounds_s = gr.Slider(4, 30, value=DEFAULTS_T1[1], step=1, label="Deliberation rounds T")
|
| 353 |
+
d0_s = gr.Slider(0.2, 2.5, value=DEFAULTS_T1[2], step=0.05, label="Initial disagreement ‖H(0)−H*‖")
|
| 354 |
+
tau_s = gr.Slider(0.70, 0.99, value=DEFAULTS_T1[3], step=0.01, label="Consensus threshold τ")
|
| 355 |
+
reset1_btn = gr.Button("↺ Reset to defaults", size="sm", variant="secondary")
|
| 356 |
+
stats1 = gr.Markdown()
|
| 357 |
+
with gr.Column(scale=2):
|
| 358 |
+
plot1 = gr.Plot()
|
| 359 |
+
|
| 360 |
+
t1_inputs = [gamma_s, rounds_s, d0_s, tau_s, is_dark_state]
|
| 361 |
+
for ctrl in (gamma_s, rounds_s, d0_s, tau_s):
|
| 362 |
+
ctrl.change(theorem1, inputs=t1_inputs, outputs=[plot1, stats1])
|
| 363 |
+
reset1_btn.click(reset_theorem1, inputs=[is_dark_state],
|
| 364 |
+
outputs=[gamma_s, rounds_s, d0_s, tau_s, plot1, stats1])
|
| 365 |
+
demo.load(theorem1, inputs=t1_inputs, outputs=[plot1, stats1])
|
| 366 |
+
|
| 367 |
+
with gr.Tab("Theorem 2 — Multi-Expert Safety Bound"):
|
| 368 |
+
gr.Markdown(
|
| 369 |
+
"> *With n experts, individual error rate p, pairwise error correlation ρ, "
|
| 370 |
+
"and k experts required for a high-confidence output: "
|
| 371 |
+
"P(catastrophic error) ≤ p·ρ^(k−1) when p ≤ ρ (the practically relevant regime); "
|
| 372 |
+
"the tighter bound pᵏ applies when p > ρ.*"
|
| 373 |
+
)
|
| 374 |
+
with gr.Row():
|
| 375 |
+
with gr.Column(scale=1):
|
| 376 |
+
p_s = gr.Slider(0.001, 0.15, value=DEFAULTS_T2[0], step=0.001, label="Individual error rate p")
|
| 377 |
+
rho_s = gr.Slider(0.0, 1.0, value=DEFAULTS_T2[1], step=0.01, label="Error correlation ρ")
|
| 378 |
+
n_s = gr.Slider(2, 15, value=DEFAULTS_T2[2], step=1, label="Total experts n")
|
| 379 |
+
k_s = gr.Slider(1, 15, value=DEFAULTS_T2[3], step=1, label="Agreement required k")
|
| 380 |
+
reset2_btn = gr.Button("↺ Reset to defaults", size="sm", variant="secondary")
|
| 381 |
+
stats2 = gr.Markdown()
|
| 382 |
+
with gr.Column(scale=2):
|
| 383 |
+
plot2 = gr.Plot()
|
| 384 |
+
|
| 385 |
+
t2_inputs = [p_s, rho_s, n_s, k_s, is_dark_state]
|
| 386 |
+
for ctrl in (p_s, rho_s, n_s, k_s):
|
| 387 |
+
ctrl.change(theorem2, inputs=t2_inputs, outputs=[plot2, stats2])
|
| 388 |
+
reset2_btn.click(reset_theorem2, inputs=[is_dark_state],
|
| 389 |
+
outputs=[p_s, rho_s, n_s, k_s, plot2, stats2])
|
| 390 |
+
demo.load(theorem2, inputs=t2_inputs, outputs=[plot2, stats2])
|
| 391 |
+
|
| 392 |
+
# theme toggle: one click, two effects — CSS chrome flips instantly (js),
|
| 393 |
+
# and both plots re-render server-side in the matching palette (fn)
|
| 394 |
+
theme_btn.click(fn=None, inputs=None, outputs=None, js=TOGGLE_JS)
|
| 395 |
+
theme_btn.click(
|
| 396 |
+
toggle_theme,
|
| 397 |
+
inputs=[is_dark_state, gamma_s, rounds_s, d0_s, tau_s, p_s, rho_s, n_s, k_s],
|
| 398 |
+
outputs=[is_dark_state, plot1, stats1, plot2, stats2, theme_btn],
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
gr.Markdown(
|
| 402 |
+
"---\n"
|
| 403 |
+
"*This demo illustrates the mathematics proven in Theorems 1 and 2 of the Council AI paper. "
|
| 404 |
+
"It does not run any trained model. Numbers are user-selected inputs to the formal bounds, "
|
| 405 |
+
"not measurements from a deployed system.*"
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
if __name__ == "__main__":
|
| 409 |
+
demo.launch(css=CUSTOM_CSS, theme=NEXENDA_THEME, head=HEAD_HTML)
|
index.html
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Gradio-Lite: Serverless Gradio Running Entirely in Your Browser</title>
|
| 7 |
+
<meta name="description" content="Gradio-Lite: Serverless Gradio Running Entirely in Your Browser">
|
| 8 |
+
|
| 9 |
+
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.js"></script>
|
| 10 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.css" />
|
| 11 |
+
|
| 12 |
+
<style>
|
| 13 |
+
html, body {
|
| 14 |
+
margin: 0;
|
| 15 |
+
padding: 0;
|
| 16 |
+
height: 100%;
|
| 17 |
+
}
|
| 18 |
+
</style>
|
| 19 |
+
</head>
|
| 20 |
+
<body>
|
| 21 |
+
<gradio-lite>
|
| 22 |
+
<gradio-file name="app.py" entrypoint>
|
| 23 |
+
import gradio as gr
|
| 24 |
+
|
| 25 |
+
from filters import as_gray
|
| 26 |
+
|
| 27 |
+
def process(input_image):
|
| 28 |
+
output_image = as_gray(input_image)
|
| 29 |
+
return output_image
|
| 30 |
+
|
| 31 |
+
demo = gr.Interface(
|
| 32 |
+
process,
|
| 33 |
+
"image",
|
| 34 |
+
"image",
|
| 35 |
+
examples=["lion.jpg", "logo.png"],
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
demo.launch()
|
| 39 |
+
</gradio-file>
|
| 40 |
+
|
| 41 |
+
<gradio-file name="filters.py">
|
| 42 |
+
from skimage.color import rgb2gray
|
| 43 |
+
|
| 44 |
+
def as_gray(image):
|
| 45 |
+
return rgb2gray(image)
|
| 46 |
+
</gradio-file>
|
| 47 |
+
|
| 48 |
+
<gradio-file name="lion.jpg" url="https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/test_data/lion.jpg" />
|
| 49 |
+
<gradio-file name="logo.png" url="https://raw.githubusercontent.com/gradio-app/gradio/main/guides/assets/logo.png" />
|
| 50 |
+
|
| 51 |
+
<gradio-requirements>
|
| 52 |
+
# Same syntax as requirements.txt
|
| 53 |
+
scikit-image
|
| 54 |
+
</gradio-requirements>
|
| 55 |
+
</gradio-lite>
|
| 56 |
+
</body>
|
| 57 |
+
</html>
|
pyproject.toml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "council-ai-theorem-explorer"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"gradio==6.24.0",
|
| 9 |
+
"matplotlib>=3.8",
|
| 10 |
+
"numpy>=1.26",
|
| 11 |
+
]
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.24.0
|
| 2 |
+
numpy>=1.26
|
| 3 |
+
matplotlib>=3.8
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|