TeLLMgramBot 3.9.2__tar.gz → 3.10.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.
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/PKG-INFO +16 -8
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/README.md +15 -7
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/TeLLMgramBot.py +100 -9
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/database.py +58 -15
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/initialize.py +2 -2
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/anthropic_provider.py +31 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/PKG-INFO +16 -8
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/setup.py +1 -1
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/LICENSE +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/conversation.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/message_handlers.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/requires.txt +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.10.0
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -44,7 +44,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
44
44
|
* This uses a separate model (configurable via `url_model`) to support more URL content with its higher token limit.
|
|
45
45
|
* Ask questions about message history across all your chats using natural language via LLM tool calling.
|
|
46
46
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "What did we discuss about DMs?" or simply "Show me the last few messages" without specifying a search query.
|
|
47
|
-
* The bot automatically searches your private chat and all shared groups (where both you and the bot are active), attributing messages to speakers and returning results from the full history (beyond the limited in-memory token budget). All search filters are optional - you can ask for recent messages broadly without specifying content or speakers. Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found). Results are ordered most-recent-first; configure `search_limit` to control how many results to return (default: 30).
|
|
47
|
+
* The bot automatically searches your private chat and all shared groups (where both you and the bot are active), attributing messages to speakers and returning results from the full history (beyond the limited in-memory token budget). Messages from other bots in group chats are also indexed for search, enabling discovery of bot summaries and responses. All search filters are optional - you can ask for recent messages broadly without specifying content or speakers. Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found). Results are ordered most-recent-first; configure `search_limit` to control how many results to return (default: 30).
|
|
48
48
|
* Tokens are used to measure the length of all conversation messages between the Telegram bot assistant and the user. This is useful to:
|
|
49
49
|
* Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
|
|
50
50
|
* Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. In private chats, shared group context (messages from groups where both user and bot are active) fills the remaining budget, enabling the bot to reference group conversations from a private context. This eliminates amnesia when users switch between contexts.
|
|
@@ -154,14 +154,17 @@ Each file with the associated API key will update its respective environment var
|
|
|
154
154
|
- `/nick <name>` - Set your personal nickname (for bot use in group chats).
|
|
155
155
|
- `/forget` - Clear all of your conversation history. In private chats, clears everything. In group chats, removes only your messages.
|
|
156
156
|
- `/private` - Toggle private mode (private chats only). When enabled, your messages are excluded from group context loading, providing selective privacy in shared groups.
|
|
157
|
+
- `/wipe` - Permanently delete all conversation data from the database (owner-only, irreversible).
|
|
157
158
|
- `/help` - Display all available commands and usage information.
|
|
158
159
|
|
|
159
160
|
### Group Chat Triggers
|
|
160
|
-
In group and supergroup chats, the bot
|
|
161
|
+
In group and supergroup chats, the bot automatically captures and indexes messages from other bots, making them available via message history searches and conversation context. For example, you can ask "What did Bot B say about the project?" in a private chat and the bot will search across all shared groups.
|
|
162
|
+
|
|
163
|
+
For non-bot messages, the bot responds when any of the following conditions are met:
|
|
161
164
|
- You mention the bot by username (e.g., `@botname`)
|
|
162
|
-
- You mention the bot by nickname (configured via `config.yaml`)
|
|
163
|
-
- You mention the bot by initials (configured via `config.yaml`)
|
|
164
|
-
- You directly reply to one of the bot's messages (Telegram reply-to feature)
|
|
165
|
+
- You mention the bot by nickname (configured via `config.yaml`), unless the message explicitly @mentions another account
|
|
166
|
+
- You mention the bot by initials (configured via `config.yaml`), unless the message explicitly @mentions another account
|
|
167
|
+
- You directly reply to one of the bot's messages (Telegram reply-to feature), unless the message explicitly @mentions another account
|
|
165
168
|
|
|
166
169
|
When a reply-to-bot message explicitly @mentions another account (e.g., "@otherbot please help" or "@alice can you help?"), the bot politely defers with "Looks like that message is for @otherbot!" rather than generating an LLM response. Note: Telegram does not distinguish bots from regular users in @mentions, so deflection fires for any foreign @mention. Deflections intentionally skip the read receipt.
|
|
167
170
|
|
|
@@ -186,12 +189,17 @@ This library includes an example script `test_local.py`, which uses files from t
|
|
|
186
189
|
persona_prompt = <System prompt summarizing bot personality>
|
|
187
190
|
)
|
|
188
191
|
```
|
|
189
|
-
4.
|
|
192
|
+
4. **Disable group privacy mode in BotFather** to enable full group message capture (required for foreign bot message indexing and cross-chat context):
|
|
193
|
+
```
|
|
194
|
+
/setprivacy -> select your bot -> Disable
|
|
195
|
+
```
|
|
196
|
+
With privacy mode on (the default), Telegram only delivers messages that mention the bot, are replies to it, or are commands - other group messages including those from other bots are not delivered.
|
|
197
|
+
5. Turn on TeLLMgramBot by calling:
|
|
190
198
|
```
|
|
191
199
|
telegram_bot.start_polling()
|
|
192
200
|
```
|
|
193
201
|
Once you see `TeLLMgramBot polling...`, the bot is online in Telegram.
|
|
194
|
-
|
|
202
|
+
6. Converse! Type `/help` for all available commands.
|
|
195
203
|
|
|
196
204
|
## Resources
|
|
197
205
|
* GitHub repository [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) has guides to create a Telegram bot.
|
|
@@ -12,7 +12,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
12
12
|
* This uses a separate model (configurable via `url_model`) to support more URL content with its higher token limit.
|
|
13
13
|
* Ask questions about message history across all your chats using natural language via LLM tool calling.
|
|
14
14
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "What did we discuss about DMs?" or simply "Show me the last few messages" without specifying a search query.
|
|
15
|
-
* The bot automatically searches your private chat and all shared groups (where both you and the bot are active), attributing messages to speakers and returning results from the full history (beyond the limited in-memory token budget). All search filters are optional - you can ask for recent messages broadly without specifying content or speakers. Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found). Results are ordered most-recent-first; configure `search_limit` to control how many results to return (default: 30).
|
|
15
|
+
* The bot automatically searches your private chat and all shared groups (where both you and the bot are active), attributing messages to speakers and returning results from the full history (beyond the limited in-memory token budget). Messages from other bots in group chats are also indexed for search, enabling discovery of bot summaries and responses. All search filters are optional - you can ask for recent messages broadly without specifying content or speakers. Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found). Results are ordered most-recent-first; configure `search_limit` to control how many results to return (default: 30).
|
|
16
16
|
* Tokens are used to measure the length of all conversation messages between the Telegram bot assistant and the user. This is useful to:
|
|
17
17
|
* Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
|
|
18
18
|
* Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. In private chats, shared group context (messages from groups where both user and bot are active) fills the remaining budget, enabling the bot to reference group conversations from a private context. This eliminates amnesia when users switch between contexts.
|
|
@@ -122,14 +122,17 @@ Each file with the associated API key will update its respective environment var
|
|
|
122
122
|
- `/nick <name>` - Set your personal nickname (for bot use in group chats).
|
|
123
123
|
- `/forget` - Clear all of your conversation history. In private chats, clears everything. In group chats, removes only your messages.
|
|
124
124
|
- `/private` - Toggle private mode (private chats only). When enabled, your messages are excluded from group context loading, providing selective privacy in shared groups.
|
|
125
|
+
- `/wipe` - Permanently delete all conversation data from the database (owner-only, irreversible).
|
|
125
126
|
- `/help` - Display all available commands and usage information.
|
|
126
127
|
|
|
127
128
|
### Group Chat Triggers
|
|
128
|
-
In group and supergroup chats, the bot
|
|
129
|
+
In group and supergroup chats, the bot automatically captures and indexes messages from other bots, making them available via message history searches and conversation context. For example, you can ask "What did Bot B say about the project?" in a private chat and the bot will search across all shared groups.
|
|
130
|
+
|
|
131
|
+
For non-bot messages, the bot responds when any of the following conditions are met:
|
|
129
132
|
- You mention the bot by username (e.g., `@botname`)
|
|
130
|
-
- You mention the bot by nickname (configured via `config.yaml`)
|
|
131
|
-
- You mention the bot by initials (configured via `config.yaml`)
|
|
132
|
-
- You directly reply to one of the bot's messages (Telegram reply-to feature)
|
|
133
|
+
- You mention the bot by nickname (configured via `config.yaml`), unless the message explicitly @mentions another account
|
|
134
|
+
- You mention the bot by initials (configured via `config.yaml`), unless the message explicitly @mentions another account
|
|
135
|
+
- You directly reply to one of the bot's messages (Telegram reply-to feature), unless the message explicitly @mentions another account
|
|
133
136
|
|
|
134
137
|
When a reply-to-bot message explicitly @mentions another account (e.g., "@otherbot please help" or "@alice can you help?"), the bot politely defers with "Looks like that message is for @otherbot!" rather than generating an LLM response. Note: Telegram does not distinguish bots from regular users in @mentions, so deflection fires for any foreign @mention. Deflections intentionally skip the read receipt.
|
|
135
138
|
|
|
@@ -154,12 +157,17 @@ This library includes an example script `test_local.py`, which uses files from t
|
|
|
154
157
|
persona_prompt = <System prompt summarizing bot personality>
|
|
155
158
|
)
|
|
156
159
|
```
|
|
157
|
-
4.
|
|
160
|
+
4. **Disable group privacy mode in BotFather** to enable full group message capture (required for foreign bot message indexing and cross-chat context):
|
|
161
|
+
```
|
|
162
|
+
/setprivacy -> select your bot -> Disable
|
|
163
|
+
```
|
|
164
|
+
With privacy mode on (the default), Telegram only delivers messages that mention the bot, are replies to it, or are commands - other group messages including those from other bots are not delivered.
|
|
165
|
+
5. Turn on TeLLMgramBot by calling:
|
|
158
166
|
```
|
|
159
167
|
telegram_bot.start_polling()
|
|
160
168
|
```
|
|
161
169
|
Once you see `TeLLMgramBot polling...`, the bot is online in Telegram.
|
|
162
|
-
|
|
170
|
+
6. Converse! Type `/help` for all available commands.
|
|
163
171
|
|
|
164
172
|
## Resources
|
|
165
173
|
* GitHub repository [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) has guides to create a Telegram bot.
|
|
@@ -17,12 +17,17 @@ from .utils import format_dt
|
|
|
17
17
|
from .database import (
|
|
18
18
|
get_private_mode,
|
|
19
19
|
set_private_mode,
|
|
20
|
+
insert_message,
|
|
20
21
|
delete_messages_for_user,
|
|
21
22
|
delete_messages_for_chat,
|
|
22
23
|
delete_private_messages_for_user,
|
|
23
24
|
delete_bot_replies_for_user,
|
|
24
25
|
get_shared_group_chat_ids,
|
|
26
|
+
prune_foreign_bot_messages,
|
|
25
27
|
search_messages,
|
|
28
|
+
upsert_chat,
|
|
29
|
+
upsert_user,
|
|
30
|
+
wipe_all_data,
|
|
26
31
|
)
|
|
27
32
|
from .initialize import (
|
|
28
33
|
INIT_BOT_CONFIG,
|
|
@@ -38,11 +43,15 @@ from .utils import exact_word_match, log_error
|
|
|
38
43
|
|
|
39
44
|
logger = logging.getLogger(__name__)
|
|
40
45
|
|
|
46
|
+
_FOREIGN_BOT_MESSAGE_CAP = 50
|
|
47
|
+
|
|
41
48
|
_SEARCH_TOOL = {
|
|
42
49
|
"name": "search_messages",
|
|
43
50
|
"description": (
|
|
44
51
|
"Search the full message history across the user's private chat and shared group chats. "
|
|
45
52
|
"Use whenever the user asks who said something, what someone said, or what was discussed. "
|
|
53
|
+
"Always search before claiming a person has no message history -- do not assume from context alone. "
|
|
54
|
+
"Run the search immediately when it would help answer the question -- do not ask the user for permission to search. "
|
|
46
55
|
"All filters are optional -- omit them to retrieve recent messages broadly. "
|
|
47
56
|
"Results are ordered most-recent-first; to find the earliest message, look at the last result."
|
|
48
57
|
),
|
|
@@ -91,6 +100,7 @@ class TelegramBot:
|
|
|
91
100
|
"Administrator-only commands:\n"
|
|
92
101
|
"/start - Go online to receive new responses (default).\n"
|
|
93
102
|
"/stop - Go offline to prevent new responses.\n"
|
|
103
|
+
"/wipe - Permanently delete all conversation data (irreversible).\n"
|
|
94
104
|
)
|
|
95
105
|
|
|
96
106
|
async def tele_start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
@@ -112,6 +122,18 @@ class TelegramBot:
|
|
|
112
122
|
else:
|
|
113
123
|
await update.message.reply_text("Sorry, I can't do that for you.")
|
|
114
124
|
|
|
125
|
+
async def tele_wipe_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
126
|
+
"""A bot_owner-only command to permanently delete all messages, users, and chats from the database."""
|
|
127
|
+
uname = update.message.from_user.username
|
|
128
|
+
if uname != self.telegram['owner']:
|
|
129
|
+
await update.message.reply_text("Sorry, I can't do that for you.")
|
|
130
|
+
return
|
|
131
|
+
await wipe_all_data()
|
|
132
|
+
self.conversations.clear()
|
|
133
|
+
self.token_warning.clear()
|
|
134
|
+
context.application.bot_data.pop('_foreign_bot_reply_seen', None)
|
|
135
|
+
await update.message.reply_text("All conversation data wiped.")
|
|
136
|
+
|
|
115
137
|
async def tele_nick_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
116
138
|
"""
|
|
117
139
|
Let bot assistant know to call the user by a different name other than the Telegram username.
|
|
@@ -360,6 +382,31 @@ class TelegramBot:
|
|
|
360
382
|
|
|
361
383
|
return reply
|
|
362
384
|
|
|
385
|
+
async def _store_foreign_bot_message(self, msg: Message, chat: Chat) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Persist a group message from another bot to the database.
|
|
388
|
+
|
|
389
|
+
Called when a bot message is received in a group chat. Stores the message with
|
|
390
|
+
is_foreign_bot=True so it is searchable via the LLM search tool. Enforces the
|
|
391
|
+
per-chat cap (_FOREIGN_BOT_MESSAGE_CAP) by pruning the oldest rows after insert.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
msg: The incoming Telegram Message from the foreign bot.
|
|
395
|
+
chat: The group chat the message was sent in.
|
|
396
|
+
"""
|
|
397
|
+
bot = msg.from_user
|
|
398
|
+
speaker = bot.first_name or (f'@{bot.username}' if bot.username else 'Bot')
|
|
399
|
+
await upsert_user(bot.id, bot.username, bot.first_name, bot.last_name)
|
|
400
|
+
await upsert_chat(chat.id, chat.type, chat.title)
|
|
401
|
+
await insert_message(
|
|
402
|
+
chat_id=chat.id,
|
|
403
|
+
user_id=bot.id,
|
|
404
|
+
role='user',
|
|
405
|
+
content=f'[{speaker}]: {msg.text}',
|
|
406
|
+
is_foreign_bot=True,
|
|
407
|
+
)
|
|
408
|
+
await prune_foreign_bot_messages(chat.id, _FOREIGN_BOT_MESSAGE_CAP)
|
|
409
|
+
|
|
363
410
|
def _foreign_bot_mention(self, msg: Message) -> str | None:
|
|
364
411
|
"""
|
|
365
412
|
Return the @username of the first explicitly @mentioned account that is not us, or None.
|
|
@@ -418,7 +465,12 @@ class TelegramBot:
|
|
|
418
465
|
"""
|
|
419
466
|
Route incoming Telegram messages to appropriate handlers based on chat type and trigger conditions.
|
|
420
467
|
|
|
421
|
-
In group/supergroup chats,
|
|
468
|
+
In group/supergroup chats, messages from other bots (user.is_bot=True, user.id != our bot_id)
|
|
469
|
+
are immediately persisted to the database via _store_foreign_bot_message() and the handler returns
|
|
470
|
+
without generating an LLM response. This makes foreign bot messages searchable via the search tool.
|
|
471
|
+
|
|
472
|
+
For non-bot messages in group/supergroup chats, the bot responds when any of the following
|
|
473
|
+
conditions are met:
|
|
422
474
|
- User mentions the bot by @username
|
|
423
475
|
- User mentions the bot by nickname (set via config)
|
|
424
476
|
- User mentions the bot by initials (set via config)
|
|
@@ -427,7 +479,7 @@ class TelegramBot:
|
|
|
427
479
|
In all matching group scenarios, a read receipt acknowledgement (👀 reaction or "Got it!" fallback)
|
|
428
480
|
is sent immediately via _send_read_receipt() before generating the full LLM response.
|
|
429
481
|
|
|
430
|
-
In private chats, the bot responds to all messages.
|
|
482
|
+
In private chats, the bot responds to all messages (Telegram does not deliver bot-to-bot DMs).
|
|
431
483
|
|
|
432
484
|
Args:
|
|
433
485
|
update: The Telegram Update object containing the incoming message.
|
|
@@ -438,6 +490,41 @@ class TelegramBot:
|
|
|
438
490
|
return
|
|
439
491
|
(msg, chat, user) = validated
|
|
440
492
|
|
|
493
|
+
# Store foreign bot messages in group chats and return - no LLM response needed.
|
|
494
|
+
if (
|
|
495
|
+
user.is_bot and
|
|
496
|
+
user.id != self.telegram['bot_id'] and
|
|
497
|
+
msg.text and
|
|
498
|
+
chat.type in ('group', 'supergroup')
|
|
499
|
+
):
|
|
500
|
+
await self._store_foreign_bot_message(msg, chat)
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
# Opportunistically capture foreign bot messages via reply_to_message.
|
|
504
|
+
# Telegram does not deliver bot messages as standalone updates; this extracts
|
|
505
|
+
# them when a user replies to another bot so they become searchable.
|
|
506
|
+
# Deduplicate: multiple users replying to the same bot message would each
|
|
507
|
+
# trigger capture; use a short-lived in-memory set keyed by (chat, bot, msg_id).
|
|
508
|
+
reply = msg.reply_to_message
|
|
509
|
+
if (
|
|
510
|
+
reply and
|
|
511
|
+
reply.from_user and
|
|
512
|
+
reply.from_user.is_bot and
|
|
513
|
+
reply.from_user.id != self.telegram['bot_id'] and
|
|
514
|
+
reply.text and
|
|
515
|
+
chat.type in ('group', 'supergroup')
|
|
516
|
+
):
|
|
517
|
+
recently_seen = context.application.bot_data.setdefault('_foreign_bot_reply_seen', {})
|
|
518
|
+
dedupe_key = (chat.id, reply.from_user.id, reply.message_id)
|
|
519
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
520
|
+
cutoff = now - datetime.timedelta(hours=1)
|
|
521
|
+
stale = [k for k, seen_at in recently_seen.items() if seen_at < cutoff]
|
|
522
|
+
for k in stale:
|
|
523
|
+
recently_seen.pop(k, None)
|
|
524
|
+
if dedupe_key not in recently_seen:
|
|
525
|
+
recently_seen[dedupe_key] = now
|
|
526
|
+
await self._store_foreign_bot_message(reply, chat)
|
|
527
|
+
|
|
441
528
|
# If it's a group text, only reply if the bot is named
|
|
442
529
|
# The real magic of how the bot behaves is in tele_handle_response()
|
|
443
530
|
response = "Sorry, I couldn't process your message! Please contact my creator."
|
|
@@ -448,21 +535,21 @@ class TelegramBot:
|
|
|
448
535
|
msg.reply_to_message.from_user.id == self.telegram['bot_id']
|
|
449
536
|
)
|
|
450
537
|
if exact_word_match(self.telegram['username'], msg.text):
|
|
538
|
+
# Explicit @username mention: strongest signal - respond even if another
|
|
539
|
+
# account is also @mentioned (both bots may be intentionally addressed).
|
|
451
540
|
pattern = r'@?\b' + re.escape(self.telegram['username']) + r'\b'
|
|
452
541
|
new_text = re.sub(pattern, '', msg.text).strip()
|
|
453
542
|
await self._send_read_receipt(msg, context)
|
|
454
543
|
response = await self.tele_handle_response(new_text, msg)
|
|
455
544
|
elif (
|
|
456
545
|
exact_word_match(self.telegram['nickname'], msg.text) or
|
|
457
|
-
exact_word_match(self.telegram['initials'], msg.text)
|
|
546
|
+
exact_word_match(self.telegram['initials'], msg.text) or
|
|
547
|
+
is_reply_to_bot
|
|
458
548
|
):
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
elif is_reply_to_bot:
|
|
549
|
+
# Weaker trigger (nickname, initials, or reply): check for a foreign
|
|
550
|
+
# @mention first. If present, the message is likely for another account.
|
|
462
551
|
foreign = self._foreign_bot_mention(msg)
|
|
463
552
|
if foreign:
|
|
464
|
-
# The reply is directed at a different account - deflect without
|
|
465
|
-
# sending a read receipt, since we are not handling this message.
|
|
466
553
|
await msg.reply_text(f"Looks like that message is for {foreign}!")
|
|
467
554
|
return
|
|
468
555
|
await self._send_read_receipt(msg, context)
|
|
@@ -630,7 +717,10 @@ class TelegramBot:
|
|
|
630
717
|
def start_polling(self):
|
|
631
718
|
"""The main polling "loop" the user interacts with via Telegram."""
|
|
632
719
|
logger.info(f"TeLLMgramBot {self.telegram['username']} polling...")
|
|
633
|
-
self.telegram['app'].run_polling(
|
|
720
|
+
self.telegram['app'].run_polling(
|
|
721
|
+
poll_interval=self.telegram['pollinterval'],
|
|
722
|
+
allowed_updates=Update.ALL_TYPES,
|
|
723
|
+
)
|
|
634
724
|
logger.info(f"TeLLMgramBot {self.telegram['username']} polling ended.")
|
|
635
725
|
|
|
636
726
|
# Initialization
|
|
@@ -702,6 +792,7 @@ class TelegramBot:
|
|
|
702
792
|
self.telegram['app'].add_handler(CommandHandler('help', self.tele_commands))
|
|
703
793
|
self.telegram['app'].add_handler(CommandHandler('start', self.tele_start_command))
|
|
704
794
|
self.telegram['app'].add_handler(CommandHandler('stop', self.tele_stop_command))
|
|
795
|
+
self.telegram['app'].add_handler(CommandHandler('wipe', self.tele_wipe_command))
|
|
705
796
|
self.telegram['app'].add_handler(CommandHandler('nick', self.tele_nick_command))
|
|
706
797
|
self.telegram['app'].add_handler(CommandHandler('forget', self.tele_forget_command))
|
|
707
798
|
self.telegram['app'].add_handler(CommandHandler('private', self.tele_private_command))
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
SQLite-backed conversation storage for TeLLMgramBot.
|
|
3
3
|
|
|
4
4
|
Provides async helpers for persisting and loading messages, managing per-user private mode,
|
|
5
|
-
searching message history, and retrieving conversation context.
|
|
6
|
-
|
|
7
|
-
and a
|
|
5
|
+
searching message history, and retrieving conversation context. Supports foreign bot message
|
|
6
|
+
storage with per-chat pruning. The schema includes a messages table (indexed by chat_id and
|
|
7
|
+
user_id, with is_foreign_bot and is_private flags), a users table (profile data and
|
|
8
|
+
private_mode flag), and a chats table for speaker/chat resolution in search results.
|
|
8
9
|
"""
|
|
9
10
|
import os
|
|
10
11
|
from typing import Optional
|
|
@@ -23,14 +24,15 @@ _DDL = """
|
|
|
23
24
|
PRAGMA journal_mode=WAL;
|
|
24
25
|
|
|
25
26
|
CREATE TABLE IF NOT EXISTS messages (
|
|
26
|
-
id
|
|
27
|
-
chat_id
|
|
28
|
-
user_id
|
|
29
|
-
role
|
|
30
|
-
content
|
|
31
|
-
is_private
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
chat_id INTEGER NOT NULL,
|
|
29
|
+
user_id INTEGER NOT NULL,
|
|
30
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
|
|
31
|
+
content TEXT NOT NULL,
|
|
32
|
+
is_private INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
is_foreign_bot INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
reply_to_id INTEGER,
|
|
35
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
34
36
|
);
|
|
35
37
|
|
|
36
38
|
CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id, created_at);
|
|
@@ -77,6 +79,7 @@ async def init_db() -> None:
|
|
|
77
79
|
- Drops username column from messages table (now in normalized users table).
|
|
78
80
|
- Adds chat_type column to chats table.
|
|
79
81
|
- Adds private_mode column to users table and merges legacy private_mode table into it.
|
|
82
|
+
- Adds is_foreign_bot column to messages table for foreign bot message storage.
|
|
80
83
|
"""
|
|
81
84
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
82
85
|
await db.executescript(_DDL)
|
|
@@ -84,6 +87,9 @@ async def init_db() -> None:
|
|
|
84
87
|
# Migration: add reply_to_id to existing databases that predate this column.
|
|
85
88
|
if 'reply_to_id' not in existing:
|
|
86
89
|
await db.execute("ALTER TABLE messages ADD COLUMN reply_to_id INTEGER")
|
|
90
|
+
# Migration: add is_foreign_bot to existing databases that predate this column.
|
|
91
|
+
if 'is_foreign_bot' not in existing:
|
|
92
|
+
await db.execute("ALTER TABLE messages ADD COLUMN is_foreign_bot INTEGER NOT NULL DEFAULT 0")
|
|
87
93
|
# Migration: drop username column now stored in the normalized users table.
|
|
88
94
|
if 'username' in existing:
|
|
89
95
|
await db.execute("ALTER TABLE messages DROP COLUMN username")
|
|
@@ -115,6 +121,7 @@ async def insert_message(
|
|
|
115
121
|
content: str,
|
|
116
122
|
is_private: bool = False,
|
|
117
123
|
reply_to_id: Optional[int] = None,
|
|
124
|
+
is_foreign_bot: bool = False,
|
|
118
125
|
) -> int:
|
|
119
126
|
"""
|
|
120
127
|
Persist a single message row and return its database id.
|
|
@@ -124,17 +131,18 @@ async def insert_message(
|
|
|
124
131
|
user_id: Telegram user ID of the sender.
|
|
125
132
|
role: 'user', 'assistant', or 'system'.
|
|
126
133
|
content: Message text.
|
|
127
|
-
is_private: If True, this message is excluded from
|
|
134
|
+
is_private: If True, this message is excluded from all context loading.
|
|
128
135
|
reply_to_id: Database id of the message this is a reply to (None if not a reply).
|
|
136
|
+
is_foreign_bot: If True, marks this as a message from another bot for search indexing.
|
|
129
137
|
|
|
130
138
|
Returns:
|
|
131
139
|
The integer id of the newly inserted row.
|
|
132
140
|
"""
|
|
133
141
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
134
142
|
cursor = await db.execute(
|
|
135
|
-
"INSERT INTO messages (chat_id, user_id, role, content, is_private, reply_to_id) "
|
|
136
|
-
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
137
|
-
(chat_id, user_id, role, content, int(is_private), reply_to_id),
|
|
143
|
+
"INSERT INTO messages (chat_id, user_id, role, content, is_private, reply_to_id, is_foreign_bot) "
|
|
144
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
145
|
+
(chat_id, user_id, role, content, int(is_private), reply_to_id, int(is_foreign_bot)),
|
|
138
146
|
)
|
|
139
147
|
await db.commit()
|
|
140
148
|
return cursor.lastrowid
|
|
@@ -696,6 +704,28 @@ async def search_messages(
|
|
|
696
704
|
})
|
|
697
705
|
return results
|
|
698
706
|
|
|
707
|
+
async def prune_foreign_bot_messages(chat_id: int, cap: int) -> None:
|
|
708
|
+
"""
|
|
709
|
+
Enforce a per-chat cap on foreign bot messages.
|
|
710
|
+
|
|
711
|
+
Deletes the oldest is_foreign_bot=1 rows for this chat when the count exceeds cap,
|
|
712
|
+
keeping the most recent `cap` rows. No-ops when the count is at or below the cap.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
chat_id: Telegram chat ID of the group to prune.
|
|
716
|
+
cap: Maximum number of foreign bot messages to retain per chat.
|
|
717
|
+
"""
|
|
718
|
+
async with aiosqlite.connect(get_db_path()) as db:
|
|
719
|
+
await db.execute(
|
|
720
|
+
"DELETE FROM messages WHERE chat_id = ? AND is_foreign_bot = 1 "
|
|
721
|
+
"AND id NOT IN ("
|
|
722
|
+
" SELECT id FROM messages WHERE chat_id = ? AND is_foreign_bot = 1 "
|
|
723
|
+
" ORDER BY created_at DESC, id DESC LIMIT ?"
|
|
724
|
+
")",
|
|
725
|
+
(chat_id, chat_id, cap),
|
|
726
|
+
)
|
|
727
|
+
await db.commit()
|
|
728
|
+
|
|
699
729
|
async def delete_bot_replies_for_user(user_id: int) -> None:
|
|
700
730
|
"""
|
|
701
731
|
Delete assistant rows whose reply_to_id references any message from this user.
|
|
@@ -754,6 +784,19 @@ async def delete_private_messages_for_user(user_id: int) -> None:
|
|
|
754
784
|
)
|
|
755
785
|
await db.commit()
|
|
756
786
|
|
|
787
|
+
async def wipe_all_data() -> None:
|
|
788
|
+
"""
|
|
789
|
+
Delete all rows from the messages, users, and chats tables.
|
|
790
|
+
|
|
791
|
+
Irreversible. Used by the /wipe admin command to reset the bot's
|
|
792
|
+
conversation database entirely. Schema and indexes are preserved.
|
|
793
|
+
"""
|
|
794
|
+
async with aiosqlite.connect(get_db_path()) as db:
|
|
795
|
+
await db.execute("DELETE FROM messages")
|
|
796
|
+
await db.execute("DELETE FROM users")
|
|
797
|
+
await db.execute("DELETE FROM chats")
|
|
798
|
+
await db.commit()
|
|
799
|
+
|
|
757
800
|
async def get_private_mode(user_id: int) -> bool:
|
|
758
801
|
"""
|
|
759
802
|
Return whether private mode is currently enabled for this user.
|
|
@@ -121,8 +121,8 @@ _SYSTEM_APPENDIX = (
|
|
|
121
121
|
"- Group chats may include the user's private history; private chats may include shared group history.\n"
|
|
122
122
|
"- /private mode excludes that user's messages from group contexts only; don't generalize it to deny other context.\n\n"
|
|
123
123
|
"Date/time:\n"
|
|
124
|
-
"- The injected UTC datetime at the end of this prompt is authoritative. Use it for all time questions and relative references.
|
|
125
|
-
"- State UTC time and stop. Never offer, suggest, or ask about timezone conversion
|
|
124
|
+
"- The injected UTC datetime at the end of this prompt is authoritative. Use it for all time questions and relative references. When stating the time, reproduce it verbatim (YYYY/MM/DD HH:MM:SS AM/PM UTC) - never reformat. Do not include the current time in responses unless the user asks.\n"
|
|
125
|
+
"- State UTC time and stop. Never offer, suggest, or ask about timezone conversion - not even based on prior context. Convert only when the user explicitly requests it in the current message.\n"
|
|
126
126
|
"- Decline requests to set a timezone; explain UTC is always used.\n\n"
|
|
127
127
|
"Current user: (not yet known)\n"
|
|
128
128
|
)
|
|
@@ -7,6 +7,31 @@ from .base import LLMProvider, ContextLengthExceededError, ProviderAuthError, Pr
|
|
|
7
7
|
# Safe default for max output tokens; configurable in Phase 3
|
|
8
8
|
_DEFAULT_MAX_OUTPUT_TOKENS = 8192
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
def _merge_consecutive_roles(messages: list[dict]) -> list[dict]:
|
|
12
|
+
"""
|
|
13
|
+
Merge consecutive messages with the same role into a single message.
|
|
14
|
+
|
|
15
|
+
Anthropic requires strictly alternating user/assistant turns. When foreign bot messages
|
|
16
|
+
stored as role='user' appear immediately before the triggering user's reply, this
|
|
17
|
+
produces two consecutive user messages, which the API rejects. This function merges
|
|
18
|
+
them by concatenating their content with a blank line separator.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
messages: List of message dicts with 'role' and 'content' keys.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A new list with consecutive same-role messages merged.
|
|
25
|
+
"""
|
|
26
|
+
merged: list[dict] = []
|
|
27
|
+
for m in messages:
|
|
28
|
+
if merged and merged[-1]['role'] == m['role']:
|
|
29
|
+
merged[-1]['content'] += f'\n\n{m["content"]}'
|
|
30
|
+
else:
|
|
31
|
+
merged.append(dict(m))
|
|
32
|
+
return merged
|
|
33
|
+
|
|
34
|
+
|
|
10
35
|
class AnthropicProvider(LLMProvider):
|
|
11
36
|
"""LLM provider implementation for Anthropic Claude models."""
|
|
12
37
|
_client: AsyncAnthropic | None = None
|
|
@@ -32,6 +57,11 @@ class AnthropicProvider(LLMProvider):
|
|
|
32
57
|
else:
|
|
33
58
|
chat_messages.append(msg)
|
|
34
59
|
|
|
60
|
+
# Anthropic requires strictly alternating user/assistant turns. Merge consecutive
|
|
61
|
+
# messages of the same role (e.g. a foreign bot's user-role message immediately
|
|
62
|
+
# followed by the actual user's message) by concatenating their content.
|
|
63
|
+
chat_messages = _merge_consecutive_roles(chat_messages)
|
|
64
|
+
|
|
35
65
|
kwargs = {
|
|
36
66
|
'model': model,
|
|
37
67
|
'max_tokens': _DEFAULT_MAX_OUTPUT_TOKENS,
|
|
@@ -98,6 +128,7 @@ class AnthropicProvider(LLMProvider):
|
|
|
98
128
|
chat_messages.append(msg)
|
|
99
129
|
if not chat_messages:
|
|
100
130
|
return 0
|
|
131
|
+
chat_messages = _merge_consecutive_roles(chat_messages)
|
|
101
132
|
kwargs = {'model': model, 'messages': chat_messages}
|
|
102
133
|
if system:
|
|
103
134
|
kwargs['system'] = system
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.10.0
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -44,7 +44,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
44
44
|
* This uses a separate model (configurable via `url_model`) to support more URL content with its higher token limit.
|
|
45
45
|
* Ask questions about message history across all your chats using natural language via LLM tool calling.
|
|
46
46
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "What did we discuss about DMs?" or simply "Show me the last few messages" without specifying a search query.
|
|
47
|
-
* The bot automatically searches your private chat and all shared groups (where both you and the bot are active), attributing messages to speakers and returning results from the full history (beyond the limited in-memory token budget). All search filters are optional - you can ask for recent messages broadly without specifying content or speakers. Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found). Results are ordered most-recent-first; configure `search_limit` to control how many results to return (default: 30).
|
|
47
|
+
* The bot automatically searches your private chat and all shared groups (where both you and the bot are active), attributing messages to speakers and returning results from the full history (beyond the limited in-memory token budget). Messages from other bots in group chats are also indexed for search, enabling discovery of bot summaries and responses. All search filters are optional - you can ask for recent messages broadly without specifying content or speakers. Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found). Results are ordered most-recent-first; configure `search_limit` to control how many results to return (default: 30).
|
|
48
48
|
* Tokens are used to measure the length of all conversation messages between the Telegram bot assistant and the user. This is useful to:
|
|
49
49
|
* Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
|
|
50
50
|
* Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. In private chats, shared group context (messages from groups where both user and bot are active) fills the remaining budget, enabling the bot to reference group conversations from a private context. This eliminates amnesia when users switch between contexts.
|
|
@@ -154,14 +154,17 @@ Each file with the associated API key will update its respective environment var
|
|
|
154
154
|
- `/nick <name>` - Set your personal nickname (for bot use in group chats).
|
|
155
155
|
- `/forget` - Clear all of your conversation history. In private chats, clears everything. In group chats, removes only your messages.
|
|
156
156
|
- `/private` - Toggle private mode (private chats only). When enabled, your messages are excluded from group context loading, providing selective privacy in shared groups.
|
|
157
|
+
- `/wipe` - Permanently delete all conversation data from the database (owner-only, irreversible).
|
|
157
158
|
- `/help` - Display all available commands and usage information.
|
|
158
159
|
|
|
159
160
|
### Group Chat Triggers
|
|
160
|
-
In group and supergroup chats, the bot
|
|
161
|
+
In group and supergroup chats, the bot automatically captures and indexes messages from other bots, making them available via message history searches and conversation context. For example, you can ask "What did Bot B say about the project?" in a private chat and the bot will search across all shared groups.
|
|
162
|
+
|
|
163
|
+
For non-bot messages, the bot responds when any of the following conditions are met:
|
|
161
164
|
- You mention the bot by username (e.g., `@botname`)
|
|
162
|
-
- You mention the bot by nickname (configured via `config.yaml`)
|
|
163
|
-
- You mention the bot by initials (configured via `config.yaml`)
|
|
164
|
-
- You directly reply to one of the bot's messages (Telegram reply-to feature)
|
|
165
|
+
- You mention the bot by nickname (configured via `config.yaml`), unless the message explicitly @mentions another account
|
|
166
|
+
- You mention the bot by initials (configured via `config.yaml`), unless the message explicitly @mentions another account
|
|
167
|
+
- You directly reply to one of the bot's messages (Telegram reply-to feature), unless the message explicitly @mentions another account
|
|
165
168
|
|
|
166
169
|
When a reply-to-bot message explicitly @mentions another account (e.g., "@otherbot please help" or "@alice can you help?"), the bot politely defers with "Looks like that message is for @otherbot!" rather than generating an LLM response. Note: Telegram does not distinguish bots from regular users in @mentions, so deflection fires for any foreign @mention. Deflections intentionally skip the read receipt.
|
|
167
170
|
|
|
@@ -186,12 +189,17 @@ This library includes an example script `test_local.py`, which uses files from t
|
|
|
186
189
|
persona_prompt = <System prompt summarizing bot personality>
|
|
187
190
|
)
|
|
188
191
|
```
|
|
189
|
-
4.
|
|
192
|
+
4. **Disable group privacy mode in BotFather** to enable full group message capture (required for foreign bot message indexing and cross-chat context):
|
|
193
|
+
```
|
|
194
|
+
/setprivacy -> select your bot -> Disable
|
|
195
|
+
```
|
|
196
|
+
With privacy mode on (the default), Telegram only delivers messages that mention the bot, are replies to it, or are commands - other group messages including those from other bots are not delivered.
|
|
197
|
+
5. Turn on TeLLMgramBot by calling:
|
|
190
198
|
```
|
|
191
199
|
telegram_bot.start_polling()
|
|
192
200
|
```
|
|
193
201
|
Once you see `TeLLMgramBot polling...`, the bot is online in Telegram.
|
|
194
|
-
|
|
202
|
+
6. Converse! Type `/help` for all available commands.
|
|
195
203
|
|
|
196
204
|
## Resources
|
|
197
205
|
* GitHub repository [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) has guides to create a Telegram bot.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|