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.
Files changed (24) hide show
  1. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/PKG-INFO +16 -8
  2. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/README.md +15 -7
  3. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/TeLLMgramBot.py +100 -9
  4. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/database.py +58 -15
  5. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/initialize.py +2 -2
  6. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/anthropic_provider.py +31 -0
  7. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/PKG-INFO +16 -8
  8. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/setup.py +1 -1
  9. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/LICENSE +0 -0
  10. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/__init__.py +0 -0
  11. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/conversation.py +0 -0
  12. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/message_handlers.py +0 -0
  13. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/models.py +0 -0
  14. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/__init__.py +0 -0
  15. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/base.py +0 -0
  16. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/factory.py +0 -0
  17. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/openai_provider.py +0 -0
  18. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/utils.py +0 -0
  19. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot/web_utils.py +0 -0
  20. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
  21. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
  22. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/requires.txt +0 -0
  23. {tellmgrambot-3.9.2 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
  24. {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.9.2
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 responds when any of the following conditions are met:
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. Turn on TeLLMgramBot by calling:
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
- 5. Converse! Type `/help` for all available commands.
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 responds when any of the following conditions are met:
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. Turn on TeLLMgramBot by calling:
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
- 5. Converse! Type `/help` for all available commands.
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, the bot responds when any of the following conditions are met:
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
- await self._send_read_receipt(msg, context)
460
- response = await self.tele_handle_response(msg.text, msg)
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(poll_interval=self.telegram['pollinterval'])
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. The schema includes a messages
6
- table (indexed by chat_id and user_id), a users table (profile data and private_mode flag),
7
- and a chats table for speaker/chat resolution in search results.
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 INTEGER PRIMARY KEY AUTOINCREMENT,
27
- chat_id INTEGER NOT NULL,
28
- user_id INTEGER NOT NULL,
29
- role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
30
- content TEXT NOT NULL,
31
- is_private INTEGER NOT NULL DEFAULT 0,
32
- reply_to_id INTEGER,
33
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
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 group context loading.
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. Always reproduce it verbatim (YYYY/MM/DD HH:MM:SS AM/PM UTC) never reformat.\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"
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.9.2
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 responds when any of the following conditions are met:
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. Turn on TeLLMgramBot by calling:
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
- 5. Converse! Type `/help` for all available commands.
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.
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='TeLLMgramBot',
8
- version='3.9.2',
8
+ version='3.10.0',
9
9
  packages=find_packages(),
10
10
  license='MIT',
11
11
  author='Digital Heresy',
File without changes
File without changes