OpenRouter is used when no provider flag is set. Claude is selected with .cl, and GLM stays available via .glm.
95 lines
8.8 KiB
Markdown
95 lines
8.8 KiB
Markdown
# AGENTS.md
|
|
|
|
Repository guidance for automated coding tools and contributors.
|
|
|
|
## Running the bot
|
|
|
|
```bash
|
|
uv sync
|
|
uv run python -m playwright install chromium
|
|
uv run python main.py
|
|
```
|
|
|
|
Do not use `crawl4ai-setup` in unprivileged containers such as Pterodactyl: it invokes Playwright with `--with-deps` and attempts to become root. Installing Chromium with the command above only writes to the current user's Playwright cache. If Chromium reports missing shared libraries at runtime, they must be included in the Docker image by its administrator.
|
|
|
|
Logs go to `bot.log` by default (rotating, 10 MB, 3 backups). Override with `LOG_FILE=<path>`.
|
|
|
|
Run the test suite with:
|
|
|
|
```bash
|
|
uv run python -m unittest discover -s tests -v
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Copy `config.example.toml` to `config.toml` and fill in credentials, or use environment variables (`.env.example` shows the env-var form). Both sources are merged at startup — env vars take precedence.
|
|
|
|
Key env vars: `TG_API_ID`, `TG_API_HASH`, `SESSION_DIR`, `ACCOUNTS_JSON` (JSON list) or `ACCOUNT_SESSIONS` (comma-separated session names), `BOT_CONFIG` (path to config file), and `BOT_DOTENV_PATH` (path to `.env`).
|
|
|
|
Set `use_qr = true` on an account to authorize a new user session by scanning the QR code printed in the terminal. Existing authorized sessions start normally. If the account has 2FA enabled, Kurigram asks for its password in the terminal after the QR code is scanned.
|
|
|
|
The `.txt` transcription command uses `[elevenlabs]` settings or `ELEVENLABS_API_KEY`, `ELEVENLABS_MODEL_ID`, `ELEVENLABS_LANGUAGE_CODE`, and `ELEVENLABS_REQUEST_TIMEOUT_SECONDS`.
|
|
|
|
The `.ai` agent command uses `[ai.openrouter]` when no provider flag is given. `[ai.glm]` is selected with `.glm`, `[ai.deepseek]` with `.ds`, `[ai.claude]` with `.cl` (or `.claude`), and `[ai.openrouter]` explicitly with `.or` (or `.openrouter`). If several provider flags are present, the last one wins. Provider environment variables are `GLM_API_URL`, `GLM_API_KEY`, `GLM_MODEL`, `DEEPSEEK_API_URL`, `DEEPSEEK_API_KEY`, `DEEPSEEK_MODEL`, `CLAUDE_API_URL`, `CLAUDE_API_KEY`, `CLAUDE_MODEL`, `OPENROUTER_API_URL`, `OPENROUTER_API_KEY`, and `OPENROUTER_MODEL`. Z.AI GLM uses the Chat Completions protocol; DeepSeek uses the Responses protocol; Claude uses the Anthropic Messages API; OpenRouter uses its OpenAI-compatible Chat Completions API. The default Claude model is `claude-haiku-5-5`. The default OpenRouter model is `anthropic/claude-haiku-5.5`. `.th` enables reasoning. For Claude, `.th` sends adaptive thinking with summarized display; without it, thinking is disabled. For OpenRouter, `.th` sends `reasoning.effort=medium` and later tool turns echo `reasoning_details`. Brave Search is available in every AI session and requires `BRAVE_SEARCH_API_KEY` or `ai.brave_api_key`. Crawl4AI's `read_web_page` tool is also always available and uses headless Playwright Chromium to render public pages into Markdown before analysis. Configure it with `ai.web_page_timeout_seconds` / `AI_WEB_PAGE_TIMEOUT_SECONDS` and `ai.web_page_max_chars` / `AI_WEB_PAGE_MAX_CHARS`.
|
|
|
|
AI prompt templates are always loaded from `MASTER.txt` and `USER.txt` in `ai.prompt_dir` (or `AI_PROMPT_DIR`) for every invocation. The directory defaults to `prompts`; relative paths are resolved from the repository root. C-style `/* ... */` blocks and `//` comments (at line start or following whitespace) are removed before rendering. Both `{name}` and `{{name}}` placeholders are supported, including dotted values such as `caller_user.id`, `caller_user.name`, `owner.id`, `owner.name`, `is_owner`, `text`, `chat.*`, `message.*`, `reply.*`, `account.*`, `provider`, `thinking`, and `context_messages`. Unknown placeholders fail the command with 👎.
|
|
|
|
## Architecture
|
|
|
|
### Boot path
|
|
`main.py` → `load_app_config()` → `AccountManager` → one Pyrogram (`kurigram`) `Client` per account → `register_common_handlers()` → `pyrogram.idle()`.
|
|
|
|
On supported platforms, `main.py` runs the application with `uvloop`; Windows automatically falls back to the standard `asyncio` event loop.
|
|
|
|
### Command system (`app/commands/`)
|
|
|
|
- **Dispatcher** (`dispatcher.py`): parses `.command` (owner prefix) or `..command` (shared/guest prefix) from message text, resolves to a `CommandDefinition`, runs pre-checks, calls handler.
|
|
- **Registry** (`core.py`): `CommandRegistry` holds `CommandDefinition` objects. `DEFAULT_REGISTRY` is the singleton used at runtime; commands self-register via decorator.
|
|
- **Built-in commands** (`builtins.py`): `.ping`, `.status`, `.restart`, `.id`, `.help`, `.rights`/`.acl`, `.allow`, `.deny` (access), `.echo` (shared), `.txt` (shared ElevenLabs transcription), and `.ai` (shared agent loop). Add new commands here using the `@command_registry.command(...)` decorator pattern.
|
|
- **Check system** (`core.py`): `CommandCheck` functions (e.g. `owner_only_check()`, `shared_access_check()`, `require_reply_check()`, `require_args_check()`) gate handler execution. Chain them with `.check(...)` or `.owner_only()` / `.require_reply()` / `.require_args()` on the builder.
|
|
- **ACL** (`acl.py`): `RuntimeAclStore` persists per-session `<session>.acl.json` allow/deny rules. Only shared (`.is_shared`) commands can be granted to guests.
|
|
|
|
### Transcription (`app/transcription.py`)
|
|
|
|
`.txt` / `..txt` must reply to a voice message, video note, or video. The command downloads the Telegram media temporarily and sends it to ElevenLabs Speech to Text. Invalid usage and transcription failures react to the command message with 👎 and send no error reply. Guests need an ACL grant for `txt`.
|
|
|
|
### Agent loop (`app/agentic.py`)
|
|
|
|
`.ai [count] [.glm|.ds|.cl|.or] [.th] <prompt>` runs `micro-agents` with OpenRouter by default. `.glm` selects GLM, `.ds` selects DeepSeek, `.cl` selects Claude, `.or` selects OpenRouter explicitly, and `.th` enables reasoning. Every session exposes the `micro-agents-tools` Brave Search tool. Unknown leading dot-arguments are ignored. The optional count adds preceding Telegram messages; a replied message is always injected once. Commands are accepted from either message text or a media caption. Media at or below 5 MiB, including media attached directly to the command message, is attached. Invalid syntax and runtime/API failures only react with 👎. Guests need an ACL grant for `ai` and invoke it as `..ai`.
|
|
|
|
While an `.ai` command is running, its Telegram message has a 🤔 reaction. The reaction is removed after a successful answer and replaced with 👎 on failure.
|
|
|
|
The master and user prompt callbacks must render `MASTER.txt` and `USER.txt` from the configured prompt directory; do not add a hard-coded fallback that bypasses those files.
|
|
|
|
Every historical or replied Telegram message is a `UserMessage` rendered through `USER.txt`; `message.id`, `message.text`, `message.date`, `message.user.*`, and `message.is_reply` describe that specific message. Filesystem and Python execution tools are registered through `AgentToolManager` but exposed and executable only when `is_owner` is true. Telegram `send_message`, `send_image`, and `send_file` tools target the current chat. Owner file sends are confined to the repository root; guest file sends are confined to the current session's temporary media directory to prevent repository data disclosure.
|
|
|
|
Each account keeps a bounded in-memory `AgentSessionStore`. Replies to Telegram messages previously emitted as final `.ai` answers reuse the corresponding `micro-agents` session. Session reuse is checked only inside the `.ai` command handler; ordinary replies without `.ai`/`..ai` never trigger the agent. All chunks of a long final answer are mapped to the same session, and session workspaces are cleaned up when evicted or when the account manager stops.
|
|
|
|
### Key types
|
|
|
|
- `CommandContext` — per-invocation context passed to every handler; holds `client`, `manager`, `account`, `message`, parsed `args`/`argv`, and convenience properties (`actor_id`, `owner_id`, `is_owner`, `replied_user`).
|
|
- `AccountState` — runtime state per account (client, ACL store, status, `me` object).
|
|
|
|
## Adding a new command
|
|
|
|
```python
|
|
@command_registry.command(
|
|
"mycommand",
|
|
"Short description",
|
|
usage=".mycommand <arg>",
|
|
category="general",
|
|
).shared() # omit for owner-only; add .require_args() / .require_reply() as needed
|
|
async def mycommand(ctx: "CommandContext") -> None:
|
|
await ctx.reply(ctx.args)
|
|
```
|
|
|
|
Shared commands use `..mycommand` for guests and `.mycommand` for the owner. Owner-only commands use `.mycommand` only.
|
|
|
|
## Change guidelines
|
|
|
|
- Preserve unrelated local and untracked changes.
|
|
- Manage dependencies through `pyproject.toml` and `uv.lock`; use `uv sync` instead of `pip install`.
|
|
- Register commands through `command_registry`; do not add separate Pyrogram handlers for individual commands.
|
|
- Keep shared commands behind `shared_access_check()` and persist ACL changes through `RuntimeAclStore`.
|
|
- Add or update focused `unittest` coverage for behavioral changes.
|