benjaminogbonna commited on
Commit
375f1ea
·
verified ·
1 Parent(s): b94021c
Files changed (7) hide show
  1. HANDOFF.md +99 -0
  2. README.md +32 -8
  3. app.py +409 -0
  4. index.html +57 -0
  5. pyproject.toml +11 -0
  6. requirements.txt +3 -0
  7. 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 Ai Theorem Explorer
3
- emoji: 🔥
4
- colorFrom: blue
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.27.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
- short_description: Nexenda council-ai-theorem-explorer
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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