Workflow Architecture¶
Every message runs through a four-step Agno Workflow:
on_message (ChatCog)
└── Workflow.arun()
├── Step 1: FetchHistory
├── Step 2: LLMChat
├── Step 3: ExtractRenderTables
└── Step 4: SendResponse
The workflow is defined in dango/workflow.py and created once at startup via create_discord_workflow().
Step 1 — FetchHistory¶
File: dango/steps/fetch_history.py
Pulls recent messages from the channel using the Discord API and converts them to Agno Message objects. Key behaviours:
- Stops at a
[new chat]marker (dropped by/newchat) - Respects
history_limitfromruntime.yml - Resolves
dango_deep_*.jsonattachments back to proper user turns (used by/deep) - Builds a
mention_map(<@123>→"Alice") for mention resolution in the next step
Step 2 — LLMChat¶
File: dango/steps/call_agent.py
Runs the Agno Agent against the formatted history plus the current message. Key behaviours:
- Selects fast or deep agent based on
AUTO_ROUTE,_force_deepflag, or URL-upgrade logic - Resolves
@mentiontokens in message content using themention_map - Downloads image attachments and passes them to the model
- Trims history to
CONTEXT_TOKEN_BUDGETbefore sending - Handles retries (Gemini: model-level; others: 2× at agent level) and bidirectional fallback
The Agno Agent uses a dynamic instructions callback that reads session_state on every call, injecting the user's display name and current time into the system prompt when ENABLE_CONTEXTUAL_SYSTEM_PROMPT=on.
Step 3 — ExtractRenderTables¶
File: dango/steps/table_steps.py
Scans the LLM response for Markdown tables and renders each one as a PNG using Pillow. Noto Sans CJK fonts are used so Chinese, Japanese, and Korean text renders correctly.
If no tables are found, this step passes through transparently.
Step 4 — SendResponse¶
File: dango/steps/send_response.py
Sends the final response back to Discord:
- Splits messages longer than Discord's 2000-character limit across multiple messages
- Attaches rendered table PNG files (and cleans up the temp files afterward)
- Replies to the original message when it can still be fetched, otherwise falls back to
channel.send() - Appends any
[dango-sysinfo]notes (e.g. fallback notifications) - Handles error messages forwarded from earlier steps
Entry points¶
The workflow is invoked from two discord.py Cogs:
| Cog | File | Triggers |
|---|---|---|
ChatCog |
dango/commands/chat_commands.py |
on_message, /newchat, /deep |
AdminCog |
dango/commands/admin_commands.py |
All admin slash commands |
The workflow is driven by ChatCog.on_message: it fires for every Discord message, checks whether the bot should respond (mention, allowed channel, or allowed DM user), builds the message_data structure, and calls workflow.arun(). The /deep command runs the same pipeline with the deep model forced on.
Custom slash commands and agent tools added via dango.extensions plug in separately — see Custom Commands & Tools.