Skip to main content

awdiscord

Put any agent on Discord as a real bot, with an operator in the loop.

An agent behind an OpenAI-compatible endpoint (an adk serve agent, llama.cpp / Bonsai, vLLM, Ollama, LM Studio, a gateway) becomes a Discord bot with slash commands, plugins each server turns on for itself, rate limits, and approval buttons that only your operators can press. Your model, your machine.

pip install "awdiscord[gateway]"
awdiscord setup        # paste the bot token once; it does the rest
awdiscord run

setup checks the token is a bot token, writes the app's profile and least-permission install settings, finds a model server running on this machine (Bonsai/llama.cpp :8090/:8080, Ollama :11434, LM Studio :1234, vLLM :8000), makes the app's owner (or team) the operators, saves everything to ~/.awdiscord/awdiscord.env (mode 600), registers the commands and prints the install link. The one step Discord does not allow from an API is creating the application: do that at https://discord.com/developers/applications, open Bot, press Reset Token.

command what it does
/ask question [persona] [private] the agent answers; private shows only you
/share posts your last answer in the channel
/status is the agent up, which personas exist
/plugins list|info|enable|disable what this server has on (enable/disable: Manage Server)

Plugins

Built in:

plugin default commands
community on /poll with vote buttons, /faq ask|list|set|remove (the agent answers from the FAQ only), /rules
projects off /task add|list|start|done|assign|drop, /standup written by the agent from the board
github off /gh board|untracked|issue|connect for a GitHub Projects board (AWDISCORD_GITHUB_TOKEN)

Each server turns plugins on and off for itself; its data is kept per server and never visible to another. A plugin's persona text is added to /ask only where it is on.

Write your own:

awdiscord new-plugin standup-bot      # scaffolds ~/.awdiscord/plugins/standup_bot.py
awdiscord plugins                     # proves it loads (exit 1 names anything that failed)
from awdiscord.plugins import Command, Option, Plugin

def hello(ctx):                        # ctx.user, ctx.guild, ctx.store, ctx.agent, ctx.opt()
    return ctx.reply(f"hello <@{ctx.user}>")

PLUGIN = Plugin(name="hello", version="0.1.0", description="Says hello",
                commands=[Command("hello", "Say hello", hello)])

Plugins are found in awdiscord.builtin, the awdiscord.plugins entry-point group (so pip install a plugin package and it loads), AWDISCORD_PLUGIN_DIRS and ~/.awdiscord/plugins. AWDISCORD_PLUGINS=a,b narrows the set.

Packs

awdiscord pack install ./packs/dgg_pm                       # a plugin pack
awdiscord pack install ~/my-agent-pack                      # an awdk agent pack
awdiscord pack install https://github.com/org/repo#packs/x  # from git
awdiscord pack list | remove NAME

An agent pack (brain_pack.yaml / agent.yaml / skills/, the awdk and awpack shape) becomes a plugin: its system prompt rides along on /ask where it is on, its skills are listed to the agent, and /<pack> ask answers in its voice. packs/dgg_pm is an example plugin pack: Digital Ground Game's org board with their own status flow.

Run it under a supervisor

awdiscord run --inbox-port 8371 opens GET /health for a supervisor. The awdk addon manifest does exactly that, so adk addon (and awsh) can start, health-check and restart the bot like any other local component.

Rails

  • Bot tokens only. Every request is sent as Authorization: Bot <token>, and the runtime asks Discord who the token belongs to before anything else. A user account is refused. Driving a person's account ("self-bot") is against Discord's terms.
  • Least permission. The install link asks for bot + applications.commands and four permissions: view channel, send messages, embed links, read message history. install_url() refuses administrator, manage-server, manage-roles, manage-webhooks, ban, kick and mention-everyone.
  • Rate limits. Token buckets per user, per server and for the whole bot; a request must find room in all three, and a refusal says how long to wait.
  • Mentions off. Every answer goes out with allowed_mentions: {parse: []}, so an agent cannot ping @everyone.
  • Approvals. A card with Approve / Deny buttons. The button id carries an HMAC of the approval id and the verb, only user ids in AWDISCORD_OPERATORS count, a decision is taken exactly once, and the result is POSTed to your callback with an X-Awdiscord-Signature header (HMAC-SHA256 of the body under the same secret).

Approvals from your own code

Run the bot with an inbox, then raise cards from anything on the same machine:

export AWDISCORD_OPERATORS=<your user id>
export AWDISCORD_APPROVAL_SECRET=$(python -c "import secrets;print(secrets.token_hex(32))")
export AWDISCORD_INBOX_PORT=8371 AWDISCORD_INBOX_TOKEN=<a long random string>
awdiscord run &
awdiscord approval "Publish the release notes?" "v1.4 notes are drafted" \
    --callback http://127.0.0.1:9000/decided

Without AWDISCORD_APPROVAL_CHANNEL the card is sent to each operator by DM.

Use the pieces alone

from awdiscord import install_url, RateLimiter, ApprovalDesk, post_webhook, Dispatcher

install_url("123456789012345678")
post_webhook("https://discord.com/api/webhooks/...", "deploy finished")

Dispatcher takes the interaction JSON Discord sends and returns the response JSON, with no network, so the bot's behaviour is testable with plain dicts.

Configuration

variable meaning
DISCORD_BOT_TOKEN / AWDISCORD_TOKEN the bot token
DISCORD_APP_ID / AWDISCORD_APP_ID application id (looked up from the token if unset)
AWDISCORD_AGENT_URL, AWDISCORD_AGENT_MODEL, AWDISCORD_AGENT_KEY the agent
AWDISCORD_PERSONAS_DIR <name>.md or <name>/SOUL.md files become /ask persona: choices
AWDISCORD_DEFAULT_PERSONA persona used when none is chosen
AWDISCORD_CA_BUNDLE CA file for an agent served over a private CA
AWDISCORD_OPERATORS, AWDISCORD_APPROVAL_SECRET enable approvals
AWDISCORD_APPROVAL_CHANNEL post cards here instead of DMing operators
AWDISCORD_INBOX_PORT, AWDISCORD_INBOX_TOKEN, AWDISCORD_INBOX_HOST local inbox
AWDISCORD_DEV_GUILD also register commands on one server (instant, for testing)
AWDISCORD_STATE_DIR where approvals are stored (default ~/.awdiscord)

awdiscord --self-test proves every rail offline and exits non-zero if one does not hold.

License

Apache-2.0

Metadata

Release files for awdiscord 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for awdiscord 0.2.1
File Size Uploaded
awdiscord-0.2.1.tar.gz 82.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for awdiscord 0.2.1
File Interpreter ABI Platform
awdiscord-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 159.1 kB

Release files / awdiscord-0.2.1.tar.gz

Download URL awdiscord-0.2.1.tar.gz
Size 82.8 kB
Tags Source
SHA-256 checksum
How to use checksums
567ac087de4e1c069f1a8c3faa27ae9476ba6f0a17cc191465f5d4282356c78c
BLAKE2b-256 checksum
How to use checksums
df6d4c785413e3c6ff11b505069fbeade636673dd4a89af2bade9d251d075727
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / awdiscord-0.2.1-py3-none-any.whl

Download URL awdiscord-0.2.1-py3-none-any.whl
Size 76.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
08931cc911aa5f1e59f2ed72df773f13b834fb9cad79b23131de96a1469f8625
BLAKE2b-256 checksum
How to use checksums
9524570d2d33b07bb1f8740edd53f831d3cc55db813bff3f6a9725da2a34295b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page