TeLLMgramBot 3.14.0__tar.gz → 3.14.2__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.14.0 → tellmgrambot-3.14.2}/PKG-INFO +4 -3
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/README.md +3 -2
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/TeLLMgramBot.py +134 -77
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/conversation.py +1 -4
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/database.py +37 -13
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/initialize.py +35 -21
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/PKG-INFO +4 -3
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/setup.py +1 -1
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/LICENSE +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/archive.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/message_handlers.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/tools.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/requires.txt +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-3.14.0 → tellmgrambot-3.14.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.14.
|
|
3
|
+
Version: 3.14.2
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -83,6 +83,7 @@ TeLLMgramBot creates the following directories:
|
|
|
83
83
|
- A system appendix is automatically appended to every persona at runtime, teaching the LLM about cross-chat memory and search behavior. User messages include speaker annotations with chat context and timestamps so the LLM always knows who is speaking, in which chat, and when.
|
|
84
84
|
- **`logs`** - Bot instance logs (one per startup, named after the bot's Telegram username or `instance_name` config, e.g. `my_bot_2026-03-29_10-30-45.log`)
|
|
85
85
|
- Logs include anonymized Telegram IDs for privacy. Console shows INFO-level TeLLMgramBot messages only, prefixed with an `[identity label]` (the bot's Telegram username by default, or `instance_name` when configured).
|
|
86
|
+
- Log file timestamps are UTC in `[YYYY-MM-DD HH:MM:SS.mmm]` format.
|
|
86
87
|
- Bot keeps the 10 most recent logs per bot instance, automatically pruning older ones.
|
|
87
88
|
- Pass `-v` or `--verbose` on startup for DEBUG-level logging.
|
|
88
89
|
- **`data`** - SQLite database (default `conversations.db`, customizable via `instance_name` config) storing all messages, users, and chats
|
|
@@ -167,9 +168,9 @@ When the bot is triggered in a group and about to respond (not deferring to anot
|
|
|
167
168
|
```python
|
|
168
169
|
from TeLLMgramBot import TelegramBot
|
|
169
170
|
telegram_bot = TelegramBot.set()
|
|
170
|
-
telegram_bot.
|
|
171
|
+
telegram_bot.poll()
|
|
171
172
|
```
|
|
172
|
-
Once you see `TeLLMgramBot polling
|
|
173
|
+
Once you see `TeLLMgramBot polling started`, the bot is online.
|
|
173
174
|
6. Type `/help` in Telegram to see all available commands.
|
|
174
175
|
|
|
175
176
|
## Resources
|
|
@@ -51,6 +51,7 @@ TeLLMgramBot creates the following directories:
|
|
|
51
51
|
- A system appendix is automatically appended to every persona at runtime, teaching the LLM about cross-chat memory and search behavior. User messages include speaker annotations with chat context and timestamps so the LLM always knows who is speaking, in which chat, and when.
|
|
52
52
|
- **`logs`** - Bot instance logs (one per startup, named after the bot's Telegram username or `instance_name` config, e.g. `my_bot_2026-03-29_10-30-45.log`)
|
|
53
53
|
- Logs include anonymized Telegram IDs for privacy. Console shows INFO-level TeLLMgramBot messages only, prefixed with an `[identity label]` (the bot's Telegram username by default, or `instance_name` when configured).
|
|
54
|
+
- Log file timestamps are UTC in `[YYYY-MM-DD HH:MM:SS.mmm]` format.
|
|
54
55
|
- Bot keeps the 10 most recent logs per bot instance, automatically pruning older ones.
|
|
55
56
|
- Pass `-v` or `--verbose` on startup for DEBUG-level logging.
|
|
56
57
|
- **`data`** - SQLite database (default `conversations.db`, customizable via `instance_name` config) storing all messages, users, and chats
|
|
@@ -135,9 +136,9 @@ When the bot is triggered in a group and about to respond (not deferring to anot
|
|
|
135
136
|
```python
|
|
136
137
|
from TeLLMgramBot import TelegramBot
|
|
137
138
|
telegram_bot = TelegramBot.set()
|
|
138
|
-
telegram_bot.
|
|
139
|
+
telegram_bot.poll()
|
|
139
140
|
```
|
|
140
|
-
Once you see `TeLLMgramBot polling
|
|
141
|
+
Once you see `TeLLMgramBot polling started`, the bot is online.
|
|
141
142
|
6. Type `/help` in Telegram to see all available commands.
|
|
142
143
|
|
|
143
144
|
## Resources
|
|
@@ -38,7 +38,6 @@ from .initialize import (
|
|
|
38
38
|
INIT_BOT_CONFIG,
|
|
39
39
|
ApiKeyStatus,
|
|
40
40
|
bind_log_identity,
|
|
41
|
-
init_logging,
|
|
42
41
|
init_structure,
|
|
43
42
|
)
|
|
44
43
|
from .message_handlers import handle_greetings, handle_common_queries, handle_url_ask
|
|
@@ -410,7 +409,7 @@ class TelegramBot:
|
|
|
410
409
|
"from group conversation contexts. Use /private off to disable."
|
|
411
410
|
)
|
|
412
411
|
|
|
413
|
-
async def tele_handle_response(self, text: str, msg: Message
|
|
412
|
+
async def tele_handle_response(self, text: str, msg: Message) -> tuple[str, int | None]:
|
|
414
413
|
"""
|
|
415
414
|
Primary function for handling any response including Generative AI, ensuring:
|
|
416
415
|
- The owner started up the bot assistant for user interactions.
|
|
@@ -429,8 +428,6 @@ class TelegramBot:
|
|
|
429
428
|
Args:
|
|
430
429
|
text: The user's message text.
|
|
431
430
|
msg: The Telegram Message object containing chat/user context.
|
|
432
|
-
context_prefix: Optional context prepended to the message before the live
|
|
433
|
-
speaker annotation. Used for reply-to-thread context. Defaults to ''.
|
|
434
431
|
|
|
435
432
|
Returns:
|
|
436
433
|
Tuple of (response_text, assistant_db_id). response_text is the bot's response
|
|
@@ -442,6 +439,7 @@ class TelegramBot:
|
|
|
442
439
|
- Adds user message to conversation history (keyed by chat_id).
|
|
443
440
|
- Upserts user (first_name, last_name, username) and chat metadata.
|
|
444
441
|
- Loads past interactions if this is the first message in this chat_id.
|
|
442
|
+
- Surfaces the replied-to message into context via _surface_replied_to_message().
|
|
445
443
|
- May invoke LLM tool calling (search_messages) and perform a second LLM round-trip.
|
|
446
444
|
- Generates a token warning if conversation nears the limit.
|
|
447
445
|
- Prunes conversation if token count exceeds the threshold.
|
|
@@ -489,6 +487,9 @@ class TelegramBot:
|
|
|
489
487
|
# Already loaded - check for new cross-chat messages since last load.
|
|
490
488
|
await conv.refresh_user_context(user_id, token_budget)
|
|
491
489
|
|
|
490
|
+
# Surface the replied-to message into context before adding the triggering message.
|
|
491
|
+
await self._surface_replied_to_message(msg, conv)
|
|
492
|
+
|
|
492
493
|
# Short-circuit for trivial greetings/queries - not persisted to conversation history
|
|
493
494
|
quick_reply = handle_greetings(text) or handle_common_queries(text)
|
|
494
495
|
if quick_reply:
|
|
@@ -499,7 +500,7 @@ class TelegramBot:
|
|
|
499
500
|
user_private_mode = await get_private_mode(user_id)
|
|
500
501
|
is_private = (chat_type == 'private') and user_private_mode
|
|
501
502
|
user_msg_id = await conv.add_user_message(
|
|
502
|
-
text, user_id, username, first_name, last_name, is_private,
|
|
503
|
+
text, user_id, username, first_name, last_name, is_private, msg.message_id
|
|
503
504
|
)
|
|
504
505
|
|
|
505
506
|
# Check if the user is asking about a [URL]
|
|
@@ -507,10 +508,10 @@ class TelegramBot:
|
|
|
507
508
|
|
|
508
509
|
# Form the assistant's message based on low level easy stuff or send to the LLM
|
|
509
510
|
reply = _MSG_PROCESS_ERROR
|
|
510
|
-
if url_match and self.
|
|
511
|
+
if url_match and self._key_status.url_analysis_enabled:
|
|
511
512
|
await msg.reply_text("Sure, give me a moment to look at that URL...")
|
|
512
513
|
reply = await handle_url_ask(text, self.llm['url_model'], conv.system_content)
|
|
513
|
-
elif self._online and self.
|
|
514
|
+
elif self._online and self._key_status.chat_enabled:
|
|
514
515
|
# This is the transition point between quick Telegram replies and the LLM.
|
|
515
516
|
tools = self._build_tool_list(chat_type, username)
|
|
516
517
|
result = await self.llm_completion(chat_id, tools)
|
|
@@ -549,65 +550,51 @@ class TelegramBot:
|
|
|
549
550
|
|
|
550
551
|
return reply, assistant_db_id
|
|
551
552
|
|
|
552
|
-
async def
|
|
553
|
+
async def _surface_replied_to_message(self, msg: Message, conv: Conversation) -> None:
|
|
553
554
|
"""
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
from_user is not required — messages from channel-linked bots or anonymous admins
|
|
575
|
-
may omit it; sender_chat.title or 'unknown' is used as the name fallback.
|
|
576
|
-
Correctly re-surfaces bot messages that are no longer in context (e.g. post-/forget).
|
|
555
|
+
Persist and inject the replied-to message into conversation context.
|
|
556
|
+
|
|
557
|
+
Replaces the old prefix-string approach: instead of prepending replied-to content
|
|
558
|
+
to the triggering message, the replied-to message is injected as its own entry in
|
|
559
|
+
conv.messages (and stored to DB if not already there), matching the format used
|
|
560
|
+
for DB-loaded messages.
|
|
561
|
+
|
|
562
|
+
Three cases:
|
|
563
|
+
Own bot: inject as role='assistant' with raw content (no speaker prefix). If not
|
|
564
|
+
already in DB (checked via message_id_exists), persists via upsert_user (bot_id
|
|
565
|
+
as user), upsert_chat, and insert_message with created_at=reply.date. Covers
|
|
566
|
+
both post-/forget (not in DB) and outside-token-window (in DB but not loaded).
|
|
567
|
+
Foreign bot or regular user with from_user: if not already in DB, insert with
|
|
568
|
+
original timestamp (upsert_user, upsert_chat, insert_message with created_at).
|
|
569
|
+
Inject as role='user' with '[Replying to Name, dt]:' format for explicit context.
|
|
570
|
+
from_user=None (channel-linked bot, anonymous admin): inject only - no DB write
|
|
571
|
+
since there is no user_id to reference.
|
|
572
|
+
|
|
573
|
+
No-op if msg has no reply, the reply has no text, or the replied-to message is
|
|
574
|
+
already in conv._loaded_message_ids (already in context window).
|
|
577
575
|
|
|
578
576
|
Args:
|
|
579
577
|
msg: The incoming Telegram Message that triggered the bot.
|
|
580
|
-
|
|
581
|
-
Returns:
|
|
582
|
-
Formatted prefix string, e.g.
|
|
583
|
-
"[BotB, 2026-04-05 14:30 UTC]: The ancient sword was forged in...\n"
|
|
584
|
-
or empty string if the replied-to message is already in context.
|
|
578
|
+
conv: The active Conversation for this chat.
|
|
585
579
|
"""
|
|
586
580
|
reply = msg.reply_to_message
|
|
587
581
|
if not reply or not reply.text:
|
|
588
|
-
return
|
|
582
|
+
return
|
|
589
583
|
|
|
590
584
|
r_tg_id = reply.message_id
|
|
591
|
-
|
|
585
|
+
if r_tg_id in conv._loaded_message_ids:
|
|
586
|
+
return
|
|
587
|
+
|
|
592
588
|
is_our_reply = (
|
|
593
589
|
reply.from_user is not None and
|
|
594
590
|
reply.from_user.id == self.telegram['bot_id']
|
|
595
591
|
)
|
|
592
|
+
is_foreign_bot = (
|
|
593
|
+
reply.from_user is not None and
|
|
594
|
+
reply.from_user.is_bot and
|
|
595
|
+
not is_our_reply
|
|
596
|
+
)
|
|
596
597
|
|
|
597
|
-
if is_our_reply:
|
|
598
|
-
# Dedup only our own bot's replies to avoid repeating content already visible.
|
|
599
|
-
# Tier 1: in-session check via loaded message ID set
|
|
600
|
-
if conv and r_tg_id in conv._loaded_message_ids:
|
|
601
|
-
return ''
|
|
602
|
-
# Tier 2: DB fallback for cross-session bot replies
|
|
603
|
-
if await message_id_exists(msg.chat.id, r_tg_id):
|
|
604
|
-
return ''
|
|
605
|
-
|
|
606
|
-
# For own-bot replies, mark as seen to skip dedup on subsequent replies
|
|
607
|
-
# to the same message within the session. Foreign/human IDs are tracked
|
|
608
|
-
# by tele_handle_message after the response is sent.
|
|
609
|
-
if is_our_reply and conv:
|
|
610
|
-
conv._loaded_message_ids.add(r_tg_id)
|
|
611
598
|
if reply.from_user:
|
|
612
599
|
sender = reply.from_user
|
|
613
600
|
name_parts = [p for p in [sender.first_name, sender.last_name] if p]
|
|
@@ -616,16 +603,76 @@ class TelegramBot:
|
|
|
616
603
|
name = reply.sender_chat.title or reply.sender_chat.username or 'unknown'
|
|
617
604
|
else:
|
|
618
605
|
name = 'unknown'
|
|
606
|
+
|
|
619
607
|
dt = format_dt(reply.date)
|
|
620
|
-
|
|
608
|
+
|
|
609
|
+
if is_our_reply:
|
|
610
|
+
if not await message_id_exists(msg.chat.id, r_tg_id):
|
|
611
|
+
is_private = (
|
|
612
|
+
msg.chat.type == 'private' and
|
|
613
|
+
msg.from_user is not None and
|
|
614
|
+
await get_private_mode(msg.from_user.id)
|
|
615
|
+
)
|
|
616
|
+
await upsert_user(
|
|
617
|
+
self.telegram['bot_id'],
|
|
618
|
+
self.telegram['username'],
|
|
619
|
+
self.telegram.get('first_name'),
|
|
620
|
+
self.telegram.get('last_name'),
|
|
621
|
+
is_bot=True,
|
|
622
|
+
)
|
|
623
|
+
await upsert_chat(msg.chat.id, msg.chat.type, msg.chat.title or msg.chat.username or '')
|
|
624
|
+
await insert_message(
|
|
625
|
+
chat_id=msg.chat.id,
|
|
626
|
+
user_id=self.telegram['bot_id'],
|
|
627
|
+
role='assistant',
|
|
628
|
+
content=reply.text,
|
|
629
|
+
is_private=is_private,
|
|
630
|
+
tg_message_id=r_tg_id,
|
|
631
|
+
created_at=reply.date,
|
|
632
|
+
)
|
|
633
|
+
conv.messages.append({"role": "assistant", "content": reply.text})
|
|
634
|
+
conv._loaded_message_ids.add(r_tg_id)
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
# For regular users and foreign bots with a known user_id: persist to DB if new.
|
|
638
|
+
if reply.from_user is not None:
|
|
639
|
+
if not await message_id_exists(msg.chat.id, r_tg_id):
|
|
640
|
+
is_private = (
|
|
641
|
+
msg.chat.type == 'private' and
|
|
642
|
+
msg.from_user is not None and
|
|
643
|
+
await get_private_mode(msg.from_user.id)
|
|
644
|
+
)
|
|
645
|
+
await upsert_user(
|
|
646
|
+
reply.from_user.id,
|
|
647
|
+
reply.from_user.username,
|
|
648
|
+
reply.from_user.first_name,
|
|
649
|
+
reply.from_user.last_name,
|
|
650
|
+
is_bot=is_foreign_bot,
|
|
651
|
+
)
|
|
652
|
+
await upsert_chat(msg.chat.id, msg.chat.type, msg.chat.title or msg.chat.username or '')
|
|
653
|
+
await insert_message(
|
|
654
|
+
chat_id=msg.chat.id,
|
|
655
|
+
user_id=reply.from_user.id,
|
|
656
|
+
role='user',
|
|
657
|
+
content=reply.text,
|
|
658
|
+
is_private=is_private,
|
|
659
|
+
tg_message_id=r_tg_id,
|
|
660
|
+
created_at=reply.date,
|
|
661
|
+
)
|
|
662
|
+
if is_foreign_bot:
|
|
663
|
+
await prune_bot_messages(msg.chat.id)
|
|
664
|
+
|
|
665
|
+
conv.messages.append({"role": "user", "content": f"[Replying to {name}, {dt}]: {reply.text}"})
|
|
666
|
+
conv._loaded_message_ids.add(r_tg_id)
|
|
621
667
|
|
|
622
668
|
async def _store_bot_message(self, msg: Message) -> None:
|
|
623
669
|
"""
|
|
624
670
|
Persist a foreign bot's message to the DB for ambient group context.
|
|
625
671
|
|
|
626
672
|
Registers the bot as a user (is_bot=True) and its chat, inserts the message
|
|
627
|
-
with role='user'
|
|
628
|
-
message has no text or is already
|
|
673
|
+
with role='user' and original timestamp (created_at=msg.date), then prunes
|
|
674
|
+
the per-chat bot message cap. Skips if the message has no text or is already
|
|
675
|
+
stored (tg_message_id dedup).
|
|
629
676
|
|
|
630
677
|
Args:
|
|
631
678
|
msg: The Telegram Message from a foreign bot (msg.from_user.is_bot must be True).
|
|
@@ -644,6 +691,7 @@ class TelegramBot:
|
|
|
644
691
|
content=msg.text,
|
|
645
692
|
is_private=False,
|
|
646
693
|
tg_message_id=msg.message_id,
|
|
694
|
+
created_at=msg.date,
|
|
647
695
|
)
|
|
648
696
|
await prune_bot_messages(msg.chat.id)
|
|
649
697
|
|
|
@@ -711,7 +759,9 @@ class TelegramBot:
|
|
|
711
759
|
|
|
712
760
|
Path A - foreign bot message (standalone update): Stores the message for ambient context and returns without LLM response.
|
|
713
761
|
Path B - human reply to foreign bot: Stores the replied-to message before normal routing (fires regardless of whether we are triggered).
|
|
714
|
-
|
|
762
|
+
In a foreign-bot reply thread, an exclusive foreign @mention yields the bot before
|
|
763
|
+
nickname/initials are evaluated - @username takes absolute precedence in that context.
|
|
764
|
+
Nickname/initials triggers engage unconditionally in all other contexts.
|
|
715
765
|
Reply-to-bot trigger yields silently when the message is exclusively addressed to a foreign @account.
|
|
716
766
|
|
|
717
767
|
In private chats, the bot responds to all messages (Telegram does not deliver bot-to-bot DMs).
|
|
@@ -753,43 +803,48 @@ class TelegramBot:
|
|
|
753
803
|
# The real magic of how the bot behaves is in tele_handle_response()
|
|
754
804
|
response = _MSG_PROCESS_ERROR
|
|
755
805
|
assistant_db_id = None
|
|
756
|
-
reply_context = ''
|
|
757
806
|
if chat.type == 'supergroup' or chat.type == 'group':
|
|
758
807
|
is_reply_to_bot = (
|
|
759
808
|
msg.reply_to_message is not None and
|
|
760
809
|
msg.reply_to_message.from_user is not None and
|
|
761
810
|
msg.reply_to_message.from_user.id == self.telegram['bot_id']
|
|
762
811
|
)
|
|
812
|
+
is_reply_to_foreign_bot = (
|
|
813
|
+
msg.reply_to_message is not None and
|
|
814
|
+
msg.reply_to_message.from_user is not None and
|
|
815
|
+
msg.reply_to_message.from_user.is_bot and
|
|
816
|
+
msg.reply_to_message.from_user.id != self.telegram['bot_id']
|
|
817
|
+
)
|
|
818
|
+
# In a foreign-bot reply thread, an exclusive @mention of another account
|
|
819
|
+
# takes absolute precedence over any nickname/initials match in the text.
|
|
820
|
+
if is_reply_to_foreign_bot and self._exclusive_foreign_mention(msg):
|
|
821
|
+
return
|
|
763
822
|
if exact_word_match(self.telegram['username'], msg.text):
|
|
764
823
|
# Explicit @username mention: strongest signal - respond even if another
|
|
765
824
|
# bot is also @mentioned (both may be intentionally addressed).
|
|
766
825
|
pattern = r'@?\b' + re.escape(self.telegram['username']) + r'\b'
|
|
767
826
|
new_text = re.sub(pattern, '', msg.text).strip()
|
|
768
|
-
reply_context = await self._build_reply_context(msg)
|
|
769
827
|
await self._send_read_receipt(msg, context)
|
|
770
|
-
response, assistant_db_id = await self.tele_handle_response(new_text, msg
|
|
828
|
+
response, assistant_db_id = await self.tele_handle_response(new_text, msg)
|
|
771
829
|
elif (
|
|
772
830
|
exact_word_match(self.telegram['nickname'], msg.text) or
|
|
773
831
|
exact_word_match(self.telegram['initials'], msg.text)
|
|
774
832
|
):
|
|
775
833
|
# Nickname/initials: always engage - no reliable way to distinguish
|
|
776
834
|
# our name as addressee vs topic from text position alone.
|
|
777
|
-
reply_context = await self._build_reply_context(msg)
|
|
778
835
|
await self._send_read_receipt(msg, context)
|
|
779
|
-
response, assistant_db_id = await self.tele_handle_response(msg.text, msg
|
|
836
|
+
response, assistant_db_id = await self.tele_handle_response(msg.text, msg)
|
|
780
837
|
elif is_reply_to_bot:
|
|
781
838
|
# Reply-to-bot: weaker signal - yield silently if the message is
|
|
782
839
|
# exclusively addressed to a foreign account via @mention.
|
|
783
840
|
if self._exclusive_foreign_mention(msg):
|
|
784
841
|
return
|
|
785
|
-
reply_context = await self._build_reply_context(msg)
|
|
786
842
|
await self._send_read_receipt(msg, context)
|
|
787
|
-
response, assistant_db_id = await self.tele_handle_response(msg.text, msg
|
|
843
|
+
response, assistant_db_id = await self.tele_handle_response(msg.text, msg)
|
|
788
844
|
else:
|
|
789
845
|
return
|
|
790
846
|
elif chat.type == 'private':
|
|
791
|
-
|
|
792
|
-
response, assistant_db_id = await self.tele_handle_response(msg.text, msg, reply_context)
|
|
847
|
+
response, assistant_db_id = await self.tele_handle_response(msg.text, msg)
|
|
793
848
|
else:
|
|
794
849
|
return
|
|
795
850
|
|
|
@@ -797,10 +852,6 @@ class TelegramBot:
|
|
|
797
852
|
chunk_length = MessageLimit.MAX_TEXT_LENGTH - 1
|
|
798
853
|
chunks = [response[i:i+chunk_length] for i in range(0, len(response), chunk_length)]
|
|
799
854
|
conv = self.conversations.get(msg.chat.id)
|
|
800
|
-
# If reply context was surfaced on the first interaction (conv was None when
|
|
801
|
-
# _build_reply_context ran), record the ID now that the conversation exists.
|
|
802
|
-
if conv and reply_context and msg.reply_to_message:
|
|
803
|
-
conv._loaded_message_ids.add(msg.reply_to_message.message_id)
|
|
804
855
|
for chunk in chunks:
|
|
805
856
|
sent = await msg.reply_text(chunk)
|
|
806
857
|
if sent:
|
|
@@ -1022,11 +1073,19 @@ class TelegramBot:
|
|
|
1022
1073
|
log_error(e, f"{self.llm['chat_model']} Other")
|
|
1023
1074
|
return "Sorry, something went wrong on my end. Please try again!"
|
|
1024
1075
|
|
|
1025
|
-
def
|
|
1026
|
-
"""
|
|
1027
|
-
|
|
1076
|
+
async def tele_unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
1077
|
+
"""Reply to unrecognized commands so the LLM never sees them."""
|
|
1078
|
+
await update.message.reply_text("Unknown command. Use /help to see available commands.")
|
|
1079
|
+
|
|
1080
|
+
def poll(self):
|
|
1081
|
+
"""
|
|
1082
|
+
Start the main polling loop for Telegram updates.
|
|
1083
|
+
|
|
1084
|
+
Blocks indefinitely, logging UTC datetime to file and console at startup and shutdown.
|
|
1085
|
+
"""
|
|
1086
|
+
logger.info(f"TeLLMgramBot {self.telegram['username']} polling started at {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
1028
1087
|
self.telegram['app'].run_polling(poll_interval=self.telegram['pollinterval'])
|
|
1029
|
-
logger.info(f"TeLLMgramBot {self.telegram['username']} polling ended.")
|
|
1088
|
+
logger.info(f"TeLLMgramBot {self.telegram['username']} polling ended at {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
1030
1089
|
|
|
1031
1090
|
# Initialization
|
|
1032
1091
|
def __init__(self,
|
|
@@ -1079,10 +1138,7 @@ class TelegramBot:
|
|
|
1079
1138
|
# Starting to initialize, not online yet
|
|
1080
1139
|
self._online = False
|
|
1081
1140
|
self._instance_name = instance_name
|
|
1082
|
-
|
|
1083
|
-
# Bootstrap structure and logging; init_logging() is a no-op if init_structure already ran it.
|
|
1084
|
-
self.key_status = key_status if key_status is not None else init_structure()[0]
|
|
1085
|
-
init_logging()
|
|
1141
|
+
self._key_status = key_status if key_status is not None else init_structure()[0]
|
|
1086
1142
|
|
|
1087
1143
|
# Initialize some variables
|
|
1088
1144
|
self.token_warning = {} # Determines whether user has reached token limit by AI model
|
|
@@ -1122,6 +1178,7 @@ class TelegramBot:
|
|
|
1122
1178
|
self.telegram['app'].add_handler(CommandHandler('nick', self.tele_nick_command))
|
|
1123
1179
|
self.telegram['app'].add_handler(CommandHandler('forget', self.tele_forget_command))
|
|
1124
1180
|
self.telegram['app'].add_handler(CommandHandler('private', self.tele_private_command))
|
|
1181
|
+
self.telegram['app'].add_handler(MessageHandler(filters.COMMAND, self.tele_unknown_command))
|
|
1125
1182
|
self.telegram['app'].add_handler(MessageHandler(filters.TEXT & ~filters.UpdateType.EDITED_MESSAGE, self.tele_handle_message))
|
|
1126
1183
|
self.telegram['app'].add_error_handler(self.tele_error)
|
|
1127
1184
|
|
|
@@ -152,7 +152,6 @@ class Conversation:
|
|
|
152
152
|
first_name: str | None = None,
|
|
153
153
|
last_name: str | None = None,
|
|
154
154
|
is_private: bool = False,
|
|
155
|
-
context_prefix: str = '',
|
|
156
155
|
tg_message_id: int | None = None,
|
|
157
156
|
) -> int:
|
|
158
157
|
"""
|
|
@@ -175,8 +174,6 @@ class Conversation:
|
|
|
175
174
|
is_private: If True, marks the message as private-mode (excluded from all context
|
|
176
175
|
loads) and flags it in the speaker annotation so the LLM understands
|
|
177
176
|
it should not reference this in group contexts.
|
|
178
|
-
context_prefix: Optional inline context prepended to the in-memory message before
|
|
179
|
-
the live prefix (e.g. reply-to-thread annotation). Not persisted to DB.
|
|
180
177
|
tg_message_id: Telegram message ID; added to _loaded_message_ids so future
|
|
181
178
|
reply-to-thread checks know this message is already in context.
|
|
182
179
|
|
|
@@ -186,7 +183,7 @@ class Conversation:
|
|
|
186
183
|
await upsert_user(user_id, username, first_name, last_name)
|
|
187
184
|
await upsert_chat(self.chat_id, self.chat_type, self.chat_title)
|
|
188
185
|
prefix = self._live_prefix(first_name, last_name, username, is_private)
|
|
189
|
-
msg_dict = {"role": "user", "content":
|
|
186
|
+
msg_dict = {"role": "user", "content": prefix + content}
|
|
190
187
|
self.messages.append(msg_dict)
|
|
191
188
|
if is_private:
|
|
192
189
|
self._private_message_ids.add(id(msg_dict))
|
|
@@ -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 datetime
|
|
9
10
|
import json
|
|
10
11
|
import os
|
|
11
12
|
from typing import Optional
|
|
@@ -183,6 +184,7 @@ async def insert_message(
|
|
|
183
184
|
is_private: bool = False,
|
|
184
185
|
reply_to_id: Optional[int] = None,
|
|
185
186
|
tg_message_id: Optional[int] = None,
|
|
187
|
+
created_at: Optional[datetime.datetime] = None,
|
|
186
188
|
) -> int:
|
|
187
189
|
"""
|
|
188
190
|
Persist a single message row and return its database id.
|
|
@@ -195,17 +197,28 @@ async def insert_message(
|
|
|
195
197
|
is_private: If True, excluded from all context loading.
|
|
196
198
|
reply_to_id: DB id of the replied-to message, or None.
|
|
197
199
|
tg_message_id: Telegram message ID; enables cross-session reply-to dedup.
|
|
200
|
+
created_at: Original message timestamp; when provided, stored instead of NOW().
|
|
201
|
+
Used to preserve the original send time for replied-to messages.
|
|
198
202
|
|
|
199
203
|
Returns:
|
|
200
204
|
Inserted row id.
|
|
201
205
|
"""
|
|
202
206
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
if created_at is not None:
|
|
208
|
+
created_at_str = created_at.strftime('%Y-%m-%dT%H:%M:%S.000Z')
|
|
209
|
+
cursor = await db.execute(
|
|
210
|
+
"INSERT INTO messages "
|
|
211
|
+
"(chat_id, user_id, role, content, is_private, reply_to_id, tg_message_id, created_at) "
|
|
212
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
213
|
+
(chat_id, user_id, role, content, int(is_private), reply_to_id, tg_message_id, created_at_str),
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
cursor = await db.execute(
|
|
217
|
+
"INSERT INTO messages "
|
|
218
|
+
"(chat_id, user_id, role, content, is_private, reply_to_id, tg_message_id) "
|
|
219
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
220
|
+
(chat_id, user_id, role, content, int(is_private), reply_to_id, tg_message_id),
|
|
221
|
+
)
|
|
209
222
|
await db.commit()
|
|
210
223
|
return cursor.lastrowid
|
|
211
224
|
|
|
@@ -322,6 +335,7 @@ def _build_speaker_prefix(
|
|
|
322
335
|
username: Optional[str],
|
|
323
336
|
chat_title: Optional[str],
|
|
324
337
|
created_at: Optional[str] = None,
|
|
338
|
+
is_bot: bool = False,
|
|
325
339
|
) -> str:
|
|
326
340
|
"""
|
|
327
341
|
Build a speaker attribution prefix for a context message.
|
|
@@ -329,6 +343,8 @@ def _build_speaker_prefix(
|
|
|
329
343
|
Returns '' for assistant/system messages or when no identity is resolvable.
|
|
330
344
|
Same-chat produces "[Name, YYYY-MM-DD HH:MM UTC]: ";
|
|
331
345
|
cross-chat produces "[Name (ChatTitle), YYYY-MM-DD HH:MM UTC]: ".
|
|
346
|
+
Bot senders (is_bot=True) are prefixed with "BOT: " inside the brackets so the
|
|
347
|
+
LLM can distinguish peer bots from human participants without a system prompt change.
|
|
332
348
|
|
|
333
349
|
Args:
|
|
334
350
|
role: Message role ('user', 'assistant', 'system').
|
|
@@ -340,6 +356,7 @@ def _build_speaker_prefix(
|
|
|
340
356
|
username: Telegram @handle without @, may be None.
|
|
341
357
|
chat_title: Group/channel display name, or None for private chats.
|
|
342
358
|
created_at: ISO timestamp; date portion included in prefix if present.
|
|
359
|
+
is_bot: True for Telegram bot accounts; prepends "BOT: " to the name.
|
|
343
360
|
|
|
344
361
|
Returns:
|
|
345
362
|
Speaker prefix string, or '' for assistant/system/unknown-identity messages.
|
|
@@ -350,6 +367,8 @@ def _build_speaker_prefix(
|
|
|
350
367
|
name = ' '.join(name_parts) if name_parts else username
|
|
351
368
|
if not name:
|
|
352
369
|
return ''
|
|
370
|
+
if is_bot:
|
|
371
|
+
name = f'BOT: {name}'
|
|
353
372
|
date = (created_at[:16].replace('T', ' ') + ' UTC') if created_at else None
|
|
354
373
|
if chat_id != current_chat_id:
|
|
355
374
|
if chat_title:
|
|
@@ -382,6 +401,7 @@ async def load_full_user_context(
|
|
|
382
401
|
User messages receive a speaker prefix (assistant messages do not) built by _build_speaker_prefix:
|
|
383
402
|
"[Name, YYYY-MM-DD HH:MM UTC]: " for same-chat messages.
|
|
384
403
|
"[Name (ChatTitle), YYYY-MM-DD HH:MM UTC]: " for cross-chat.
|
|
404
|
+
Bot senders (is_bot=True) are prefixed as "[BOT: Name, YYYY-MM-DD HH:MM UTC]: "
|
|
385
405
|
|
|
386
406
|
The exclude_private flag is controlled by the caller and applies regardless of chat type.
|
|
387
407
|
Conversation.get_past_interaction always passes exclude_private=True so that is_private=1
|
|
@@ -397,7 +417,7 @@ async def load_full_user_context(
|
|
|
397
417
|
"""
|
|
398
418
|
raw_query = (
|
|
399
419
|
"SELECT m.role, m.content, m.user_id, m.chat_id, "
|
|
400
|
-
"u.first_name, u.last_name, u.username, c.chat_title, m.created_at, m.tg_message_id "
|
|
420
|
+
"u.first_name, u.last_name, u.username, c.chat_title, m.created_at, m.tg_message_id, u.is_bot "
|
|
401
421
|
"FROM messages m "
|
|
402
422
|
"LEFT JOIN users u ON m.user_id = u.user_id "
|
|
403
423
|
"LEFT JOIN chats c ON m.chat_id = c.chat_id "
|
|
@@ -440,7 +460,8 @@ async def load_full_user_context(
|
|
|
440
460
|
{
|
|
441
461
|
"role": row[0],
|
|
442
462
|
"content": _build_speaker_prefix(
|
|
443
|
-
row[0], row[3], current_chat_id, row[4], row[5], row[6], row[7], row[8]
|
|
463
|
+
row[0], row[3], current_chat_id, row[4], row[5], row[6], row[7], row[8],
|
|
464
|
+
is_bot=bool(row[10]),
|
|
444
465
|
) + row[1],
|
|
445
466
|
"_tg_id": row[9],
|
|
446
467
|
"_sort_key": row[8],
|
|
@@ -511,7 +532,7 @@ async def load_new_cross_chat_context(
|
|
|
511
532
|
Load the delta of cross-chat context since a given cursor (same three-arm query
|
|
512
533
|
as load_full_user_context, restricted to id > since_id). User message content is prefixed
|
|
513
534
|
with "[Name, YYYY-MM-DD HH:MM UTC]: " or "[Name (ChatTitle), YYYY-MM-DD HH:MM UTC]: "
|
|
514
|
-
when user identity is available.
|
|
535
|
+
when user identity is available. Bot senders are prefixed as "[BOT: Name, ...]".
|
|
515
536
|
|
|
516
537
|
Args:
|
|
517
538
|
user_id: Requesting user's Telegram ID.
|
|
@@ -524,7 +545,7 @@ async def load_new_cross_chat_context(
|
|
|
524
545
|
"""
|
|
525
546
|
query = (
|
|
526
547
|
"SELECT m.role, m.content, m.user_id, m.chat_id, "
|
|
527
|
-
"u.first_name, u.last_name, u.username, c.chat_title, m.created_at, m.tg_message_id "
|
|
548
|
+
"u.first_name, u.last_name, u.username, c.chat_title, m.created_at, m.tg_message_id, u.is_bot "
|
|
528
549
|
"FROM messages m "
|
|
529
550
|
"LEFT JOIN users u ON m.user_id = u.user_id "
|
|
530
551
|
"LEFT JOIN chats c ON m.chat_id = c.chat_id "
|
|
@@ -544,7 +565,8 @@ async def load_new_cross_chat_context(
|
|
|
544
565
|
{
|
|
545
566
|
"role": row[0],
|
|
546
567
|
"content": _build_speaker_prefix(
|
|
547
|
-
row[0], row[3], current_chat_id, row[4], row[5], row[6], row[7], row[8]
|
|
568
|
+
row[0], row[3], current_chat_id, row[4], row[5], row[6], row[7], row[8],
|
|
569
|
+
is_bot=bool(row[10]),
|
|
548
570
|
) + row[1],
|
|
549
571
|
"_tg_id": row[9],
|
|
550
572
|
}
|
|
@@ -591,6 +613,7 @@ async def load_shared_group_context(
|
|
|
591
613
|
Used to fill remaining token budget in a private chat with shared-group context.
|
|
592
614
|
Batches queries to stay within SQLite's 999-parameter limit. All rows are treated
|
|
593
615
|
as cross-chat so every user message gets a "[Name (ChatTitle), date]: " prefix.
|
|
616
|
+
Bot senders are prefixed as "[BOT: Name (ChatTitle), date]: ".
|
|
594
617
|
|
|
595
618
|
Args:
|
|
596
619
|
group_chat_ids: List of negative group chat_ids to include.
|
|
@@ -610,7 +633,7 @@ async def load_shared_group_context(
|
|
|
610
633
|
placeholders = ','.join('?' * len(chunk))
|
|
611
634
|
query = (
|
|
612
635
|
f"SELECT m.role, m.content, m.created_at, m.id, "
|
|
613
|
-
f"m.chat_id, u.first_name, u.last_name, u.username, c.chat_title, m.tg_message_id "
|
|
636
|
+
f"m.chat_id, u.first_name, u.last_name, u.username, c.chat_title, m.tg_message_id, u.is_bot "
|
|
614
637
|
f"FROM messages m "
|
|
615
638
|
f"LEFT JOIN users u ON m.user_id = u.user_id "
|
|
616
639
|
f"LEFT JOIN chats c ON m.chat_id = c.chat_id "
|
|
@@ -627,7 +650,8 @@ async def load_shared_group_context(
|
|
|
627
650
|
{
|
|
628
651
|
"role": row[0],
|
|
629
652
|
"content": _build_speaker_prefix(
|
|
630
|
-
row[0], row[4], 0, row[5], row[6], row[7], row[8], row[2]
|
|
653
|
+
row[0], row[4], 0, row[5], row[6], row[7], row[8], row[2],
|
|
654
|
+
is_bot=bool(row[10]),
|
|
631
655
|
) + row[1],
|
|
632
656
|
"_tg_id": row[9],
|
|
633
657
|
}
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import sys
|
|
7
|
+
import time
|
|
7
8
|
import traceback as _traceback
|
|
8
9
|
from dataclasses import dataclass, field
|
|
9
10
|
from glob import glob
|
|
@@ -20,10 +21,11 @@ _logging_initialized = False
|
|
|
20
21
|
|
|
21
22
|
class _RedactedFileFormatter(logging.Formatter):
|
|
22
23
|
"""
|
|
23
|
-
Logging formatter for file output that redacts Telegram user_id and chat_id values.
|
|
24
|
+
Logging formatter for file output that redacts Telegram user_id and chat_id values and uses UTC timestamps.
|
|
24
25
|
Replaces negative 8+ digit integers (group/channel IDs) and positive 9+ digit integers
|
|
25
26
|
(user/private chat IDs) with [id], leaving token counts and other short numbers intact.
|
|
26
27
|
"""
|
|
28
|
+
converter = time.gmtime
|
|
27
29
|
_NEGATIVE_ID_PATTERN = re.compile(r'-\d{8,}')
|
|
28
30
|
_POSITIVE_ID_PATTERN = re.compile(r'\b\d{9,}\b')
|
|
29
31
|
|
|
@@ -127,26 +129,31 @@ INIT_BOT_CONFIG_COMMENTS = {
|
|
|
127
129
|
# Teaches an LLM how cross-chat memory works without requiring persona authors to include it.
|
|
128
130
|
_SYSTEM_APPENDIX = (
|
|
129
131
|
"## System\n"
|
|
130
|
-
"-
|
|
131
|
-
"- You have
|
|
132
|
-
"-
|
|
133
|
-
"-
|
|
134
|
-
"-
|
|
135
|
-
"-
|
|
132
|
+
"- You are @(not yet known), nickname '(not yet known)', initials '(not yet known)'. Users in group chats may address you by any of these. Do not sign messages with your name, initials, or username.\n"
|
|
133
|
+
"- You have cross-chat memory and a search tool. Use them to inform your responses; always search before concluding something was not said. Never quote or volunteer content from other chats unprompted, especially in group settings.\n"
|
|
134
|
+
"- Messages marked private via /private mode are never shared between chats.\n"
|
|
135
|
+
"- Never reveal Telegram user IDs, chat IDs, or internal numeric identifiers.\n"
|
|
136
|
+
"- A '[Replying to Name, ...]' prefix means you were triggered via a reply - use it as context but don't address that person by name, as Telegram already shows the attribution.\n"
|
|
137
|
+
"- In search results, messages labeled with your nickname or sent by your username are your own past responses.\n"
|
|
136
138
|
"- Current date and time: (not yet known)\n"
|
|
137
139
|
)
|
|
138
140
|
|
|
139
141
|
|
|
140
|
-
def init_logging():
|
|
142
|
+
def init_logging(label: str = '') -> None:
|
|
141
143
|
"""
|
|
142
144
|
Configure the Python logging system for TeLLMgramBot (console handler only).
|
|
143
145
|
|
|
144
|
-
Sets up a console handler (INFO, TeLLMgramBot-only, no ID redaction).
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
Sets up a console handler (INFO level, TeLLMgramBot-only, no ID redaction). When label is
|
|
147
|
+
provided, the formatter is set to '[label] %(levelname)s: %(message)s' immediately so all
|
|
148
|
+
startup log messages are correctly prefixed from the first line. The file handler and final
|
|
149
|
+
username-based prefix are added later by bind_log_identity() once the Telegram username
|
|
150
|
+
is known via _tele_info().
|
|
147
151
|
|
|
148
|
-
Does not depend on TELLMGRAMBOT_LOGS_PATH or init_directories(). Only
|
|
149
|
-
|
|
152
|
+
Does not depend on TELLMGRAMBOT_LOGS_PATH or init_directories(). Only bind_log_identity()
|
|
153
|
+
requires the logs path to have been resolved.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
label: Optional instance label for the console prefix (e.g. 'TestBot').
|
|
150
157
|
"""
|
|
151
158
|
global _logging_initialized
|
|
152
159
|
if _logging_initialized:
|
|
@@ -157,7 +164,8 @@ def init_logging():
|
|
|
157
164
|
|
|
158
165
|
console_handler = logging.StreamHandler()
|
|
159
166
|
console_handler.setLevel(logging.INFO)
|
|
160
|
-
|
|
167
|
+
fmt = f'[{label}] %(levelname)s: %(message)s' if label else '%(levelname)s: %(message)s'
|
|
168
|
+
console_handler.setFormatter(_ConsoleFormatter(fmt))
|
|
161
169
|
console_handler.addFilter(lambda r: r.name.startswith('TeLLMgramBot'))
|
|
162
170
|
|
|
163
171
|
root_logger.addHandler(console_handler)
|
|
@@ -170,10 +178,11 @@ def init_logging():
|
|
|
170
178
|
|
|
171
179
|
def bind_log_identity(username: str, instance_name: Optional[str] = None):
|
|
172
180
|
"""
|
|
173
|
-
Bind the bot's identity to the logging system.
|
|
181
|
+
Bind the bot's identity to the logging system with UTC timestamps.
|
|
174
182
|
|
|
175
183
|
Updates the console handler formatter to prefix every line with [instance_name] and opens
|
|
176
|
-
the per-instance log file named {instance_name}_{timestamp}.log.
|
|
184
|
+
the per-instance log file named {instance_name}_{timestamp}.log. File handler uses UTC
|
|
185
|
+
timestamps via _RedactedFileFormatter with format [YYYY-MM-DD HH:MM:SS.mmm] LEVEL logger_name: message.
|
|
177
186
|
|
|
178
187
|
Must be called after the bot's Telegram username is resolved by _tele_info().
|
|
179
188
|
|
|
@@ -206,7 +215,10 @@ def bind_log_identity(username: str, instance_name: Optional[str] = None):
|
|
|
206
215
|
log_path = os.path.join(logs_dir, generate_filename(label))
|
|
207
216
|
file_handler = logging.FileHandler(log_path, encoding='utf-8')
|
|
208
217
|
file_handler.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
209
|
-
file_handler.setFormatter(_RedactedFileFormatter(
|
|
218
|
+
file_handler.setFormatter(_RedactedFileFormatter(
|
|
219
|
+
'[%(asctime)s.%(msecs)03d] %(levelname)s %(name)s: %(message)s',
|
|
220
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
221
|
+
))
|
|
210
222
|
root_logger.addHandler(file_handler)
|
|
211
223
|
|
|
212
224
|
# Prune old log files for this bot - keep the 10 most recent
|
|
@@ -396,7 +408,8 @@ def init_bot_config(file: str = 'config.yaml') -> dict:
|
|
|
396
408
|
Creates a basic bot configuration file in TELLMGRAMBOT_CONFIGS_PATH with default values
|
|
397
409
|
and inline parameter comments. Optional parameters (those with None values in INIT_BOT_CONFIG)
|
|
398
410
|
are populated with descriptions from INIT_BOT_CONFIG_COMMENTS instead of values, helping
|
|
399
|
-
users understand when to set them.
|
|
411
|
+
users understand when to set them. Calls init_logging() with the instance_name from config
|
|
412
|
+
so the console prefix is correct from the first startup log message.
|
|
400
413
|
|
|
401
414
|
Args:
|
|
402
415
|
file: Name of the configuration file to create (default: 'config.yaml').
|
|
@@ -417,6 +430,7 @@ def init_bot_config(file: str = 'config.yaml') -> dict:
|
|
|
417
430
|
config = read_yaml(
|
|
418
431
|
generate_file_path(os.environ[env_var], file, "bot configuration", text)
|
|
419
432
|
) or {}
|
|
433
|
+
init_logging((config.get('instance_name', '') or '').strip())
|
|
420
434
|
for parameter, value in INIT_BOT_CONFIG.items():
|
|
421
435
|
if parameter != 'persona_prompt' and parameter not in config:
|
|
422
436
|
config[parameter] = value
|
|
@@ -524,9 +538,9 @@ def init_structure(
|
|
|
524
538
|
system appendix automatically appended).
|
|
525
539
|
"""
|
|
526
540
|
init_directories()
|
|
527
|
-
init_logging()
|
|
528
541
|
|
|
529
|
-
# Configurations for bot and LLM models
|
|
542
|
+
# Configurations for bot and LLM models; init_logging() is called inside init_bot_config()
|
|
543
|
+
# once instance_name is known so the console prefix is set from the first log line.
|
|
530
544
|
config = init_bot_config(config_file)
|
|
531
545
|
init_models_config()
|
|
532
546
|
|
|
@@ -586,7 +600,7 @@ def init_structure(
|
|
|
586
600
|
await init_db()
|
|
587
601
|
await run_archival(config)
|
|
588
602
|
except Exception:
|
|
589
|
-
logger.error("Background startup initialization/archive task failed", exc_info=True)
|
|
603
|
+
logger.error(f"Background startup initialization/archive task failed", exc_info=True)
|
|
590
604
|
loop.create_task(_init_and_archive())
|
|
591
605
|
else:
|
|
592
606
|
asyncio.run(run_archival(config))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.14.
|
|
3
|
+
Version: 3.14.2
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -83,6 +83,7 @@ TeLLMgramBot creates the following directories:
|
|
|
83
83
|
- A system appendix is automatically appended to every persona at runtime, teaching the LLM about cross-chat memory and search behavior. User messages include speaker annotations with chat context and timestamps so the LLM always knows who is speaking, in which chat, and when.
|
|
84
84
|
- **`logs`** - Bot instance logs (one per startup, named after the bot's Telegram username or `instance_name` config, e.g. `my_bot_2026-03-29_10-30-45.log`)
|
|
85
85
|
- Logs include anonymized Telegram IDs for privacy. Console shows INFO-level TeLLMgramBot messages only, prefixed with an `[identity label]` (the bot's Telegram username by default, or `instance_name` when configured).
|
|
86
|
+
- Log file timestamps are UTC in `[YYYY-MM-DD HH:MM:SS.mmm]` format.
|
|
86
87
|
- Bot keeps the 10 most recent logs per bot instance, automatically pruning older ones.
|
|
87
88
|
- Pass `-v` or `--verbose` on startup for DEBUG-level logging.
|
|
88
89
|
- **`data`** - SQLite database (default `conversations.db`, customizable via `instance_name` config) storing all messages, users, and chats
|
|
@@ -167,9 +168,9 @@ When the bot is triggered in a group and about to respond (not deferring to anot
|
|
|
167
168
|
```python
|
|
168
169
|
from TeLLMgramBot import TelegramBot
|
|
169
170
|
telegram_bot = TelegramBot.set()
|
|
170
|
-
telegram_bot.
|
|
171
|
+
telegram_bot.poll()
|
|
171
172
|
```
|
|
172
|
-
Once you see `TeLLMgramBot polling
|
|
173
|
+
Once you see `TeLLMgramBot polling started`, the bot is online.
|
|
173
174
|
6. Type `/help` in Telegram to see all available commands.
|
|
174
175
|
|
|
175
176
|
## Resources
|
|
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
|
|
File without changes
|