TeLLMgramBot 3.15.2__tar.gz → 3.15.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.
Files changed (26) hide show
  1. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/PKG-INFO +2 -1
  2. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/README.md +1 -0
  3. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/TeLLMgramBot.py +96 -9
  4. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/initialize.py +2 -0
  5. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot.egg-info/PKG-INFO +2 -1
  6. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/setup.py +1 -1
  7. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/LICENSE +0 -0
  8. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/__init__.py +0 -0
  9. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/archive.py +0 -0
  10. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/conversation.py +0 -0
  11. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/database.py +0 -0
  12. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/message_handlers.py +0 -0
  13. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/models.py +0 -0
  14. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/providers/__init__.py +0 -0
  15. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
  16. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/providers/base.py +0 -0
  17. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/providers/factory.py +0 -0
  18. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/providers/openai_provider.py +0 -0
  19. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/tools.py +0 -0
  20. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/utils.py +0 -0
  21. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot/web_utils.py +0 -0
  22. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
  23. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
  24. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot.egg-info/requires.txt +0 -0
  25. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/TeLLMgramBot.egg-info/top_level.txt +0 -0
  26. {tellmgrambot-3.15.2 → tellmgrambot-3.15.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.15.2
3
+ Version: 3.15.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
@@ -168,6 +168,7 @@ When the bot is triggered in a group and about to respond (not deferring to anot
168
168
  - `archive_days`: Days before messages are eligible for archival (optional; default 60, minimum 1). Older messages are distilled into daily summaries, then progressively compressed into monthly digests. Once archived their respective raw messages do not return to the LLM context any more, only when searching messages.
169
169
  - `document_processing`: Optional bool (default: true). Set to false to disable document and text file summarisation.
170
170
  - `allow_local_webhooks`: Set to `true` to permit webhook/MCP URLs targeting loopback or link-local addresses (optional; default `false`). Useful when tools like Home Assistant run on the same host.
171
+ - `max_conversations`: Optional max chats kept in memory at once (default: 500, minimum 1). Least-recently-used chats beyond this cap are evicted and reload from the database on their next message. Useful for deployments with memory constraints; evicted chats retain all persisted data.
171
172
  - `tools`: Optional list of webhook and MCP tool definitions (admin-only, private chat only). See [docs/tools.md](docs/tools.md) for schema and examples.
172
173
  4. **Disable group privacy mode in BotFather:**
173
174
  ```
@@ -131,6 +131,7 @@ When the bot is triggered in a group and about to respond (not deferring to anot
131
131
  - `archive_days`: Days before messages are eligible for archival (optional; default 60, minimum 1). Older messages are distilled into daily summaries, then progressively compressed into monthly digests. Once archived their respective raw messages do not return to the LLM context any more, only when searching messages.
132
132
  - `document_processing`: Optional bool (default: true). Set to false to disable document and text file summarisation.
133
133
  - `allow_local_webhooks`: Set to `true` to permit webhook/MCP URLs targeting loopback or link-local addresses (optional; default `false`). Useful when tools like Home Assistant run on the same host.
134
+ - `max_conversations`: Optional max chats kept in memory at once (default: 500, minimum 1). Least-recently-used chats beyond this cap are evicted and reload from the database on their next message. Useful for deployments with memory constraints; evicted chats retain all persisted data.
134
135
  - `tools`: Optional list of webhook and MCP tool definitions (admin-only, private chat only). See [docs/tools.md](docs/tools.md) for schema and examples.
135
136
  4. **Disable group privacy mode in BotFather:**
136
137
  ```
@@ -5,6 +5,7 @@ import logging
5
5
  import json
6
6
  import os
7
7
  import re
8
+ from collections import OrderedDict
8
9
  from math import floor
9
10
 
10
11
  from telegram import Bot, Update, Message, Chat, User, ReactionTypeEmoji, MessageEntity, InlineKeyboardButton, InlineKeyboardMarkup
@@ -79,6 +80,23 @@ def _validated_allow_local(value) -> bool:
79
80
  return value is True
80
81
 
81
82
 
83
+ def _validated_max_conversations(value) -> int:
84
+ """
85
+ Validate the max_conversations config value, defaulting to 500 on invalid input.
86
+
87
+ Caps how many Conversation objects self.conversations keeps in memory at once;
88
+ least-recently-used chats beyond this cap are evicted and reload from DB on
89
+ their next message.
90
+
91
+ Args:
92
+ value: Raw `max_conversations` value from bot config.
93
+ """
94
+ if value is not None and not (type(value) is int and value >= 1):
95
+ logger.warning(f"Invalid max_conversations '{value}' (must be an integer >= 1); using default 500")
96
+ return 500
97
+ return value if value is not None else 500
98
+
99
+
82
100
  _SEARCH_TOOL = {
83
101
  "name": "search_messages",
84
102
  "description": (
@@ -438,6 +456,12 @@ class TelegramBot:
438
456
  session (get_past_interaction) or checks for new messages since the last load
439
457
  (refresh_user_context).
440
458
 
459
+ self.conversations is an OrderedDict capped at self.llm['max_conversations'] entries.
460
+ Every call moves chat_id to the most-recently-used end; inserting past the cap evicts
461
+ the least-recently-used chat (and its token_warning entry) from memory. Eviction never
462
+ deletes persisted data - an evicted chat's next message cold-loads from DB exactly like
463
+ a chat the bot has never seen before.
464
+
441
465
  Args:
442
466
  chat_id: Telegram chat ID.
443
467
  chat_type: 'private', 'group', or 'supergroup'.
@@ -447,10 +471,15 @@ class TelegramBot:
447
471
  Returns:
448
472
  The active Conversation for this chat.
449
473
  """
450
- if chat_id not in self.conversations:
474
+ if chat_id in self.conversations:
475
+ self.conversations.move_to_end(chat_id)
476
+ else:
451
477
  self.conversations[chat_id] = Conversation(
452
478
  chat_id, chat_type, self.llm['prompt'], self.llm['chat_model'], chat_title,
453
479
  )
480
+ if len(self.conversations) > self.llm['max_conversations']:
481
+ evicted_id, _ = self.conversations.popitem(last=False)
482
+ self.token_warning.pop(evicted_id, None)
454
483
  conv = self.conversations[chat_id]
455
484
  conv.update_datetime()
456
485
 
@@ -1250,12 +1279,60 @@ class TelegramBot:
1250
1279
 
1251
1280
  Registered as python-telegram-bot's post_init hook so a large archival backlog or slow/unreachable MCP
1252
1281
  servers never block tele_handle_message/tele_handle_document from answering the first incoming update.
1253
- Uses application.create_task() rather than asyncio.create_task() so both tasks are tracked and
1254
- cancelled cleanly on shutdown.
1282
+
1283
+ Schedules both tasks via _schedule_background_task(), which wraps asyncio.create_task()
1284
+ directly. PTB's own Application.create_task() only tracks a task for graceful shutdown
1285
+ once application.running is True; post_init runs before Application.start() sets that
1286
+ flag, so scheduling through Application.create_task() here would go untracked anyway and
1287
+ emit a PTBUserWarning. Task references are kept in self._background_tasks until each task
1288
+ completes, since asyncio does not hold a strong reference to a task on your behalf.
1255
1289
  """
1256
1290
  if self._mcp_entries:
1257
- application.create_task(self._discover_mcp_tools_background())
1258
- application.create_task(run_archival(self.llm))
1291
+ self._schedule_background_task(self._discover_mcp_tools_background())
1292
+ self._schedule_background_task(run_archival(self.llm))
1293
+
1294
+ def _schedule_background_task(self, coro) -> asyncio.Task:
1295
+ """
1296
+ Run coro as a fire-and-forget asyncio task, keeping a reference until it completes.
1297
+
1298
+ asyncio does not keep a strong reference to a task once its creator's local
1299
+ variable goes out of scope, so a task can be garbage-collected mid-execution
1300
+ without this. The done-callback removes the task from self._background_tasks
1301
+ and retrieves any exception via _on_background_task_done(), so an unhandled
1302
+ failure surfaces through our own logging instead of asyncio's "Task exception
1303
+ was never retrieved" warning on garbage collection.
1304
+
1305
+ Args:
1306
+ coro: The coroutine to schedule.
1307
+
1308
+ Returns:
1309
+ The created asyncio.Task.
1310
+ """
1311
+ task = asyncio.create_task(coro)
1312
+ self._background_tasks.add(task)
1313
+ task.add_done_callback(self._on_background_task_done)
1314
+ return task
1315
+
1316
+ def _on_background_task_done(self, task: asyncio.Task) -> None:
1317
+ """
1318
+ Done-callback for _schedule_background_task(): discard the reference and log
1319
+ any unhandled exception.
1320
+
1321
+ Retrieving the exception here (rather than leaving it unread) prevents asyncio's
1322
+ default exception handler from emitting "Task exception was never retrieved" when
1323
+ the task is garbage-collected. A cancelled task has no exception to retrieve -
1324
+ task.exception() raises CancelledError in that case, so cancellation is checked
1325
+ first and treated as a normal, silent outcome.
1326
+
1327
+ Args:
1328
+ task: The completed (or cancelled) background task.
1329
+ """
1330
+ self._background_tasks.discard(task)
1331
+ if task.cancelled():
1332
+ return
1333
+ exc = task.exception()
1334
+ if exc is not None:
1335
+ log_error(exc, 'BackgroundTask')
1259
1336
 
1260
1337
  async def _discover_mcp_tools_background(self) -> None:
1261
1338
  """
@@ -1298,6 +1375,7 @@ class TelegramBot:
1298
1375
  persona_temp = INIT_BOT_CONFIG['persona_temp'],
1299
1376
  archive_days = INIT_BOT_CONFIG['archive_days'],
1300
1377
  document_processing = INIT_BOT_CONFIG['document_processing'],
1378
+ max_conversations = INIT_BOT_CONFIG['max_conversations'],
1301
1379
  persona_prompt = INIT_BOT_CONFIG['persona_prompt'],
1302
1380
  key_status: ApiKeyStatus | None = None,
1303
1381
  instance_name: str | None = None,
@@ -1326,6 +1404,9 @@ class TelegramBot:
1326
1404
  archive_days: Days before messages are eligible for Tier 1 archival (default: 60).
1327
1405
  Must be an integer >= 1; invalid values log a warning and fall back to 60.
1328
1406
  Tier 2 compression triggers at archive_days * 2.
1407
+ max_conversations: Max chats kept in self.conversations at once (default: 500).
1408
+ Validated by _validated_max_conversations(); least-recently-used
1409
+ chats beyond this cap are evicted in _get_or_load_conversation().
1329
1410
  webhook_schemas: Provider-compatible tool schema dicts for webhook tools (from build_tool_registry).
1330
1411
  If None, no webhook tools are registered.
1331
1412
  webhook_defs: Resolved webhook tool definitions keyed by tool name (from build_tool_registry).
@@ -1348,11 +1429,14 @@ class TelegramBot:
1348
1429
 
1349
1430
  # Initialize some variables
1350
1431
  self.token_warning = {} # Determines whether user has reached token limit by AI model
1351
- self.conversations = {} # Provides Conversation class per user based on bot response
1432
+ # Provides Conversation class per chat_id; an OrderedDict so _get_or_load_conversation()
1433
+ # can track recency via move_to_end() and evict the least-recently-used entry on overflow.
1434
+ self.conversations: OrderedDict = OrderedDict()
1352
1435
  self.webhook_schemas = webhook_schemas or [] # Provider-compatible schemas for webhook tools
1353
1436
  self.webhook_defs = webhook_defs or {} # Resolved tool definitions keyed by name
1354
1437
  self._mcp_entries = mcp_entries or [] # Raw mcp_server entries; discovered in _post_init()
1355
1438
  self._allow_local_webhooks = allow_local_webhooks
1439
+ self._background_tasks: set = set() # Keeps fire-and-forget tasks referenced until done
1356
1440
  owners = bot_owner if isinstance(bot_owner, list) else [bot_owner]
1357
1441
  self.telegram = {
1358
1442
  'bot_id' : 0, # overwritten by _tele_info(); 0 is a safe sentinel
@@ -1366,13 +1450,14 @@ class TelegramBot:
1366
1450
  }
1367
1451
  # Check for event running loops before getting the bot's information
1368
1452
  try:
1369
- loop = asyncio.get_running_loop()
1453
+ asyncio.get_running_loop()
1370
1454
  except RuntimeError:
1371
1455
  # No running event loop; safe to run synchronously
1372
1456
  asyncio.run(self._tele_info())
1373
1457
  else:
1374
- # Already in an event loop; schedule as a background task
1375
- loop.create_task(self._tele_info())
1458
+ # Already in an event loop; schedule as a background task, keeping a
1459
+ # reference via self._background_tasks so it isn't garbage-collected mid-run
1460
+ self._schedule_background_task(self._tele_info())
1376
1461
 
1377
1462
  # Build our application with handlers for Commands, Messages, and Errors
1378
1463
  self.telegram['app'] = (
@@ -1422,6 +1507,7 @@ class TelegramBot:
1422
1507
  'top_p' : 0.9,
1423
1508
  'archive_days' : archive_days if archive_days is not None else 60,
1424
1509
  'document_processing' : document_processing if document_processing is not None else True,
1510
+ 'max_conversations' : _validated_max_conversations(max_conversations),
1425
1511
  }
1426
1512
  # Set a rounded-down integer to prune a lengthy conversation by 500 tokens
1427
1513
  # Note if the upper limit is below 500, the lower limit is set to 0
@@ -1499,6 +1585,7 @@ class TelegramBot:
1499
1585
  persona_temp = config['persona_temp'],
1500
1586
  archive_days = config['archive_days'],
1501
1587
  document_processing = config['document_processing'],
1588
+ max_conversations = config['max_conversations'],
1502
1589
  persona_prompt = prompt,
1503
1590
  key_status = key_status,
1504
1591
  instance_name = config['instance_name'],
@@ -113,6 +113,7 @@ INIT_BOT_CONFIG = {
113
113
  'archive_days': None,
114
114
  'allow_local_webhooks': None,
115
115
  'document_processing': None,
116
+ 'max_conversations': None,
116
117
  'persona_prompt': 'You are a generic test bot powered by a user-configured LLM.'
117
118
  }
118
119
 
@@ -124,6 +125,7 @@ INIT_BOT_CONFIG_COMMENTS = {
124
125
  'archive_days': '# Optional, days before messages are eligible for Tier 1 archival (default: 60, min: 1). Tier 2 triggers at 2x this value.',
125
126
  'allow_local_webhooks': '# Optional, set to true to permit webhook/MCP URLs targeting loopback or link-local addresses (default: false)',
126
127
  'document_processing': '# Optional, set to false to disable document summarisation (default: true)',
128
+ 'max_conversations': '# Optional, max chats kept in memory at once; least-recently-used chats beyond this cap are evicted and reload from DB on their next message (default: 500, min: 1)',
127
129
  }
128
130
 
129
131
  # Append the framework-owned system appendix to the persona prompt.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.15.2
3
+ Version: 3.15.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
@@ -168,6 +168,7 @@ When the bot is triggered in a group and about to respond (not deferring to anot
168
168
  - `archive_days`: Days before messages are eligible for archival (optional; default 60, minimum 1). Older messages are distilled into daily summaries, then progressively compressed into monthly digests. Once archived their respective raw messages do not return to the LLM context any more, only when searching messages.
169
169
  - `document_processing`: Optional bool (default: true). Set to false to disable document and text file summarisation.
170
170
  - `allow_local_webhooks`: Set to `true` to permit webhook/MCP URLs targeting loopback or link-local addresses (optional; default `false`). Useful when tools like Home Assistant run on the same host.
171
+ - `max_conversations`: Optional max chats kept in memory at once (default: 500, minimum 1). Least-recently-used chats beyond this cap are evicted and reload from the database on their next message. Useful for deployments with memory constraints; evicted chats retain all persisted data.
171
172
  - `tools`: Optional list of webhook and MCP tool definitions (admin-only, private chat only). See [docs/tools.md](docs/tools.md) for schema and examples.
172
173
  4. **Disable group privacy mode in BotFather:**
173
174
  ```
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='TeLLMgramBot',
8
- version='3.15.2',
8
+ version='3.15.4',
9
9
  packages=find_packages(),
10
10
  license='MIT',
11
11
  author='Digital Heresy',
File without changes
File without changes