digitalheresy-forgellm 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. digitalheresy_forgellm-1.0.0/LICENSE +21 -0
  2. digitalheresy_forgellm-1.0.0/PKG-INFO +267 -0
  3. digitalheresy_forgellm-1.0.0/README.md +228 -0
  4. digitalheresy_forgellm-1.0.0/Tests/__init__.py +0 -0
  5. digitalheresy_forgellm-1.0.0/Tests/test_core/__init__.py +0 -0
  6. digitalheresy_forgellm-1.0.0/Tests/test_core/test_admin.py +42 -0
  7. digitalheresy_forgellm-1.0.0/Tests/test_core/test_api_key_status.py +684 -0
  8. digitalheresy_forgellm-1.0.0/Tests/test_core/test_archive.py +1128 -0
  9. digitalheresy_forgellm-1.0.0/Tests/test_core/test_base_adapter.py +434 -0
  10. digitalheresy_forgellm-1.0.0/Tests/test_core/test_conversation_db.py +3991 -0
  11. digitalheresy_forgellm-1.0.0/Tests/test_core/test_db_connection_efficiency.py +199 -0
  12. digitalheresy_forgellm-1.0.0/Tests/test_core/test_image_manipulation.py +570 -0
  13. digitalheresy_forgellm-1.0.0/Tests/test_core/test_message_handlers.py +715 -0
  14. digitalheresy_forgellm-1.0.0/Tests/test_core/test_providers.py +874 -0
  15. digitalheresy_forgellm-1.0.0/Tests/test_core/test_tools.py +1705 -0
  16. digitalheresy_forgellm-1.0.0/Tests/test_core/test_web_utils.py +25 -0
  17. digitalheresy_forgellm-1.0.0/Tests/test_discord/__init__.py +0 -0
  18. digitalheresy_forgellm-1.0.0/Tests/test_discord/test_adapter.py +878 -0
  19. digitalheresy_forgellm-1.0.0/Tests/test_discord/test_commands.py +554 -0
  20. digitalheresy_forgellm-1.0.0/Tests/test_image_generation.py +667 -0
  21. digitalheresy_forgellm-1.0.0/Tests/test_telegram/__init__.py +0 -0
  22. digitalheresy_forgellm-1.0.0/Tests/test_telegram/test_admin.py +957 -0
  23. digitalheresy_forgellm-1.0.0/Tests/test_telegram/test_message_handlers.py +1643 -0
  24. digitalheresy_forgellm-1.0.0/digitalheresy_forgellm.egg-info/PKG-INFO +267 -0
  25. digitalheresy_forgellm-1.0.0/digitalheresy_forgellm.egg-info/SOURCES.txt +49 -0
  26. digitalheresy_forgellm-1.0.0/digitalheresy_forgellm.egg-info/dependency_links.txt +1 -0
  27. digitalheresy_forgellm-1.0.0/digitalheresy_forgellm.egg-info/requires.txt +17 -0
  28. digitalheresy_forgellm-1.0.0/digitalheresy_forgellm.egg-info/top_level.txt +2 -0
  29. digitalheresy_forgellm-1.0.0/forgellm/__init__.py +1 -0
  30. digitalheresy_forgellm-1.0.0/forgellm/core/__init__.py +0 -0
  31. digitalheresy_forgellm-1.0.0/forgellm/core/archive.py +360 -0
  32. digitalheresy_forgellm-1.0.0/forgellm/core/base_adapter.py +1508 -0
  33. digitalheresy_forgellm-1.0.0/forgellm/core/conversation.py +506 -0
  34. digitalheresy_forgellm-1.0.0/forgellm/core/database.py +2244 -0
  35. digitalheresy_forgellm-1.0.0/forgellm/core/initialize.py +705 -0
  36. digitalheresy_forgellm-1.0.0/forgellm/core/message_handlers.py +865 -0
  37. digitalheresy_forgellm-1.0.0/forgellm/core/models.py +80 -0
  38. digitalheresy_forgellm-1.0.0/forgellm/core/providers/__init__.py +1 -0
  39. digitalheresy_forgellm-1.0.0/forgellm/core/providers/anthropic_provider.py +182 -0
  40. digitalheresy_forgellm-1.0.0/forgellm/core/providers/base.py +72 -0
  41. digitalheresy_forgellm-1.0.0/forgellm/core/providers/factory.py +38 -0
  42. digitalheresy_forgellm-1.0.0/forgellm/core/providers/openai_provider.py +132 -0
  43. digitalheresy_forgellm-1.0.0/forgellm/core/tools.py +638 -0
  44. digitalheresy_forgellm-1.0.0/forgellm/core/utils.py +149 -0
  45. digitalheresy_forgellm-1.0.0/forgellm/core/web_utils.py +183 -0
  46. digitalheresy_forgellm-1.0.0/forgellm/discord/__init__.py +0 -0
  47. digitalheresy_forgellm-1.0.0/forgellm/discord/adapter.py +887 -0
  48. digitalheresy_forgellm-1.0.0/forgellm/telegram/__init__.py +0 -0
  49. digitalheresy_forgellm-1.0.0/forgellm/telegram/adapter.py +1621 -0
  50. digitalheresy_forgellm-1.0.0/setup.cfg +4 -0
  51. digitalheresy_forgellm-1.0.0/setup.py +37 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2026 Digital Heresy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: digitalheresy-forgellm
3
+ Version: 1.0.0
4
+ Summary: LLM-powered multi-platform bot framework (Telegram + Discord; OpenAI + Anthropic)
5
+ Home-page: https://github.com/Digital-Heresy/ForgeLLM
6
+ Author: Digital Heresy
7
+ Author-email: ronin.atx@gmail.com
8
+ License: MIT
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: openai>2.0
13
+ Requires-Dist: anthropic>=0.40
14
+ Requires-Dist: PyYAML
15
+ Requires-Dist: httpx
16
+ Requires-Dist: beautifulsoup4
17
+ Requires-Dist: validators
18
+ Requires-Dist: tiktoken>=0.12
19
+ Requires-Dist: python-telegram-bot>=20.8
20
+ Requires-Dist: discord.py>=2.4
21
+ Requires-Dist: aiosqlite>=0.19
22
+ Requires-Dist: tzdata>=2025.2
23
+ Requires-Dist: pypdf>=6.0
24
+ Requires-Dist: defusedxml>=0.7
25
+ Requires-Dist: charset-normalizer>=3.0
26
+ Requires-Dist: python-docx>=1.2
27
+ Requires-Dist: openpyxl>=3.1
28
+ Requires-Dist: Pillow>=11.0
29
+ Dynamic: author
30
+ Dynamic: author-email
31
+ Dynamic: description
32
+ Dynamic: description-content-type
33
+ Dynamic: home-page
34
+ Dynamic: license
35
+ Dynamic: license-file
36
+ Dynamic: requires-dist
37
+ Dynamic: requires-python
38
+ Dynamic: summary
39
+
40
+ # ForgeLLM
41
+ ForgeLLM bridges a chat platform and a Large Language Model (OpenAI GPT or Anthropic Claude). One bot identity can run multiple platform adapters at once, sharing the same persona and memory - see [Supported Platforms](#supported-platforms).
42
+
43
+ <img src="assets/ForgeLLM_Logo.png" width=200 align=center />
44
+
45
+ ## Supported Platforms
46
+
47
+ | Platform | Requirements | Bot creation |
48
+ |---|---|---|
49
+ | Telegram | A Telegram account **with a username** (not just a phone number) - [create one](https://telegram.org/). In groups, the bot must be [administrator](https://www.alphr.com/add-admin-telegram/) to respond to its name/nickname/initials. | [BotFather](https://t.me/BotFather) |
50
+ | Discord | Invite the bot with the `bot` and `applications.commands` scopes. Guild channels are opt-in - run `/enable` in each channel you want it active in (see [Commands and Interactions](#commands-and-interactions)). | [Discord Developer Portal](https://discord.com/developers/applications) |
51
+
52
+ Every feature below - conversation memory, tools, image generation, search, account linking - is implemented once in the shared response engine (`core/base_adapter.py`) and works identically on every platform. Adding a future platform means adding a row here and a new adapter under `forgellm/<platform>/`, not touching the feature logic itself.
53
+
54
+ ## Core Features
55
+ * Each platform's interface handles special commands and basic "chatty" responses that don't require an LLM, like "Hello". Dynamic conversations are handed off to the LLM while the platform acts as the interaction broker.
56
+ * Pass URLs in [square brackets] and mention how the bot should interpret them.
57
+ * Example: "What do you think of this article? [https://some_site/article]"
58
+ * Uses a separate model (configurable via `url_model`) to handle larger URL content.
59
+ * Share documents and text files for analysis and summarisation.
60
+ * Supported formats: PDF (via pypdf), Microsoft Office documents (.docx via python-docx, .xlsx via openpyxl), plain-text files (.txt, .md, .rst, .csv, .json, etc.), HTML, and XML (via defusedxml).
61
+ * The bot extracts and summarises content, with automatic encoding detection for non-UTF-8 files. Files over 20 MB are rejected.
62
+ * Can be disabled via `document_processing: false` in config.
63
+ * Share images for visual analysis.
64
+ * The bot analyzes photos using your configured LLM's vision capabilities. Optional captions are preserved; EXIF metadata is stripped before sending to any provider.
65
+ * Can be disabled via `image_analysis: false` in config. Requires the chat_model to support vision (controlled per-model in models.yaml, default true).
66
+ * Generate images from text descriptions.
67
+ * Request images using natural language. The bot creates them via gpt-image-1 and sends them to your chat with a brief description as the caption.
68
+ * Requires an OpenAI API key. Supports three sizes: 1024x1024 (default), 1536x1024, and 1024x1536.
69
+ * Configurable cooldown between requests via `image_gen_cooldown_s` (default 30 seconds per user).
70
+ * The bot stores generation prompts in conversation history (the full prompt, not the caption), enabling it to recall past images and refine them across sessions. Ask for variations (e.g., "make it darker", "different composition") without repeating the full original prompt.
71
+ * Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
72
+ * Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
73
+ * All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
74
+ * Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
75
+ * Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
76
+ * The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
77
+ * This eliminates amnesia when switching between private and group chats.
78
+ * Conversation archive preserves long-term context without consuming token budget.
79
+ * Older messages are automatically distilled into concise daily summaries (Tier 1), then progressively compressed into monthly digests (Tier 2). Raw messages are never deleted; archive rows surface seamlessly in search results and context loading.
80
+ * Configurable via `archive_days` (default 60 days before Tier 1 triggers; Tier 2 triggers at 2x this value).
81
+ * Persistent tone preference, carried across every chat and linked platform.
82
+ * Just ask, in natural conversation - e.g. "talk to me in a cute voice from now on" or "be more formal with me." The bot recognizes the request and remembers it for you specifically, without affecting how it talks to anyone else in a shared chat.
83
+ * Clear it the same way (e.g. "go back to normal"). Not a slash command - the LLM detects and applies the request automatically.
84
+ * Users can manage privacy via two commands:
85
+ * `/forget` - In private chats, clears your full conversation and resets all active sessions. In group/guild chats, removes only your messages and cleans up paired bot replies.
86
+ * `/private` - Show status, or use `/private on`/`/private off` to change it (private chats only). When ON, your messages in private chats are excluded from group conversation contexts, enabling selective privacy even in shared groups.
87
+ * Link your accounts across platforms with `/link`/`/unlink`, so the bot shares one unified memory for you regardless of which platform you're talking on - facts, search results, and tone preference all carry over.
88
+
89
+ ## Why a Chat Platform?
90
+ Using a chat platform as the interface not only solves "exposing" the interface, but gives you boatloads of interactivity over a standard Command Line interface, or trying to create a website with input boxes and submit buttons to try to handle everything:
91
+ 1. Chat platforms already let you paste in verbose, multiline messages.
92
+ 2. Chat platforms already let you paste in pictures, videos, links, etc.
93
+ 3. Chat platforms already let you react with emojis, stickers, etc.
94
+ 4. Message reactions (👀) provide a lightweight read receipt without breaking conversation flow.
95
+
96
+ ## Supported LLM Providers
97
+ ForgeLLM selects the LLM provider automatically based on the model name:
98
+
99
+ | Model prefix | Provider | Example models |
100
+ |---|---|---|
101
+ | `gpt-` | OpenAI | `gpt-4o`, `gpt-4o-mini`, `gpt-5-mini` |
102
+ | `claude-` | Anthropic | `claude-sonnet-4-6`, `claude-haiku-4-5` |
103
+
104
+ Simply set `chat_model` (and optionally `url_model`) in your `config.yaml` to any supported model and supply the corresponding API key - no other changes needed. A future provider only needs a new prefix mapping in `core/providers/factory.py`.
105
+
106
+ ## Directories
107
+ ForgeLLM creates the following directories:
108
+
109
+ - **`configs`** - Bot configuration and model parameters (path configurable via `FORGELLM_CONFIGS_PATH`)
110
+ - `config.yaml` - Default bot configuration file (filename used throughout this README); can be changed by passing `config_file` to any adapter's `.set()`
111
+ - `models.yaml` - Token limits for each LLM model. Sensible defaults are built into the code, so the bot runs without this file. To override or add models, copy `configs/models.yaml.example` to `models.yaml` in this directory and edit it (an instance `models.yaml` replaces the built-in defaults entirely - list every model you use, not just the ones you change)
112
+ - **`prompts`** - Bot personas (path configurable via `FORGELLM_PROMPTS_PATH`)
113
+ - `test_personality.prmpt` - Default bot persona file (filename used throughout this README); can be changed by passing `prompt_file` to any adapter's `.set()`
114
+ - A system appendix is automatically appended to every persona at runtime, teaching the LLM about cross-chat memory, search behavior, self-addressing, and confidentiality guidelines. User messages include speaker annotations with chat context and timestamps so the LLM always knows who is speaking, in which chat, and when.
115
+ - **`logs`** - Bot instance logs (one per startup, named after the bot's platform username or `instance_name` config, e.g. `my_bot_2026-03-29_10-30-45.log`)
116
+ - Logs include anonymized platform IDs (user/chat/channel numeric identifiers) for privacy. Console shows INFO-level ForgeLLM messages only, prefixed with an `[identity label]` (the bot's platform username by default, or `instance_name` when configured).
117
+ - Log file timestamps are UTC in `[YYYY-MM-DD HH:MM:SS.mmm]` format.
118
+ - Bot keeps the 10 most recent logs per bot instance, automatically pruning older ones.
119
+ - Pass `-v` or `--verbose` on startup for DEBUG-level logging.
120
+ - **`data`** - SQLite database (default `conversations.db`, customizable via `instance_name` config) storing all messages, users, and chats
121
+ - Users manage their data via `/forget` and `/private` commands.
122
+
123
+ ### Environment Variables for Paths
124
+ Override default directory locations by setting these environment variables (useful for containerized deployments):
125
+
126
+ | Variable | Purpose | Default |
127
+ |----------|---------|---------|
128
+ | `FORGELLM_CONFIGS_PATH` | Directory containing `config.yaml` and `models.yaml` | `{exec_dir}/configs` |
129
+ | `FORGELLM_PROMPTS_PATH` | Directory containing prompt files | `{exec_dir}/prompts` |
130
+ | `FORGELLM_LOGS_PATH` | Directory for log files | `{exec_dir}/logs` |
131
+ | `FORGELLM_DATA_PATH` | Directory containing `conversations.db` | `{exec_dir}/data` |
132
+
133
+ If unset, all paths default to subdirectories of the execution directory (the directory containing your entry-point script).
134
+
135
+ ## API Keys
136
+ ForgeLLM supports five API keys. OpenAI, Anthropic, and VirusTotal keys load from environment variables or `.key` files. Platform keys load from their own `platforms.<name>.api_key` config field or env var (no `.key` file); config wins when explicitly set:
137
+
138
+ | Key | Env Var | File/Config | When required |
139
+ |-----|---------|-------------|---------------|
140
+ | [OpenAI](https://platform.openai.com/api-keys) | `FORGELLM_OPENAI_API_KEY` | `openai.key` | For `gpt-*` models, or for image generation in any deployment |
141
+ | [Anthropic](https://console.anthropic.com/settings/keys) | `FORGELLM_ANTHROPIC_API_KEY` | `anthropic.key` | For `claude-*` models |
142
+ | [Telegram](https://t.me/BotFather) | `FORGELLM_TELEGRAM_API_KEY` | `platforms.telegram.api_key` in `config.yaml` | Required to run the Telegram adapter |
143
+ | [Discord](https://discord.com/developers/applications) | `FORGELLM_DISCORD_API_KEY` | `platforms.discord.api_key` in `config.yaml` | Required to run the Discord adapter |
144
+ | [VirusTotal](https://www.virustotal.com/gui/my-apikey) | `FORGELLM_VIRUSTOTAL_API_KEY` | `virustotal.key` | For URL analysis |
145
+
146
+ Missing provider keys (OpenAI or Anthropic) disable chat and URL analysis but allow the bot to start. Missing OpenAI key disables image generation. Missing VirusTotal disables URL analysis. At least one platform key is required - whichever adapter(s) you `.set()` will not start without its own key.
147
+
148
+ Key files are created in the execution directory (or `FORGELLM_KEYS_PATH` for legacy deployments). Alternatively, set environment variables before launching, e.g.:
149
+ ```python
150
+ os.environ['FORGELLM_OPENAI_API_KEY'] = my_vault.get('openai_key')
151
+ os.environ['FORGELLM_ANTHROPIC_API_KEY'] = my_vault.get('anthropic_key')
152
+ os.environ['FORGELLM_TELEGRAM_API_KEY'] = my_vault.get('telegram_key')
153
+ os.environ['FORGELLM_DISCORD_API_KEY'] = my_vault.get('discord_key')
154
+ os.environ['FORGELLM_VIRUSTOTAL_API_KEY'] = my_vault.get('virustotal_key')
155
+ ```
156
+
157
+ ## Commands and Interactions
158
+
159
+ ### User Commands
160
+ - `/nick <name>` - Set your nickname (for bot use in group/guild chats).
161
+ - `/forget` - Clear your conversation history. Shows a confirmation prompt before deletion. In private chats, clears everything and resets all active sessions. In group/guild chats, removes only your messages.
162
+ - `/private` - Show your current private mode status (private/DM chats only). Use `/private on` or `/private off` to change it. When ON, your messages are excluded from group context loading; turning OFF permanently deletes your private-flagged messages.
163
+ - `/link [code]` - Without a code, issues a 15-minute single-use linking code. Redeem it from another platform with `/link <code>` to merge your accounts into one shared memory. Private/DM chats only.
164
+ - `/unlink` - Remove your account from its linked group. Private/DM chats only.
165
+
166
+ ### Admin Commands
167
+ - `/tools` - List tools available to this bot instance (admin-only; private, group/guild, and supergroup chats - channels always denied). Shows the built-in search_messages and generate_image tools plus any webhook or MCP tools defined in config.yaml; in group/guild chats only tools with `allow_groups: true` are listed.
168
+ - `/start` / `/stop` - Bring the bot online or offline (admin-only).
169
+ - `/wipe` - Permanently delete all bot data (admin-only; confirmation prompt before deletion).
170
+ - `/help` - Display available commands and usage information. In private/DM chats, if you are a bot owner, also shows administrator-only commands.
171
+ - `/enable` / `/disable` (**Discord only**) - Opt the current guild channel into or out of bot activity (admin-only). Discord channels are opt-in by default; there's no Telegram equivalent since a Telegram group is always implicitly active.
172
+
173
+ ### Group/Guild Chat Triggers
174
+ The bot responds in a group (Telegram) or guild channel (Discord) when you mention the bot, mention it by nickname/initials (configured via `config.yaml`), or reply directly to one of its messages.
175
+
176
+ When multiple bots are @mentioned in the same message, the bot coexists: if you mention its nickname/initials or reply to its message, it always engages (you may be intentionally addressing both bots). If the only trigger is a reply to the bot's message AND the message exclusively addresses a different bot via @mention or plain-text name-address (e.g., "Cesura, response?" or "thoughts, Cesura?"), the bot yields silently - this supports threaded context without redundant responses. Plain-text addresses must match a known bot in the chat (its current username, or its current or past display name) to trigger a yield.
177
+
178
+ Admin slash commands (`/help`, `/nick`, `/forget`, `/private`, `/tools`, `/start`, `/stop`, `/wipe`) work identically everywhere in private/DM chats. In groups/guilds, addressing differs per platform - see the mechanics table below.
179
+
180
+ ### Private Chat Behavior
181
+ In a private/DM chat, the bot responds to all your messages. If you reply to an earlier message not already in the bot's context window, that message is automatically surfaced as inline context so the bot understands the full conversation thread.
182
+
183
+ ### Read Receipt
184
+ As soon as the bot is triggered and about to respond (not deferring to another bot), it sends a 👀 emoji reaction on your message and shows a typing indicator while composing the reply - both arrive before the full LLM response, so you get quick feedback that the bot received your message.
185
+
186
+ ### Platform Mechanics
187
+ Everything above is shared, platform-neutral behavior. Only the underlying wire protocol differs:
188
+
189
+ | | Telegram | Discord |
190
+ |---|---|---|
191
+ | Mentioning the bot | `@botname` | native `@` mention |
192
+ | Slash commands in groups need addressing? | Yes - use `/help@botname`, or reply to the bot's message, so co-present bots don't all answer a bare command | No - Discord routes slash commands natively to whichever bot registered them |
193
+ | Read-receipt fallback | Falls back to a "Got it!" text reply on older clients without reaction support | N/A (reactions always supported) |
194
+
195
+ ## Bot Setup
196
+ 1. Ensure API keys are set up (see [API Keys](#api-keys)) and create your bot on each platform you plan to run (see [Supported Platforms](#supported-platforms)).
197
+ 2. Install ForgeLLM: `pip install digitalheresy-forgellm` (the importable package is `forgellm` regardless of the install name, e.g. `from forgellm.telegram.adapter import TelegramAdapter`).
198
+ 3. Configure `config.yaml` (created on first run). Top-level keys apply regardless of platform; each platform gets its own `platforms.<name>` section:
199
+ - `platforms.<name>.bot_owner`: username(s) with admin access (required, no `@`). Accepts a single string or a YAML list - matched by username string on every platform (no numeric IDs anywhere).
200
+ - `platforms.<name>.api_key`: that platform's bot API key (required to run that adapter). Lookup order: (1) this config field, (2) the platform's env var from the [API Keys](#api-keys) table. Config wins when explicitly set. Exits on placeholder, missing, or malformed key.
201
+ - `chat_model` / `url_model`: LLM models for conversation and URL analysis (e.g. `gpt-4o-mini`, `claude-sonnet-4-6`).
202
+ - `bot_nickname` / `bot_initials`: Names the bot responds to in groups/guilds.
203
+ - `instance_name`: Optional label for console prefix, log filename, and database name (e.g. `MyBot` produces `[MyBot] INFO: ...` on console, `MyBot_{timestamp}.log` logs, and `MyBot.db` database); omit to use the bot's platform username for logging and `conversations.db` for database. Use distinct names when running multiple bot instances in the same directory.
204
+ - `token_limit` / `search_limit`: Max tokens (default: model's maximum) / max search results (default: 30).
205
+ - `archive_days`: Days before messages are eligible for archival (optional; default 60, minimum 1). See [Core Features](#core-features).
206
+ - `document_processing` / `image_analysis`: Optional bools (default: true) to disable document summarisation / image analysis. `image_analysis` requires the chat_model to support vision (default true per-model in models.yaml).
207
+ - `image_gen_cooldown_s`: Optional int (default 30, minimum 0). Seconds a user must wait between image generation requests.
208
+ - `allow_local_webhooks`: Set to `true` to permit webhook/MCP URLs targeting loopback or link-local addresses (optional; default `false`). Useful when tools like Home Assistant run on the same host.
209
+ - `max_conversations`: Optional max chats kept in memory at once (default: 500, minimum 1). Least-recently-used chats beyond this cap are evicted and reload from the database on their next message.
210
+ - `tools`: Optional list of webhook and MCP tool definitions (admin-only, private chat only). See [docs/tools.md](docs/tools.md) for schema and examples.
211
+
212
+ Example `config.yaml`:
213
+ ```yaml
214
+ instance_name: MyBot
215
+ bot_nickname: Panzer
216
+ bot_initials: PZ
217
+ chat_model: gpt-4o
218
+ url_model: gpt-4o
219
+
220
+ platforms:
221
+ telegram:
222
+ api_key: abc123...
223
+ bot_owner: [MyUsername]
224
+ ```
225
+ Swap `telegram:` for `discord:` (with a Discord username in `bot_owner`) to run Discord instead - everything else is identical. Include both sections to run both (see [Running Multiple Platforms Together](#running-multiple-platforms-together)).
226
+ 4. One-time platform setup:
227
+
228
+ | Platform | One-time setup |
229
+ |---|---|
230
+ | Telegram | Disable group privacy mode in BotFather: `/setprivacy` -> select your bot -> Disable. Otherwise the bot won't receive group messages that don't mention it, so it can't index other bots or load cross-chat context. |
231
+ | Discord | Run `/enable` in each guild channel you want the bot active in - channels are opt-in by default. |
232
+ 5. Run the bot:
233
+ ```python
234
+ from forgellm.telegram.adapter import TelegramAdapter
235
+ telegram_bot = TelegramAdapter.set()
236
+ telegram_bot.run()
237
+ ```
238
+ Swap in `forgellm.discord.adapter.DiscordAdapter` to run Discord instead - same `.set()`/`.run()` shape. `run()` is synchronous and blocks until the bot is stopped.
239
+ 6. Type `/help` to see all available commands.
240
+
241
+ ### Running Multiple Platforms Together
242
+ Construct every adapter you want, then run them concurrently via `start_async()` instead of `run()` (they share one event loop) so they share one bot identity, persona, and memory - the bot's own accounts on each platform are automatically linked to each other on startup, the same way `/link` connects two user accounts:
243
+ ```python
244
+ import asyncio
245
+ from forgellm.telegram.adapter import TelegramAdapter
246
+ from forgellm.discord.adapter import DiscordAdapter
247
+
248
+ async def main():
249
+ telegram_bot = TelegramAdapter.set()
250
+ discord_bot = DiscordAdapter.set()
251
+ await asyncio.gather(telegram_bot.start_async(), discord_bot.start_async())
252
+
253
+ asyncio.run(main())
254
+ ```
255
+ The bundled `run_bots.py` wires this up for you, including the bot's own cross-platform auto-link.
256
+
257
+ ## Resources
258
+ * GitHub repository [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) has guides to create a Telegram bot.
259
+ * GitHub repository [discord.py](https://github.com/Rapptz/discord.py) has guides to create a Discord bot.
260
+ * For more information on OpenAI models and token limits:
261
+ * [OpenAI model overview and maximum tokens](https://platform.openai.com/docs/models)
262
+ * [OpenAI message conversion to tokens](https://github.com/openai/openai-python)
263
+ * [OpenAI custom fine-tuning](https://platform.openai.com/docs/guides/model-optimization)
264
+ * [OpenAI's tiktoken library](https://github.com/openai/tiktoken/tree/main)
265
+ * For more information on Anthropic Claude models:
266
+ * [Anthropic model overview and context windows](https://docs.anthropic.com/en/docs/about-claude/models)
267
+ * [Anthropic Python SDK](https://github.com/anthropic/anthropic-sdk-python)
@@ -0,0 +1,228 @@
1
+ # ForgeLLM
2
+ ForgeLLM bridges a chat platform and a Large Language Model (OpenAI GPT or Anthropic Claude). One bot identity can run multiple platform adapters at once, sharing the same persona and memory - see [Supported Platforms](#supported-platforms).
3
+
4
+ <img src="assets/ForgeLLM_Logo.png" width=200 align=center />
5
+
6
+ ## Supported Platforms
7
+
8
+ | Platform | Requirements | Bot creation |
9
+ |---|---|---|
10
+ | Telegram | A Telegram account **with a username** (not just a phone number) - [create one](https://telegram.org/). In groups, the bot must be [administrator](https://www.alphr.com/add-admin-telegram/) to respond to its name/nickname/initials. | [BotFather](https://t.me/BotFather) |
11
+ | Discord | Invite the bot with the `bot` and `applications.commands` scopes. Guild channels are opt-in - run `/enable` in each channel you want it active in (see [Commands and Interactions](#commands-and-interactions)). | [Discord Developer Portal](https://discord.com/developers/applications) |
12
+
13
+ Every feature below - conversation memory, tools, image generation, search, account linking - is implemented once in the shared response engine (`core/base_adapter.py`) and works identically on every platform. Adding a future platform means adding a row here and a new adapter under `forgellm/<platform>/`, not touching the feature logic itself.
14
+
15
+ ## Core Features
16
+ * Each platform's interface handles special commands and basic "chatty" responses that don't require an LLM, like "Hello". Dynamic conversations are handed off to the LLM while the platform acts as the interaction broker.
17
+ * Pass URLs in [square brackets] and mention how the bot should interpret them.
18
+ * Example: "What do you think of this article? [https://some_site/article]"
19
+ * Uses a separate model (configurable via `url_model`) to handle larger URL content.
20
+ * Share documents and text files for analysis and summarisation.
21
+ * Supported formats: PDF (via pypdf), Microsoft Office documents (.docx via python-docx, .xlsx via openpyxl), plain-text files (.txt, .md, .rst, .csv, .json, etc.), HTML, and XML (via defusedxml).
22
+ * The bot extracts and summarises content, with automatic encoding detection for non-UTF-8 files. Files over 20 MB are rejected.
23
+ * Can be disabled via `document_processing: false` in config.
24
+ * Share images for visual analysis.
25
+ * The bot analyzes photos using your configured LLM's vision capabilities. Optional captions are preserved; EXIF metadata is stripped before sending to any provider.
26
+ * Can be disabled via `image_analysis: false` in config. Requires the chat_model to support vision (controlled per-model in models.yaml, default true).
27
+ * Generate images from text descriptions.
28
+ * Request images using natural language. The bot creates them via gpt-image-1 and sends them to your chat with a brief description as the caption.
29
+ * Requires an OpenAI API key. Supports three sizes: 1024x1024 (default), 1536x1024, and 1024x1536.
30
+ * Configurable cooldown between requests via `image_gen_cooldown_s` (default 30 seconds per user).
31
+ * The bot stores generation prompts in conversation history (the full prompt, not the caption), enabling it to recall past images and refine them across sessions. Ask for variations (e.g., "make it darker", "different composition") without repeating the full original prompt.
32
+ * Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
33
+ * Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
34
+ * All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
35
+ * Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
36
+ * Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
37
+ * The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
38
+ * This eliminates amnesia when switching between private and group chats.
39
+ * Conversation archive preserves long-term context without consuming token budget.
40
+ * Older messages are automatically distilled into concise daily summaries (Tier 1), then progressively compressed into monthly digests (Tier 2). Raw messages are never deleted; archive rows surface seamlessly in search results and context loading.
41
+ * Configurable via `archive_days` (default 60 days before Tier 1 triggers; Tier 2 triggers at 2x this value).
42
+ * Persistent tone preference, carried across every chat and linked platform.
43
+ * Just ask, in natural conversation - e.g. "talk to me in a cute voice from now on" or "be more formal with me." The bot recognizes the request and remembers it for you specifically, without affecting how it talks to anyone else in a shared chat.
44
+ * Clear it the same way (e.g. "go back to normal"). Not a slash command - the LLM detects and applies the request automatically.
45
+ * Users can manage privacy via two commands:
46
+ * `/forget` - In private chats, clears your full conversation and resets all active sessions. In group/guild chats, removes only your messages and cleans up paired bot replies.
47
+ * `/private` - Show status, or use `/private on`/`/private off` to change it (private chats only). When ON, your messages in private chats are excluded from group conversation contexts, enabling selective privacy even in shared groups.
48
+ * Link your accounts across platforms with `/link`/`/unlink`, so the bot shares one unified memory for you regardless of which platform you're talking on - facts, search results, and tone preference all carry over.
49
+
50
+ ## Why a Chat Platform?
51
+ Using a chat platform as the interface not only solves "exposing" the interface, but gives you boatloads of interactivity over a standard Command Line interface, or trying to create a website with input boxes and submit buttons to try to handle everything:
52
+ 1. Chat platforms already let you paste in verbose, multiline messages.
53
+ 2. Chat platforms already let you paste in pictures, videos, links, etc.
54
+ 3. Chat platforms already let you react with emojis, stickers, etc.
55
+ 4. Message reactions (👀) provide a lightweight read receipt without breaking conversation flow.
56
+
57
+ ## Supported LLM Providers
58
+ ForgeLLM selects the LLM provider automatically based on the model name:
59
+
60
+ | Model prefix | Provider | Example models |
61
+ |---|---|---|
62
+ | `gpt-` | OpenAI | `gpt-4o`, `gpt-4o-mini`, `gpt-5-mini` |
63
+ | `claude-` | Anthropic | `claude-sonnet-4-6`, `claude-haiku-4-5` |
64
+
65
+ Simply set `chat_model` (and optionally `url_model`) in your `config.yaml` to any supported model and supply the corresponding API key - no other changes needed. A future provider only needs a new prefix mapping in `core/providers/factory.py`.
66
+
67
+ ## Directories
68
+ ForgeLLM creates the following directories:
69
+
70
+ - **`configs`** - Bot configuration and model parameters (path configurable via `FORGELLM_CONFIGS_PATH`)
71
+ - `config.yaml` - Default bot configuration file (filename used throughout this README); can be changed by passing `config_file` to any adapter's `.set()`
72
+ - `models.yaml` - Token limits for each LLM model. Sensible defaults are built into the code, so the bot runs without this file. To override or add models, copy `configs/models.yaml.example` to `models.yaml` in this directory and edit it (an instance `models.yaml` replaces the built-in defaults entirely - list every model you use, not just the ones you change)
73
+ - **`prompts`** - Bot personas (path configurable via `FORGELLM_PROMPTS_PATH`)
74
+ - `test_personality.prmpt` - Default bot persona file (filename used throughout this README); can be changed by passing `prompt_file` to any adapter's `.set()`
75
+ - A system appendix is automatically appended to every persona at runtime, teaching the LLM about cross-chat memory, search behavior, self-addressing, and confidentiality guidelines. User messages include speaker annotations with chat context and timestamps so the LLM always knows who is speaking, in which chat, and when.
76
+ - **`logs`** - Bot instance logs (one per startup, named after the bot's platform username or `instance_name` config, e.g. `my_bot_2026-03-29_10-30-45.log`)
77
+ - Logs include anonymized platform IDs (user/chat/channel numeric identifiers) for privacy. Console shows INFO-level ForgeLLM messages only, prefixed with an `[identity label]` (the bot's platform username by default, or `instance_name` when configured).
78
+ - Log file timestamps are UTC in `[YYYY-MM-DD HH:MM:SS.mmm]` format.
79
+ - Bot keeps the 10 most recent logs per bot instance, automatically pruning older ones.
80
+ - Pass `-v` or `--verbose` on startup for DEBUG-level logging.
81
+ - **`data`** - SQLite database (default `conversations.db`, customizable via `instance_name` config) storing all messages, users, and chats
82
+ - Users manage their data via `/forget` and `/private` commands.
83
+
84
+ ### Environment Variables for Paths
85
+ Override default directory locations by setting these environment variables (useful for containerized deployments):
86
+
87
+ | Variable | Purpose | Default |
88
+ |----------|---------|---------|
89
+ | `FORGELLM_CONFIGS_PATH` | Directory containing `config.yaml` and `models.yaml` | `{exec_dir}/configs` |
90
+ | `FORGELLM_PROMPTS_PATH` | Directory containing prompt files | `{exec_dir}/prompts` |
91
+ | `FORGELLM_LOGS_PATH` | Directory for log files | `{exec_dir}/logs` |
92
+ | `FORGELLM_DATA_PATH` | Directory containing `conversations.db` | `{exec_dir}/data` |
93
+
94
+ If unset, all paths default to subdirectories of the execution directory (the directory containing your entry-point script).
95
+
96
+ ## API Keys
97
+ ForgeLLM supports five API keys. OpenAI, Anthropic, and VirusTotal keys load from environment variables or `.key` files. Platform keys load from their own `platforms.<name>.api_key` config field or env var (no `.key` file); config wins when explicitly set:
98
+
99
+ | Key | Env Var | File/Config | When required |
100
+ |-----|---------|-------------|---------------|
101
+ | [OpenAI](https://platform.openai.com/api-keys) | `FORGELLM_OPENAI_API_KEY` | `openai.key` | For `gpt-*` models, or for image generation in any deployment |
102
+ | [Anthropic](https://console.anthropic.com/settings/keys) | `FORGELLM_ANTHROPIC_API_KEY` | `anthropic.key` | For `claude-*` models |
103
+ | [Telegram](https://t.me/BotFather) | `FORGELLM_TELEGRAM_API_KEY` | `platforms.telegram.api_key` in `config.yaml` | Required to run the Telegram adapter |
104
+ | [Discord](https://discord.com/developers/applications) | `FORGELLM_DISCORD_API_KEY` | `platforms.discord.api_key` in `config.yaml` | Required to run the Discord adapter |
105
+ | [VirusTotal](https://www.virustotal.com/gui/my-apikey) | `FORGELLM_VIRUSTOTAL_API_KEY` | `virustotal.key` | For URL analysis |
106
+
107
+ Missing provider keys (OpenAI or Anthropic) disable chat and URL analysis but allow the bot to start. Missing OpenAI key disables image generation. Missing VirusTotal disables URL analysis. At least one platform key is required - whichever adapter(s) you `.set()` will not start without its own key.
108
+
109
+ Key files are created in the execution directory (or `FORGELLM_KEYS_PATH` for legacy deployments). Alternatively, set environment variables before launching, e.g.:
110
+ ```python
111
+ os.environ['FORGELLM_OPENAI_API_KEY'] = my_vault.get('openai_key')
112
+ os.environ['FORGELLM_ANTHROPIC_API_KEY'] = my_vault.get('anthropic_key')
113
+ os.environ['FORGELLM_TELEGRAM_API_KEY'] = my_vault.get('telegram_key')
114
+ os.environ['FORGELLM_DISCORD_API_KEY'] = my_vault.get('discord_key')
115
+ os.environ['FORGELLM_VIRUSTOTAL_API_KEY'] = my_vault.get('virustotal_key')
116
+ ```
117
+
118
+ ## Commands and Interactions
119
+
120
+ ### User Commands
121
+ - `/nick <name>` - Set your nickname (for bot use in group/guild chats).
122
+ - `/forget` - Clear your conversation history. Shows a confirmation prompt before deletion. In private chats, clears everything and resets all active sessions. In group/guild chats, removes only your messages.
123
+ - `/private` - Show your current private mode status (private/DM chats only). Use `/private on` or `/private off` to change it. When ON, your messages are excluded from group context loading; turning OFF permanently deletes your private-flagged messages.
124
+ - `/link [code]` - Without a code, issues a 15-minute single-use linking code. Redeem it from another platform with `/link <code>` to merge your accounts into one shared memory. Private/DM chats only.
125
+ - `/unlink` - Remove your account from its linked group. Private/DM chats only.
126
+
127
+ ### Admin Commands
128
+ - `/tools` - List tools available to this bot instance (admin-only; private, group/guild, and supergroup chats - channels always denied). Shows the built-in search_messages and generate_image tools plus any webhook or MCP tools defined in config.yaml; in group/guild chats only tools with `allow_groups: true` are listed.
129
+ - `/start` / `/stop` - Bring the bot online or offline (admin-only).
130
+ - `/wipe` - Permanently delete all bot data (admin-only; confirmation prompt before deletion).
131
+ - `/help` - Display available commands and usage information. In private/DM chats, if you are a bot owner, also shows administrator-only commands.
132
+ - `/enable` / `/disable` (**Discord only**) - Opt the current guild channel into or out of bot activity (admin-only). Discord channels are opt-in by default; there's no Telegram equivalent since a Telegram group is always implicitly active.
133
+
134
+ ### Group/Guild Chat Triggers
135
+ The bot responds in a group (Telegram) or guild channel (Discord) when you mention the bot, mention it by nickname/initials (configured via `config.yaml`), or reply directly to one of its messages.
136
+
137
+ When multiple bots are @mentioned in the same message, the bot coexists: if you mention its nickname/initials or reply to its message, it always engages (you may be intentionally addressing both bots). If the only trigger is a reply to the bot's message AND the message exclusively addresses a different bot via @mention or plain-text name-address (e.g., "Cesura, response?" or "thoughts, Cesura?"), the bot yields silently - this supports threaded context without redundant responses. Plain-text addresses must match a known bot in the chat (its current username, or its current or past display name) to trigger a yield.
138
+
139
+ Admin slash commands (`/help`, `/nick`, `/forget`, `/private`, `/tools`, `/start`, `/stop`, `/wipe`) work identically everywhere in private/DM chats. In groups/guilds, addressing differs per platform - see the mechanics table below.
140
+
141
+ ### Private Chat Behavior
142
+ In a private/DM chat, the bot responds to all your messages. If you reply to an earlier message not already in the bot's context window, that message is automatically surfaced as inline context so the bot understands the full conversation thread.
143
+
144
+ ### Read Receipt
145
+ As soon as the bot is triggered and about to respond (not deferring to another bot), it sends a 👀 emoji reaction on your message and shows a typing indicator while composing the reply - both arrive before the full LLM response, so you get quick feedback that the bot received your message.
146
+
147
+ ### Platform Mechanics
148
+ Everything above is shared, platform-neutral behavior. Only the underlying wire protocol differs:
149
+
150
+ | | Telegram | Discord |
151
+ |---|---|---|
152
+ | Mentioning the bot | `@botname` | native `@` mention |
153
+ | Slash commands in groups need addressing? | Yes - use `/help@botname`, or reply to the bot's message, so co-present bots don't all answer a bare command | No - Discord routes slash commands natively to whichever bot registered them |
154
+ | Read-receipt fallback | Falls back to a "Got it!" text reply on older clients without reaction support | N/A (reactions always supported) |
155
+
156
+ ## Bot Setup
157
+ 1. Ensure API keys are set up (see [API Keys](#api-keys)) and create your bot on each platform you plan to run (see [Supported Platforms](#supported-platforms)).
158
+ 2. Install ForgeLLM: `pip install digitalheresy-forgellm` (the importable package is `forgellm` regardless of the install name, e.g. `from forgellm.telegram.adapter import TelegramAdapter`).
159
+ 3. Configure `config.yaml` (created on first run). Top-level keys apply regardless of platform; each platform gets its own `platforms.<name>` section:
160
+ - `platforms.<name>.bot_owner`: username(s) with admin access (required, no `@`). Accepts a single string or a YAML list - matched by username string on every platform (no numeric IDs anywhere).
161
+ - `platforms.<name>.api_key`: that platform's bot API key (required to run that adapter). Lookup order: (1) this config field, (2) the platform's env var from the [API Keys](#api-keys) table. Config wins when explicitly set. Exits on placeholder, missing, or malformed key.
162
+ - `chat_model` / `url_model`: LLM models for conversation and URL analysis (e.g. `gpt-4o-mini`, `claude-sonnet-4-6`).
163
+ - `bot_nickname` / `bot_initials`: Names the bot responds to in groups/guilds.
164
+ - `instance_name`: Optional label for console prefix, log filename, and database name (e.g. `MyBot` produces `[MyBot] INFO: ...` on console, `MyBot_{timestamp}.log` logs, and `MyBot.db` database); omit to use the bot's platform username for logging and `conversations.db` for database. Use distinct names when running multiple bot instances in the same directory.
165
+ - `token_limit` / `search_limit`: Max tokens (default: model's maximum) / max search results (default: 30).
166
+ - `archive_days`: Days before messages are eligible for archival (optional; default 60, minimum 1). See [Core Features](#core-features).
167
+ - `document_processing` / `image_analysis`: Optional bools (default: true) to disable document summarisation / image analysis. `image_analysis` requires the chat_model to support vision (default true per-model in models.yaml).
168
+ - `image_gen_cooldown_s`: Optional int (default 30, minimum 0). Seconds a user must wait between image generation requests.
169
+ - `allow_local_webhooks`: Set to `true` to permit webhook/MCP URLs targeting loopback or link-local addresses (optional; default `false`). Useful when tools like Home Assistant run on the same host.
170
+ - `max_conversations`: Optional max chats kept in memory at once (default: 500, minimum 1). Least-recently-used chats beyond this cap are evicted and reload from the database on their next message.
171
+ - `tools`: Optional list of webhook and MCP tool definitions (admin-only, private chat only). See [docs/tools.md](docs/tools.md) for schema and examples.
172
+
173
+ Example `config.yaml`:
174
+ ```yaml
175
+ instance_name: MyBot
176
+ bot_nickname: Panzer
177
+ bot_initials: PZ
178
+ chat_model: gpt-4o
179
+ url_model: gpt-4o
180
+
181
+ platforms:
182
+ telegram:
183
+ api_key: abc123...
184
+ bot_owner: [MyUsername]
185
+ ```
186
+ Swap `telegram:` for `discord:` (with a Discord username in `bot_owner`) to run Discord instead - everything else is identical. Include both sections to run both (see [Running Multiple Platforms Together](#running-multiple-platforms-together)).
187
+ 4. One-time platform setup:
188
+
189
+ | Platform | One-time setup |
190
+ |---|---|
191
+ | Telegram | Disable group privacy mode in BotFather: `/setprivacy` -> select your bot -> Disable. Otherwise the bot won't receive group messages that don't mention it, so it can't index other bots or load cross-chat context. |
192
+ | Discord | Run `/enable` in each guild channel you want the bot active in - channels are opt-in by default. |
193
+ 5. Run the bot:
194
+ ```python
195
+ from forgellm.telegram.adapter import TelegramAdapter
196
+ telegram_bot = TelegramAdapter.set()
197
+ telegram_bot.run()
198
+ ```
199
+ Swap in `forgellm.discord.adapter.DiscordAdapter` to run Discord instead - same `.set()`/`.run()` shape. `run()` is synchronous and blocks until the bot is stopped.
200
+ 6. Type `/help` to see all available commands.
201
+
202
+ ### Running Multiple Platforms Together
203
+ Construct every adapter you want, then run them concurrently via `start_async()` instead of `run()` (they share one event loop) so they share one bot identity, persona, and memory - the bot's own accounts on each platform are automatically linked to each other on startup, the same way `/link` connects two user accounts:
204
+ ```python
205
+ import asyncio
206
+ from forgellm.telegram.adapter import TelegramAdapter
207
+ from forgellm.discord.adapter import DiscordAdapter
208
+
209
+ async def main():
210
+ telegram_bot = TelegramAdapter.set()
211
+ discord_bot = DiscordAdapter.set()
212
+ await asyncio.gather(telegram_bot.start_async(), discord_bot.start_async())
213
+
214
+ asyncio.run(main())
215
+ ```
216
+ The bundled `run_bots.py` wires this up for you, including the bot's own cross-platform auto-link.
217
+
218
+ ## Resources
219
+ * GitHub repository [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) has guides to create a Telegram bot.
220
+ * GitHub repository [discord.py](https://github.com/Rapptz/discord.py) has guides to create a Discord bot.
221
+ * For more information on OpenAI models and token limits:
222
+ * [OpenAI model overview and maximum tokens](https://platform.openai.com/docs/models)
223
+ * [OpenAI message conversion to tokens](https://github.com/openai/openai-python)
224
+ * [OpenAI custom fine-tuning](https://platform.openai.com/docs/guides/model-optimization)
225
+ * [OpenAI's tiktoken library](https://github.com/openai/tiktoken/tree/main)
226
+ * For more information on Anthropic Claude models:
227
+ * [Anthropic model overview and context windows](https://docs.anthropic.com/en/docs/about-claude/models)
228
+ * [Anthropic Python SDK](https://github.com/anthropic/anthropic-sdk-python)
File without changes
@@ -0,0 +1,42 @@
1
+ """
2
+ Unit tests for a4rn - bot_owner normalisation to list[str] (platform-agnostic).
3
+ """
4
+ import unittest
5
+
6
+
7
+ def _normalise_owners(bot_owner):
8
+ """Mirrors the owner normalisation in TelegramAdapter.__init__."""
9
+ owners = bot_owner if isinstance(bot_owner, list) else [bot_owner]
10
+ return [str(u).strip() for u in owners if str(u).strip()]
11
+
12
+
13
+ class TestOwnerNormalisation(unittest.TestCase):
14
+ """Tests for bot_owner normalisation to list[str]."""
15
+
16
+ def test_single_string_produces_one_element_list(self):
17
+ """A single username string must become a one-element list."""
18
+ self.assertEqual(_normalise_owners('alice'), ['alice'])
19
+
20
+ def test_already_normalised_list_remains_unchanged(self):
21
+ """An already-normalised list of usernames remains unchanged after normalisation."""
22
+ self.assertEqual(_normalise_owners(['alice', 'bob']), ['alice', 'bob'])
23
+
24
+ def test_single_string_with_whitespace_is_stripped(self):
25
+ """Leading/trailing whitespace in a single username must be stripped."""
26
+ self.assertEqual(_normalise_owners(' alice '), ['alice'])
27
+
28
+ def test_list_entries_with_whitespace_are_stripped(self):
29
+ """Leading/trailing whitespace in list entries must be stripped."""
30
+ self.assertEqual(_normalise_owners([' alice ', ' bob']), ['alice', 'bob'])
31
+
32
+ def test_empty_string_entries_are_filtered(self):
33
+ """Empty string entries in a list must be filtered out."""
34
+ self.assertEqual(_normalise_owners(['alice', '', ' ', 'bob']), ['alice', 'bob'])
35
+
36
+ def test_single_empty_string_produces_empty_list(self):
37
+ """An empty string bot_owner must produce an empty list."""
38
+ self.assertEqual(_normalise_owners(''), [])
39
+
40
+ def test_list_with_single_entry(self):
41
+ """A one-element list behaves the same as a single string."""
42
+ self.assertEqual(_normalise_owners(['alice']), ['alice'])