Slack bot for BerriAI/litellm. Mention the bot in any channel it's in with a GitHub PR URL and it runs the litellm-pr-reviewer skill via Pydantic AI (routed through a LiteLLM proxy), posting the review back in-thread.
@litellm-bot https://github.com/BerriAI/litellm/pull/123
app.py is a FastAPI app that wires a Slack front-end to two parallel Pydantic AI agents, both routed through a LiteLLM proxy.
┌──────────────────────────┐
Slack @-mention ──► │ slack_handler.py │
(or DM) │ • verifies signing key │
│ • finds PR URL in │
│ mention or thread │
└────────────┬─────────────┘
│ on_pr_review(pr_url, channel, thread_ts)
▼
┌──────────────────────────┐
│ app.review_pr │
│ asyncio.gather( │
│ triage_agent, │ ─┐
│ pattern_agent, │ ─┤ both share one
│ ) │ │ Logfire root span
└────────────┬─────────────┘ │
│ │
┌───────────────────┴────────────────┐│
▼ ▼│
┌────────────────────┐ ┌────────────────────────┐
│ agent │ │ pattern_agent │
│ SKILL.md │ │ SKILL.md │
│ + gather_pr_data │ │ + gather_pattern_data │
└─────────┬──────────┘ └────────────┬───────────┘
│ tool call │ tool call
▼ ▼
subprocess: scripts/ subprocess: scripts/
gather_pr_triage_data.py gather_pattern_data.py
(GitHub + Greptile + CircleCI) (GitHub diff + sibling files)
│ │
└────────────────┬────────────────────┘
▼
LiteLLM proxy (OpenAI-compatible)
│
▼
upstream LLM
│
▼
slack_handler.bolt.client.chat_postMessage
(single two-section reply in the thread)
Key pieces:
slack_handler.pyowns everything Slack-specific (Bolt app, signing-secret verification,app_mention+ DMmessagehandlers, thread-scanning for a PR URL). It exposesmount(app, on_pr_review=...)soapp.pynever has to know about channels or thread timestamps. IfSLACK_BOT_TOKEN/SLACK_SIGNING_SECRETaren't set, the module is a no-op and/slack/eventssimply isn't registered — the/chatdev UI still works.- Two agents, one model.
agent(CI triage) andpattern_agent(pattern conformance) are both Pydantic AIAgents pointed at the sameOpenAIChatModelconfigured withLiteLLMProvider. Each has its own system prompt loaded from aSKILL.mdfile underskills/andskills-local/. - Skills become typed tools. Each SKILL.md tells the model to shell out to a
gather_*.pyscript. Instead of giving the agent a genericBashtool,app.pyregistersgather_pr_data/gather_pattern_datavia@agent.tool_plain, runs the script in a subprocess, and returns parsed JSON. ATOOL_REDIRECTprefix is prepended to the triage SKILL.md so the model calls the tool instead of trying to run bash. Subprocess failures and non-JSON output raiseModelRetryso Pydantic AI re-prompts the agent. review_pris the Slack callback. It runs both agents in parallel withasyncio.gather, wraps the whole thing in a singlelogfire.span("review_pr", ...)so both runs + the Slack post share one trace, and posts one combined reply (*CI Triage*+*Pattern Conformance*) in-thread._run_onecatches per-agent exceptions so one failing agent doesn't take the other down./chat+/chat/apiare a local dev UI that calls the same two agents viaasyncio.gather, keyed by an in-memorySESSIONSdict so each browser session keeps separatetriage/patternmessage histories._extract_tool_tracepullsToolCallPart/ToolReturnPartpairs out of the run so the UI can render tool calls inline.- Observability.
logfire.configure(send_to_logfire="if-token-present")makes Logfire a no-op locally. WithLOGFIRE_TOKENset,instrument_pydantic_ai,instrument_httpx, andinstrument_fastapiship traces for agent runs, all HTTP calls (including to the LiteLLM proxy and GitHub), FastAPI requests, and the gather subprocesses. Stdliblog.*calls are bridged into Logfire viaLogfireLoggingHandler.
git clone --recursive <this-repo>
cp .env.example .env # fill in values
uv sync
uv run uvicorn app:app --reloadExpose your local port to Slack with ngrok http 8000 (or cloudflared tunnel --url http://localhost:8000) and point your Slack app's Event Subscriptions → Request URL at <tunnel-url>/slack/events.
If you cloned without --recursive: git submodule update --init --recursive.
Slack creds are optional. With just LITELLM_API_KEY + GITHUB_TOKEN set, run uv run uvicorn app:app --reload and open http://localhost:8000/chat. It's a single-page UI that hits the same agent the Slack handler uses, with tool calls printed inline so you can see the agent run the gather script. Try:
Triage this PR: https://github.com/BerriAI/litellm/pull/123
There's also a JSON endpoint if you'd rather curl it:
curl -s localhost:8000/chat/api -H 'content-type: application/json' \
-d '{"message": "Triage this PR: https://github.com/BerriAI/litellm/pull/123"}'- OAuth & Permissions → Bot Token Scopes: add
chat:write— post the review backapp_mentions:read— receive@boteventschannels:history,groups:history,im:history,mpim:history— read the parent message of a thread when someone @-mentions the bot in a thread but the PR URL is in the OP (e.g.@bot please review this PR)
- Event Subscriptions: enable, set Request URL to
<your-host>/slack/events, subscribe to bot eventapp_mention. - Install to Workspace (or reinstall after scope changes). Copy the Bot Token (
xoxb-…) intoSLACK_BOT_TOKEN. - Basic Information → App Credentials: copy the Signing Secret into
SLACK_SIGNING_SECRET. - Invite the bot to whichever channels people will mention it from.
- Top-level mention in a channel (
@bot https://...pull/123): only the mention text is parsed. Channel history is not searched, to avoid grabbing unrelated old PR links. - Mention inside a thread (
@bot please review this PR): the bot fetches the whole thread viaconversations.repliesand uses the first PR URL it finds in any message (OP or earlier reply). This handles the common pattern of "user pastes a PR in the OP, then later @-mentions the bot to review it."
If the *:history scopes aren't granted, in-thread mentions silently fall back to scanning only the mention text and the bot will reply asking for a URL.
Run fly launch (Fly.io reads Procfile) or create a new Python service on Render/Railway and point it at this repo. Set the env vars below in the platform's secret store. Procfile declares the start command.
| Var | Required | Notes |
|---|---|---|
SLACK_BOT_TOKEN |
for Slack | xoxb-…, scopes chat:write + app_mentions:read. Skip to use only the /chat UI |
SLACK_SIGNING_SECRET |
for Slack | Verifies events came from Slack. Skip to use only the /chat UI |
LITELLM_API_KEY |
yes | LiteLLM proxy virtual key (or upstream provider key if not using the proxy) |
LITELLM_API_BASE |
no | LiteLLM proxy URL. Defaults to http://0.0.0.0:4000 |
LITELLM_MODEL |
no | Model alias the proxy routes. Defaults to claude-sonnet-4-6 |
GITHUB_TOKEN |
yes | PAT with public_repo — required by the skill |
PORT |
no | Defaults to 8000 |
ADMIN_USERNAME + ADMIN_PASSWORD |
no | Set BOTH to gate /chat behind a username+password form. Unset = wide-open /chat (local dev). When set, also set SESSION_SECRET so sessions survive process restarts. |
BOT_API_KEYS |
no | CSV of bearer tokens accepted on /chat/api for programmatic clients (the litellm-loop skill, CI bots, etc.). Mint with openssl rand -base64 32; clients pass Authorization: Bearer <key>. Multiple keys co-exist for rotation. Compatible with ADMIN_USERNAME/ADMIN_PASSWORD — either auth path is accepted. |
LOGFIRE_TOKEN |
no | Pydantic Logfire write token. When set, agent runs, HTTPX calls, FastAPI requests, and the gather subprocess are traced and shipped to Logfire. No-op without it. |
uv run pytest