TeLLMgramBot 3.14.1__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.1 → tellmgrambot-3.14.2}/PKG-INFO +1 -1
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/TeLLMgramBot.py +108 -72
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/conversation.py +1 -4
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/database.py +19 -6
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/initialize.py +21 -12
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/PKG-INFO +1 -1
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/setup.py +1 -1
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/LICENSE +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/README.md +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/archive.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/message_handlers.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/tools.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/requires.txt +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/setup.cfg +0 -0
|
@@ -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
|
|
|
@@ -755,7 +803,6 @@ class TelegramBot:
|
|
|
755
803
|
# The real magic of how the bot behaves is in tele_handle_response()
|
|
756
804
|
response = _MSG_PROCESS_ERROR
|
|
757
805
|
assistant_db_id = None
|
|
758
|
-
reply_context = ''
|
|
759
806
|
if chat.type == 'supergroup' or chat.type == 'group':
|
|
760
807
|
is_reply_to_bot = (
|
|
761
808
|
msg.reply_to_message is not None and
|
|
@@ -777,31 +824,27 @@ class TelegramBot:
|
|
|
777
824
|
# bot is also @mentioned (both may be intentionally addressed).
|
|
778
825
|
pattern = r'@?\b' + re.escape(self.telegram['username']) + r'\b'
|
|
779
826
|
new_text = re.sub(pattern, '', msg.text).strip()
|
|
780
|
-
reply_context = await self._build_reply_context(msg)
|
|
781
827
|
await self._send_read_receipt(msg, context)
|
|
782
|
-
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)
|
|
783
829
|
elif (
|
|
784
830
|
exact_word_match(self.telegram['nickname'], msg.text) or
|
|
785
831
|
exact_word_match(self.telegram['initials'], msg.text)
|
|
786
832
|
):
|
|
787
833
|
# Nickname/initials: always engage - no reliable way to distinguish
|
|
788
834
|
# our name as addressee vs topic from text position alone.
|
|
789
|
-
reply_context = await self._build_reply_context(msg)
|
|
790
835
|
await self._send_read_receipt(msg, context)
|
|
791
|
-
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)
|
|
792
837
|
elif is_reply_to_bot:
|
|
793
838
|
# Reply-to-bot: weaker signal - yield silently if the message is
|
|
794
839
|
# exclusively addressed to a foreign account via @mention.
|
|
795
840
|
if self._exclusive_foreign_mention(msg):
|
|
796
841
|
return
|
|
797
|
-
reply_context = await self._build_reply_context(msg)
|
|
798
842
|
await self._send_read_receipt(msg, context)
|
|
799
|
-
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)
|
|
800
844
|
else:
|
|
801
845
|
return
|
|
802
846
|
elif chat.type == 'private':
|
|
803
|
-
|
|
804
|
-
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)
|
|
805
848
|
else:
|
|
806
849
|
return
|
|
807
850
|
|
|
@@ -809,10 +852,6 @@ class TelegramBot:
|
|
|
809
852
|
chunk_length = MessageLimit.MAX_TEXT_LENGTH - 1
|
|
810
853
|
chunks = [response[i:i+chunk_length] for i in range(0, len(response), chunk_length)]
|
|
811
854
|
conv = self.conversations.get(msg.chat.id)
|
|
812
|
-
# If reply context was surfaced on the first interaction (conv was None when
|
|
813
|
-
# _build_reply_context ran), record the ID now that the conversation exists.
|
|
814
|
-
if conv and reply_context and msg.reply_to_message:
|
|
815
|
-
conv._loaded_message_ids.add(msg.reply_to_message.message_id)
|
|
816
855
|
for chunk in chunks:
|
|
817
856
|
sent = await msg.reply_text(chunk)
|
|
818
857
|
if sent:
|
|
@@ -1099,10 +1138,7 @@ class TelegramBot:
|
|
|
1099
1138
|
# Starting to initialize, not online yet
|
|
1100
1139
|
self._online = False
|
|
1101
1140
|
self._instance_name = instance_name
|
|
1102
|
-
|
|
1103
|
-
# Bootstrap structure and logging; init_logging() is a no-op if init_structure already ran it.
|
|
1104
|
-
self.key_status = key_status if key_status is not None else init_structure()[0]
|
|
1105
|
-
init_logging()
|
|
1141
|
+
self._key_status = key_status if key_status is not None else init_structure()[0]
|
|
1106
1142
|
|
|
1107
1143
|
# Initialize some variables
|
|
1108
1144
|
self.token_warning = {} # Determines whether user has reached token limit by AI model
|
|
@@ -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
|
|
|
@@ -133,21 +133,27 @@ _SYSTEM_APPENDIX = (
|
|
|
133
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
134
|
"- Messages marked private via /private mode are never shared between chats.\n"
|
|
135
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
|
|
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"
|
|
137
138
|
"- Current date and time: (not yet known)\n"
|
|
138
139
|
)
|
|
139
140
|
|
|
140
141
|
|
|
141
|
-
def init_logging():
|
|
142
|
+
def init_logging(label: str = '') -> None:
|
|
142
143
|
"""
|
|
143
144
|
Configure the Python logging system for TeLLMgramBot (console handler only).
|
|
144
145
|
|
|
145
|
-
Sets up a console handler (INFO, TeLLMgramBot-only, no ID redaction).
|
|
146
|
-
|
|
147
|
-
|
|
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().
|
|
148
151
|
|
|
149
|
-
Does not depend on TELLMGRAMBOT_LOGS_PATH or init_directories(). Only
|
|
150
|
-
|
|
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').
|
|
151
157
|
"""
|
|
152
158
|
global _logging_initialized
|
|
153
159
|
if _logging_initialized:
|
|
@@ -158,7 +164,8 @@ def init_logging():
|
|
|
158
164
|
|
|
159
165
|
console_handler = logging.StreamHandler()
|
|
160
166
|
console_handler.setLevel(logging.INFO)
|
|
161
|
-
|
|
167
|
+
fmt = f'[{label}] %(levelname)s: %(message)s' if label else '%(levelname)s: %(message)s'
|
|
168
|
+
console_handler.setFormatter(_ConsoleFormatter(fmt))
|
|
162
169
|
console_handler.addFilter(lambda r: r.name.startswith('TeLLMgramBot'))
|
|
163
170
|
|
|
164
171
|
root_logger.addHandler(console_handler)
|
|
@@ -401,7 +408,8 @@ def init_bot_config(file: str = 'config.yaml') -> dict:
|
|
|
401
408
|
Creates a basic bot configuration file in TELLMGRAMBOT_CONFIGS_PATH with default values
|
|
402
409
|
and inline parameter comments. Optional parameters (those with None values in INIT_BOT_CONFIG)
|
|
403
410
|
are populated with descriptions from INIT_BOT_CONFIG_COMMENTS instead of values, helping
|
|
404
|
-
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.
|
|
405
413
|
|
|
406
414
|
Args:
|
|
407
415
|
file: Name of the configuration file to create (default: 'config.yaml').
|
|
@@ -422,6 +430,7 @@ def init_bot_config(file: str = 'config.yaml') -> dict:
|
|
|
422
430
|
config = read_yaml(
|
|
423
431
|
generate_file_path(os.environ[env_var], file, "bot configuration", text)
|
|
424
432
|
) or {}
|
|
433
|
+
init_logging((config.get('instance_name', '') or '').strip())
|
|
425
434
|
for parameter, value in INIT_BOT_CONFIG.items():
|
|
426
435
|
if parameter != 'persona_prompt' and parameter not in config:
|
|
427
436
|
config[parameter] = value
|
|
@@ -529,9 +538,9 @@ def init_structure(
|
|
|
529
538
|
system appendix automatically appended).
|
|
530
539
|
"""
|
|
531
540
|
init_directories()
|
|
532
|
-
init_logging()
|
|
533
541
|
|
|
534
|
-
# 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.
|
|
535
544
|
config = init_bot_config(config_file)
|
|
536
545
|
init_models_config()
|
|
537
546
|
|
|
@@ -591,7 +600,7 @@ def init_structure(
|
|
|
591
600
|
await init_db()
|
|
592
601
|
await run_archival(config)
|
|
593
602
|
except Exception:
|
|
594
|
-
logger.error("Background startup initialization/archive task failed", exc_info=True)
|
|
603
|
+
logger.error(f"Background startup initialization/archive task failed", exc_info=True)
|
|
595
604
|
loop.create_task(_init_and_archive())
|
|
596
605
|
else:
|
|
597
606
|
asyncio.run(run_archival(config))
|
|
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
|
|
File without changes
|