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.
Files changed (26) hide show
  1. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/PKG-INFO +1 -1
  2. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/TeLLMgramBot.py +108 -72
  3. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/conversation.py +1 -4
  4. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/database.py +19 -6
  5. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/initialize.py +21 -12
  6. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/PKG-INFO +1 -1
  7. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/setup.py +1 -1
  8. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/LICENSE +0 -0
  9. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/README.md +0 -0
  10. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/__init__.py +0 -0
  11. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/archive.py +0 -0
  12. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/message_handlers.py +0 -0
  13. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/models.py +0 -0
  14. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/__init__.py +0 -0
  15. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
  16. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/base.py +0 -0
  17. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/factory.py +0 -0
  18. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/providers/openai_provider.py +0 -0
  19. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/tools.py +0 -0
  20. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/utils.py +0 -0
  21. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot/web_utils.py +0 -0
  22. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
  23. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
  24. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/requires.txt +0 -0
  25. {tellmgrambot-3.14.1 → tellmgrambot-3.14.2}/TeLLMgramBot.egg-info/top_level.txt +0 -0
  26. {tellmgrambot-3.14.1 → 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.1
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
@@ -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, context_prefix: str = '') -> tuple[str, int | None]:
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, context_prefix, msg.message_id
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.key_status.url_analysis_enabled:
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.key_status.chat_enabled:
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 _build_reply_context(self, msg: Message) -> str:
553
+ async def _surface_replied_to_message(self, msg: Message, conv: Conversation) -> None:
553
554
  """
554
- Build an inline context prefix when the user is replying to another message.
555
-
556
- Used to surface context when the triggering message is a reply-to another message
557
- not already in the conversation's memory window. Returns an annotated prefix string
558
- with speaker name and timestamp if the replied-to message is not in context, or an
559
- empty string if already present.
560
-
561
- Deduplication applies only to our own bot's replies (to avoid repeating content
562
- already visible in context). For all other senders - foreign bots and human users -
563
- the prefix is always prepended so the LLM has an explicit anchor to the referenced
564
- message regardless of whether it appears elsewhere in the loaded context.
565
-
566
- Own-bot dedup uses two tiers:
567
- 1. Memory - reply_to_message.message_id in conv._loaded_message_ids (O(1)).
568
- In-session bot replies are added here immediately after each send.
569
- 2. DB fallback - query for tg_message_id in the messages table.
570
- Cross-session bot replies are findable here since tg_message_id is now
571
- stored for assistant messages after each send.
572
-
573
- Fires in all chat types (group and private) when reply_to_message exists and has text.
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
- conv = self.conversations.get(msg.chat.id)
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
- return f"[Replying to {name}, {dt}]: {reply.text}\n"
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', then prunes the per-chat bot message cap. Skips if the
628
- message has no text or is already stored (tg_message_id dedup).
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, reply_context)
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, reply_context)
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, reply_context)
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
- reply_context = await self._build_reply_context(msg)
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": context_prefix + prefix + 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
- cursor = await db.execute(
204
- "INSERT INTO messages "
205
- "(chat_id, user_id, role, content, is_private, reply_to_id, tg_message_id) "
206
- "VALUES (?, ?, ?, ?, ?, ?, ?)",
207
- (chat_id, user_id, role, content, int(is_private), reply_to_id, tg_message_id),
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 use it as context but don't address that person by name, as Telegram already shows the attribution.\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"
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). The file handler
146
- and console username prefix are added later by bind_log_identity() once the bot's Telegram
147
- username is known.
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
- bind_log_identity() requires the logs path to have been resolved.
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
- console_handler.setFormatter(_ConsoleFormatter('%(levelname)s: %(message)s'))
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.14.1
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
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='TeLLMgramBot',
8
- version='3.14.1',
8
+ version='3.14.2',
9
9
  packages=find_packages(),
10
10
  license='MIT',
11
11
  author='Digital Heresy',
File without changes
File without changes
File without changes