TeLLMgramBot 3.2.2__tar.gz → 3.4.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.2.2 → tellmgrambot-3.4.0}/PKG-INFO +20 -4
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/README.md +18 -2
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/TeLLMgramBot.py +66 -27
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/conversation.py +9 -6
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/database.py +41 -3
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/initialize.py +52 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/message_handlers.py +29 -1
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/PKG-INFO +20 -4
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/requires.txt +1 -1
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/setup.py +2 -2
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/LICENSE +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-3.2.2 → tellmgrambot-3.4.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.4.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
|
|
@@ -16,7 +16,7 @@ Requires-Dist: httpx
|
|
|
16
16
|
Requires-Dist: beautifulsoup4
|
|
17
17
|
Requires-Dist: validators
|
|
18
18
|
Requires-Dist: tiktoken>=0.12
|
|
19
|
-
Requires-Dist: python-telegram-bot
|
|
19
|
+
Requires-Dist: python-telegram-bot>=20.8
|
|
20
20
|
Requires-Dist: aiosqlite>=0.19
|
|
21
21
|
Dynamic: author
|
|
22
22
|
Dynamic: author-email
|
|
@@ -45,14 +45,15 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
45
45
|
* Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
|
|
46
46
|
* 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.
|
|
47
47
|
* Users can manage privacy via two commands:
|
|
48
|
-
* `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types; other participants' messages and sessions remain.
|
|
49
|
-
* `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
|
|
48
|
+
* `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types and cleans up paired bot replies; other participants' messages and sessions remain.
|
|
49
|
+
* `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
|
|
50
50
|
|
|
51
51
|
## Why Telegram?
|
|
52
52
|
Using Telegram 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:
|
|
53
53
|
1. Telegram already lets you paste in verbose, multiline messages.
|
|
54
54
|
2. Telegram already lets you paste in pictures, videos, links, etc.
|
|
55
55
|
3. Telegram already lets you react with emojis, stickers, etc.
|
|
56
|
+
4. Telegram message reactions (👀) provide a lightweight read receipt without breaking conversation flow.
|
|
56
57
|
|
|
57
58
|
## Supported LLM Providers
|
|
58
59
|
TeLLMgramBot selects the LLM provider automatically based on the model name:
|
|
@@ -80,6 +81,9 @@ When initializing TeLLMgramBot, the following directories get created:
|
|
|
80
81
|
* A sample prompt file defining the bot's personality: generic, helpful, and multi-provider-aware.
|
|
81
82
|
* The prompt emphasizes the bot's ability to fetch and analyze URLs passed in square brackets `[]`.
|
|
82
83
|
* The user can create more prompt files as needed for different personalities.
|
|
84
|
+
* `system_appendix.prmpt`
|
|
85
|
+
* Framework-owned behavioral guidance automatically appended to the persona prompt at runtime.
|
|
86
|
+
* Teaches the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance.
|
|
83
87
|
* `url_analysis.prmpt`
|
|
84
88
|
* Prompt template used to analyze URL content passed in brackets `[]`.
|
|
85
89
|
* `errorlogs`
|
|
@@ -137,6 +141,18 @@ By default, API key files are created in the execution directory (or the directo
|
|
|
137
141
|
|
|
138
142
|
Each file with the associated API key will update its respective environment variable if not defined. Missing provider keys (OpenAI or Anthropic) will disable chat and URL analysis but allow the bot to start. Missing VirusTotal keys will disable URL analysis.
|
|
139
143
|
|
|
144
|
+
## Commands and Interactions
|
|
145
|
+
|
|
146
|
+
### Read Receipt Acknowledgement (Group Chats Only)
|
|
147
|
+
In group and supergroup chats, you can request the bot to acknowledge reading your message with a lightweight 👀 reaction instead of waiting for a full response. Simply mention the bot's username, nickname, or initials followed by an explicit request:
|
|
148
|
+
|
|
149
|
+
- `@botname acknowledge`
|
|
150
|
+
- `botname mark as read`
|
|
151
|
+
- `BN did you read that?`
|
|
152
|
+
- `botname confirm you received`
|
|
153
|
+
|
|
154
|
+
The bot will respond with a 👀 reaction emoji on your message (if the Telegram client supports message reactions), or with a short "Got it!" reply as fallback. Read receipt requests are processed immediately without touching the conversation context, making them ideal for confirming the bot is online without disrupting group flow.
|
|
155
|
+
|
|
140
156
|
## Bot Setup
|
|
141
157
|
This library includes an example script `test_local.py`, which uses files from the folders `configs` and `prompts` for TeLLMgramBot to process.
|
|
142
158
|
1. Ensure the previous sections are followed with the proper API keys and your Telegram bot set.
|
|
@@ -14,14 +14,15 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
14
14
|
* Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
|
|
15
15
|
* 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.
|
|
16
16
|
* Users can manage privacy via two commands:
|
|
17
|
-
* `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types; other participants' messages and sessions remain.
|
|
18
|
-
* `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
|
|
17
|
+
* `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types and cleans up paired bot replies; other participants' messages and sessions remain.
|
|
18
|
+
* `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
|
|
19
19
|
|
|
20
20
|
## Why Telegram?
|
|
21
21
|
Using Telegram 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:
|
|
22
22
|
1. Telegram already lets you paste in verbose, multiline messages.
|
|
23
23
|
2. Telegram already lets you paste in pictures, videos, links, etc.
|
|
24
24
|
3. Telegram already lets you react with emojis, stickers, etc.
|
|
25
|
+
4. Telegram message reactions (👀) provide a lightweight read receipt without breaking conversation flow.
|
|
25
26
|
|
|
26
27
|
## Supported LLM Providers
|
|
27
28
|
TeLLMgramBot selects the LLM provider automatically based on the model name:
|
|
@@ -49,6 +50,9 @@ When initializing TeLLMgramBot, the following directories get created:
|
|
|
49
50
|
* A sample prompt file defining the bot's personality: generic, helpful, and multi-provider-aware.
|
|
50
51
|
* The prompt emphasizes the bot's ability to fetch and analyze URLs passed in square brackets `[]`.
|
|
51
52
|
* The user can create more prompt files as needed for different personalities.
|
|
53
|
+
* `system_appendix.prmpt`
|
|
54
|
+
* Framework-owned behavioral guidance automatically appended to the persona prompt at runtime.
|
|
55
|
+
* Teaches the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance.
|
|
52
56
|
* `url_analysis.prmpt`
|
|
53
57
|
* Prompt template used to analyze URL content passed in brackets `[]`.
|
|
54
58
|
* `errorlogs`
|
|
@@ -106,6 +110,18 @@ By default, API key files are created in the execution directory (or the directo
|
|
|
106
110
|
|
|
107
111
|
Each file with the associated API key will update its respective environment variable if not defined. Missing provider keys (OpenAI or Anthropic) will disable chat and URL analysis but allow the bot to start. Missing VirusTotal keys will disable URL analysis.
|
|
108
112
|
|
|
113
|
+
## Commands and Interactions
|
|
114
|
+
|
|
115
|
+
### Read Receipt Acknowledgement (Group Chats Only)
|
|
116
|
+
In group and supergroup chats, you can request the bot to acknowledge reading your message with a lightweight 👀 reaction instead of waiting for a full response. Simply mention the bot's username, nickname, or initials followed by an explicit request:
|
|
117
|
+
|
|
118
|
+
- `@botname acknowledge`
|
|
119
|
+
- `botname mark as read`
|
|
120
|
+
- `BN did you read that?`
|
|
121
|
+
- `botname confirm you received`
|
|
122
|
+
|
|
123
|
+
The bot will respond with a 👀 reaction emoji on your message (if the Telegram client supports message reactions), or with a short "Got it!" reply as fallback. Read receipt requests are processed immediately without touching the conversation context, making them ideal for confirming the bot is online without disrupting group flow.
|
|
124
|
+
|
|
109
125
|
## Bot Setup
|
|
110
126
|
This library includes an example script `test_local.py`, which uses files from the folders `configs` and `prompts` for TeLLMgramBot to process.
|
|
111
127
|
1. Ensure the previous sections are followed with the proper API keys and your Telegram bot set.
|
|
@@ -5,16 +5,32 @@ import json
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from tempfile import gettempdir
|
|
7
7
|
from math import floor
|
|
8
|
-
from telegram import Bot, Update, Message, Chat, User
|
|
8
|
+
from telegram import Bot, Update, Message, Chat, User, ReactionTypeEmoji
|
|
9
|
+
from telegram.error import TelegramError
|
|
9
10
|
from telegram.constants import MessageLimit
|
|
10
11
|
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
|
12
|
+
|
|
11
13
|
from .providers.factory import get_provider
|
|
12
14
|
from .providers.base import ContextLengthExceededError, ProviderAuthError, ProviderConnectionError
|
|
13
|
-
from .initialize import
|
|
15
|
+
from .initialize import (
|
|
16
|
+
INIT_BOT_CONFIG,
|
|
17
|
+
ApiKeyStatus,
|
|
18
|
+
init_structure,
|
|
19
|
+
init_bot_config,
|
|
20
|
+
init_bot_prompt,
|
|
21
|
+
init_system_appendix,
|
|
22
|
+
)
|
|
14
23
|
from .conversation import Conversation
|
|
15
|
-
from .database import
|
|
24
|
+
from .database import (
|
|
25
|
+
get_private_mode,
|
|
26
|
+
set_private_mode,
|
|
27
|
+
delete_messages_for_user,
|
|
28
|
+
delete_messages_for_chat,
|
|
29
|
+
delete_private_messages_for_user,
|
|
30
|
+
delete_paired_bot_replies,
|
|
31
|
+
)
|
|
16
32
|
from .models import TokenLimits
|
|
17
|
-
from .message_handlers import handle_greetings, handle_common_queries, handle_url_ask
|
|
33
|
+
from .message_handlers import handle_greetings, handle_common_queries, handle_url_ask, handle_read_receipt
|
|
18
34
|
from .utils import read_yaml, read_text, exact_word_match, generate_error_path, log_error
|
|
19
35
|
|
|
20
36
|
class TelegramBot:
|
|
@@ -93,12 +109,14 @@ class TelegramBot:
|
|
|
93
109
|
Remove conversation history from the database (behavior depends on chat type).
|
|
94
110
|
|
|
95
111
|
In private chats: deletes all messages in the private chat (including bot replies).
|
|
96
|
-
In group chats: deletes the user's rows across all chats (private included)
|
|
112
|
+
In group chats: deletes the user's rows across all chats (private included), then
|
|
113
|
+
deletes bot replies in the current chat that were paired with the user's messages
|
|
114
|
+
or were orphaned by a previous /forget call, leaving other users' conversations intact.
|
|
97
115
|
|
|
98
116
|
In both cases, evicts only the in-memory Conversation objects that contain this
|
|
99
117
|
user's data - scoped to the current chat, the user's private chat, and any other
|
|
100
118
|
chats where their context was previously merged. Other users' active sessions are
|
|
101
|
-
not affected.
|
|
119
|
+
not affected.
|
|
102
120
|
"""
|
|
103
121
|
user_id = update.message.from_user.id
|
|
104
122
|
chat_id = update.message.chat.id
|
|
@@ -116,13 +134,14 @@ class TelegramBot:
|
|
|
116
134
|
await delete_messages_for_chat(chat_id)
|
|
117
135
|
else:
|
|
118
136
|
await delete_messages_for_user(user_id)
|
|
137
|
+
# Delete bot replies paired to this user's messages, plus any pre-existing
|
|
138
|
+
# orphaned assistant rows left by earlier /forget calls in this chat.
|
|
139
|
+
await delete_paired_bot_replies(chat_id, self.telegram['bot_id'])
|
|
119
140
|
|
|
120
141
|
# Evict only the Conversations that contain this user's data, not all sessions.
|
|
121
142
|
for evict_id in user_chat_ids:
|
|
122
143
|
self.conversations.pop(evict_id, None)
|
|
123
144
|
self.token_warning.pop(evict_id, None)
|
|
124
|
-
# Clear all private-mode notices for this user across all chats.
|
|
125
|
-
self._private_mode_warned = {k for k in self._private_mode_warned if k[1] != user_id}
|
|
126
145
|
|
|
127
146
|
await update.message.reply_text("My memories of our conversations are wiped!")
|
|
128
147
|
|
|
@@ -244,19 +263,6 @@ class TelegramBot:
|
|
|
244
263
|
is_private = (chat_type == 'private') and user_private_mode
|
|
245
264
|
await conv.add_user_message(text, user_id, username, is_private)
|
|
246
265
|
|
|
247
|
-
# Warn once per group session if the user has private mode ON.
|
|
248
|
-
# Their private-chat messages are excluded from this group's context, which can be
|
|
249
|
-
# surprising. The notice is appended to the first reply in the session for this user.
|
|
250
|
-
private_mode_notice = ""
|
|
251
|
-
if chat_type in ('group', 'supergroup') and user_private_mode:
|
|
252
|
-
warn_key = (chat_id, user_id)
|
|
253
|
-
if warn_key not in self._private_mode_warned:
|
|
254
|
-
self._private_mode_warned.add(warn_key)
|
|
255
|
-
private_mode_notice = (
|
|
256
|
-
"\n\n(Note: your private mode is ON - messages from our private chat "
|
|
257
|
-
"are not included in this group's context. To disable, send /private off in our DMs.)"
|
|
258
|
-
)
|
|
259
|
-
|
|
260
266
|
# Check if the user is asking about a [URL]
|
|
261
267
|
url_match = re.search(r'\[http(s)?://\S+]', text)
|
|
262
268
|
|
|
@@ -273,9 +279,6 @@ class TelegramBot:
|
|
|
273
279
|
# This is the transition point between quick Telegram replies and the LLM
|
|
274
280
|
reply = await self.llm_completion(chat_id)
|
|
275
281
|
|
|
276
|
-
if private_mode_notice:
|
|
277
|
-
reply += private_mode_notice
|
|
278
|
-
|
|
279
282
|
# Check token count before storing to determine if warning should be appended
|
|
280
283
|
token_count = await conv.get_message_token_count()
|
|
281
284
|
if token_count > self.chatgpt['prune_back_to'] and chat_id not in self.token_warning:
|
|
@@ -300,6 +303,27 @@ class TelegramBot:
|
|
|
300
303
|
|
|
301
304
|
return reply
|
|
302
305
|
|
|
306
|
+
async def _send_read_receipt(self, msg: Message, context: ContextTypes.DEFAULT_TYPE):
|
|
307
|
+
"""
|
|
308
|
+
Send a read receipt acknowledgement via message reaction.
|
|
309
|
+
|
|
310
|
+
Attempts to add a eyes-emoji reaction to the user's message to confirm the bot
|
|
311
|
+
read it, avoiding a full LLM response. If the message reaction API is unavailable
|
|
312
|
+
(e.g., older Telegram clients), falls back to a short "Got it!" text reply.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
msg: The Telegram Message object to react to.
|
|
316
|
+
context: The Telegram context for sending the reply if reaction fails.
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
await context.bot.set_message_reaction(
|
|
320
|
+
chat_id=msg.chat.id,
|
|
321
|
+
message_id=msg.message_id,
|
|
322
|
+
reaction=[ReactionTypeEmoji(emoji="👀")],
|
|
323
|
+
)
|
|
324
|
+
except (TelegramError, AttributeError):
|
|
325
|
+
await msg.reply_text("Got it!")
|
|
326
|
+
|
|
303
327
|
async def tele_handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
304
328
|
"""Handles the Telegram side of the message, discerning between Private and Group conversation."""
|
|
305
329
|
validated = await self.tele_validate(update)
|
|
@@ -314,11 +338,17 @@ class TelegramBot:
|
|
|
314
338
|
if exact_word_match(self.telegram['username'], msg.text):
|
|
315
339
|
pattern = r'@?\b' + re.escape(self.telegram['username']) + r'\b'
|
|
316
340
|
new_text = re.sub(pattern, '', msg.text).strip()
|
|
341
|
+
if handle_read_receipt(new_text):
|
|
342
|
+
await self._send_read_receipt(msg, context)
|
|
343
|
+
return
|
|
317
344
|
response = await self.tele_handle_response(new_text, msg)
|
|
318
345
|
elif (
|
|
319
346
|
exact_word_match(self.telegram['nickname'], msg.text) or
|
|
320
347
|
exact_word_match(self.telegram['initials'], msg.text)
|
|
321
348
|
):
|
|
349
|
+
if handle_read_receipt(msg.text):
|
|
350
|
+
await self._send_read_receipt(msg, context)
|
|
351
|
+
return
|
|
322
352
|
response = await self.tele_handle_response(msg.text, msg)
|
|
323
353
|
else:
|
|
324
354
|
return
|
|
@@ -448,7 +478,6 @@ class TelegramBot:
|
|
|
448
478
|
self.error_log = generate_error_path()
|
|
449
479
|
self.token_warning = {} # Determines whether user has reached token limit by AI model
|
|
450
480
|
self.conversations = {} # Provides Conversation class per user based on bot response
|
|
451
|
-
self._private_mode_warned = set() # (chat_id, user_id) pairs that received the private mode notice
|
|
452
481
|
self.telegram = {
|
|
453
482
|
'bot_id' : 0, # overwritten by _tele_info(); 0 is a safe sentinel
|
|
454
483
|
'owner' : bot_owner,
|
|
@@ -501,8 +530,9 @@ class TelegramBot:
|
|
|
501
530
|
|
|
502
531
|
Reads bot settings from the specified YAML configuration file and prompt file,
|
|
503
532
|
applies defaults for any missing values, and returns a fully initialized TelegramBot.
|
|
504
|
-
Also calls init_structure() to bootstrap directories and API keys
|
|
505
|
-
the
|
|
533
|
+
Also calls init_structure() to bootstrap directories and API keys. Automatically
|
|
534
|
+
appends the system appendix to the persona prompt at runtime, providing framework-owned
|
|
535
|
+
behavioral guidance that teaches the LLM about cross-chat memory semantics.
|
|
506
536
|
|
|
507
537
|
Args:
|
|
508
538
|
config_file: Path to bot configuration YAML file (default: 'config.yaml').
|
|
@@ -513,6 +543,8 @@ class TelegramBot:
|
|
|
513
543
|
|
|
514
544
|
Side Effects:
|
|
515
545
|
- Calls init_structure() which may create directories and check API keys.
|
|
546
|
+
- Calls init_system_appendix() which creates system_appendix.prmpt if missing.
|
|
547
|
+
- Appends system appendix to persona prompt before instantiation.
|
|
516
548
|
- May print warning messages if configuration values are missing or prompt file is empty.
|
|
517
549
|
"""
|
|
518
550
|
# First provide the main structure if not already there; capture API key status
|
|
@@ -521,6 +553,7 @@ class TelegramBot:
|
|
|
521
553
|
# Ensure both bot configuration and prompt files are defined and readable
|
|
522
554
|
config = read_yaml(init_bot_config(config_file))
|
|
523
555
|
prompt = read_text(init_bot_prompt(prompt_file))
|
|
556
|
+
appendix = init_system_appendix()
|
|
524
557
|
|
|
525
558
|
# Check any configuration values missing and apply default values:
|
|
526
559
|
for parameter, value in INIT_BOT_CONFIG.items():
|
|
@@ -535,6 +568,12 @@ class TelegramBot:
|
|
|
535
568
|
if value:
|
|
536
569
|
print(f"Configuration '{parameter}' not defined, set to '{value}'")
|
|
537
570
|
|
|
571
|
+
# Append the framework-owned system appendix to the persona prompt.
|
|
572
|
+
# The appendix teaches every soul how cross-chat memory works, so the
|
|
573
|
+
# persona author doesn't need to include this guidance themselves.
|
|
574
|
+
if appendix:
|
|
575
|
+
prompt = prompt.rstrip() + "\n\n" + appendix
|
|
576
|
+
|
|
538
577
|
# Apply parameters to bot:
|
|
539
578
|
return TelegramBot(
|
|
540
579
|
bot_owner = config['bot_owner'],
|
|
@@ -183,9 +183,12 @@ class Conversation:
|
|
|
183
183
|
bot answer questions about group conversations from a private chat context.
|
|
184
184
|
|
|
185
185
|
Per-user idempotent: once a user's cursor is set, subsequent calls return True immediately.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
This guard handles concurrent message arrival: if two messages from the same user arrive
|
|
187
|
+
before the first load completes and both check the cursor, the second call hits the early
|
|
188
|
+
return and skips a duplicate load, ensuring single-pass context loading even under race
|
|
189
|
+
conditions. Calling for a new user_id merges that user's cross-chat context into the
|
|
190
|
+
existing conversation. Context is considered "found" only if messages were actually
|
|
191
|
+
inserted (rows_inserted > 0 or group_rows_inserted > 0).
|
|
189
192
|
|
|
190
193
|
Args:
|
|
191
194
|
token_limit: The maximum token count to load into the in-memory context.
|
|
@@ -203,10 +206,11 @@ class Conversation:
|
|
|
203
206
|
- Sets self._context_cursor[user_id] to the max message id loaded once context is found.
|
|
204
207
|
- Sets self._history_end[user_id] to len(self.messages) after loading, marking the
|
|
205
208
|
boundary between historical context and live session messages for future refreshes.
|
|
206
|
-
- Prints
|
|
209
|
+
- Prints context loading summary (Full/Partial context with token count) if history found,
|
|
210
|
+
or "No prior context" message if no history exists (removed: early-return and refresh
|
|
211
|
+
diagnostics to reduce noisy logging).
|
|
207
212
|
"""
|
|
208
213
|
if user_id in self._context_cursor:
|
|
209
|
-
print(f"Context already loaded for User {user_id} in {self.chat_print}")
|
|
210
214
|
return True
|
|
211
215
|
|
|
212
216
|
exclude_private = True
|
|
@@ -323,7 +327,6 @@ class Conversation:
|
|
|
323
327
|
if rows_inserted > 0:
|
|
324
328
|
self._context_cursor[user_id] = current_max
|
|
325
329
|
self._history_end[user_id] = insert_pos + rows_inserted
|
|
326
|
-
print(f"Refreshed {rows_inserted} new message(s) for User {user_id} in {self.chat_print}")
|
|
327
330
|
|
|
328
331
|
return rows_inserted > 0
|
|
329
332
|
|
|
@@ -159,15 +159,12 @@ async def load_full_user_context(
|
|
|
159
159
|
|
|
160
160
|
In Telegram, a user's private chat_id always equals their user_id. Three arms cover
|
|
161
161
|
all cross-context cases:
|
|
162
|
-
|
|
163
162
|
Arm 1 - chat_id = current_chat_id:
|
|
164
163
|
All messages (user and bot) in the current chat. Always included.
|
|
165
|
-
|
|
166
164
|
Arm 2 - chat_id = user_id AND chat_id != current_chat_id:
|
|
167
165
|
All messages (both sides) from the user's private chat, included when the
|
|
168
166
|
current chat is a group. Pulling both sides restores proper role alternation
|
|
169
167
|
that was broken when only the user's own rows were fetched.
|
|
170
|
-
|
|
171
168
|
Arm 3 - user_id = user_id AND chat_id != current_chat_id AND chat_id != user_id:
|
|
172
169
|
The requesting user's own messages from any other group chats (not the private
|
|
173
170
|
chat, not the current chat). Covers group-to-private and group-to-group context.
|
|
@@ -341,6 +338,47 @@ async def load_shared_group_context(
|
|
|
341
338
|
all_rows.sort(key=lambda r: (r[2], r[3]))
|
|
342
339
|
return [{"role": row[0], "content": row[1]} for row in all_rows]
|
|
343
340
|
|
|
341
|
+
async def delete_paired_bot_replies(chat_id: int, bot_id: int) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Delete bot reply rows in a group chat that are paired with deleted user messages or orphaned.
|
|
344
|
+
|
|
345
|
+
Handles two cases in one pass:
|
|
346
|
+
1. Paired bot replies: after delete_messages_for_user() removes a user's rows, the bot
|
|
347
|
+
replies that immediately followed those messages become orphaned (no user row before
|
|
348
|
+
them). These are caught by the orphan query below.
|
|
349
|
+
2. Pre-existing orphans: assistant rows left behind by earlier /forget calls that deleted
|
|
350
|
+
user rows but not their paired replies. Cleaned up opportunistically on every group
|
|
351
|
+
/forget to heal historical debris.
|
|
352
|
+
|
|
353
|
+
Both cases are unified by a single query: delete assistant rows whose immediately preceding
|
|
354
|
+
message in the same chat is not a user message. Reliable because the bot processes messages
|
|
355
|
+
sequentially - each reply is stored immediately after its user message, so the immediately
|
|
356
|
+
preceding row unambiguously identifies the pairing. Other users' conversations are unaffected
|
|
357
|
+
as long as their user rows remain (their user rows serve as the preceding message for their
|
|
358
|
+
own bot replies).
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
chat_id: Telegram chat ID of the group chat to clean up.
|
|
362
|
+
bot_id: Telegram user ID of the bot account (identifies assistant rows by user_id).
|
|
363
|
+
"""
|
|
364
|
+
async with aiosqlite.connect(get_db_path()) as db:
|
|
365
|
+
await db.execute(
|
|
366
|
+
"DELETE FROM messages "
|
|
367
|
+
"WHERE chat_id = ? AND role = 'assistant' AND user_id = ? "
|
|
368
|
+
"AND NOT EXISTS ("
|
|
369
|
+
" SELECT 1 FROM messages prev "
|
|
370
|
+
" WHERE prev.chat_id = messages.chat_id "
|
|
371
|
+
" AND prev.id = ("
|
|
372
|
+
" SELECT MAX(m2.id) FROM messages m2 "
|
|
373
|
+
" WHERE m2.chat_id = messages.chat_id "
|
|
374
|
+
" AND m2.id < messages.id"
|
|
375
|
+
" ) "
|
|
376
|
+
" AND prev.role = 'user'"
|
|
377
|
+
")",
|
|
378
|
+
(chat_id, bot_id),
|
|
379
|
+
)
|
|
380
|
+
await db.commit()
|
|
381
|
+
|
|
344
382
|
async def delete_messages_for_user(user_id: int) -> None:
|
|
345
383
|
"""
|
|
346
384
|
Delete all message rows for a given user_id across all chats.
|
|
@@ -316,6 +316,57 @@ def init_bot_prompt(file='test_personality.prmpt') -> str:
|
|
|
316
316
|
except Exception:
|
|
317
317
|
sys.exit(f"Error while generating {env_var} file '{file}'! Exiting...")
|
|
318
318
|
|
|
319
|
+
def init_system_appendix(file='system_appendix.prmpt') -> str:
|
|
320
|
+
"""
|
|
321
|
+
Ensure the system appendix prompt file is created and return its contents.
|
|
322
|
+
|
|
323
|
+
The system appendix is a framework-owned behavioral block automatically appended
|
|
324
|
+
to every user-defined persona prompt at runtime. It teaches the bot how its
|
|
325
|
+
cross-chat memory works, so the persona author does not need to include this
|
|
326
|
+
guidance. Every soul (persona) gets the same nervous system (memory behavior).
|
|
327
|
+
|
|
328
|
+
The appendix covers:
|
|
329
|
+
- Cross-chat context is intentional and consensual, controlled by /private
|
|
330
|
+
- Group sessions include the user's private chat history (cross-pollination)
|
|
331
|
+
- Private sessions include shared group chat history (h7bz)
|
|
332
|
+
- Absence of /private means the user opted into cross-chat memory sharing
|
|
333
|
+
- /private ON means only that user's private messages are excluded; all other
|
|
334
|
+
cross-chat context remains available
|
|
335
|
+
- Never deny having context that was actually provided
|
|
336
|
+
|
|
337
|
+
Advanced users may edit system_appendix.prmpt to customize this behavior.
|
|
338
|
+
The url_analysis.prmpt prompt is separate and unaffected.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
file: Name of the system appendix prompt file (default: 'system_appendix.prmpt').
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
The contents of the system appendix prompt file.
|
|
345
|
+
"""
|
|
346
|
+
env_var = "TELLMGRAMBOT_PROMPTS_PATH"
|
|
347
|
+
try:
|
|
348
|
+
return read_text(generate_file_path(os.environ[env_var], file, "system appendix prompt",
|
|
349
|
+
"You have persistent memory across conversations. Past interactions are selectively "
|
|
350
|
+
"loaded into your current context window based on relevance and token budget. Treat "
|
|
351
|
+
"anything visible in your current conversation history as reliable memory.\n\n"
|
|
352
|
+
"Cross-chat memory rules (apply only to what is present in your current context):\n"
|
|
353
|
+
"- In a group chat: when the user's private chat history with you is included in your "
|
|
354
|
+
"current context (cross-pollination), use it naturally. Do not deny having context "
|
|
355
|
+
"that is visible to you.\n"
|
|
356
|
+
"- In a private chat: when shared group chat history from groups where both you and "
|
|
357
|
+
"the user are active is included in your current context, use it to answer questions "
|
|
358
|
+
"about those group conversations. Do not deny having context that is visible to you.\n"
|
|
359
|
+
"- If a user has /private mode ON, their private-chat messages are excluded from group "
|
|
360
|
+
"contexts. You may acknowledge this if relevant, but do not generalize it to deny all "
|
|
361
|
+
"cross-chat context that is present in your current history.\n"
|
|
362
|
+
"- Never deny having context that is present in the messages you can currently see. "
|
|
363
|
+
"Use what you know confidently.\n"
|
|
364
|
+
))
|
|
365
|
+
except KeyError:
|
|
366
|
+
sys.exit(f"{env_var} must be defined to create file '{file}'! Exiting...")
|
|
367
|
+
except Exception:
|
|
368
|
+
sys.exit(f"Error while generating {env_var} file '{file}'! Exiting...")
|
|
369
|
+
|
|
319
370
|
def init_url_prompt(file='url_analysis.prmpt') -> str:
|
|
320
371
|
"""
|
|
321
372
|
Ensure prompt file is created that defines how the bot will analyze URLs via square brackets [].
|
|
@@ -366,4 +417,5 @@ if __name__ == '__main__':
|
|
|
366
417
|
init_structure()
|
|
367
418
|
init_bot_config()
|
|
368
419
|
init_bot_prompt()
|
|
420
|
+
init_system_appendix()
|
|
369
421
|
print("TeLLMgramBot setup complete!")
|
|
@@ -5,7 +5,13 @@ import validators
|
|
|
5
5
|
|
|
6
6
|
from .initialize import init_url_prompt
|
|
7
7
|
from .models import TokenLimits
|
|
8
|
-
from .web_utils import
|
|
8
|
+
from .web_utils import (
|
|
9
|
+
fetch_url,
|
|
10
|
+
strip_html_markup,
|
|
11
|
+
InvalidURLException,
|
|
12
|
+
InsecureURLException,
|
|
13
|
+
SusURLException,
|
|
14
|
+
)
|
|
9
15
|
from .providers.factory import get_provider
|
|
10
16
|
|
|
11
17
|
def handle_greetings(text: str) -> Optional[str]:
|
|
@@ -21,6 +27,28 @@ def handle_greetings(text: str) -> Optional[str]:
|
|
|
21
27
|
return f'{word}!'
|
|
22
28
|
return None
|
|
23
29
|
|
|
30
|
+
def handle_read_receipt(text: str) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Detect explicit user requests for read receipt acknowledgement.
|
|
33
|
+
|
|
34
|
+
Matches explicit acknowledgement trigger phrases: "acknowledge", "acknowledged", "mark as read",
|
|
35
|
+
"read receipt", "did you read", "did you get that", and "confirm you received". Uses word-boundary
|
|
36
|
+
matching to avoid false positives from casual conversation (e.g., "unacknowledged" does not match).
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
text: The message text to analyze (typically after bot mention/nickname stripped).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True if the message matches a read receipt trigger phrase; False otherwise.
|
|
43
|
+
"""
|
|
44
|
+
text_clean = re.sub(r'[^\w\s]', '', text.lower().strip())
|
|
45
|
+
triggers = {
|
|
46
|
+
'acknowledge', 'acknowledged', 'mark as read',
|
|
47
|
+
'read receipt', 'did you read', 'did you get that',
|
|
48
|
+
'confirm you received',
|
|
49
|
+
}
|
|
50
|
+
return any(re.search(rf'\b{re.escape(t)}\b', text_clean) is not None for t in triggers)
|
|
51
|
+
|
|
24
52
|
def handle_common_queries(text: str) -> Optional[str]:
|
|
25
53
|
"""
|
|
26
54
|
Send messages for assistant bot to respond quickly with some example phrases:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.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
|
|
@@ -16,7 +16,7 @@ Requires-Dist: httpx
|
|
|
16
16
|
Requires-Dist: beautifulsoup4
|
|
17
17
|
Requires-Dist: validators
|
|
18
18
|
Requires-Dist: tiktoken>=0.12
|
|
19
|
-
Requires-Dist: python-telegram-bot
|
|
19
|
+
Requires-Dist: python-telegram-bot>=20.8
|
|
20
20
|
Requires-Dist: aiosqlite>=0.19
|
|
21
21
|
Dynamic: author
|
|
22
22
|
Dynamic: author-email
|
|
@@ -45,14 +45,15 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
45
45
|
* Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
|
|
46
46
|
* 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.
|
|
47
47
|
* Users can manage privacy via two commands:
|
|
48
|
-
* `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types; other participants' messages and sessions remain.
|
|
49
|
-
* `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
|
|
48
|
+
* `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types and cleans up paired bot replies; other participants' messages and sessions remain.
|
|
49
|
+
* `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
|
|
50
50
|
|
|
51
51
|
## Why Telegram?
|
|
52
52
|
Using Telegram 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:
|
|
53
53
|
1. Telegram already lets you paste in verbose, multiline messages.
|
|
54
54
|
2. Telegram already lets you paste in pictures, videos, links, etc.
|
|
55
55
|
3. Telegram already lets you react with emojis, stickers, etc.
|
|
56
|
+
4. Telegram message reactions (👀) provide a lightweight read receipt without breaking conversation flow.
|
|
56
57
|
|
|
57
58
|
## Supported LLM Providers
|
|
58
59
|
TeLLMgramBot selects the LLM provider automatically based on the model name:
|
|
@@ -80,6 +81,9 @@ When initializing TeLLMgramBot, the following directories get created:
|
|
|
80
81
|
* A sample prompt file defining the bot's personality: generic, helpful, and multi-provider-aware.
|
|
81
82
|
* The prompt emphasizes the bot's ability to fetch and analyze URLs passed in square brackets `[]`.
|
|
82
83
|
* The user can create more prompt files as needed for different personalities.
|
|
84
|
+
* `system_appendix.prmpt`
|
|
85
|
+
* Framework-owned behavioral guidance automatically appended to the persona prompt at runtime.
|
|
86
|
+
* Teaches the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance.
|
|
83
87
|
* `url_analysis.prmpt`
|
|
84
88
|
* Prompt template used to analyze URL content passed in brackets `[]`.
|
|
85
89
|
* `errorlogs`
|
|
@@ -137,6 +141,18 @@ By default, API key files are created in the execution directory (or the directo
|
|
|
137
141
|
|
|
138
142
|
Each file with the associated API key will update its respective environment variable if not defined. Missing provider keys (OpenAI or Anthropic) will disable chat and URL analysis but allow the bot to start. Missing VirusTotal keys will disable URL analysis.
|
|
139
143
|
|
|
144
|
+
## Commands and Interactions
|
|
145
|
+
|
|
146
|
+
### Read Receipt Acknowledgement (Group Chats Only)
|
|
147
|
+
In group and supergroup chats, you can request the bot to acknowledge reading your message with a lightweight 👀 reaction instead of waiting for a full response. Simply mention the bot's username, nickname, or initials followed by an explicit request:
|
|
148
|
+
|
|
149
|
+
- `@botname acknowledge`
|
|
150
|
+
- `botname mark as read`
|
|
151
|
+
- `BN did you read that?`
|
|
152
|
+
- `botname confirm you received`
|
|
153
|
+
|
|
154
|
+
The bot will respond with a 👀 reaction emoji on your message (if the Telegram client supports message reactions), or with a short "Got it!" reply as fallback. Read receipt requests are processed immediately without touching the conversation context, making them ideal for confirming the bot is online without disrupting group flow.
|
|
155
|
+
|
|
140
156
|
## Bot Setup
|
|
141
157
|
This library includes an example script `test_local.py`, which uses files from the folders `configs` and `prompts` for TeLLMgramBot to process.
|
|
142
158
|
1. Ensure the previous sections are followed with the proper API keys and your Telegram bot set.
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='TeLLMgramBot',
|
|
8
|
-
version='3.
|
|
8
|
+
version='3.4.0',
|
|
9
9
|
packages=find_packages(),
|
|
10
10
|
license='MIT',
|
|
11
11
|
author='Digital Heresy',
|
|
@@ -22,7 +22,7 @@ setup(
|
|
|
22
22
|
'beautifulsoup4',
|
|
23
23
|
'validators',
|
|
24
24
|
'tiktoken>=0.12',
|
|
25
|
-
'python-telegram-bot
|
|
25
|
+
'python-telegram-bot>=20.8',
|
|
26
26
|
'aiosqlite>=0.19'
|
|
27
27
|
],
|
|
28
28
|
python_requires='>=3.10'
|
|
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
|