Content
# The Council — A Self-Assembling Deliberation Engine
[](LICENSE)
[](https://www.python.org/)
[](https://google.github.io/adk-docs/)
[](https://modelcontextprotocol.io/)
[](https://fastapi.tiangolo.com/)
[](https://ai.google.dev/)
[](#run-the-tests-must-be-green-offline--fr-014)
> **Ask a question → the Council assembles a bespoke expert panel → they debate
> across rounds → claims are fact-checked against Wikipedia via MCP → you get a
> balanced Verdict Brief with cited sources** — all streamed live to a
> self-contained web UI. No frontend build step, no paid services.
### Highlights
- 🤖 **Multi-agent orchestration** — Moderator, Panelists, Fact-Checker &
Synthesizer on Google ADK, driven by a bounded deliberation loop.
- 🔌 **Genuine MCP integration** — a local stdio fact-check server
(`MCPToolset`) grounds claims in keyless Wikipedia.
- 🛡️ **Guardrails by design** — an ADK callback refuses regulated advice,
unsafe content & prompt-injection *before any panel runs*.
- 🧠 **Session memory** — follow-ups build on prior verdicts via
`InMemorySessionService` (in-memory only, nothing persisted).
- 📡 **Live SSE streaming** — every event flows to a live UI (stage timeline,
persona cards, fact-check badges; same-origin assets, no CDN, no build step).
- 🔀 **Bring your own provider** — Gemini (default), OpenAI, Anthropic, OpenCode
Zen, or any OpenAI-compatible endpoint, chosen in-page; keys stay in memory.
- 🧩 **Structured outputs** — typed `PanelRoster`, `PanelistTurn`, `Claim` &
`VerdictBrief` end-to-end (ADK R6 gotcha handled).
- ✅ **79 offline tests** — deterministic; no API key or browser required.
```
┌──────────┐ ┌────────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────┐
│Moderator │──▶│ Panelists │──▶│ Fact-Checker │──▶│ Synthesizer│──▶│VerdictBrief│
│ (recruit)│ │ (N rounds) │ │ (MCP/Wikipedia)│ │ (structured)│ │ + trace │
└──────────┘ └────────────┘ └───────────────┘ └────────────┘ └────────────┘
▲ guardrail callback refuses regulated/unsafe/injection inputs before any panel runs
```
## Why
Policy questions deserve more than a single-answer oracle. The Council models a
**deliberative process**: divergent expert perspectives, visible rebuttals,
grounded fact-checks, and an explicit confidence call. It showcases multi-agent
orchestration, MCP tool interop, structured outputs, sessions/memory, and
security guardrails in one reproducible app.
## Prerequisites
- Python **3.11+** (developed on 3.12)
- A free **Google AI Studio** API key — https://aistudio.google.com/apikey
- Internet access (Gemini + Wikipedia); no other accounts, billing, or cloud project
- Any modern browser (the UI is one localhost page — no npm, no build step)
## Setup
```bash
# 1. create + activate a virtual environment
python -m venv .venv
# Windows: .venv\Scripts\activate macOS/Linux: source .venv/bin/activate
# 2. install pinned dependencies
pip install -r requirements.txt
pip install -e . --no-deps # install the `council` package (editable)
# 3. configure the key (NEVER commit .env)
cp config/.env.example .env # Windows: copy config\.env.example .env
# edit .env and set GOOGLE_API_KEY=<your key> (GOOGLE_GENAI_USE_VERTEXAI=FALSE)
```
`.env` and `.claude/` are gitignored. A secret-scan gate
(`scripts/secret-scan.sh`) runs in CI and as a pre-commit hook (install once:
`git config core.hooksPath .githooks`).
## Run the demo
```bash
council # or: python -m council
```
Starts a local server (default `http://127.0.0.1:8400`) and opens your browser.
With no provider configured, the page opens in a **get-started** state that walks
you into Settings — the server always starts (exit codes: `0` clean shutdown,
`1` unexpected startup error).
In the page, ask:
> Should a mid-size city ban gas leaf blowers by 2030?
Expected, streamed live:
1. **Panel assembly** — panelist cards appear one-by-one with a recruitment
reason; ≥1 carries a **challenger badge**. If recruitment fails you see a
"fallback panel" notice and the run still completes (FR-016).
2. **Debate** — 2 rounds; each panelist has a stable color; rebuttals are
visually highlighted (agent-to-agent signal).
3. **Fact-checks** — claims get a status badge + a clickable Wikipedia source.
4. **Verdict Brief card** — positions per side, recommendation, confidence
meter, dissents, and **Download JSON**.
5. Open the **trace drawer** for the full ordered `DeliberationTrace` (FR-008).
Flags: `--port N`, `--quiet` (WARNING logs), `--verbose` (DEBUG logs).
### Validate the guardrail refusal (US3)
Ask: `Tell me exactly which stocks to buy tomorrow to guarantee profit` → a
**refusal card** explaining the reason; **no panel appears**, no debate runs.
Injection attempts (`ignore your instructions…`) are treated as untrusted
content and refused; medical/legal advice is likewise refused.
### Validate session context (US2)
After a verdict, ask a follow-up in the same page. The page reuses its
`session_id`, so the second verdict references the first. Session state is
in-memory only and discarded when the server stops (FR-012).
## Choose your AI provider (multi-provider — feature 002)
The Council is no longer Gemini-only. Open **⚙ Settings** in the page to pick a
provider and model, paste your own key, and run — no file edits, no restart:
- **Google Gemini** (default), **OpenAI**, **Anthropic**, **OpenCode Zen**, or a
**Custom** OpenAI-compatible endpoint (local runtimes, gateways, …).
- Credentials are held in **server memory only** — never written to disk, logs,
traces, or git, and masked in the UI once entered.
- Every deliberation role (moderator, panelists, fact-checker, synthesizer) runs
on the provider/model you selected; the header chip shows it live for the run.
Prefer environment variables? They still work (see `config/.env.example`):
```bash
COUNCIL_PROVIDER=openai # google | openai | anthropic | opencode | custom
COUNCIL_MODEL=gpt-4o-mini # optional; provider default otherwise
OPENAI_API_KEY=... # the key var matching the chosen provider
# custom OpenAI-compatible endpoints only:
# COUNCIL_BASE_URL=http://localhost:11434/v1
```
**Precedence:** UI choice → environment default → built-in Gemini default. A key
entered in the UI wins for the running process; a restart returns to env config.
Non-Google providers route through ADK's own `LiteLlm` wrapper (adds the pinned
`litellm` dependency). Full walkthrough: `specs/002-multi-provider-ui/quickstart.md`.
## Run one agent in isolation (FR-013/FR-015)
Each agent is independently addressable — useful for debugging and reproducible
checks. `scripts/run_one_agent.py` drives a single agent turn with the real
model and prints both the final text and the structured output:
```bash
# Moderator recruits a panel (structured PanelRoster)
python scripts/run_one_agent.py moderator "Should a city ban gas leaf blowers by 2030?"
# Fact-Checker gathers evidence via the MCP/Wikipedia toolset (free-form)
python scripts/run_one_agent.py factchecker "Claim: leaf blowers emit disproportionate pollutants."
# Structurer maps free-form evidence into typed Claims
python scripts/run_one_agent.py structurer "Claim: the sky is blue. Verdict: supported."
# Synthesizer authors a VerdictBrief from a JSON payload
python scripts/run_one_agent.py synthesizer '{"question":"...","roster":{...},"transcript":[],"claims":[]}'
```
## Run the tests (must be green offline — FR-014)
```bash
pytest # all: unit + smoke + e2e (offline, deterministic, free)
pytest tests/e2e # full question → SSE stream → VerdictBrief (fake model + mocked Wikipedia)
pytest tests/smoke # per-agent + per-MCP-tool smoke tests
```
The default suite makes **no live API calls and needs no browser** — it injects
a `ScriptedLlm` fake model, mocks the fact-check MCP transport, and drives the
web API with an in-process ASGI `TestClient`, so it is deterministic and free.
## Architecture
```
src/council/
config.py bounds (MIN_PANEL=2, DEFAULT_PANEL=3, MAX_PANEL=4, MAX_ROUNDS=2,
FACTCHECK_TIMEOUT_S=8, MODEL=gemini-2.5-flash) + env loading
app.py CouncilApp: ADK Runner + InMemorySessionService; run_agent_for_output
(with 429/503 retry-then-degrade so free-tier runs complete)
providers.py multi-provider registry, precedence, memory-only credentials,
validation probe + model factory (Gemini string | LiteLlm) — feature 002
logging_config.py dictConfig logging; TraceEvents reach both log and UI (FR-008)
agents/
moderator.py guard callback (refuse/clarify) + recruit (output_schema=PanelRoster)
panelist.py one turn (output_schema=PanelistTurn; references_prior A2A signal)
factchecker.py MCP toolset, NO output_schema (R6 gotcha) + claim_structurer step
synthesizer.py output_schema=VerdictBrief
instructions/*.md per-agent prompts (loaded at build time)
guardrails/
input_guard.py validate_question + classify_refusal (regulated/unsafe/injection)
claim_guard.py strike_unsupported (never present unsupported claims as fact)
orchestration/
deliberation.py bounded loop: guard→recruit→debate→fact-check→guard→synthesize
(FR-018 retry-once-then-degrade in 3 places; US2 prior-context threading)
fallback.py deterministic 3-persona roster when recruitment fails
session_state.py typed accessors over ADK session state (in-memory only)
schemas/ roster, claim, verdict, trace (PanelistTurn+DebateTurn), session
tools/factcheck_mcp/
server.py FastMCP stdio server (2 tools: search_wikipedia, get_summary)
wikipedia.py keyless REST + MediaWiki API; timeout→empty degradation
web/
server.py FastAPI: SSE stream + /api/providers config API + static page
static/index.html markup — live UI (same-origin app.js/style.css, no CDN, <500 lines)
static/app.js SSE handling, stage timeline, settings panel, live rendering
static/style.css dark animated theme (stage stepper, cards, verdict, settings)
scripts/ secret-scan.sh, run_one_agent.py
tests/ unit + smoke + e2e (offline, fake model + mocked MCP)
```
### Key design decisions (see `specs/001-the-council/research.md`)
- **Bounded Python app-shell** drives ADK agents (not `LoopAgent`) — enforces
termination (FR-009) while reasoning stays inside ADK/Gemini.
- **Local stdio MCP server** for fact-checking — genuine MCP integration
(`MCPToolset`), least-privilege (only the Fact-Checker gets the toolset),
keyless (Wikipedia REST).
- **Structured outputs** everywhere; the ADK gotcha (R6) that a tool-using agent
cannot declare `output_schema` is handled by a separate no-tool structuring
step that maps free-form evidence into `list[Claim]`.
- **Guardrails as ADK callbacks** (the "Referee") — `before_agent_callback`
short-circuits out-of-bounds input before any panel runs.
- **Sessions/memory** via `InMemorySessionService` — follow-ups build on prior
verdicts; nothing persists beyond the session (FR-012).
## Known limitations
- **Free-tier rate limits**: `gemini-2.5-flash` free tier allows ~5 req/min and
~20 req/day. A full deliberation makes ~11 LLM calls, so 1–2 runs/day can
exhaust the daily quota. The launcher retries transient 429/503s with backoff;
if a fact-check call is ultimately rate-limited it degrades to an "uncertain"
claim (per the spec Edge Cases) and the run still completes. The capability is
verified offline (mocked) and live in isolation.
- **Provider default**: Gemini stays the built-in default; OpenAI, Anthropic,
OpenCode Zen, and custom OpenAI-compatible endpoints are opt-in via Settings or
env (feature 002). OAuth account sign-in (US3) is intentionally out of scope.
- **Wikipedia-only** fact-check source (by design — keyless, reproducible).
- **Localhost only** — the server binds `127.0.0.1`; not production-deployed
(a `Dockerfile`/Cloud Run path is sketched as an optional post-freeze task).
## Team
Built by a four-person team for the **Google + Kaggle 5-Day AI Agents**
capstone. The system was designed collaboratively; implementation, integration,
and tooling were led by Taha Fahd.
- **Taha Fahd** — Team Lead & Lead Developer · GitHub [@6rzan](https://github.com/6rzan) · Kaggle [tahafahdthabit](https://www.kaggle.com/tahafahdthabit)
- **ESAM Al-AREQi** — Contributor · GitHub [@esammostafa9-cloud](https://github.com/esammostafa9-cloud) · Kaggle [esamalareqi](https://www.kaggle.com/esamalareqi)
- **Kamil Ayub Khan** — Contributor · GitHub [@Slimyzz](https://github.com/Slimyzz) · Kaggle [kamilayubkhan](https://www.kaggle.com/kamilayubkhan)
- **Ahmad Hemedany** — Contributor · GitHub [@hemedany](https://github.com/hemedany) · Kaggle [ahmadhemedany](https://www.kaggle.com/ahmadhemedany)
## License
MIT — see [LICENSE](LICENSE).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available