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.
Files changed (24) hide show
  1. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/PKG-INFO +20 -4
  2. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/README.md +18 -2
  3. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/TeLLMgramBot.py +66 -27
  4. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/conversation.py +9 -6
  5. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/database.py +41 -3
  6. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/initialize.py +52 -0
  7. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/message_handlers.py +29 -1
  8. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/PKG-INFO +20 -4
  9. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/requires.txt +1 -1
  10. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/setup.py +2 -2
  11. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/LICENSE +0 -0
  12. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/__init__.py +0 -0
  13. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/models.py +0 -0
  14. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/__init__.py +0 -0
  15. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
  16. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/base.py +0 -0
  17. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/factory.py +0 -0
  18. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/providers/openai_provider.py +0 -0
  19. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/utils.py +0 -0
  20. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot/web_utils.py +0 -0
  21. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
  22. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
  23. {tellmgrambot-3.2.2 → tellmgrambot-3.4.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
  24. {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.2.2
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>20.0
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. If you have private mode ON and message a group, the bot will remind you once per session.
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. If you have private mode ON and message a group, the bot will remind you once per session.
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 INIT_BOT_CONFIG, ApiKeyStatus, init_structure, init_bot_config, init_bot_prompt
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 get_private_mode, set_private_mode, delete_messages_for_user, delete_messages_for_chat, delete_private_messages_for_user
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. All private-mode notices for this user are also cleared.
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, and returns
505
- the resulting ApiKeyStatus to gate features.
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
- Calling for a new user_id merges that user's cross-chat context into the existing
187
- conversation. Context is considered "found" only if messages were actually inserted
188
- (rows_inserted > 0 or group_rows_inserted > 0).
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 to stdout showing token count and whether the token limit was reached.
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 fetch_url, strip_html_markup, InvalidURLException, InsecureURLException, SusURLException
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.2.2
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>20.0
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. If you have private mode ON and message a group, the bot will remind you once per session.
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,5 +5,5 @@ httpx
5
5
  beautifulsoup4
6
6
  validators
7
7
  tiktoken>=0.12
8
- python-telegram-bot>20.0
8
+ python-telegram-bot>=20.8
9
9
  aiosqlite>=0.19
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='TeLLMgramBot',
8
- version='3.2.2',
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>20.0',
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