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.
- 🤖 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&VerdictBriefend-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
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.
- 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)
# 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).
council # or: python -m councilStarts 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:
- 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).
- Debate — 2 rounds; each panelist has a stable color; rebuttals are visually highlighted (agent-to-agent signal).
- Fact-checks — claims get a status badge + a clickable Wikipedia source.
- Verdict Brief card — positions per side, recommendation, confidence meter, dissents, and Download JSON.
- Open the trace drawer for the full ordered
DeliberationTrace(FR-008).
Flags: --port N, --quiet (WARNING logs), --verbose (DEBUG logs).
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.
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).
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):
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/v1Precedence: 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.
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:
# 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":[]}'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 testsThe 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.
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)
- 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_schemais handled by a separate no-tool structuring step that maps free-form evidence intolist[Claim]. - Guardrails as ADK callbacks (the "Referee") —
before_agent_callbackshort-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).
- Free-tier rate limits:
gemini-2.5-flashfree 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 (aDockerfile/Cloud Run path is sketched as an optional post-freeze task).
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 · Kaggle tahafahdthabit
- ESAM Al-AREQi — Contributor · GitHub @esammostafa9-cloud · Kaggle esamalareqi
- Kamil Ayub Khan — Contributor · GitHub @Slimyzz · Kaggle kamilayubkhan
- Ahmad Hemedany — Contributor · GitHub @hemedany · Kaggle ahmadhemedany
MIT — see LICENSE.