TeLLMgramBot 3.0.4__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/PKG-INFO +5 -5
  2. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/README.md +4 -4
  3. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/TeLLMgramBot.py +65 -19
  4. tellmgrambot-3.2.0/TeLLMgramBot/conversation.py +325 -0
  5. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/database.py +167 -8
  6. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot.egg-info/PKG-INFO +5 -5
  7. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/setup.py +1 -1
  8. tellmgrambot-3.0.4/TeLLMgramBot/conversation.py +0 -196
  9. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/LICENSE +0 -0
  10. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/__init__.py +0 -0
  11. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/initialize.py +0 -0
  12. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/message_handlers.py +0 -0
  13. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/models.py +0 -0
  14. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/providers/__init__.py +0 -0
  15. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
  16. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/providers/base.py +0 -0
  17. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/providers/factory.py +0 -0
  18. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/providers/openai_provider.py +0 -0
  19. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/utils.py +0 -0
  20. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot/web_utils.py +0 -0
  21. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
  22. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
  23. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot.egg-info/requires.txt +0 -0
  24. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
  25. {tellmgrambot-3.0.4 → tellmgrambot-3.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.0.4
3
+ Version: 3.2.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,10 +43,10 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
43
43
  * This uses a separate model (configurable via `url_model`) to support more URL content with its higher token limit.
44
44
  * Tokens are used to measure the length of all conversation messages between the Telegram bot assistant and the user. This is useful to:
45
45
  * Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
46
- * Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. This eliminates amnesia when users switch between contexts.
46
+ * Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. In private chats, shared group context (messages from groups where both user and bot are active) fills the remaining budget, enabling the bot to reference group conversations from a private context. This eliminates amnesia when users switch between contexts.
47
47
  * Users can manage privacy via two commands:
48
- * `/forget` — In private chats, clears the full conversation (including bot replies). In group chats, removes only your messages; other participants' messages remain.
49
- * `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
48
+ * `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types; other participants' messages and sessions remain.
49
+ * `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups. If you have private mode ON and message a group, the bot will remind you once per session.
50
50
 
51
51
  ## Why Telegram?
52
52
  Using Telegram as the interface not only solves "exposing" the interface, but gives you boatloads of interactivity over a standard Command Line interface, or trying to create a website with input boxes and submit buttons to try to handle everything:
@@ -87,7 +87,7 @@ When initializing TeLLMgramBot, the following directories get created:
87
87
  * User will also get notified to contact the owner.
88
88
  * `data`
89
89
  * Contains `conversations.db` — a SQLite database storing all conversations between the bot and users across all chats.
90
- * When a user messages in any chat, their full history is available for context: private messages appear in group contexts, group messages appear in private contexts. This creates seamless cross-context awareness.
90
+ * When a user messages in any chat, their full history is available for context: private messages appear in group contexts, group messages appear in private contexts. This creates seamless cross-context awareness. The bot dynamically refreshes each user's context during a session to pick up messages sent between chats. In private chats, the bot also loads shared group context (messages from groups where both user and bot are active) to provide group awareness.
91
91
  * Users can manage their context via `/forget` (private chat: clears full conversation; group chat: removes only your messages) or `/private` (toggles per-user privacy for group contexts).
92
92
 
93
93
  ### Environment Variables
@@ -12,10 +12,10 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
12
12
  * This uses a separate model (configurable via `url_model`) to support more URL content with its higher token limit.
13
13
  * Tokens are used to measure the length of all conversation messages between the Telegram bot assistant and the user. This is useful to:
14
14
  * Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
15
- * Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. This eliminates amnesia when users switch between contexts.
15
+ * Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. In private chats, shared group context (messages from groups where both user and bot are active) fills the remaining budget, enabling the bot to reference group conversations from a private context. This eliminates amnesia when users switch between contexts.
16
16
  * Users can manage privacy via two commands:
17
- * `/forget` — In private chats, clears the full conversation (including bot replies). In group chats, removes only your messages; other participants' messages remain.
18
- * `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
17
+ * `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types; other participants' messages and sessions remain.
18
+ * `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups. If you have private mode ON and message a group, the bot will remind you once per session.
19
19
 
20
20
  ## Why Telegram?
21
21
  Using Telegram as the interface not only solves "exposing" the interface, but gives you boatloads of interactivity over a standard Command Line interface, or trying to create a website with input boxes and submit buttons to try to handle everything:
@@ -56,7 +56,7 @@ When initializing TeLLMgramBot, the following directories get created:
56
56
  * User will also get notified to contact the owner.
57
57
  * `data`
58
58
  * Contains `conversations.db` — a SQLite database storing all conversations between the bot and users across all chats.
59
- * When a user messages in any chat, their full history is available for context: private messages appear in group contexts, group messages appear in private contexts. This creates seamless cross-context awareness.
59
+ * When a user messages in any chat, their full history is available for context: private messages appear in group contexts, group messages appear in private contexts. This creates seamless cross-context awareness. The bot dynamically refreshes each user's context during a session to pick up messages sent between chats. In private chats, the bot also loads shared group context (messages from groups where both user and bot are active) to provide group awareness.
60
60
  * Users can manage their context via `/forget` (private chat: clears full conversation; group chat: removes only your messages) or `/private` (toggles per-user privacy for group contexts).
61
61
 
62
62
  ### Environment Variables
@@ -92,21 +92,38 @@ class TelegramBot:
92
92
  """
93
93
  Remove conversation history from the database (behavior depends on chat type).
94
94
 
95
- In private chats: deletes all messages in the chat (including bot replies), fully clearing the conversation.
96
- In group chats: deletes only this user's rows; other participants' messages remain.
97
- Also removes the chat's Conversation object from this bot session's memory if it exists.
95
+ In private chats: deletes all messages in the private chat (including bot replies).
96
+ In group chats: deletes the user's rows across all chats (private included).
97
+
98
+ In both cases, evicts only the in-memory Conversation objects that contain this
99
+ user's data — scoped to the current chat, the user's private chat, and any other
100
+ chats where their context was previously merged. Other users' active sessions are
101
+ not affected. All private-mode notices for this user are also cleared.
98
102
  """
99
103
  user_id = update.message.from_user.id
100
104
  chat_id = update.message.chat.id
101
105
  chat_type = update.message.chat.type
102
- # Private chats: delete all messages in the chat (including bot replies)
103
- # Group chats: delete only the user's rows (other participants' messages remain)
106
+
107
+ # Find all chat_ids where this user's context has been merged in memory,
108
+ # identified by user_id being present in the Conversation's _context_cursor.
109
+ user_chat_ids = {
110
+ gid for gid, conv in self.conversations.items() if user_id in conv._context_cursor
111
+ }
112
+ # Always include the private chat (chat_id == user_id in Telegram) and current chat.
113
+ user_chat_ids.update({chat_id, user_id})
114
+
104
115
  if chat_type == 'private':
105
116
  await delete_messages_for_chat(chat_id)
106
117
  else:
107
118
  await delete_messages_for_user(user_id)
108
- if chat_id in self.conversations:
109
- del self.conversations[chat_id]
119
+
120
+ # Evict only the Conversations that contain this user's data, not all sessions.
121
+ for evict_id in user_chat_ids:
122
+ self.conversations.pop(evict_id, None)
123
+ self.token_warning.pop(evict_id, None)
124
+ # Clear all private-mode notices for this user across all chats.
125
+ self._private_mode_warned = {k for k in self._private_mode_warned if k[1] != user_id}
126
+
110
127
  await update.message.reply_text("My memories of our conversations are wiped!")
111
128
 
112
129
  async def tele_private_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -193,7 +210,7 @@ class TelegramBot:
193
210
  chat_type = msg.chat.type
194
211
  print(f"User {user_id} ({username}) in {chat_type} Chat {chat_id}")
195
212
 
196
- # For a new session, create a Conversation keyed by chat_id and load past history
213
+ # For a new session, create a Conversation keyed by chat_id
197
214
  if chat_id not in self.conversations:
198
215
  self.conversations[chat_id] = Conversation(
199
216
  chat_id,
@@ -201,14 +218,37 @@ class TelegramBot:
201
218
  self.chatgpt['prompt'],
202
219
  self.chatgpt['chat_model']
203
220
  )
204
- await self.conversations[chat_id].get_past_interaction(
205
- floor(self.chatgpt['prune_threshold'] / 2), user_id
206
- )
221
+
222
+ conv = self.conversations[chat_id]
223
+ token_budget = floor(self.chatgpt['prune_threshold'] / 2)
224
+
225
+ if user_id not in conv._context_cursor:
226
+ # First appearance of this user in this session — load their cross-chat history.
227
+ # Pass bot_id so private chats can also load shared group context.
228
+ # If no history exists yet, the cursor stays unset so the next message retries.
229
+ await conv.get_past_interaction(token_budget, user_id, self.telegram['bot_id'])
230
+ else:
231
+ # Already loaded — check for new cross-chat messages since last load.
232
+ await conv.refresh_user_context(user_id, token_budget)
207
233
 
208
234
  # Add the user's message to our conversation, respecting private mode
209
235
  # Private mode only applies in private chats — group messages are never flagged private
210
- is_private = (chat_type == 'private') and await get_private_mode(user_id)
211
- await self.conversations[chat_id].add_user_message(text, user_id, username, is_private)
236
+ user_private_mode = await get_private_mode(user_id)
237
+ is_private = (chat_type == 'private') and user_private_mode
238
+ await conv.add_user_message(text, user_id, username, is_private)
239
+
240
+ # Warn once per group session if the user has private mode ON.
241
+ # Their private-chat messages are excluded from this group's context, which can be
242
+ # surprising. The notice is appended to the first reply in the session for this user.
243
+ private_mode_notice = ""
244
+ if chat_type in ('group', 'supergroup') and user_private_mode:
245
+ warn_key = (chat_id, user_id)
246
+ if warn_key not in self._private_mode_warned:
247
+ self._private_mode_warned.add(warn_key)
248
+ private_mode_notice = (
249
+ "\n\n(Note: your private mode is ON — messages from our private chat "
250
+ "are not included in this group's context. To disable, send /private off in our DMs.)"
251
+ )
212
252
 
213
253
  # Check if the user is asking about a [URL]
214
254
  url_match = re.search(r'\[http(s)?://\S+]', text)
@@ -226,8 +266,11 @@ class TelegramBot:
226
266
  # This is the transition point between quick Telegram replies and the LLM
227
267
  reply = await self.llm_completion(chat_id)
228
268
 
269
+ if private_mode_notice:
270
+ reply += private_mode_notice
271
+
229
272
  # Check token count before storing to determine if warning should be appended
230
- token_count = await self.conversations[chat_id].get_message_token_count()
273
+ token_count = await conv.get_message_token_count()
231
274
  if token_count > self.chatgpt['prune_back_to'] and chat_id not in self.token_warning:
232
275
  reply += ("\n\n"
233
276
  "By the way, our conversation will soon reach my token limit, so I may"
@@ -236,15 +279,17 @@ class TelegramBot:
236
279
  )
237
280
  self.token_warning[chat_id] = True
238
281
 
239
- # Add the full reply (including any warning) to the conversation
240
- await self.conversations[chat_id].add_assistant_message(
241
- reply, self.telegram['bot_id'], self.telegram['username']
282
+ # Add the full reply (including any warning) to the conversation.
283
+ # Propagate is_private so bot replies in private-mode exchanges are also excluded
284
+ # from group context loading — otherwise the assistant's half of the conversation leaks.
285
+ await conv.add_assistant_message(
286
+ reply, self.telegram['bot_id'], self.telegram['username'], is_private
242
287
  )
243
288
 
244
289
  # Recompute token count after full exchange is stored, then prune if needed
245
- token_count = await self.conversations[chat_id].get_message_token_count()
290
+ token_count = await conv.get_message_token_count()
246
291
  if token_count > self.chatgpt['prune_threshold']:
247
- await self.conversations[chat_id].prune_conversation(self.chatgpt['prune_back_to'])
292
+ await conv.prune_conversation(self.chatgpt['prune_back_to'])
248
293
 
249
294
  return reply
250
295
 
@@ -396,6 +441,7 @@ class TelegramBot:
396
441
  self.error_log = generate_error_path()
397
442
  self.token_warning = {} # Determines whether user has reached token limit by AI model
398
443
  self.conversations = {} # Provides Conversation class per user based on bot response
444
+ self._private_mode_warned = set() # (chat_id, user_id) pairs that received the private mode notice
399
445
  self.telegram = {
400
446
  'bot_id' : 0, # overwritten by _tele_info(); 0 is a safe sentinel
401
447
  'owner' : bot_owner,
@@ -0,0 +1,325 @@
1
+ """
2
+ Manages conversation history, token counting, and SQLite-backed persistence for any LLM provider.
3
+
4
+ The Conversation class is keyed by chat_id (Telegram chat ID) and maintains an in-memory message
5
+ history, delegating persistence to the database module. Supports async token counting via provider-
6
+ specific APIs and automatic pruning when token limits are approached.
7
+ """
8
+ from .database import (
9
+ insert_message,
10
+ load_full_user_context,
11
+ get_max_cross_chat_message_id,
12
+ load_new_cross_chat_context,
13
+ get_shared_group_chat_ids,
14
+ load_shared_group_context,
15
+ delete_messages_for_user,
16
+ delete_messages_for_chat,
17
+ )
18
+ from .models import TokenLimits
19
+ from .utils import generate_error_path, log_error
20
+
21
+ # Minimum remaining token headroom required before loading shared group context
22
+ # in a private chat. Prevents loading group history when the private context
23
+ # already fills nearly the entire budget.
24
+ MIN_GROUP_CONTEXT_TOKENS = 500
25
+
26
+ class Conversation:
27
+ """
28
+ Manages a chat conversation including message history, token counting, and SQLite persistence.
29
+
30
+ Keyed by chat_id (Telegram chat ID): negative for groups, positive (== user_id) for private chats.
31
+ Works with any LLM provider (OpenAI, Anthropic). When the token limit is approached, oldest
32
+ messages are pruned from memory; the database retains the full history.
33
+ """
34
+
35
+ def __init__(self, chat_id: int, chat_type: str, system_content: str, system_model: str = "gpt-4o-mini"):
36
+ """
37
+ Initialize a conversation keyed by chat_id.
38
+
39
+ Args:
40
+ chat_id: Telegram chat ID (negative for groups, positive/==user_id for private chats).
41
+ chat_type: Chat type ('private', 'group', 'supergroup', 'channel').
42
+ system_content: System prompt (bot persona).
43
+ system_model: LLM model to use for token counting (default: "gpt-4o-mini").
44
+ """
45
+ self.error_log = generate_error_path()
46
+
47
+ self.chat_id = chat_id
48
+ self.chat_type = chat_type
49
+ self.chat_print = f"{self.chat_type} Chat {self.chat_id}"
50
+
51
+ self.system_content = system_content
52
+ self.system_model = TokenLimits(system_model)
53
+ self.messages = [{"role": "system", "content": system_content}]
54
+
55
+ # Maps user_id → max message id at the time their cross-chat context was loaded.
56
+ # Used for both per-user idempotency (is the user loaded?) and mid-session refresh
57
+ # detection (have new cross-chat messages appeared since the last load?).
58
+ self._context_cursor: dict[int, int] = {}
59
+
60
+ # Maps user_id → len(self.messages) after their history was loaded.
61
+ # Marks the boundary between historical context and live session messages so that
62
+ # refresh_user_context() inserts delta rows at the right position (after history,
63
+ # before live messages) rather than at index 1 (before all existing history).
64
+ self._history_end: dict[int, int] = {}
65
+
66
+ def set_system_content(self, new_content: str):
67
+ """
68
+ Replace the system content (bot's personality prompt) in the conversation.
69
+
70
+ Updates both self.system_content and the first message in self.messages.
71
+
72
+ Args:
73
+ new_content: The new system prompt text.
74
+ """
75
+ self.system_content = new_content
76
+ self.messages[0] = {"role": "system", "content": new_content}
77
+
78
+ async def add_user_message(self, content: str, user_id: int, username: str | None, is_private: bool = False):
79
+ """
80
+ Add a user message to conversation memory and persist it to the database.
81
+
82
+ Args:
83
+ content: The message text.
84
+ user_id: Telegram user ID of the sender.
85
+ username: Telegram username (display only, may be None).
86
+ is_private: If True, excludes this message from group context loading (used for /forget or private commands).
87
+ """
88
+ self.messages.append({"role": "user", "content": content})
89
+ await insert_message(self.chat_id, user_id, username, "user", content, is_private)
90
+
91
+ async def add_assistant_message(self, content: str, bot_user_id: int, bot_username: str | None, is_private: bool = False):
92
+ """
93
+ Add an assistant message to conversation memory and persist it to the database.
94
+
95
+ Args:
96
+ content: The message text (LLM response).
97
+ bot_user_id: Telegram user ID of the bot account.
98
+ bot_username: Telegram username of the bot (display only, may be None).
99
+ is_private: If True, excludes this reply from group context loading. Should match
100
+ the is_private flag of the user message it responds to, so that both
101
+ sides of a private-mode exchange are excluded from shared contexts.
102
+ """
103
+ self.messages.append({"role": "assistant", "content": content})
104
+ await insert_message(self.chat_id, bot_user_id, bot_username, "assistant", content, is_private)
105
+
106
+ async def get_message_token_count(self) -> int:
107
+ """
108
+ Get the current token count of all messages in this conversation.
109
+
110
+ Delegates to the configured model's provider to count tokens accurately.
111
+ This is an async operation because it may contact the provider API (e.g., for Anthropic).
112
+
113
+ Returns:
114
+ The total token count for all messages (system, user, assistant).
115
+
116
+ Raises:
117
+ ProviderAuthError: If API credentials are missing or invalid.
118
+ ProviderConnectionError: If the provider API cannot be reached.
119
+ """
120
+ return await self.system_model.num_tokens_from_messages(self.messages)
121
+
122
+ async def prune_conversation(self, token_limit: int):
123
+ """
124
+ Remove oldest user-assistant messages until token count is below threshold.
125
+
126
+ Recursively removes messages starting from index 1 (preserving the system message at index 0)
127
+ until the conversation fits within the token limit. The database is not modified — pruning
128
+ only affects the in-memory context window for the current session.
129
+
130
+ Args:
131
+ token_limit: The maximum token count threshold. Messages are removed until
132
+ the conversation is strictly below this limit.
133
+ """
134
+ try:
135
+ self.messages.pop(1)
136
+ if await self.get_message_token_count() > token_limit:
137
+ await self.prune_conversation(token_limit)
138
+ except Exception as e:
139
+ log_error(e, f"{type(e).__name__} pruning under {token_limit} tokens for {self.chat_print}", self.error_log)
140
+
141
+ async def get_past_interaction(self, token_limit: int, user_id: int, bot_id: int | None = None) -> bool:
142
+ """
143
+ Load past conversation messages from the database up to a token limit.
144
+
145
+ Implements bidirectional cross-pollination: loads all messages from the current chat
146
+ (all participants) plus all messages from the user's private chat (both user and
147
+ assistant turns), merged chronologically. This eliminates amnesia when a user switches
148
+ between private and group contexts.
149
+
150
+ For group and supergroup chats, messages marked as private (is_private=1) are excluded
151
+ so private-mode messages don't surface in shared contexts.
152
+
153
+ For private chats, if bot_id is provided and remaining token budget allows, also loads
154
+ full message history from any groups shared between the user and the bot. This lets the
155
+ bot answer questions about group conversations from a private chat context.
156
+
157
+ Per-user idempotent: once a user's cursor is set, subsequent calls return True immediately.
158
+ Calling for a new user_id merges that user's cross-chat context into the existing
159
+ conversation. Context is considered "found" only if messages were actually inserted
160
+ (rows_inserted > 0 or group_rows_inserted > 0).
161
+
162
+ Args:
163
+ token_limit: The maximum token count to load into the in-memory context.
164
+ user_id: Telegram user ID of the requesting user; used to fetch bidirectional context.
165
+ bot_id: Telegram user ID of the bot account. When provided in a private chat,
166
+ enables shared group context loading into any remaining token budget.
167
+
168
+ Returns:
169
+ True if past interactions were found (either on this call or previously),
170
+ False if no past interactions exist for this user.
171
+
172
+ Side Effects:
173
+ - Modifies self.messages in-place by inserting historical messages after the system message
174
+ (attempting to fit messages starting from most recent, stopping when limit is reached).
175
+ - Sets self._context_cursor[user_id] to the max message id loaded once context is found.
176
+ - Sets self._history_end[user_id] to len(self.messages) after loading, marking the
177
+ boundary between historical context and live session messages for future refreshes.
178
+ - Prints to stdout showing token count and whether the token limit was reached.
179
+ """
180
+ if user_id in self._context_cursor:
181
+ print(f"Context already loaded for User {user_id} in {self.chat_print}")
182
+ return True
183
+
184
+ exclude_private = self.chat_type in ('group', 'supergroup')
185
+ rows = await load_full_user_context(user_id, self.chat_id, exclude_private=exclude_private)
186
+
187
+ token_limit_reached = False
188
+ token_count = 0
189
+ rows_inserted = 0
190
+
191
+ for row in reversed(rows):
192
+ self.messages.insert(1, row)
193
+ token_count = await self.get_message_token_count()
194
+ if token_count > token_limit:
195
+ self.messages.pop(1)
196
+ token_count = await self.get_message_token_count()
197
+ token_limit_reached = True
198
+ break
199
+ rows_inserted += 1
200
+
201
+ # Load shared group context for private chats when token budget allows.
202
+ # Private context fills the primary budget; group context fills the remainder.
203
+ group_rows_inserted = 0
204
+ group_limit_reached = False
205
+ if self.chat_type == 'private' and bot_id is not None:
206
+ if token_count == 0:
207
+ token_count = await self.get_message_token_count()
208
+ remaining = token_limit - token_count
209
+ if remaining > MIN_GROUP_CONTEXT_TOKENS:
210
+ shared_groups = await get_shared_group_chat_ids(user_id, bot_id)
211
+ if shared_groups:
212
+ group_rows = await load_shared_group_context(shared_groups, exclude_private=True)
213
+ insert_at = 1 + rows_inserted
214
+ for row in reversed(group_rows):
215
+ self.messages.insert(insert_at, row)
216
+ token_count = await self.get_message_token_count()
217
+ if token_count > token_limit:
218
+ self.messages.pop(insert_at)
219
+ token_count = await self.get_message_token_count()
220
+ group_limit_reached = True
221
+ break
222
+ group_rows_inserted += 1
223
+
224
+ has_context = rows_inserted > 0 or group_rows_inserted > 0
225
+
226
+ if has_context:
227
+ self._context_cursor[user_id] = await get_max_cross_chat_message_id(
228
+ user_id, self.chat_id, exclude_private=exclude_private
229
+ )
230
+ self._history_end[user_id] = len(self.messages)
231
+ label = "Full" if not token_limit_reached and not group_limit_reached else "Partial"
232
+ group_note = f", plus {group_rows_inserted} group message(s)" if group_rows_inserted > 0 else ""
233
+ print(
234
+ f"{label} context loaded for User {user_id} in {self.chat_print} — "
235
+ f"storing {token_count} token(s){group_note}"
236
+ )
237
+ else:
238
+ print(f"No prior context found for User {user_id} in {self.chat_print}")
239
+
240
+ return has_context
241
+
242
+ async def refresh_user_context(self, user_id: int, token_limit: int) -> bool:
243
+ """
244
+ Merge any new cross-chat messages that arrived since this user's context was last loaded.
245
+
246
+ Compares the current max message id in the cross-chat context against the stored
247
+ cursor for this user. If new rows exist, loads only the delta and merges it into
248
+ the in-memory conversation, respecting the token budget.
249
+
250
+ This handles the mid-session staleness case: a user whose context was loaded
251
+ at session start (with no private history) will pick up new private messages they
252
+ send before returning to the group, without requiring a full reload.
253
+
254
+ Args:
255
+ user_id: Telegram user ID of the user to refresh.
256
+ token_limit: Maximum token budget; new messages are discarded if they would
257
+ push the conversation over this limit.
258
+
259
+ Returns:
260
+ True if at least one new message was merged, False otherwise.
261
+
262
+ Side Effects:
263
+ - Modifies self.messages in-place, inserting new rows at self._history_end[user_id]
264
+ (after historical context, before any live session messages).
265
+ - Advances self._context_cursor[user_id] and self._history_end[user_id] on success.
266
+ """
267
+ if user_id not in self._context_cursor:
268
+ return False
269
+
270
+ exclude_private = self.chat_type in ('group', 'supergroup')
271
+ current_max = await get_max_cross_chat_message_id(user_id, self.chat_id, exclude_private)
272
+
273
+ if current_max <= self._context_cursor[user_id]:
274
+ return False
275
+
276
+ new_rows = await load_new_cross_chat_context(
277
+ user_id, self.chat_id, self._context_cursor[user_id], exclude_private
278
+ )
279
+
280
+ if not new_rows:
281
+ return False
282
+
283
+ # Insert at the tracked history boundary so delta rows land after existing historical
284
+ # context and before any live session messages, preserving chronological ordering.
285
+ insert_pos = self._history_end.get(user_id, 1)
286
+ rows_inserted = 0
287
+ for row in reversed(new_rows):
288
+ self.messages.insert(insert_pos, row)
289
+ token_count = await self.get_message_token_count()
290
+ if token_count > token_limit:
291
+ self.messages.pop(insert_pos)
292
+ break
293
+ rows_inserted += 1
294
+
295
+ if rows_inserted > 0:
296
+ self._context_cursor[user_id] = current_max
297
+ self._history_end[user_id] = insert_pos + rows_inserted
298
+ print(f"Refreshed {rows_inserted} new message(s) for User {user_id} in {self.chat_print}")
299
+
300
+ return rows_inserted > 0
301
+
302
+ async def clear_interaction(self, user_id: int):
303
+ """
304
+ Clear this user's messages from memory and from the database.
305
+
306
+ In private chats, all messages in the chat are deleted (including bot replies),
307
+ fully clearing the conversation. In group chats, only this user's rows are removed;
308
+ other members' messages remain. Resets both _context_cursor and _history_end.
309
+
310
+ Args:
311
+ user_id: Telegram user ID of the user requesting the forget.
312
+
313
+ Side Effects:
314
+ - Clears self.messages to contain only the system message (index 0).
315
+ - Resets self._context_cursor to empty dict.
316
+ - Resets self._history_end to empty dict.
317
+ - Deletes all messages for this chat (private) or this user (group) from database.
318
+ """
319
+ self.messages = [self.messages[0]]
320
+ self._context_cursor = {}
321
+ self._history_end = {}
322
+ if self.chat_type == 'private':
323
+ await delete_messages_for_chat(self.chat_id)
324
+ else:
325
+ await delete_messages_for_user(user_id)
@@ -14,6 +14,10 @@ from .utils import execution_dir
14
14
  # Allows tests to override the DB path without touching env vars
15
15
  _DB_PATH: Optional[str] = None
16
16
 
17
+ # Maximum bound parameters per SQLite statement (SQLite default limit is 999).
18
+ # Defined at module level so tests can patch it to force batching with small inputs.
19
+ _SQLITE_MAX_PARAMS = 999
20
+
17
21
  _DDL = """
18
22
  PRAGMA journal_mode=WAL;
19
23
 
@@ -149,13 +153,27 @@ async def load_full_user_context(
149
153
  Load bidirectional conversation context for a user across all chats.
150
154
 
151
155
  Implements bidirectional cross-pollination: returns all messages from the current
152
- chat (all participants) plus only the requesting user's own messages from every
153
- other chat, merged in chronological order. This eliminates amnesia when a user
154
- switches between private and group contexts.
156
+ chat (all participants) plus all messages from the user's private chat (both user
157
+ and assistant turns), merged in chronological order. This eliminates amnesia when
158
+ a user switches between private and group contexts.
159
+
160
+ In Telegram, a user's private chat_id always equals their user_id. Three arms cover
161
+ all cross-context cases:
162
+
163
+ Arm 1 — chat_id = current_chat_id:
164
+ All messages (user and bot) in the current chat. Always included.
165
+
166
+ Arm 2 — chat_id = user_id AND chat_id != current_chat_id:
167
+ All messages (both sides) from the user's private chat, included when the
168
+ current chat is a group. Pulling both sides restores proper role alternation
169
+ that was broken when only the user's own rows were fetched.
170
+
171
+ Arm 3 — user_id = user_id AND chat_id != current_chat_id AND chat_id != user_id:
172
+ The requesting user's own messages from any other group chats (not the private
173
+ chat, not the current chat). Covers group-to-private and group-to-group context.
174
+ Only the user's own rows are fetched from other groups (not bot replies).
155
175
 
156
- Query logic: loads ALL rows from current_chat_id (all participants), plus only the requesting
157
- user's own rows from every other chat, merged chronologically. The exclude_private filter
158
- is applied globally when the current chat is a group.
176
+ The exclude_private filter is applied globally when the current chat is a group.
159
177
 
160
178
  Args:
161
179
  user_id: Telegram user ID of the requesting user.
@@ -169,9 +187,76 @@ async def load_full_user_context(
169
187
  """
170
188
  query = (
171
189
  "SELECT role, content FROM messages "
172
- "WHERE (chat_id = ? OR (user_id = ? AND chat_id != ?))"
190
+ "WHERE (chat_id = ? "
191
+ "OR (chat_id = ? AND chat_id != ?) "
192
+ "OR (user_id = ? AND chat_id != ? AND chat_id != ?))"
193
+ )
194
+ params: tuple = (current_chat_id, user_id, current_chat_id, user_id, current_chat_id, user_id)
195
+ if exclude_private:
196
+ query += " AND is_private = 0"
197
+ query += " ORDER BY created_at ASC, id ASC"
198
+ async with aiosqlite.connect(get_db_path()) as db:
199
+ async with db.execute(query, params) as cursor:
200
+ rows = await cursor.fetchall()
201
+ return [{"role": row[0], "content": row[1]} for row in rows]
202
+
203
+ async def get_max_cross_chat_message_id(
204
+ user_id: int,
205
+ current_chat_id: int,
206
+ exclude_private: bool = False,
207
+ ) -> int:
208
+ """
209
+ Return the maximum message id from the cross-chat context for this user.
210
+
211
+ Uses the same three-arm query as load_full_user_context, returning only the
212
+ MAX(id) scalar. Used to establish a cursor for mid-session refresh detection.
213
+
214
+ Returns:
215
+ Maximum message id as an integer, or 0 if no rows exist.
216
+ """
217
+ query = (
218
+ "SELECT MAX(id) FROM messages "
219
+ "WHERE (chat_id = ? "
220
+ "OR (chat_id = ? AND chat_id != ?) "
221
+ "OR (user_id = ? AND chat_id != ? AND chat_id != ?))"
222
+ )
223
+ params: tuple = (current_chat_id, user_id, current_chat_id, user_id, current_chat_id, user_id)
224
+ if exclude_private:
225
+ query += " AND is_private = 0"
226
+ async with aiosqlite.connect(get_db_path()) as db:
227
+ async with db.execute(query, params) as cursor:
228
+ row = await cursor.fetchone()
229
+ return row[0] if row and row[0] is not None else 0
230
+
231
+ async def load_new_cross_chat_context(
232
+ user_id: int,
233
+ current_chat_id: int,
234
+ since_id: int,
235
+ exclude_private: bool = False,
236
+ ) -> list[dict]:
237
+ """
238
+ Load cross-chat context rows with id strictly greater than since_id.
239
+
240
+ Uses the same three-arm query as load_full_user_context but restricted to rows
241
+ newer than the given cursor. Returns only the delta since the last context load.
242
+
243
+ Args:
244
+ user_id: Telegram user ID of the requesting user.
245
+ current_chat_id: The chat_id of the current conversation.
246
+ since_id: Exclusive lower bound — only rows with id > since_id are returned.
247
+ exclude_private: If True, rows with is_private=1 are excluded.
248
+
249
+ Returns:
250
+ List of {"role": ..., "content": ...} dicts in ascending chronological order.
251
+ """
252
+ query = (
253
+ "SELECT role, content FROM messages "
254
+ "WHERE (chat_id = ? "
255
+ "OR (chat_id = ? AND chat_id != ?) "
256
+ "OR (user_id = ? AND chat_id != ? AND chat_id != ?)) "
257
+ "AND id > ?"
173
258
  )
174
- params: tuple = (current_chat_id, user_id, current_chat_id)
259
+ params: tuple = (current_chat_id, user_id, current_chat_id, user_id, current_chat_id, user_id, since_id)
175
260
  if exclude_private:
176
261
  query += " AND is_private = 0"
177
262
  query += " ORDER BY created_at ASC, id ASC"
@@ -180,6 +265,80 @@ async def load_full_user_context(
180
265
  rows = await cursor.fetchall()
181
266
  return [{"role": row[0], "content": row[1]} for row in rows]
182
267
 
268
+ async def get_shared_group_chat_ids(user_id: int, bot_id: int) -> list[int]:
269
+ """
270
+ Return chat_ids of groups where both the user and the bot have sent messages.
271
+
272
+ Eligibility conditions:
273
+ 1. The requesting user has sent at least one message in the group.
274
+ 2. The bot has also sent at least one message in the group (bot is active there).
275
+
276
+ Condition 2 prevents surfacing history from groups the bot was removed from
277
+ or never properly joined.
278
+
279
+ Args:
280
+ user_id: Telegram user ID of the requesting user.
281
+ bot_id: Telegram user ID of the bot account.
282
+
283
+ Returns:
284
+ List of negative chat_ids (Telegram group IDs).
285
+ """
286
+ query = """
287
+ SELECT DISTINCT chat_id
288
+ FROM messages
289
+ WHERE user_id = ? AND chat_id < 0
290
+ AND EXISTS (
291
+ SELECT 1 FROM messages m2
292
+ WHERE m2.chat_id = messages.chat_id AND m2.user_id = ?
293
+ )
294
+ """
295
+ async with aiosqlite.connect(get_db_path()) as db:
296
+ async with db.execute(query, (user_id, bot_id)) as cursor:
297
+ rows = await cursor.fetchall()
298
+ return [row[0] for row in rows]
299
+
300
+ async def load_shared_group_context(
301
+ group_chat_ids: list[int],
302
+ exclude_private: bool = True,
303
+ ) -> list[dict]:
304
+ """
305
+ Load all messages from multiple group chats, merged in chronological order.
306
+
307
+ Used to fill remaining token budget in a private chat with context from
308
+ shared groups (full group context in private chat).
309
+
310
+ Automatically batches the query if group_chat_ids exceeds _SQLITE_MAX_PARAMS
311
+ (999, SQLite's bound parameter limit) to handle users who share many groups
312
+ with the bot. Results are merged and sorted chronologically across batches.
313
+
314
+ Args:
315
+ group_chat_ids: List of group chat_ids (negative integers) to include.
316
+ exclude_private: If True, rows with is_private=1 are excluded (default True).
317
+
318
+ Returns:
319
+ List of {"role": ..., "content": ...} dicts in ascending chronological order.
320
+ Empty list if group_chat_ids is empty.
321
+ """
322
+ if not group_chat_ids:
323
+ return []
324
+ # SQLite has a max of 999 bound parameters per statement. Batch group_chat_ids to
325
+ # avoid exceeding this limit for users who share many groups with the bot.
326
+ all_rows: list[tuple] = []
327
+ async with aiosqlite.connect(get_db_path()) as db:
328
+ for i in range(0, len(group_chat_ids), _SQLITE_MAX_PARAMS):
329
+ chunk = group_chat_ids[i:i + _SQLITE_MAX_PARAMS]
330
+ placeholders = ','.join('?' * len(chunk))
331
+ query = (
332
+ f"SELECT role, content, created_at, id FROM messages "
333
+ f"WHERE chat_id IN ({placeholders})"
334
+ )
335
+ if exclude_private:
336
+ query += " AND is_private = 0"
337
+ async with db.execute(query, tuple(chunk)) as cursor:
338
+ all_rows.extend(await cursor.fetchall())
339
+ all_rows.sort(key=lambda r: (r[2], r[3]))
340
+ return [{"role": row[0], "content": row[1]} for row in all_rows]
341
+
183
342
  async def delete_messages_for_user(user_id: int) -> None:
184
343
  """
185
344
  Delete all message rows for a given user_id across all chats.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.0.4
3
+ Version: 3.2.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,10 +43,10 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
43
43
  * This uses a separate model (configurable via `url_model`) to support more URL content with its higher token limit.
44
44
  * Tokens are used to measure the length of all conversation messages between the Telegram bot assistant and the user. This is useful to:
45
45
  * Ensure the length does not go over the model limit. If it does, prune oldest messages to fit within the limit.
46
- * Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. This eliminates amnesia when users switch between contexts.
46
+ * Remember past conversations when restarting: loads the user's full history across all chats (private and groups) plus all other participants' messages in the current chat, up to 50% of the token budget. In private chats, shared group context (messages from groups where both user and bot are active) fills the remaining budget, enabling the bot to reference group conversations from a private context. This eliminates amnesia when users switch between contexts.
47
47
  * Users can manage privacy via two commands:
48
- * `/forget` — In private chats, clears the full conversation (including bot replies). In group chats, removes only your messages; other participants' messages remain.
49
- * `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups.
48
+ * `/forget` — In private chats, clears the full conversation (including bot replies) and resets all of your active sessions (any sessions where your context was merged). In group chats, removes your messages across all chat types; other participants' messages and sessions remain.
49
+ * `/private` — Toggles per-user private mode (private chats only). When ON, messages are excluded from group conversation contexts, enabling selective privacy even in shared groups. If you have private mode ON and message a group, the bot will remind you once per session.
50
50
 
51
51
  ## Why Telegram?
52
52
  Using Telegram as the interface not only solves "exposing" the interface, but gives you boatloads of interactivity over a standard Command Line interface, or trying to create a website with input boxes and submit buttons to try to handle everything:
@@ -87,7 +87,7 @@ When initializing TeLLMgramBot, the following directories get created:
87
87
  * User will also get notified to contact the owner.
88
88
  * `data`
89
89
  * Contains `conversations.db` — a SQLite database storing all conversations between the bot and users across all chats.
90
- * When a user messages in any chat, their full history is available for context: private messages appear in group contexts, group messages appear in private contexts. This creates seamless cross-context awareness.
90
+ * When a user messages in any chat, their full history is available for context: private messages appear in group contexts, group messages appear in private contexts. This creates seamless cross-context awareness. The bot dynamically refreshes each user's context during a session to pick up messages sent between chats. In private chats, the bot also loads shared group context (messages from groups where both user and bot are active) to provide group awareness.
91
91
  * Users can manage their context via `/forget` (private chat: clears full conversation; group chat: removes only your messages) or `/private` (toggles per-user privacy for group contexts).
92
92
 
93
93
  ### Environment Variables
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='TeLLMgramBot',
8
- version='3.0.4',
8
+ version='3.2.0',
9
9
  packages=find_packages(),
10
10
  license='MIT',
11
11
  author='Digital Heresy',
@@ -1,196 +0,0 @@
1
- """
2
- Manages conversation history, token counting, and SQLite-backed persistence for any LLM provider.
3
-
4
- The Conversation class is keyed by chat_id (Telegram chat ID) and maintains an in-memory message
5
- history, delegating persistence to the database module. Supports async token counting via provider-
6
- specific APIs and automatic pruning when token limits are approached.
7
- """
8
- from .database import insert_message, load_full_user_context, delete_messages_for_user, delete_messages_for_chat
9
- from .models import TokenLimits
10
- from .utils import generate_error_path, log_error
11
-
12
- class Conversation:
13
- """
14
- Manages a chat conversation including message history, token counting, and SQLite persistence.
15
-
16
- Keyed by chat_id (Telegram chat ID): negative for groups, positive (== user_id) for private chats.
17
- Works with any LLM provider (OpenAI, Anthropic). When the token limit is approached, oldest
18
- messages are pruned from memory; the database retains the full history.
19
- """
20
-
21
- def __init__(self, chat_id: int, chat_type: str, system_content: str, system_model: str = "gpt-4o-mini"):
22
- """
23
- Initialize a conversation keyed by chat_id.
24
-
25
- Args:
26
- chat_id: Telegram chat ID (negative for groups, positive/==user_id for private chats).
27
- chat_type: Chat type ('private', 'group', 'supergroup', 'channel').
28
- system_content: System prompt (bot persona).
29
- system_model: LLM model to use for token counting (default: "gpt-4o-mini").
30
- """
31
- self.error_log = generate_error_path()
32
-
33
- self.chat_id = chat_id
34
- self.chat_type = chat_type
35
- self.chat_print = f"{self.chat_type} Chat {self.chat_id}"
36
-
37
- self.system_content = system_content
38
- self.system_model = TokenLimits(system_model)
39
- self.messages = [{"role": "system", "content": system_content}]
40
-
41
- self._has_past_interaction = False
42
-
43
- def set_system_content(self, new_content: str):
44
- """
45
- Replace the system content (bot's personality prompt) in the conversation.
46
-
47
- Updates both self.system_content and the first message in self.messages.
48
-
49
- Args:
50
- new_content: The new system prompt text.
51
- """
52
- self.system_content = new_content
53
- self.messages[0] = {"role": "system", "content": new_content}
54
-
55
- async def add_user_message(self, content: str, user_id: int, username: str | None, is_private: bool = False):
56
- """
57
- Add a user message to conversation memory and persist it to the database.
58
-
59
- Args:
60
- content: The message text.
61
- user_id: Telegram user ID of the sender.
62
- username: Telegram username (display only, may be None).
63
- is_private: If True, excludes this message from group context loading (used for /forget or private commands).
64
- """
65
- self.messages.append({"role": "user", "content": content})
66
- await insert_message(self.chat_id, user_id, username, "user", content, is_private)
67
-
68
- async def add_assistant_message(self, content: str, bot_user_id: int, bot_username: str | None):
69
- """
70
- Add an assistant message to conversation memory and persist it to the database.
71
-
72
- Args:
73
- content: The message text (LLM response).
74
- bot_user_id: Telegram user ID of the bot account.
75
- bot_username: Telegram username of the bot (display only, may be None).
76
- """
77
- self.messages.append({"role": "assistant", "content": content})
78
- await insert_message(self.chat_id, bot_user_id, bot_username, "assistant", content, False)
79
-
80
- async def get_message_token_count(self) -> int:
81
- """
82
- Get the current token count of all messages in this conversation.
83
-
84
- Delegates to the configured model's provider to count tokens accurately.
85
- This is an async operation because it may contact the provider API (e.g., for Anthropic).
86
-
87
- Returns:
88
- The total token count for all messages (system, user, assistant).
89
-
90
- Raises:
91
- ProviderAuthError: If API credentials are missing or invalid.
92
- ProviderConnectionError: If the provider API cannot be reached.
93
- """
94
- return await self.system_model.num_tokens_from_messages(self.messages)
95
-
96
- async def prune_conversation(self, token_limit: int):
97
- """
98
- Remove oldest user-assistant messages until token count is below threshold.
99
-
100
- Recursively removes messages starting from index 1 (preserving the system message at index 0)
101
- until the conversation fits within the token limit. The database is not modified — pruning
102
- only affects the in-memory context window for the current session.
103
-
104
- Args:
105
- token_limit: The maximum token count threshold. Messages are removed until
106
- the conversation is strictly below this limit.
107
- """
108
- try:
109
- self.messages.pop(1)
110
- if await self.get_message_token_count() > token_limit:
111
- await self.prune_conversation(token_limit)
112
- except Exception as e:
113
- log_error(e, f"{type(e).__name__} pruning under {token_limit} tokens for {self.chat_print}", self.error_log)
114
-
115
- async def get_past_interaction(self, token_limit: int, user_id: int) -> bool:
116
- """
117
- Load past conversation messages from the database up to a token limit.
118
-
119
- Implements bidirectional cross-pollination: loads all messages from the current chat
120
- (all participants) plus only the requesting user's own messages from other chats,
121
- merged chronologically. This eliminates amnesia when a user switches between private
122
- and group contexts.
123
-
124
- For group and supergroup chats, messages marked as private (is_private=1) are excluded
125
- so private-mode messages don't surface in shared contexts.
126
-
127
- Idempotent: calling again returns True without reloading if messages were already loaded.
128
-
129
- Args:
130
- token_limit: The maximum token count to load into the in-memory context.
131
- user_id: Telegram user ID of the requesting user; used to fetch bidirectional context.
132
-
133
- Returns:
134
- True if past interactions were found (either on this call or previously),
135
- False if no past interactions exist.
136
-
137
- Side Effects:
138
- - Modifies self.messages in-place by inserting historical messages after the system message
139
- (attempting to fit messages starting from most recent, stopping when limit is reached).
140
- - Sets self._has_past_interaction to True if any messages are loaded.
141
- - Prints to stdout showing token count and whether the token limit was reached.
142
- """
143
- if self._has_past_interaction:
144
- print(f"Context already loaded for User {user_id} in {self.chat_print}")
145
- return True
146
-
147
- exclude_private = self.chat_type in ('group', 'supergroup')
148
- rows = await load_full_user_context(user_id, self.chat_id, exclude_private=exclude_private)
149
-
150
- token_limit_reached = False
151
- token_count = 0
152
-
153
- for row in reversed(rows):
154
- self.messages.insert(1, row)
155
- token_count = await self.get_message_token_count()
156
- if token_count > token_limit:
157
- self.messages.pop(1)
158
- token_count = await self.get_message_token_count()
159
- token_limit_reached = True
160
- break
161
-
162
- self._has_past_interaction = len(self.messages) > 1
163
-
164
- if self._has_past_interaction:
165
- if token_limit_reached:
166
- print(
167
- f"Partial context loaded for User {user_id} in {self.chat_print} — "
168
- f"full history exceeded token limit ({token_limit}), storing {token_count} token(s)"
169
- )
170
- else:
171
- print(
172
- f"Full context loaded for User {user_id} in {self.chat_print} — "
173
- f"storing {token_count} token(s)"
174
- )
175
- else:
176
- print(f"No prior context found for User {user_id} in {self.chat_print}")
177
-
178
- return self._has_past_interaction
179
-
180
- async def clear_interaction(self, user_id: int):
181
- """
182
- Clear this user's messages from memory and from the database.
183
-
184
- In private chats, all messages in the chat are deleted (including bot replies),
185
- fully clearing the conversation. In group chats, only this user's rows are removed;
186
- other members' messages remain.
187
-
188
- Args:
189
- user_id: Telegram user ID of the user requesting the forget.
190
- """
191
- self.messages = [self.messages[0]]
192
- self._has_past_interaction = False
193
- if self.chat_type == 'private':
194
- await delete_messages_for_chat(self.chat_id)
195
- else:
196
- await delete_messages_for_user(user_id)
File without changes
File without changes