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.
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/PKG-INFO +19 -10
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/README.md +18 -9
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/TeLLMgramBot.py +121 -12
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/conversation.py +24 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/database.py +69 -24
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/initialize.py +8 -4
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/anthropic_provider.py +31 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/PKG-INFO +19 -10
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/setup.py +1 -1
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/LICENSE +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/message_handlers.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/requires.txt +0 -0
- {tellmgrambot-3.9.1 → tellmgrambot-3.10.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
458
|
-
|
|
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(
|
|
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.
|
|
6
|
-
|
|
7
|
-
and a
|
|
5
|
+
searching message history, and retrieving conversation context. Supports foreign bot message
|
|
6
|
+
storage with per-chat pruning. The schema includes a messages table (indexed by chat_id and
|
|
7
|
+
user_id, with is_foreign_bot and is_private flags), a users table (profile data and
|
|
8
|
+
private_mode flag), and a chats table for speaker/chat resolution in search results.
|
|
8
9
|
"""
|
|
9
10
|
import os
|
|
10
11
|
from typing import Optional
|
|
@@ -23,14 +24,15 @@ _DDL = """
|
|
|
23
24
|
PRAGMA journal_mode=WAL;
|
|
24
25
|
|
|
25
26
|
CREATE TABLE IF NOT EXISTS messages (
|
|
26
|
-
id
|
|
27
|
-
chat_id
|
|
28
|
-
user_id
|
|
29
|
-
role
|
|
30
|
-
content
|
|
31
|
-
is_private
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
chat_id INTEGER NOT NULL,
|
|
29
|
+
user_id INTEGER NOT NULL,
|
|
30
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
|
|
31
|
+
content TEXT NOT NULL,
|
|
32
|
+
is_private INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
is_foreign_bot INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
reply_to_id INTEGER,
|
|
35
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
34
36
|
);
|
|
35
37
|
|
|
36
38
|
CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id, created_at);
|
|
@@ -77,6 +79,7 @@ async def init_db() -> None:
|
|
|
77
79
|
- Drops username column from messages table (now in normalized users table).
|
|
78
80
|
- Adds chat_type column to chats table.
|
|
79
81
|
- Adds private_mode column to users table and merges legacy private_mode table into it.
|
|
82
|
+
- Adds is_foreign_bot column to messages table for foreign bot message storage.
|
|
80
83
|
"""
|
|
81
84
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
82
85
|
await db.executescript(_DDL)
|
|
@@ -84,6 +87,9 @@ async def init_db() -> None:
|
|
|
84
87
|
# Migration: add reply_to_id to existing databases that predate this column.
|
|
85
88
|
if 'reply_to_id' not in existing:
|
|
86
89
|
await db.execute("ALTER TABLE messages ADD COLUMN reply_to_id INTEGER")
|
|
90
|
+
# Migration: add is_foreign_bot to existing databases that predate this column.
|
|
91
|
+
if 'is_foreign_bot' not in existing:
|
|
92
|
+
await db.execute("ALTER TABLE messages ADD COLUMN is_foreign_bot INTEGER NOT NULL DEFAULT 0")
|
|
87
93
|
# Migration: drop username column now stored in the normalized users table.
|
|
88
94
|
if 'username' in existing:
|
|
89
95
|
await db.execute("ALTER TABLE messages DROP COLUMN username")
|
|
@@ -115,6 +121,7 @@ async def insert_message(
|
|
|
115
121
|
content: str,
|
|
116
122
|
is_private: bool = False,
|
|
117
123
|
reply_to_id: Optional[int] = None,
|
|
124
|
+
is_foreign_bot: bool = False,
|
|
118
125
|
) -> int:
|
|
119
126
|
"""
|
|
120
127
|
Persist a single message row and return its database id.
|
|
@@ -124,17 +131,18 @@ async def insert_message(
|
|
|
124
131
|
user_id: Telegram user ID of the sender.
|
|
125
132
|
role: 'user', 'assistant', or 'system'.
|
|
126
133
|
content: Message text.
|
|
127
|
-
is_private: If True, this message is excluded from
|
|
134
|
+
is_private: If True, this message is excluded from all context loading.
|
|
128
135
|
reply_to_id: Database id of the message this is a reply to (None if not a reply).
|
|
136
|
+
is_foreign_bot: If True, marks this as a message from another bot for search indexing.
|
|
129
137
|
|
|
130
138
|
Returns:
|
|
131
139
|
The integer id of the newly inserted row.
|
|
132
140
|
"""
|
|
133
141
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
134
142
|
cursor = await db.execute(
|
|
135
|
-
"INSERT INTO messages (chat_id, user_id, role, content, is_private, reply_to_id) "
|
|
136
|
-
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
137
|
-
(chat_id, user_id, role, content, int(is_private), reply_to_id),
|
|
143
|
+
"INSERT INTO messages (chat_id, user_id, role, content, is_private, reply_to_id, is_foreign_bot) "
|
|
144
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
145
|
+
(chat_id, user_id, role, content, int(is_private), reply_to_id, int(is_foreign_bot)),
|
|
138
146
|
)
|
|
139
147
|
await db.commit()
|
|
140
148
|
return cursor.lastrowid
|
|
@@ -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 =
|
|
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
|
|
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.
|
|
123
|
-
"- State UTC time and stop. Never offer, suggest, or ask about timezone conversion
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|