TeLLMgramBot 3.10.2__tar.gz → 3.10.4__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.10.2 → tellmgrambot-3.10.4}/PKG-INFO +4 -3
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/README.md +3 -2
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/TeLLMgramBot.py +69 -17
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/database.py +103 -34
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/initialize.py +1 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot.egg-info/PKG-INFO +4 -3
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/setup.py +1 -1
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/LICENSE +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/conversation.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/message_handlers.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot.egg-info/requires.txt +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.10.
|
|
3
|
+
Version: 3.10.4
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -44,6 +44,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
44
44
|
* Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
|
|
45
45
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
|
|
46
46
|
* All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
|
|
47
|
+
* Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
|
|
47
48
|
* Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
|
|
48
49
|
* The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
|
|
49
50
|
* This eliminates amnesia when switching between private and group chats.
|
|
@@ -123,7 +124,7 @@ os.environ['TELLMGRAMBOT_VIRUSTOTAL_API_KEY'] = my_vault.get('virustotal_key')
|
|
|
123
124
|
- `/nick <name>` - Set your nickname (for bot use in group chats).
|
|
124
125
|
- `/forget` - Clear your conversation history. In private chats, clears everything and resets all active sessions. In group chats, removes only your messages.
|
|
125
126
|
- `/private` - Toggle private mode (private chats only). When ON, your messages are excluded from group context loading.
|
|
126
|
-
- `/help` - Display
|
|
127
|
+
- `/help` - Display available commands and usage information. In private chats, if you are a bot owner, also shows administrator-only commands (`/start`, `/stop`, `/wipe`).
|
|
127
128
|
|
|
128
129
|
### Group Chat Triggers
|
|
129
130
|
The bot responds in groups when you:
|
|
@@ -143,7 +144,7 @@ When the bot is triggered in a group and about to respond (not deferring to anot
|
|
|
143
144
|
1. Ensure API keys are set up and your Telegram bot is created via BotFather.
|
|
144
145
|
2. Install TeLLMgramBot: `pip install TeLLMgramBot`
|
|
145
146
|
3. Configure the bot via `config.yaml` (created on first run):
|
|
146
|
-
- `bot_owner`:
|
|
147
|
+
- `bot_owner`: Telegram username(s) with admin access (required, no `@`). Accepts a single string or a YAML list of usernames.
|
|
147
148
|
- `chat_model`: LLM model for conversation (e.g. `gpt-4o-mini` or `claude-sonnet-4-6`)
|
|
148
149
|
- `url_model`: LLM model for URL analysis (e.g. `gpt-4o` or `claude-haiku-4-5`)
|
|
149
150
|
- `bot_nickname` / `bot_initials`: Names the bot responds to in groups
|
|
@@ -12,6 +12,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
12
12
|
* Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
|
|
13
13
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
|
|
14
14
|
* All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
|
|
15
|
+
* Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
|
|
15
16
|
* Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
|
|
16
17
|
* The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
|
|
17
18
|
* This eliminates amnesia when switching between private and group chats.
|
|
@@ -91,7 +92,7 @@ os.environ['TELLMGRAMBOT_VIRUSTOTAL_API_KEY'] = my_vault.get('virustotal_key')
|
|
|
91
92
|
- `/nick <name>` - Set your nickname (for bot use in group chats).
|
|
92
93
|
- `/forget` - Clear your conversation history. In private chats, clears everything and resets all active sessions. In group chats, removes only your messages.
|
|
93
94
|
- `/private` - Toggle private mode (private chats only). When ON, your messages are excluded from group context loading.
|
|
94
|
-
- `/help` - Display
|
|
95
|
+
- `/help` - Display available commands and usage information. In private chats, if you are a bot owner, also shows administrator-only commands (`/start`, `/stop`, `/wipe`).
|
|
95
96
|
|
|
96
97
|
### Group Chat Triggers
|
|
97
98
|
The bot responds in groups when you:
|
|
@@ -111,7 +112,7 @@ When the bot is triggered in a group and about to respond (not deferring to anot
|
|
|
111
112
|
1. Ensure API keys are set up and your Telegram bot is created via BotFather.
|
|
112
113
|
2. Install TeLLMgramBot: `pip install TeLLMgramBot`
|
|
113
114
|
3. Configure the bot via `config.yaml` (created on first run):
|
|
114
|
-
- `bot_owner`:
|
|
115
|
+
- `bot_owner`: Telegram username(s) with admin access (required, no `@`). Accepts a single string or a YAML list of usernames.
|
|
115
116
|
- `chat_model`: LLM model for conversation (e.g. `gpt-4o-mini` or `claude-sonnet-4-6`)
|
|
116
117
|
- `url_model`: LLM model for URL analysis (e.g. `gpt-4o` or `claude-haiku-4-5`)
|
|
117
118
|
- `bot_nickname` / `bot_initials`: Names the bot responds to in groups
|
|
@@ -91,23 +91,49 @@ class TelegramBot:
|
|
|
91
91
|
raise
|
|
92
92
|
|
|
93
93
|
async def tele_commands(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
94
|
-
"""
|
|
95
|
-
|
|
94
|
+
"""
|
|
95
|
+
Show available commands when the user sends `/help` to the bot.
|
|
96
|
+
|
|
97
|
+
Displays general commands (/nick, /forget, /private, /help) to all users.
|
|
98
|
+
Admin commands (/start, /stop, /wipe) are shown only to usernames in bot_owner list,
|
|
99
|
+
and only in private chats (not in groups).
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
update: Telegram Update object containing the /help message.
|
|
103
|
+
context: Telegram context for sending the reply.
|
|
104
|
+
"""
|
|
105
|
+
uname = update.message.from_user.username
|
|
106
|
+
chat_type = update.message.chat.type
|
|
107
|
+
text = (
|
|
108
|
+
"TeLLMgramBot commands:\n"
|
|
96
109
|
"/nick - Set your nickname (e.g. \"/nick B0b #2\").\n"
|
|
97
|
-
"/forget - Clear
|
|
110
|
+
"/forget - Clear memories of all your conversations.\n"
|
|
98
111
|
"/private - Toggle private mode (private chats only).\n"
|
|
99
|
-
"/help - Display this usage information
|
|
100
|
-
"Chat history will still show in your Telegram client.\n\n"
|
|
101
|
-
"Administrator-only commands:\n"
|
|
102
|
-
"/start - Go online to receive new responses (default).\n"
|
|
103
|
-
"/stop - Go offline to prevent new responses.\n"
|
|
104
|
-
"/wipe - Permanently delete all conversation data (irreversible).\n"
|
|
112
|
+
"/help - Display this usage information."
|
|
105
113
|
)
|
|
114
|
+
if uname in self.telegram['owners'] and chat_type == 'private':
|
|
115
|
+
text += (
|
|
116
|
+
"\n\nAdmin commands:\n"
|
|
117
|
+
"/start - Go online to receive new responses (default).\n"
|
|
118
|
+
"/stop - Go offline to prevent new responses.\n"
|
|
119
|
+
"/wipe - Permanently delete all conversation data (irreversible)."
|
|
120
|
+
)
|
|
121
|
+
await update.message.reply_text(text)
|
|
106
122
|
|
|
107
123
|
async def tele_start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
108
|
-
"""
|
|
124
|
+
"""
|
|
125
|
+
Start the bot and resume handling new messages (admin-only).
|
|
126
|
+
|
|
127
|
+
Only usernames in self.telegram['owners'] can execute this command.
|
|
128
|
+
If the sender is an owner, sets self._online to True and confirms via reply.
|
|
129
|
+
Otherwise, denies access with "Sorry, I can't do that for you."
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
update: Telegram Update object containing the /start message.
|
|
133
|
+
context: Telegram context for sending the reply.
|
|
134
|
+
"""
|
|
109
135
|
uname = update.message.from_user.username
|
|
110
|
-
if uname
|
|
136
|
+
if uname in self.telegram['owners']:
|
|
111
137
|
self._online = True
|
|
112
138
|
greeting_text = f"Oh, hello {update.message.from_user.first_name}! Let me get to work!"
|
|
113
139
|
await update.message.reply_text(greeting_text)
|
|
@@ -115,18 +141,41 @@ class TelegramBot:
|
|
|
115
141
|
await update.message.reply_text("Sorry, I can't do that for you.")
|
|
116
142
|
|
|
117
143
|
async def tele_stop_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
118
|
-
"""
|
|
144
|
+
"""
|
|
145
|
+
Stop the bot and pause handling new messages (admin-only).
|
|
146
|
+
|
|
147
|
+
Only usernames in self.telegram['owners'] can execute this command.
|
|
148
|
+
If the sender is an owner, sets self._online to False and confirms via reply.
|
|
149
|
+
Otherwise, denies access with "Sorry, I can't do that for you."
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
update: Telegram Update object containing the /stop message.
|
|
153
|
+
context: Telegram context for sending the reply.
|
|
154
|
+
"""
|
|
119
155
|
uname = update.message.from_user.username
|
|
120
|
-
if uname
|
|
156
|
+
if uname in self.telegram['owners']:
|
|
121
157
|
self._online = False
|
|
122
158
|
await update.message.reply_text("Sure thing boss, cutting out!")
|
|
123
159
|
else:
|
|
124
160
|
await update.message.reply_text("Sorry, I can't do that for you.")
|
|
125
161
|
|
|
126
162
|
async def tele_wipe_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
127
|
-
"""
|
|
163
|
+
"""
|
|
164
|
+
Permanently delete all conversation data from the database (admin-only).
|
|
165
|
+
|
|
166
|
+
Only usernames in self.telegram['owners'] can execute this command.
|
|
167
|
+
If the sender is an owner, deletes all messages, users, and chats via wipe_all_data(),
|
|
168
|
+
clears in-memory conversations and token warnings, and confirms via reply.
|
|
169
|
+
Otherwise, denies access with "Sorry, I can't do that for you."
|
|
170
|
+
|
|
171
|
+
This action is irreversible - all conversation history is permanently lost.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
update: Telegram Update object containing the /wipe message.
|
|
175
|
+
context: Telegram context for sending the reply.
|
|
176
|
+
"""
|
|
128
177
|
uname = update.message.from_user.username
|
|
129
|
-
if uname
|
|
178
|
+
if uname not in self.telegram['owners']:
|
|
130
179
|
await update.message.reply_text("Sorry, I can't do that for you.")
|
|
131
180
|
return
|
|
132
181
|
await wipe_all_data()
|
|
@@ -759,7 +808,8 @@ class TelegramBot:
|
|
|
759
808
|
Initialize the Telegram bot with LLM configuration and API keys.
|
|
760
809
|
|
|
761
810
|
Args:
|
|
762
|
-
bot_owner: Telegram username
|
|
811
|
+
bot_owner: Telegram username(s) with admin access (no `@`).
|
|
812
|
+
Accepts a single string or a list of strings; normalised to list[str] internally.
|
|
763
813
|
bot_nickname: Nickname the bot responds to in group chats.
|
|
764
814
|
bot_initials: Initials the bot responds to in group chats.
|
|
765
815
|
chat_model: LLM model for conversation (e.g., 'gpt-5-mini', 'claude-sonnet-4-6').
|
|
@@ -771,6 +821,7 @@ class TelegramBot:
|
|
|
771
821
|
key_status: ApiKeyStatus object indicating available features. If None, calls init_structure().
|
|
772
822
|
|
|
773
823
|
Side Effects:
|
|
824
|
+
- Normalises bot_owner to list[str] and stores in self.telegram['owners'].
|
|
774
825
|
- Fetches bot metadata (username, first_name, last_name, bot_id) from Telegram API via _tele_info().
|
|
775
826
|
- Initializes command and message handlers for the Telegram application.
|
|
776
827
|
- Sets self._online to True once initialization completes (ready for polling).
|
|
@@ -785,9 +836,10 @@ class TelegramBot:
|
|
|
785
836
|
# Initialize some variables
|
|
786
837
|
self.token_warning = {} # Determines whether user has reached token limit by AI model
|
|
787
838
|
self.conversations = {} # Provides Conversation class per user based on bot response
|
|
839
|
+
owners = bot_owner if isinstance(bot_owner, list) else [bot_owner]
|
|
788
840
|
self.telegram = {
|
|
789
841
|
'bot_id' : 0, # overwritten by _tele_info(); 0 is a safe sentinel
|
|
790
|
-
'
|
|
842
|
+
'owners' : [str(u).strip() for u in owners if str(u).strip()],
|
|
791
843
|
'nickname' : bot_nickname,
|
|
792
844
|
'initials' : bot_initials,
|
|
793
845
|
'username' : None,
|
|
@@ -6,6 +6,7 @@ searching message history, and retrieving conversation context. The schema inclu
|
|
|
6
6
|
table (indexed by chat_id and user_id, with is_private flag), a users table (profile data
|
|
7
7
|
and private_mode flag), and a chats table for speaker/chat resolution in search results.
|
|
8
8
|
"""
|
|
9
|
+
import json
|
|
9
10
|
import os
|
|
10
11
|
from typing import Optional
|
|
11
12
|
|
|
@@ -42,19 +43,21 @@ CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id, created_at)
|
|
|
42
43
|
CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id, created_at);
|
|
43
44
|
|
|
44
45
|
CREATE TABLE IF NOT EXISTS users (
|
|
45
|
-
user_id
|
|
46
|
-
username
|
|
47
|
-
first_name
|
|
48
|
-
last_name
|
|
49
|
-
private_mode
|
|
50
|
-
|
|
46
|
+
user_id INTEGER PRIMARY KEY,
|
|
47
|
+
username TEXT,
|
|
48
|
+
first_name TEXT,
|
|
49
|
+
last_name TEXT,
|
|
50
|
+
private_mode INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
previous_names TEXT,
|
|
52
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
51
53
|
);
|
|
52
54
|
|
|
53
55
|
CREATE TABLE IF NOT EXISTS chats (
|
|
54
|
-
chat_id
|
|
55
|
-
chat_type
|
|
56
|
-
chat_title
|
|
57
|
-
|
|
56
|
+
chat_id INTEGER PRIMARY KEY,
|
|
57
|
+
chat_type TEXT,
|
|
58
|
+
chat_title TEXT,
|
|
59
|
+
previous_titles TEXT,
|
|
60
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
58
61
|
);
|
|
59
62
|
"""
|
|
60
63
|
|
|
@@ -71,7 +74,7 @@ def set_db_filename(filename: str) -> None:
|
|
|
71
74
|
Args:
|
|
72
75
|
filename: DB filename, e.g. 'MyBot_conversations.db'. A '.db' suffix is
|
|
73
76
|
appended automatically if not already present. Must be a plain
|
|
74
|
-
basename
|
|
77
|
+
basename - path separators and absolute paths are rejected.
|
|
75
78
|
|
|
76
79
|
Raises:
|
|
77
80
|
ValueError: If filename contains path components or is absolute.
|
|
@@ -133,14 +136,19 @@ async def init_db() -> None:
|
|
|
133
136
|
# Migration: drop username column now stored in the normalized users table.
|
|
134
137
|
if 'username' in existing:
|
|
135
138
|
await db.execute("ALTER TABLE messages DROP COLUMN username")
|
|
136
|
-
# Migration: add chat_type to chats table if absent.
|
|
139
|
+
# Migration: add chat_type and previous_titles to chats table if absent.
|
|
137
140
|
chats_cols = {row[1] async for row in await db.execute("PRAGMA table_info(chats)")}
|
|
138
141
|
if 'chat_type' not in chats_cols:
|
|
139
142
|
await db.execute("ALTER TABLE chats ADD COLUMN chat_type TEXT")
|
|
143
|
+
if 'previous_titles' not in chats_cols:
|
|
144
|
+
await db.execute("ALTER TABLE chats ADD COLUMN previous_titles TEXT")
|
|
140
145
|
# Migration: merge private_mode table into users.private_mode and drop the old table.
|
|
146
|
+
# Also add previous_names column for name history tracking.
|
|
141
147
|
users_cols = {row[1] async for row in await db.execute("PRAGMA table_info(users)")}
|
|
142
148
|
if 'private_mode' not in users_cols:
|
|
143
149
|
await db.execute("ALTER TABLE users ADD COLUMN private_mode INTEGER NOT NULL DEFAULT 0")
|
|
150
|
+
if 'previous_names' not in users_cols:
|
|
151
|
+
await db.execute("ALTER TABLE users ADD COLUMN previous_names TEXT")
|
|
144
152
|
tables = {row[0] async for row in await db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
145
153
|
if 'private_mode' in tables:
|
|
146
154
|
await db.execute(
|
|
@@ -595,7 +603,10 @@ async def upsert_user(
|
|
|
595
603
|
Insert or update a user record in the users table.
|
|
596
604
|
|
|
597
605
|
Always reflects the latest Telegram profile data. Called on every interaction
|
|
598
|
-
so that name changes are picked up automatically.
|
|
606
|
+
so that name changes are picked up automatically. When first/last name changes,
|
|
607
|
+
the old display name (first_name + space + last_name) is appended to the
|
|
608
|
+
previous_names column (JSON array, deduplicated) to enable search by
|
|
609
|
+
historical identities.
|
|
599
610
|
|
|
600
611
|
Args:
|
|
601
612
|
user_id: Telegram user ID.
|
|
@@ -604,13 +615,40 @@ async def upsert_user(
|
|
|
604
615
|
last_name: Telegram last name, may be None.
|
|
605
616
|
"""
|
|
606
617
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
618
|
+
async with db.execute(
|
|
619
|
+
"SELECT first_name, last_name, previous_names FROM users WHERE user_id = ?",
|
|
620
|
+
(user_id,),
|
|
621
|
+
) as cur:
|
|
622
|
+
existing = await cur.fetchone()
|
|
623
|
+
|
|
624
|
+
updated_prev = None
|
|
625
|
+
if existing is not None:
|
|
626
|
+
old_first, old_last, prev = existing
|
|
627
|
+
old_display = f"{old_first or ''} {old_last or ''}".strip()
|
|
628
|
+
new_display = f"{first_name or ''} {last_name or ''}".strip()
|
|
629
|
+
if old_display and old_display != new_display:
|
|
630
|
+
if prev:
|
|
631
|
+
try:
|
|
632
|
+
seen = json.loads(prev)
|
|
633
|
+
if not isinstance(seen, list):
|
|
634
|
+
seen = [seen] if seen else []
|
|
635
|
+
except json.JSONDecodeError:
|
|
636
|
+
seen = [n.strip() for n in prev.split(",") if n.strip()]
|
|
637
|
+
else:
|
|
638
|
+
seen = []
|
|
639
|
+
if old_display not in seen:
|
|
640
|
+
seen.append(old_display)
|
|
641
|
+
updated_prev = json.dumps(seen)
|
|
642
|
+
else:
|
|
643
|
+
updated_prev = prev
|
|
644
|
+
|
|
607
645
|
await db.execute(
|
|
608
|
-
"INSERT INTO users (user_id, username, first_name, last_name) VALUES (?, ?, ?, ?) "
|
|
646
|
+
"INSERT INTO users (user_id, username, first_name, last_name, previous_names) VALUES (?, ?, ?, ?, ?) "
|
|
609
647
|
"ON CONFLICT(user_id) DO UPDATE SET "
|
|
610
648
|
"username = excluded.username, first_name = excluded.first_name, "
|
|
611
|
-
"last_name = excluded.last_name, "
|
|
649
|
+
"last_name = excluded.last_name, previous_names = excluded.previous_names, "
|
|
612
650
|
"updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
|
|
613
|
-
(user_id, username, first_name, last_name),
|
|
651
|
+
(user_id, username, first_name, last_name, updated_prev),
|
|
614
652
|
)
|
|
615
653
|
await db.commit()
|
|
616
654
|
|
|
@@ -624,7 +662,9 @@ async def upsert_chat(
|
|
|
624
662
|
Insert or update a chat record in the chats table.
|
|
625
663
|
|
|
626
664
|
Always reflects the latest Telegram chat metadata. Called on every interaction
|
|
627
|
-
so that chat type and title changes are picked up automatically.
|
|
665
|
+
so that chat type and title changes are picked up automatically. When the
|
|
666
|
+
chat_title changes, the old title is appended to the previous_titles column
|
|
667
|
+
(JSON array, deduplicated) to enable search by historical identities.
|
|
628
668
|
|
|
629
669
|
Args:
|
|
630
670
|
chat_id: Telegram chat ID.
|
|
@@ -632,12 +672,38 @@ async def upsert_chat(
|
|
|
632
672
|
chat_title: Display name of the group/channel, or None for private chats.
|
|
633
673
|
"""
|
|
634
674
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
675
|
+
async with db.execute(
|
|
676
|
+
"SELECT chat_title, previous_titles FROM chats WHERE chat_id = ?",
|
|
677
|
+
(chat_id,),
|
|
678
|
+
) as cur:
|
|
679
|
+
existing = await cur.fetchone()
|
|
680
|
+
|
|
681
|
+
updated_prev = None
|
|
682
|
+
if existing is not None:
|
|
683
|
+
old_title, prev = existing
|
|
684
|
+
if old_title and old_title != chat_title:
|
|
685
|
+
if prev:
|
|
686
|
+
try:
|
|
687
|
+
seen = json.loads(prev)
|
|
688
|
+
if not isinstance(seen, list):
|
|
689
|
+
seen = [seen] if seen else []
|
|
690
|
+
except json.JSONDecodeError:
|
|
691
|
+
seen = [t.strip() for t in prev.split(",") if t.strip()]
|
|
692
|
+
else:
|
|
693
|
+
seen = []
|
|
694
|
+
if old_title not in seen:
|
|
695
|
+
seen.append(old_title)
|
|
696
|
+
updated_prev = json.dumps(seen)
|
|
697
|
+
else:
|
|
698
|
+
updated_prev = prev
|
|
699
|
+
|
|
635
700
|
await db.execute(
|
|
636
|
-
"INSERT INTO chats (chat_id, chat_type, chat_title) VALUES (?, ?, ?) "
|
|
701
|
+
"INSERT INTO chats (chat_id, chat_type, chat_title, previous_titles) VALUES (?, ?, ?, ?) "
|
|
637
702
|
"ON CONFLICT(chat_id) DO UPDATE SET "
|
|
638
703
|
"chat_type = excluded.chat_type, chat_title = excluded.chat_title, "
|
|
704
|
+
"previous_titles = excluded.previous_titles, "
|
|
639
705
|
"updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
|
|
640
|
-
(chat_id, chat_type, chat_title),
|
|
706
|
+
(chat_id, chat_type, chat_title, updated_prev),
|
|
641
707
|
)
|
|
642
708
|
await db.commit()
|
|
643
709
|
|
|
@@ -679,9 +745,12 @@ async def search_messages(
|
|
|
679
745
|
|
|
680
746
|
Resolves speaker_query and chat_query using exact-match-first logic:
|
|
681
747
|
1. Case-insensitive exact match - if exactly one entity matches, proceed.
|
|
682
|
-
2. Partial LIKE match - if exactly one accessible
|
|
683
|
-
|
|
684
|
-
|
|
748
|
+
2. Partial LIKE match (including historical names/titles) - if exactly one accessible
|
|
749
|
+
entity matches, proceed. Searches current username/first_name/last_name for speakers
|
|
750
|
+
and current chat_title for chats; falls back to previous_names and previous_titles
|
|
751
|
+
columns to find entities by their past identities. Zero matches: colloquial fallback
|
|
752
|
+
(chat: all accessible; speaker: return empty). Two or more matches: return an
|
|
753
|
+
ambiguity sentinel dict instead of results.
|
|
685
754
|
|
|
686
755
|
Leading `@` is stripped from speaker_query automatically.
|
|
687
756
|
Access control: results are always scoped to accessible_chat_ids (the caller's
|
|
@@ -716,7 +785,7 @@ async def search_messages(
|
|
|
716
785
|
# Resolve speaker_query -> user_ids (exact match first, then LIKE)
|
|
717
786
|
# Scoped to users who have at least one message in accessible_chat_ids (s3kp).
|
|
718
787
|
# Batched over accessible_chat_ids to stay within SQLite's 999-parameter limit;
|
|
719
|
-
# each batch leaves
|
|
788
|
+
# each batch leaves 5 slots for name-matching params (exact: 3, partial: 5).
|
|
720
789
|
speaker_ids: Optional[list[int]] = None
|
|
721
790
|
speaker_amb: Optional[dict] = None
|
|
722
791
|
if speaker_query:
|
|
@@ -746,8 +815,8 @@ async def search_messages(
|
|
|
746
815
|
else:
|
|
747
816
|
like = f"%{q}%"
|
|
748
817
|
partial_seen: dict[int, tuple] = {}
|
|
749
|
-
for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS -
|
|
750
|
-
chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS -
|
|
818
|
+
for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS - 5):
|
|
819
|
+
chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS - 5]
|
|
751
820
|
chunk_ph = ','.join('?' * len(chunk))
|
|
752
821
|
async with db.execute(
|
|
753
822
|
f"SELECT DISTINCT u.user_id, "
|
|
@@ -756,8 +825,8 @@ async def search_messages(
|
|
|
756
825
|
f"FROM users u JOIN messages m ON m.user_id = u.user_id "
|
|
757
826
|
f"WHERE m.chat_id IN ({chunk_ph}) "
|
|
758
827
|
f"AND (u.username LIKE ? OR u.first_name LIKE ? OR u.last_name LIKE ? "
|
|
759
|
-
f"OR (u.first_name || ' ' || u.last_name) LIKE ?)",
|
|
760
|
-
(*chunk, like, like, like, like),
|
|
828
|
+
f"OR (u.first_name || ' ' || u.last_name) LIKE ? OR u.previous_names LIKE ?)",
|
|
829
|
+
(*chunk, like, like, like, like, like),
|
|
761
830
|
) as cur:
|
|
762
831
|
for row in await cur.fetchall():
|
|
763
832
|
partial_seen.setdefault(row[0], row)
|
|
@@ -792,23 +861,23 @@ async def search_messages(
|
|
|
792
861
|
# Check for any global match first to distinguish colloquial fallback
|
|
793
862
|
# (zero global matches) from access-control rejection (global match, not accessible).
|
|
794
863
|
async with db.execute(
|
|
795
|
-
"SELECT 1 FROM chats WHERE chat_title LIKE ? LIMIT 1",
|
|
796
|
-
(like,),
|
|
864
|
+
"SELECT 1 FROM chats WHERE chat_title LIKE ? OR previous_titles LIKE ? LIMIT 1",
|
|
865
|
+
(like, like),
|
|
797
866
|
) as cur:
|
|
798
867
|
has_global = await cur.fetchone() is not None
|
|
799
868
|
if not has_global:
|
|
800
869
|
pass # Zero global matches: colloquial fallback (resolved_chat_ids stays None)
|
|
801
870
|
else:
|
|
802
871
|
# Batch accessible_chat_ids to stay within SQLite's 999-parameter limit;
|
|
803
|
-
# reserve
|
|
872
|
+
# reserve 2 slots for the LIKE parameters (chat_title + previous_titles).
|
|
804
873
|
partial_accessible_seen: dict[int, tuple] = {}
|
|
805
|
-
for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS -
|
|
806
|
-
chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS -
|
|
874
|
+
for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS - 2):
|
|
875
|
+
chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS - 2]
|
|
807
876
|
chunk_ph = ','.join('?' * len(chunk))
|
|
808
877
|
async with db.execute(
|
|
809
878
|
f"SELECT chat_id, chat_title FROM chats "
|
|
810
|
-
f"WHERE chat_title LIKE ? AND chat_id IN ({chunk_ph})",
|
|
811
|
-
(like, *chunk),
|
|
879
|
+
f"WHERE (chat_title LIKE ? OR previous_titles LIKE ?) AND chat_id IN ({chunk_ph})",
|
|
880
|
+
(like, like, *chunk),
|
|
812
881
|
) as cur:
|
|
813
882
|
for row in await cur.fetchall():
|
|
814
883
|
partial_accessible_seen.setdefault(row[0], row)
|
|
@@ -122,6 +122,7 @@ _SYSTEM_APPENDIX = (
|
|
|
122
122
|
"Always invoke the search tool before concluding a message does not exist.\n"
|
|
123
123
|
"Messages marked private are never shared between chats.\n"
|
|
124
124
|
"Never reveal or repeat Telegram user IDs, chat IDs, or any other internal numeric identifiers in your responses.\n"
|
|
125
|
+
"When your context includes a '[Replying to Name, ...]' prefix indicating you were triggered via a reply, acknowledge the original author by name in your response.\n"
|
|
125
126
|
"Current date and time: (not yet known)\n"
|
|
126
127
|
)
|
|
127
128
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.10.
|
|
3
|
+
Version: 3.10.4
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -44,6 +44,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
44
44
|
* Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
|
|
45
45
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
|
|
46
46
|
* All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
|
|
47
|
+
* Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
|
|
47
48
|
* Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
|
|
48
49
|
* The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
|
|
49
50
|
* This eliminates amnesia when switching between private and group chats.
|
|
@@ -123,7 +124,7 @@ os.environ['TELLMGRAMBOT_VIRUSTOTAL_API_KEY'] = my_vault.get('virustotal_key')
|
|
|
123
124
|
- `/nick <name>` - Set your nickname (for bot use in group chats).
|
|
124
125
|
- `/forget` - Clear your conversation history. In private chats, clears everything and resets all active sessions. In group chats, removes only your messages.
|
|
125
126
|
- `/private` - Toggle private mode (private chats only). When ON, your messages are excluded from group context loading.
|
|
126
|
-
- `/help` - Display
|
|
127
|
+
- `/help` - Display available commands and usage information. In private chats, if you are a bot owner, also shows administrator-only commands (`/start`, `/stop`, `/wipe`).
|
|
127
128
|
|
|
128
129
|
### Group Chat Triggers
|
|
129
130
|
The bot responds in groups when you:
|
|
@@ -143,7 +144,7 @@ When the bot is triggered in a group and about to respond (not deferring to anot
|
|
|
143
144
|
1. Ensure API keys are set up and your Telegram bot is created via BotFather.
|
|
144
145
|
2. Install TeLLMgramBot: `pip install TeLLMgramBot`
|
|
145
146
|
3. Configure the bot via `config.yaml` (created on first run):
|
|
146
|
-
- `bot_owner`:
|
|
147
|
+
- `bot_owner`: Telegram username(s) with admin access (required, no `@`). Accepts a single string or a YAML list of usernames.
|
|
147
148
|
- `chat_model`: LLM model for conversation (e.g. `gpt-4o-mini` or `claude-sonnet-4-6`)
|
|
148
149
|
- `url_model`: LLM model for URL analysis (e.g. `gpt-4o` or `claude-haiku-4-5`)
|
|
149
150
|
- `bot_nickname` / `bot_initials`: Names the bot responds to in groups
|
|
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
|
|
File without changes
|
|
File without changes
|