TeLLMgramBot 3.9.1__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.1 → tellmgrambot-3.10.0}/PKG-INFO +19 -10
  2. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/README.md +18 -9
  3. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/TeLLMgramBot.py +121 -12
  4. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/conversation.py +24 -0
  5. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/database.py +69 -24
  6. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/initialize.py +8 -4
  7. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/anthropic_provider.py +31 -0
  8. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/PKG-INFO +19 -10
  9. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/setup.py +1 -1
  10. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/LICENSE +0 -0
  11. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/__init__.py +0 -0
  12. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/message_handlers.py +0 -0
  13. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/models.py +0 -0
  14. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/__init__.py +0 -0
  15. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/base.py +0 -0
  16. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/factory.py +0 -0
  17. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/openai_provider.py +0 -0
  18. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/utils.py +0 -0
  19. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/web_utils.py +0 -0
  20. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
  21. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
  22. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/requires.txt +0 -0
  23. {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
  24. {tellmgrambot-3.9.1 → 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.1
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
@@ -43,8 +43,8 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
43
43
  * Example: "What do you think of this article? [https://some_site/article]"
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
- * Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "What did we discuss about DMs?"
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). Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found).
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). 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.
@@ -77,6 +77,7 @@ When initializing TeLLMgramBot, the following directories get created:
77
77
  * `chat_model` - the model used for normal conversation (e.g. `gpt-5-mini` or `claude-sonnet-4-6`).
78
78
  * `url_model` - the model used to read and summarize URL content, can differ from `chat_model`.
79
79
  * An empty `token_limit` will use the maximum tokens supported by the `chat_model`.
80
+ * `search_limit` - optional; maximum number of message history search results returned (default: 30). When the user asks "show me recent messages" or searches message history via natural language, this limits the number of results. Omit to use the default.
80
81
  * `models.yaml`
81
82
  * Contains token size parameters for all supported models.
82
83
  * On first run, GPT and Claude model families are pre-populated. Additional models can be added manually.
@@ -85,7 +86,7 @@ When initializing TeLLMgramBot, the following directories get created:
85
86
  * A sample prompt file defining the bot's personality: generic, helpful, and multi-provider-aware.
86
87
  * The prompt emphasizes the bot's ability to fetch and analyze URLs passed in square brackets `[]`.
87
88
  * The user can create more prompt files as needed for different personalities.
88
- * At initialization, the bot automatically appends framework-owned behavioral guidance (system appendix) to teach the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance.
89
+ * At initialization, the bot automatically appends framework-owned behavioral guidance (system appendix) to teach the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance. The appendix includes two framework-managed injectable values: the current UTC datetime and the current user identity, refreshed on every message so the LLM always has accurate context.
89
90
  * `url_analysis.prmpt`
90
91
  * Prompt template used to analyze URL content passed in brackets `[]`.
91
92
  * `logs`
@@ -153,14 +154,17 @@ Each file with the associated API key will update its respective environment var
153
154
  - `/nick <name>` - Set your personal nickname (for bot use in group chats).
154
155
  - `/forget` - Clear all of your conversation history. In private chats, clears everything. In group chats, removes only your messages.
155
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).
156
158
  - `/help` - Display all available commands and usage information.
157
159
 
158
160
  ### Group Chat Triggers
159
- 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:
160
164
  - You mention the bot by username (e.g., `@botname`)
161
- - You mention the bot by nickname (configured via `config.yaml`)
162
- - You mention the bot by initials (configured via `config.yaml`)
163
- - 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
164
168
 
165
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.
166
170
 
@@ -185,12 +189,17 @@ This library includes an example script `test_local.py`, which uses files from t
185
189
  persona_prompt = <System prompt summarizing bot personality>
186
190
  )
187
191
  ```
188
- 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:
189
198
  ```
190
199
  telegram_bot.start_polling()
191
200
  ```
192
201
  Once you see `TeLLMgramBot polling...`, the bot is online in Telegram.
193
- 5. Converse! Type `/help` for all available commands.
202
+ 6. Converse! Type `/help` for all available commands.
194
203
 
195
204
  ## Resources
196
205
  * GitHub repository [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) has guides to create a Telegram bot.
@@ -11,8 +11,8 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
11
11
  * Example: "What do you think of this article? [https://some_site/article]"
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
- * Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "What did we discuss about DMs?"
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). Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found).
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). 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.
@@ -45,6 +45,7 @@ When initializing TeLLMgramBot, the following directories get created:
45
45
  * `chat_model` - the model used for normal conversation (e.g. `gpt-5-mini` or `claude-sonnet-4-6`).
46
46
  * `url_model` - the model used to read and summarize URL content, can differ from `chat_model`.
47
47
  * An empty `token_limit` will use the maximum tokens supported by the `chat_model`.
48
+ * `search_limit` - optional; maximum number of message history search results returned (default: 30). When the user asks "show me recent messages" or searches message history via natural language, this limits the number of results. Omit to use the default.
48
49
  * `models.yaml`
49
50
  * Contains token size parameters for all supported models.
50
51
  * On first run, GPT and Claude model families are pre-populated. Additional models can be added manually.
@@ -53,7 +54,7 @@ When initializing TeLLMgramBot, the following directories get created:
53
54
  * A sample prompt file defining the bot's personality: generic, helpful, and multi-provider-aware.
54
55
  * The prompt emphasizes the bot's ability to fetch and analyze URLs passed in square brackets `[]`.
55
56
  * The user can create more prompt files as needed for different personalities.
56
- * At initialization, the bot automatically appends framework-owned behavioral guidance (system appendix) to teach the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance.
57
+ * At initialization, the bot automatically appends framework-owned behavioral guidance (system appendix) to teach the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance. The appendix includes two framework-managed injectable values: the current UTC datetime and the current user identity, refreshed on every message so the LLM always has accurate context.
57
58
  * `url_analysis.prmpt`
58
59
  * Prompt template used to analyze URL content passed in brackets `[]`.
59
60
  * `logs`
@@ -121,14 +122,17 @@ Each file with the associated API key will update its respective environment var
121
122
  - `/nick <name>` - Set your personal nickname (for bot use in group chats).
122
123
  - `/forget` - Clear all of your conversation history. In private chats, clears everything. In group chats, removes only your messages.
123
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).
124
126
  - `/help` - Display all available commands and usage information.
125
127
 
126
128
  ### Group Chat Triggers
127
- 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:
128
132
  - You mention the bot by username (e.g., `@botname`)
129
- - You mention the bot by nickname (configured via `config.yaml`)
130
- - You mention the bot by initials (configured via `config.yaml`)
131
- - 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
132
136
 
133
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.
134
138
 
@@ -153,12 +157,17 @@ This library includes an example script `test_local.py`, which uses files from t
153
157
  persona_prompt = <System prompt summarizing bot personality>
154
158
  )
155
159
  ```
156
- 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:
157
166
  ```
158
167
  telegram_bot.start_polling()
159
168
  ```
160
169
  Once you see `TeLLMgramBot polling...`, the bot is online in Telegram.
161
- 5. Converse! Type `/help` for all available commands.
170
+ 6. Converse! Type `/help` for all available commands.
162
171
 
163
172
  ## Resources
164
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,18 +43,23 @@ 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. "
46
- "At least one of content_query or speaker_query must be provided."
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. "
55
+ "All filters are optional -- omit them to retrieve recent messages broadly. "
56
+ "Results are ordered most-recent-first; to find the earliest message, look at the last result."
47
57
  ),
48
58
  "parameters": {
49
59
  "type": "object",
50
60
  "properties": {
51
61
  "content_query": {"type": "string", "description": "Text to search for in message content"},
52
- "speaker_query": {"type": "string", "description": "Name or @username of the person who sent the message. Use the exact name or @username if known. If multiple users match, the search will return an ambiguity error asking you to clarify."},
62
+ "speaker_query": {"type": "string", "description": "Name or @username of the person who sent the message. When the user refers to their own messages ('I said', 'my messages', 'what did I say'), use the current user's name or @username from the system prompt. If multiple users match, the search will return an ambiguity error asking you to clarify."},
53
63
  "chat_query": {"type": "string", "description": "Name of the group chat to search within. Use the exact chat title if known. If multiple chats match, the search will return an ambiguity error asking you to clarify."},
54
64
  "date_from": {"type": "string", "description": "ISO date string (YYYY-MM-DD) for start of range"},
55
65
  "date_to": {"type": "string", "description": "ISO date string (YYYY-MM-DD) for end of range"},
@@ -90,6 +100,7 @@ class TelegramBot:
90
100
  "Administrator-only commands:\n"
91
101
  "/start - Go online to receive new responses (default).\n"
92
102
  "/stop - Go offline to prevent new responses.\n"
103
+ "/wipe - Permanently delete all conversation data (irreversible).\n"
93
104
  )
94
105
 
95
106
  async def tele_start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -111,6 +122,18 @@ class TelegramBot:
111
122
  else:
112
123
  await update.message.reply_text("Sorry, I can't do that for you.")
113
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
+
114
137
  async def tele_nick_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
115
138
  """
116
139
  Let bot assistant know to call the user by a different name other than the Telegram username.
@@ -285,8 +308,9 @@ class TelegramBot:
285
308
 
286
309
  conv = self.conversations[chat_id]
287
310
 
288
- # Refresh datetime on every message so timestamps stay accurate across long sessions
311
+ # Refresh datetime and current user identity on every message
289
312
  conv.update_datetime()
313
+ conv.update_current_user(first_name, last_name, username)
290
314
 
291
315
  token_budget = floor(self.llm['prune_threshold'] / 2)
292
316
 
@@ -358,6 +382,31 @@ class TelegramBot:
358
382
 
359
383
  return reply
360
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
+
361
410
  def _foreign_bot_mention(self, msg: Message) -> str | None:
362
411
  """
363
412
  Return the @username of the first explicitly @mentioned account that is not us, or None.
@@ -416,7 +465,12 @@ class TelegramBot:
416
465
  """
417
466
  Route incoming Telegram messages to appropriate handlers based on chat type and trigger conditions.
418
467
 
419
- 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:
420
474
  - User mentions the bot by @username
421
475
  - User mentions the bot by nickname (set via config)
422
476
  - User mentions the bot by initials (set via config)
@@ -425,7 +479,7 @@ class TelegramBot:
425
479
  In all matching group scenarios, a read receipt acknowledgement (👀 reaction or "Got it!" fallback)
426
480
  is sent immediately via _send_read_receipt() before generating the full LLM response.
427
481
 
428
- 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).
429
483
 
430
484
  Args:
431
485
  update: The Telegram Update object containing the incoming message.
@@ -436,6 +490,41 @@ class TelegramBot:
436
490
  return
437
491
  (msg, chat, user) = validated
438
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
+
439
528
  # If it's a group text, only reply if the bot is named
440
529
  # The real magic of how the bot behaves is in tele_handle_response()
441
530
  response = "Sorry, I couldn't process your message! Please contact my creator."
@@ -446,21 +535,21 @@ class TelegramBot:
446
535
  msg.reply_to_message.from_user.id == self.telegram['bot_id']
447
536
  )
448
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).
449
540
  pattern = r'@?\b' + re.escape(self.telegram['username']) + r'\b'
450
541
  new_text = re.sub(pattern, '', msg.text).strip()
451
542
  await self._send_read_receipt(msg, context)
452
543
  response = await self.tele_handle_response(new_text, msg)
453
544
  elif (
454
545
  exact_word_match(self.telegram['nickname'], msg.text) or
455
- exact_word_match(self.telegram['initials'], msg.text)
546
+ exact_word_match(self.telegram['initials'], msg.text) or
547
+ is_reply_to_bot
456
548
  ):
457
- await self._send_read_receipt(msg, context)
458
- response = await self.tele_handle_response(msg.text, msg)
459
- 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.
460
551
  foreign = self._foreign_bot_mention(msg)
461
552
  if foreign:
462
- # The reply is directed at a different account - deflect without
463
- # sending a read receipt, since we are not handling this message.
464
553
  await msg.reply_text(f"Looks like that message is for {foreign}!")
465
554
  return
466
555
  await self._send_read_receipt(msg, context)
@@ -544,6 +633,7 @@ class TelegramBot:
544
633
  args.get('chat_query'),
545
634
  args.get('date_from'),
546
635
  args.get('date_to'),
636
+ self.llm['search_limit'],
547
637
  )
548
638
  if isinstance(results, list):
549
639
  for r in results:
@@ -627,7 +717,10 @@ class TelegramBot:
627
717
  def start_polling(self):
628
718
  """The main polling "loop" the user interacts with via Telegram."""
629
719
  logger.info(f"TeLLMgramBot {self.telegram['username']} polling...")
630
- 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
+ )
631
724
  logger.info(f"TeLLMgramBot {self.telegram['username']} polling ended.")
632
725
 
633
726
  # Initialization
@@ -638,6 +731,7 @@ class TelegramBot:
638
731
  chat_model = INIT_BOT_CONFIG['chat_model'],
639
732
  url_model = INIT_BOT_CONFIG['url_model'],
640
733
  token_limit = INIT_BOT_CONFIG['token_limit'],
734
+ search_limit = INIT_BOT_CONFIG['search_limit'],
641
735
  persona_temp = INIT_BOT_CONFIG['persona_temp'],
642
736
  persona_prompt = INIT_BOT_CONFIG['persona_prompt'],
643
737
  key_status: ApiKeyStatus | None = None,
@@ -653,6 +747,7 @@ class TelegramBot:
653
747
  chat_model: LLM model for conversation (e.g., 'gpt-5-mini', 'claude-sonnet-4-6').
654
748
  url_model: LLM model for URL analysis (can differ from chat_model).
655
749
  token_limit: Maximum tokens for conversations. If None, uses the chat_model default.
750
+ search_limit: Maximum results returned by message search. If None, defaults to 30.
656
751
  persona_temp: LLM temperature (0.0-2.0). If None, defaults to 1.0.
657
752
  persona_prompt: System prompt defining the bot's behavior and personality.
658
753
  key_status: ApiKeyStatus object indicating available features. If None, calls init_structure().
@@ -697,12 +792,24 @@ class TelegramBot:
697
792
  self.telegram['app'].add_handler(CommandHandler('help', self.tele_commands))
698
793
  self.telegram['app'].add_handler(CommandHandler('start', self.tele_start_command))
699
794
  self.telegram['app'].add_handler(CommandHandler('stop', self.tele_stop_command))
795
+ self.telegram['app'].add_handler(CommandHandler('wipe', self.tele_wipe_command))
700
796
  self.telegram['app'].add_handler(CommandHandler('nick', self.tele_nick_command))
701
797
  self.telegram['app'].add_handler(CommandHandler('forget', self.tele_forget_command))
702
798
  self.telegram['app'].add_handler(CommandHandler('private', self.tele_private_command))
703
799
  self.telegram['app'].add_handler(MessageHandler(filters.TEXT & ~filters.UpdateType.EDITED_MESSAGE, self.tele_handle_message))
704
800
  self.telegram['app'].add_error_handler(self.tele_error)
705
801
 
802
+ # Validate optional config values before storing; warn and fall back to defaults on bad input
803
+ if token_limit is not None and not (isinstance(token_limit, int) and token_limit > 0):
804
+ logger.warning(f"Invalid token_limit '{token_limit}' (must be a positive integer), using model default")
805
+ token_limit = None
806
+ if search_limit is not None and not (isinstance(search_limit, int) and search_limit > 0):
807
+ logger.warning(f"Invalid search_limit '{search_limit}' (must be a positive integer), using default 30")
808
+ search_limit = None
809
+ if persona_temp is not None and not (isinstance(persona_temp, (int, float)) and 0.0 <= persona_temp <= 2.0):
810
+ logger.warning(f"Invalid persona_temp '{persona_temp}' (must be a decimal between 0.0 and 2.0), using default 1.0")
811
+ persona_temp = None
812
+
706
813
  # Get our LLM spun up with defaults if not defined by user input
707
814
  # Tokens as integers measure the length of conversation messages
708
815
  self.llm = {
@@ -710,6 +817,7 @@ class TelegramBot:
710
817
  'chat_model' : chat_model,
711
818
  'url_model' : url_model,
712
819
  'token_limit' : token_limit or TokenLimits(chat_model).max_tokens(),
820
+ 'search_limit': search_limit or 30,
713
821
  'temperature' : persona_temp or 1.0,
714
822
  'top_p' : 0.9
715
823
  }
@@ -756,6 +864,7 @@ class TelegramBot:
756
864
  chat_model = config['chat_model'],
757
865
  url_model = config['url_model'],
758
866
  token_limit = config['token_limit'],
867
+ search_limit = config['search_limit'],
759
868
  persona_temp = config['persona_temp'],
760
869
  persona_prompt = prompt,
761
870
  key_status = key_status,
@@ -105,6 +105,30 @@ class Conversation:
105
105
  )
106
106
  self.set_system_content(new_content)
107
107
 
108
+ def update_current_user(self, first_name: str | None, last_name: str | None, username: str | None) -> None:
109
+ """
110
+ Refresh the 'Current user:' line in the active system prompt.
111
+
112
+ Called on every incoming message so the LLM always knows who it is talking to,
113
+ enabling it to use the correct name in speaker_query without asking the user.
114
+
115
+ Args:
116
+ first_name: Telegram first name of the current user, may be None.
117
+ last_name: Telegram last name of the current user, may be None.
118
+ username: Telegram @handle (without @) of the current user, may be None.
119
+ """
120
+ name = ' '.join(filter(None, [first_name, last_name]))
121
+ if username:
122
+ label = f"{name} (@{username})" if name else f"@{username}"
123
+ else:
124
+ label = name or "unknown"
125
+ new_content = re.sub(
126
+ r'\nCurrent user:.*',
127
+ f'\nCurrent user: {label}',
128
+ self.system_content,
129
+ )
130
+ self.set_system_content(new_content)
131
+
108
132
  async def add_user_message(
109
133
  self,
110
134
  content: str,
@@ -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
@@ -466,11 +474,15 @@ async def search_messages(
466
474
  chat_query: Optional[str] = None,
467
475
  date_from: Optional[str] = None,
468
476
  date_to: Optional[str] = None,
469
- limit: int = 20,
477
+ limit: int = 30,
470
478
  ) -> list[dict] | dict:
471
479
  """
472
480
  Search message history across accessible chats with optional filters.
473
481
 
482
+ All filters (content_query, speaker_query, chat_query, date_from, date_to) are
483
+ optional. If all filters are omitted, returns recent messages from all accessible
484
+ chats, ordered most-recent-first. Filters are combined as AND conditions.
485
+
474
486
  Resolves speaker_query and chat_query using exact-match-first logic:
475
487
  1. Case-insensitive exact match - if exactly one entity matches, proceed.
476
488
  2. Partial LIKE match - if exactly one accessible entity matches, proceed.
@@ -483,15 +495,15 @@ async def search_messages(
483
495
 
484
496
  Args:
485
497
  accessible_chat_ids: Chat IDs the caller is allowed to search within.
486
- content_query: Text to search for in message content (LIKE match).
498
+ content_query: Text to search for in message content (LIKE match). Optional.
487
499
  speaker_query: Name or @handle to resolve to a set of user_ids. Exact match
488
- is tried first; falls back to LIKE. Leading `@` stripped.
500
+ is tried first; falls back to LIKE. Leading `@` stripped. Optional.
489
501
  chat_query: Chat title to resolve to a set of chat_ids. Exact match tried first;
490
502
  falls back to LIKE. Zero global matches fall back to all accessible.
491
- If a matching chat is found but not accessible, returns empty.
492
- date_from: ISO date string (YYYY-MM-DD) for start of range (inclusive).
493
- date_to: ISO date string (YYYY-MM-DD) for end of range (inclusive).
494
- limit: Maximum number of results to return (default 20).
503
+ If a matching chat is found but not accessible, returns empty. Optional.
504
+ date_from: ISO date string (YYYY-MM-DD) for start of range (inclusive). Optional.
505
+ date_to: ISO date string (YYYY-MM-DD) for end of range (inclusive). Optional.
506
+ limit: Maximum number of results to return (default 30).
495
507
 
496
508
  Returns:
497
509
  List of result dicts with keys: speaker, chat, chat_id, content, timestamp (normal case).
@@ -501,8 +513,6 @@ async def search_messages(
501
513
  and a 'message' string for the LLM to surface as a disambiguation prompt.
502
514
  Ordered by created_at DESC (most recent first).
503
515
  """
504
- if not content_query and not speaker_query:
505
- return []
506
516
  if not accessible_chat_ids:
507
517
  return []
508
518
 
@@ -694,6 +704,28 @@ async def search_messages(
694
704
  })
695
705
  return results
696
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
+
697
729
  async def delete_bot_replies_for_user(user_id: int) -> None:
698
730
  """
699
731
  Delete assistant rows whose reply_to_id references any message from this user.
@@ -752,6 +784,19 @@ async def delete_private_messages_for_user(user_id: int) -> None:
752
784
  )
753
785
  await db.commit()
754
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
+
755
800
  async def get_private_mode(user_id: int) -> bool:
756
801
  """
757
802
  Return whether private mode is currently enabled for this user.
@@ -101,12 +101,14 @@ INIT_BOT_CONFIG = {
101
101
  'chat_model': 'gpt-5-mini',
102
102
  'url_model': 'gpt-5',
103
103
  'token_limit': None,
104
+ 'search_limit': None,
104
105
  'persona_temp': None,
105
106
  'persona_prompt': 'You are a generic test bot powered by a user-configured LLM.'
106
107
  }
107
108
 
108
109
  INIT_BOT_CONFIG_COMMENTS = {
109
110
  'token_limit': '# Optional, overrides the chat_model\'s default max token limit',
111
+ 'search_limit': '# Optional, max results returned by message search (default: 30)',
110
112
  'persona_temp': '# Optional, LLM temperature 0.0-2.0 (default: model\'s default)',
111
113
  }
112
114
 
@@ -119,9 +121,10 @@ _SYSTEM_APPENDIX = (
119
121
  "- Group chats may include the user's private history; private chats may include shared group history.\n"
120
122
  "- /private mode excludes that user's messages from group contexts only; don't generalize it to deny other context.\n\n"
121
123
  "Date/time:\n"
122
- "- 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"
123
- "- 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
- "- Decline requests to set a timezone; explain UTC is always used.\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
+ "- Decline requests to set a timezone; explain UTC is always used.\n\n"
127
+ "Current user: (not yet known)\n"
125
128
  )
126
129
 
127
130
  def init_logging(log_name: str = 'tellmgrambot'):
@@ -393,7 +396,8 @@ def init_bot_config(file: str = 'config.yaml') -> dict:
393
396
  for parameter, value in INIT_BOT_CONFIG.items():
394
397
  if parameter != 'persona_prompt' and parameter not in config:
395
398
  config[parameter] = value
396
- logger.warning(f"Configuration '{parameter}' not defined, set to '{value}'")
399
+ if value is not None:
400
+ logger.warning(f"Configuration '{parameter}' not defined, set to '{value}'")
397
401
  return config
398
402
  except KeyError:
399
403
  sys.exit(f"{env_var} must be defined to create file '{file}'! Exiting...")
@@ -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.1
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
@@ -43,8 +43,8 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
43
43
  * Example: "What do you think of this article? [https://some_site/article]"
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
- * Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "What did we discuss about DMs?"
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). Chat title queries support colloquial terms like "DMs" (falls back to all accessible chats if no exact match is found).
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). 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.
@@ -77,6 +77,7 @@ When initializing TeLLMgramBot, the following directories get created:
77
77
  * `chat_model` - the model used for normal conversation (e.g. `gpt-5-mini` or `claude-sonnet-4-6`).
78
78
  * `url_model` - the model used to read and summarize URL content, can differ from `chat_model`.
79
79
  * An empty `token_limit` will use the maximum tokens supported by the `chat_model`.
80
+ * `search_limit` - optional; maximum number of message history search results returned (default: 30). When the user asks "show me recent messages" or searches message history via natural language, this limits the number of results. Omit to use the default.
80
81
  * `models.yaml`
81
82
  * Contains token size parameters for all supported models.
82
83
  * On first run, GPT and Claude model families are pre-populated. Additional models can be added manually.
@@ -85,7 +86,7 @@ When initializing TeLLMgramBot, the following directories get created:
85
86
  * A sample prompt file defining the bot's personality: generic, helpful, and multi-provider-aware.
86
87
  * The prompt emphasizes the bot's ability to fetch and analyze URLs passed in square brackets `[]`.
87
88
  * The user can create more prompt files as needed for different personalities.
88
- * At initialization, the bot automatically appends framework-owned behavioral guidance (system appendix) to teach the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance.
89
+ * At initialization, the bot automatically appends framework-owned behavioral guidance (system appendix) to teach the LLM how cross-chat memory works (cross-pollination, private mode, shared group context) without requiring persona authors to include this guidance. The appendix includes two framework-managed injectable values: the current UTC datetime and the current user identity, refreshed on every message so the LLM always has accurate context.
89
90
  * `url_analysis.prmpt`
90
91
  * Prompt template used to analyze URL content passed in brackets `[]`.
91
92
  * `logs`
@@ -153,14 +154,17 @@ Each file with the associated API key will update its respective environment var
153
154
  - `/nick <name>` - Set your personal nickname (for bot use in group chats).
154
155
  - `/forget` - Clear all of your conversation history. In private chats, clears everything. In group chats, removes only your messages.
155
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).
156
158
  - `/help` - Display all available commands and usage information.
157
159
 
158
160
  ### Group Chat Triggers
159
- 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:
160
164
  - You mention the bot by username (e.g., `@botname`)
161
- - You mention the bot by nickname (configured via `config.yaml`)
162
- - You mention the bot by initials (configured via `config.yaml`)
163
- - 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
164
168
 
165
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.
166
170
 
@@ -185,12 +189,17 @@ This library includes an example script `test_local.py`, which uses files from t
185
189
  persona_prompt = <System prompt summarizing bot personality>
186
190
  )
187
191
  ```
188
- 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:
189
198
  ```
190
199
  telegram_bot.start_polling()
191
200
  ```
192
201
  Once you see `TeLLMgramBot polling...`, the bot is online in Telegram.
193
- 5. Converse! Type `/help` for all available commands.
202
+ 6. Converse! Type `/help` for all available commands.
194
203
 
195
204
  ## Resources
196
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.1',
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