TeLLMgramBot 3.13.7__tar.gz → 3.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/PKG-INFO +2 -2
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/README.md +1 -1
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/TeLLMgramBot.py +133 -50
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/database.py +44 -5
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/initialize.py +1 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/tools.py +6 -2
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot.egg-info/PKG-INFO +2 -2
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/setup.py +1 -1
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/LICENSE +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/archive.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/conversation.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/message_handlers.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot.egg-info/requires.txt +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-3.13.7 → tellmgrambot-3.14.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.14.0
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -135,7 +135,7 @@ The bot responds in groups when you:
|
|
|
135
135
|
- Mention the bot by nickname or initials (configured via `config.yaml`)
|
|
136
136
|
- Reply directly to one of the bot's messages
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
When multiple bots are @mentioned in the same message, the bot coexists: if you mention the bot's nickname or initials, or reply to its message, the bot always engages (you may be intentionally addressing both bots). If the only trigger is a reply to the bot's message AND the message exclusively addresses a different bot via @mention (no mention of this bot), the bot yields silently - this supports threaded context without redundant responses.
|
|
139
139
|
|
|
140
140
|
### Private Chat Behavior
|
|
141
141
|
In private chats, the bot responds to all your messages. If you reply to an earlier message in the conversation that is not already in the bot's context window, that message is automatically surfaced as inline context so the bot can understand the full conversation thread.
|
|
@@ -103,7 +103,7 @@ The bot responds in groups when you:
|
|
|
103
103
|
- Mention the bot by nickname or initials (configured via `config.yaml`)
|
|
104
104
|
- Reply directly to one of the bot's messages
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
When multiple bots are @mentioned in the same message, the bot coexists: if you mention the bot's nickname or initials, or reply to its message, the bot always engages (you may be intentionally addressing both bots). If the only trigger is a reply to the bot's message AND the message exclusively addresses a different bot via @mention (no mention of this bot), the bot yields silently - this supports threaded context without redundant responses.
|
|
107
107
|
|
|
108
108
|
### Private Chat Behavior
|
|
109
109
|
In private chats, the bot responds to all your messages. If you reply to an earlier message in the conversation that is not already in the bot's context window, that message is automatically surfaced as inline context so the bot can understand the full conversation thread.
|
|
@@ -27,6 +27,7 @@ from .database import (
|
|
|
27
27
|
get_shared_group_chat_ids,
|
|
28
28
|
message_id_exists,
|
|
29
29
|
update_message_tg_id,
|
|
30
|
+
prune_bot_messages,
|
|
30
31
|
search_messages,
|
|
31
32
|
upsert_chat,
|
|
32
33
|
upsert_user,
|
|
@@ -67,9 +68,9 @@ _SEARCH_TOOL = {
|
|
|
67
68
|
"Search the full message history across the user's private chat and shared group chats. "
|
|
68
69
|
"Results include both raw messages and archived summaries of older content. "
|
|
69
70
|
"Use whenever the user asks who said something, what someone said, or what was discussed. "
|
|
70
|
-
"Always search before claiming a person has no message history
|
|
71
|
-
"Run the search immediately when it would help answer the question
|
|
72
|
-
"All filters are optional
|
|
71
|
+
"Always search before claiming a person has no message history - do not assume from context alone. "
|
|
72
|
+
"Run the search immediately when it would help answer the question - do not ask the user for permission to search. "
|
|
73
|
+
"All filters are optional - omit them to retrieve recent messages broadly. "
|
|
73
74
|
"Results are ordered most-recent-first by default; use ascending=true for oldest-first."
|
|
74
75
|
),
|
|
75
76
|
"parameters": {
|
|
@@ -180,11 +181,13 @@ class TelegramBot:
|
|
|
180
181
|
|
|
181
182
|
async def tele_tools_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
182
183
|
"""
|
|
183
|
-
List
|
|
184
|
+
List tools available to this bot instance (admin-only, private/group/supergroup).
|
|
184
185
|
|
|
185
186
|
Shows name and description for every tool the bot exposes, including the built-in
|
|
186
187
|
search_messages tool and any webhook tools defined in bot config. Restricted to
|
|
187
|
-
|
|
188
|
+
usernames in bot_owner list. Works in private, group, and supergroup chats;
|
|
189
|
+
channels always receive an admin-only denial. In group/supergroup chats, only
|
|
190
|
+
tools with allow_groups=True are shown.
|
|
188
191
|
|
|
189
192
|
Args:
|
|
190
193
|
update: Telegram Update object containing the /tools message.
|
|
@@ -192,15 +195,15 @@ class TelegramBot:
|
|
|
192
195
|
"""
|
|
193
196
|
uname = update.message.from_user.username
|
|
194
197
|
chat_type = update.message.chat.type
|
|
195
|
-
if uname not in self.telegram['owners'] or chat_type
|
|
198
|
+
if uname not in self.telegram['owners'] or chat_type not in ('private', 'group', 'supergroup'):
|
|
196
199
|
await update.message.reply_text(_MSG_ADMIN_ONLY)
|
|
197
200
|
return
|
|
201
|
+
|
|
198
202
|
def _first_sentence(text: str) -> str:
|
|
199
203
|
return text.split('. ')[0].rstrip('.!?') + '.'
|
|
200
204
|
|
|
201
205
|
lines = ["Registered tools:"]
|
|
202
|
-
|
|
203
|
-
for schema in self.webhook_schemas:
|
|
206
|
+
for schema in self._build_tool_list(chat_type, uname):
|
|
204
207
|
lines.append(f"- {schema['name']} - {_first_sentence(schema['description'])}")
|
|
205
208
|
await update.message.reply_text('\n'.join(lines))
|
|
206
209
|
|
|
@@ -555,7 +558,12 @@ class TelegramBot:
|
|
|
555
558
|
with speaker name and timestamp if the replied-to message is not in context, or an
|
|
556
559
|
empty string if already present.
|
|
557
560
|
|
|
558
|
-
|
|
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:
|
|
559
567
|
1. Memory - reply_to_message.message_id in conv._loaded_message_ids (O(1)).
|
|
560
568
|
In-session bot replies are added here immediately after each send.
|
|
561
569
|
2. DB fallback - query for tg_message_id in the messages table.
|
|
@@ -563,6 +571,8 @@ class TelegramBot:
|
|
|
563
571
|
stored for assistant messages after each send.
|
|
564
572
|
|
|
565
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.
|
|
566
576
|
Correctly re-surfaces bot messages that are no longer in context (e.g. post-/forget).
|
|
567
577
|
|
|
568
578
|
Args:
|
|
@@ -574,61 +584,98 @@ class TelegramBot:
|
|
|
574
584
|
or empty string if the replied-to message is already in context.
|
|
575
585
|
"""
|
|
576
586
|
reply = msg.reply_to_message
|
|
577
|
-
if not reply or not reply.text
|
|
587
|
+
if not reply or not reply.text:
|
|
578
588
|
return ''
|
|
579
589
|
|
|
580
590
|
r_tg_id = reply.message_id
|
|
581
|
-
|
|
582
|
-
# Tier 1: memory check
|
|
583
591
|
conv = self.conversations.get(msg.chat.id)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if await message_id_exists(msg.chat.id, r_tg_id):
|
|
589
|
-
return ''
|
|
592
|
+
is_our_reply = (
|
|
593
|
+
reply.from_user is not None and
|
|
594
|
+
reply.from_user.id == self.telegram['bot_id']
|
|
595
|
+
)
|
|
590
596
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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:
|
|
594
610
|
conv._loaded_message_ids.add(r_tg_id)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
611
|
+
if reply.from_user:
|
|
612
|
+
sender = reply.from_user
|
|
613
|
+
name_parts = [p for p in [sender.first_name, sender.last_name] if p]
|
|
614
|
+
name = ' '.join(name_parts) if name_parts else (sender.username or 'unknown')
|
|
615
|
+
elif reply.sender_chat:
|
|
616
|
+
name = reply.sender_chat.title or reply.sender_chat.username or 'unknown'
|
|
617
|
+
else:
|
|
618
|
+
name = 'unknown'
|
|
598
619
|
dt = format_dt(reply.date)
|
|
599
620
|
return f"[Replying to {name}, {dt}]: {reply.text}\n"
|
|
600
621
|
|
|
601
|
-
def
|
|
622
|
+
async def _store_bot_message(self, msg: Message) -> None:
|
|
602
623
|
"""
|
|
603
|
-
|
|
624
|
+
Persist a foreign bot's message to the DB for ambient group context.
|
|
604
625
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
Telegram does not distinguish bots from regular users in MENTION entities; this
|
|
609
|
-
method detects any foreign @mention. Deflections skip the read receipt -- the 👀
|
|
610
|
-
reaction is only sent when we are going to generate a full response.
|
|
626
|
+
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).
|
|
611
629
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
630
|
+
Args:
|
|
631
|
+
msg: The Telegram Message from a foreign bot (msg.from_user.is_bot must be True).
|
|
632
|
+
"""
|
|
633
|
+
if not msg.text:
|
|
634
|
+
return
|
|
635
|
+
bot_user = msg.from_user
|
|
636
|
+
await upsert_user(bot_user.id, bot_user.username, bot_user.first_name, bot_user.last_name, is_bot=True)
|
|
637
|
+
await upsert_chat(msg.chat.id, msg.chat.type, msg.chat.title or msg.chat.username or '')
|
|
638
|
+
existing = await message_id_exists(msg.chat.id, msg.message_id)
|
|
639
|
+
if not existing:
|
|
640
|
+
await insert_message(
|
|
641
|
+
chat_id=msg.chat.id,
|
|
642
|
+
user_id=bot_user.id,
|
|
643
|
+
role='user',
|
|
644
|
+
content=msg.text,
|
|
645
|
+
is_private=False,
|
|
646
|
+
tg_message_id=msg.message_id,
|
|
647
|
+
)
|
|
648
|
+
await prune_bot_messages(msg.chat.id)
|
|
649
|
+
|
|
650
|
+
def _exclusive_foreign_mention(self, msg: Message) -> str | None:
|
|
651
|
+
"""
|
|
652
|
+
Return the first foreign @mention if all @mention entities are exclusively foreign.
|
|
653
|
+
|
|
654
|
+
Used in the reply-to-bot path: when the user threads Kowi's message but addresses
|
|
655
|
+
a different account via @mention, return that account's username so the caller can
|
|
656
|
+
detect a redirect. Returns None when we are also @mentioned (co-mention),
|
|
657
|
+
when there are no @mention entities, or when msg.entities is absent.
|
|
615
658
|
|
|
616
659
|
Args:
|
|
617
660
|
msg: The incoming Telegram Message object.
|
|
618
661
|
|
|
619
662
|
Returns:
|
|
620
|
-
The @username string (with leading @)
|
|
621
|
-
|
|
663
|
+
The first foreign @username string (with leading @) if all @mentions are
|
|
664
|
+
foreign, or None if we are co-mentioned or no @mention entities exist.
|
|
622
665
|
"""
|
|
623
666
|
if not msg.entities:
|
|
624
667
|
return None
|
|
625
668
|
our_username = self.telegram['username'].lower()
|
|
669
|
+
first_foreign = None
|
|
626
670
|
for entity in msg.entities:
|
|
627
671
|
if entity.type == MessageEntity.MENTION:
|
|
628
672
|
mentioned = msg.parse_entity(entity)
|
|
629
|
-
if mentioned.lstrip('@').lower()
|
|
630
|
-
return
|
|
631
|
-
|
|
673
|
+
if mentioned.lstrip('@').lower() == our_username:
|
|
674
|
+
return None
|
|
675
|
+
if first_foreign is None:
|
|
676
|
+
first_foreign = mentioned
|
|
677
|
+
return first_foreign
|
|
678
|
+
|
|
632
679
|
|
|
633
680
|
async def _send_read_receipt(self, msg: Message, context: ContextTypes.DEFAULT_TYPE):
|
|
634
681
|
"""
|
|
@@ -662,6 +709,11 @@ class TelegramBot:
|
|
|
662
709
|
- User mentions the bot by initials (set via config)
|
|
663
710
|
- User directly replies to one of the bot's messages (Telegram reply-to feature)
|
|
664
711
|
|
|
712
|
+
Path A - foreign bot message (standalone update): Stores the message for ambient context and returns without LLM response.
|
|
713
|
+
Path B - human reply to foreign bot: Stores the replied-to message before normal routing (fires regardless of whether we are triggered).
|
|
714
|
+
Nickname/initials triggers always engage even when foreign bots are also @mentioned.
|
|
715
|
+
Reply-to-bot trigger yields silently when the message is exclusively addressed to a foreign @account.
|
|
716
|
+
|
|
665
717
|
In private chats, the bot responds to all messages (Telegram does not deliver bot-to-bot DMs).
|
|
666
718
|
|
|
667
719
|
When the triggering message is itself a reply to a non-bot message not already in context,
|
|
@@ -680,10 +732,23 @@ class TelegramBot:
|
|
|
680
732
|
return
|
|
681
733
|
(msg, chat, user) = validated
|
|
682
734
|
|
|
683
|
-
#
|
|
735
|
+
# Path A: foreign bot message delivered directly as a standalone update (rare).
|
|
736
|
+
# Store for ambient context and return - no LLM response triggered by bot messages.
|
|
684
737
|
if user.is_bot and user.id != self.telegram['bot_id'] and chat.type in ('group', 'supergroup'):
|
|
738
|
+
await self._store_bot_message(msg)
|
|
685
739
|
return
|
|
686
740
|
|
|
741
|
+
# Path B: capture a foreign bot's message when a human replies to it.
|
|
742
|
+
# Fires regardless of whether the reply also triggers us.
|
|
743
|
+
if (
|
|
744
|
+
chat.type in ('group', 'supergroup') and
|
|
745
|
+
msg.reply_to_message is not None and
|
|
746
|
+
msg.reply_to_message.from_user is not None and
|
|
747
|
+
msg.reply_to_message.from_user.is_bot and
|
|
748
|
+
msg.reply_to_message.from_user.id != self.telegram['bot_id']
|
|
749
|
+
):
|
|
750
|
+
await self._store_bot_message(msg.reply_to_message)
|
|
751
|
+
|
|
687
752
|
# If it's a group text, only reply if the bot is named
|
|
688
753
|
# The real magic of how the bot behaves is in tele_handle_response()
|
|
689
754
|
response = _MSG_PROCESS_ERROR
|
|
@@ -697,7 +762,7 @@ class TelegramBot:
|
|
|
697
762
|
)
|
|
698
763
|
if exact_word_match(self.telegram['username'], msg.text):
|
|
699
764
|
# Explicit @username mention: strongest signal - respond even if another
|
|
700
|
-
#
|
|
765
|
+
# bot is also @mentioned (both may be intentionally addressed).
|
|
701
766
|
pattern = r'@?\b' + re.escape(self.telegram['username']) + r'\b'
|
|
702
767
|
new_text = re.sub(pattern, '', msg.text).strip()
|
|
703
768
|
reply_context = await self._build_reply_context(msg)
|
|
@@ -705,14 +770,17 @@ class TelegramBot:
|
|
|
705
770
|
response, assistant_db_id = await self.tele_handle_response(new_text, msg, reply_context)
|
|
706
771
|
elif (
|
|
707
772
|
exact_word_match(self.telegram['nickname'], msg.text) or
|
|
708
|
-
exact_word_match(self.telegram['initials'], msg.text)
|
|
709
|
-
is_reply_to_bot
|
|
773
|
+
exact_word_match(self.telegram['initials'], msg.text)
|
|
710
774
|
):
|
|
711
|
-
#
|
|
712
|
-
#
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
775
|
+
# Nickname/initials: always engage - no reliable way to distinguish
|
|
776
|
+
# our name as addressee vs topic from text position alone.
|
|
777
|
+
reply_context = await self._build_reply_context(msg)
|
|
778
|
+
await self._send_read_receipt(msg, context)
|
|
779
|
+
response, assistant_db_id = await self.tele_handle_response(msg.text, msg, reply_context)
|
|
780
|
+
elif is_reply_to_bot:
|
|
781
|
+
# Reply-to-bot: weaker signal - yield silently if the message is
|
|
782
|
+
# exclusively addressed to a foreign account via @mention.
|
|
783
|
+
if self._exclusive_foreign_mention(msg):
|
|
716
784
|
return
|
|
717
785
|
reply_context = await self._build_reply_context(msg)
|
|
718
786
|
await self._send_read_receipt(msg, context)
|
|
@@ -892,7 +960,7 @@ class TelegramBot:
|
|
|
892
960
|
Builds an ephemeral message slice that appends a placeholder assistant acknowledgement
|
|
893
961
|
and a user message containing the tool results to conv.messages, then calls the
|
|
894
962
|
provider for a final natural-language answer. The ephemeral messages are not stored
|
|
895
|
-
in conv.messages
|
|
963
|
+
in conv.messages - only the final answer is persisted by the caller.
|
|
896
964
|
|
|
897
965
|
Args:
|
|
898
966
|
tool_call: The ToolCall returned by the first LLM call.
|
|
@@ -1088,6 +1156,21 @@ class TelegramBot:
|
|
|
1088
1156
|
self.llm['prune_threshold'] = floor(0.95 * self.llm['token_limit'])
|
|
1089
1157
|
self.llm['prune_back_to'] = max(0, self.llm['prune_threshold'] - 500)
|
|
1090
1158
|
|
|
1159
|
+
# Patch identity placeholders now that both self.llm and self.telegram are set.
|
|
1160
|
+
# _tele_info() has already populated username, nickname, and initials.
|
|
1161
|
+
identity_line = (
|
|
1162
|
+
f"\n- Your Telegram username is @{self.telegram['username']}, "
|
|
1163
|
+
f"nickname '{self.telegram['nickname']}', "
|
|
1164
|
+
f"initials '{self.telegram['initials']}'. "
|
|
1165
|
+
"Users in group chats may address you by any of these. "
|
|
1166
|
+
"Do not sign or close your messages with your name, initials, or username."
|
|
1167
|
+
)
|
|
1168
|
+
self.llm['prompt'] = re.sub(
|
|
1169
|
+
r'\n-?\s*Your Telegram username is.*',
|
|
1170
|
+
identity_line,
|
|
1171
|
+
self.llm['prompt'],
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1091
1174
|
# Bot is now ready and active by default
|
|
1092
1175
|
self._online = True
|
|
1093
1176
|
|
|
@@ -47,6 +47,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|
|
47
47
|
username TEXT,
|
|
48
48
|
first_name TEXT,
|
|
49
49
|
last_name TEXT,
|
|
50
|
+
is_bot INTEGER NOT NULL DEFAULT 0,
|
|
50
51
|
private_mode INTEGER NOT NULL DEFAULT 0,
|
|
51
52
|
previous_names TEXT,
|
|
52
53
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
@@ -117,7 +118,7 @@ async def init_db() -> None:
|
|
|
117
118
|
Migrations applied to existing databases:
|
|
118
119
|
messages - adds reply_to_id, tg_message_id, archived_at; drops username, is_foreign_bot.
|
|
119
120
|
chats - adds chat_type, previous_titles.
|
|
120
|
-
users - adds private_mode, previous_names; merges and drops legacy private_mode table.
|
|
121
|
+
users - adds private_mode, previous_names, is_bot; merges and drops legacy private_mode table.
|
|
121
122
|
summary_archive - created (tier 1 and 2 conversation summaries).
|
|
122
123
|
"""
|
|
123
124
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
@@ -158,6 +159,8 @@ async def init_db() -> None:
|
|
|
158
159
|
await db.execute("ALTER TABLE users ADD COLUMN private_mode INTEGER NOT NULL DEFAULT 0")
|
|
159
160
|
if 'previous_names' not in users_cols:
|
|
160
161
|
await db.execute("ALTER TABLE users ADD COLUMN previous_names TEXT")
|
|
162
|
+
if 'is_bot' not in users_cols:
|
|
163
|
+
await db.execute("ALTER TABLE users ADD COLUMN is_bot INTEGER NOT NULL DEFAULT 0")
|
|
161
164
|
tables = {row[0] async for row in await db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
162
165
|
if 'private_mode' in tables:
|
|
163
166
|
await db.execute(
|
|
@@ -207,6 +210,40 @@ async def insert_message(
|
|
|
207
210
|
return cursor.lastrowid
|
|
208
211
|
|
|
209
212
|
|
|
213
|
+
async def prune_bot_messages(chat_id: int, cap: int = 50) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Delete the oldest bot-authored rows for a chat when the per-chat cap is exceeded.
|
|
216
|
+
|
|
217
|
+
Called after every bot message insert to keep ambient peer bot context from growing
|
|
218
|
+
unboundedly. Deletes rows where the sender is marked is_bot=1 in the users table,
|
|
219
|
+
oldest first, until at most `cap` such rows remain for the chat.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
chat_id: Telegram chat ID to prune.
|
|
223
|
+
cap: Maximum number of bot message rows to retain per chat.
|
|
224
|
+
"""
|
|
225
|
+
async with aiosqlite.connect(get_db_path()) as db:
|
|
226
|
+
async with db.execute(
|
|
227
|
+
"SELECT COUNT(*) FROM messages m "
|
|
228
|
+
"JOIN users u ON m.user_id = u.user_id "
|
|
229
|
+
"WHERE m.chat_id = ? AND u.is_bot = 1",
|
|
230
|
+
(chat_id,),
|
|
231
|
+
) as cur:
|
|
232
|
+
(count,) = await cur.fetchone()
|
|
233
|
+
if count > cap:
|
|
234
|
+
excess = count - cap
|
|
235
|
+
await db.execute(
|
|
236
|
+
"DELETE FROM messages WHERE id IN ("
|
|
237
|
+
" SELECT m.id FROM messages m "
|
|
238
|
+
" JOIN users u ON m.user_id = u.user_id "
|
|
239
|
+
" WHERE m.chat_id = ? AND u.is_bot = 1 "
|
|
240
|
+
" ORDER BY m.created_at ASC, m.id ASC LIMIT ?"
|
|
241
|
+
")",
|
|
242
|
+
(chat_id, excess),
|
|
243
|
+
)
|
|
244
|
+
await db.commit()
|
|
245
|
+
|
|
246
|
+
|
|
210
247
|
async def update_message_tg_id(db_id: int, tg_message_id: int) -> None:
|
|
211
248
|
"""
|
|
212
249
|
Store the Telegram message ID on an assistant row after send.
|
|
@@ -603,6 +640,7 @@ async def upsert_user(
|
|
|
603
640
|
username: Optional[str],
|
|
604
641
|
first_name: Optional[str],
|
|
605
642
|
last_name: Optional[str],
|
|
643
|
+
is_bot: bool = False,
|
|
606
644
|
) -> None:
|
|
607
645
|
"""
|
|
608
646
|
Insert or update a user record. Called on every interaction to capture name changes.
|
|
@@ -615,6 +653,7 @@ async def upsert_user(
|
|
|
615
653
|
username: @handle without @, may be None.
|
|
616
654
|
first_name: Telegram first name, may be None.
|
|
617
655
|
last_name: Telegram last name, may be None.
|
|
656
|
+
is_bot: True for Telegram bot accounts; stored so context loaders can label them.
|
|
618
657
|
"""
|
|
619
658
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
620
659
|
async with db.execute(
|
|
@@ -645,12 +684,12 @@ async def upsert_user(
|
|
|
645
684
|
updated_prev = prev
|
|
646
685
|
|
|
647
686
|
await db.execute(
|
|
648
|
-
"INSERT INTO users (user_id, username, first_name, last_name, previous_names) VALUES (?, ?, ?, ?, ?) "
|
|
687
|
+
"INSERT INTO users (user_id, username, first_name, last_name, is_bot, previous_names) VALUES (?, ?, ?, ?, ?, ?) "
|
|
649
688
|
"ON CONFLICT(user_id) DO UPDATE SET "
|
|
650
689
|
"username = excluded.username, first_name = excluded.first_name, "
|
|
651
|
-
"last_name = excluded.last_name,
|
|
652
|
-
"updated_at = ?",
|
|
653
|
-
(user_id, username, first_name, last_name, updated_prev, now_iso()),
|
|
690
|
+
"last_name = excluded.last_name, is_bot = excluded.is_bot, "
|
|
691
|
+
"previous_names = excluded.previous_names, updated_at = ?",
|
|
692
|
+
(user_id, username, first_name, last_name, int(is_bot), updated_prev, now_iso()),
|
|
654
693
|
)
|
|
655
694
|
await db.commit()
|
|
656
695
|
|
|
@@ -127,6 +127,7 @@ INIT_BOT_CONFIG_COMMENTS = {
|
|
|
127
127
|
# Teaches an LLM how cross-chat memory works without requiring persona authors to include it.
|
|
128
128
|
_SYSTEM_APPENDIX = (
|
|
129
129
|
"## System\n"
|
|
130
|
+
"- Your Telegram username is @(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 or close your messages with your name, initials, or username.\n"
|
|
130
131
|
"- You have access to past messages across chats and a search tool - use them to inform your responses, but never quote or volunteer content from other chats unprompted, especially in group settings.\n"
|
|
131
132
|
"- Always invoke the search tool before concluding a message does not exist.\n"
|
|
132
133
|
"- Messages marked private are never shared between chats.\n"
|
|
@@ -11,7 +11,7 @@ import logging
|
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
13
|
import socket
|
|
14
|
-
from urllib.parse import quote, urlparse, urlunparse
|
|
14
|
+
from urllib.parse import parse_qs, quote, urlparse, urlunparse
|
|
15
15
|
|
|
16
16
|
import httpx
|
|
17
17
|
|
|
@@ -591,7 +591,11 @@ async def execute_webhook(tool_def: dict, arguments: dict) -> str:
|
|
|
591
591
|
try:
|
|
592
592
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
593
593
|
if method == 'GET':
|
|
594
|
-
|
|
594
|
+
# Merge any static query params already in the URL (e.g. ?appid=KEY) with runtime args -
|
|
595
|
+
# httpx replaces the query string when 'params=' is passed separately, dropping static ones.
|
|
596
|
+
url_params = {k: v[0] for k, v in parse_qs(parsed.query).items()}
|
|
597
|
+
merged = {**url_params, **args}
|
|
598
|
+
resp = await client.get(base_url, params=merged if merged else None, headers=headers)
|
|
595
599
|
else:
|
|
596
600
|
resp = await client.request(method, url, json=args if args else None, headers=headers)
|
|
597
601
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.14.0
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -135,7 +135,7 @@ The bot responds in groups when you:
|
|
|
135
135
|
- Mention the bot by nickname or initials (configured via `config.yaml`)
|
|
136
136
|
- Reply directly to one of the bot's messages
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
When multiple bots are @mentioned in the same message, the bot coexists: if you mention the bot's nickname or initials, or reply to its message, the bot always engages (you may be intentionally addressing both bots). If the only trigger is a reply to the bot's message AND the message exclusively addresses a different bot via @mention (no mention of this bot), the bot yields silently - this supports threaded context without redundant responses.
|
|
139
139
|
|
|
140
140
|
### Private Chat Behavior
|
|
141
141
|
In private chats, the bot responds to all your messages. If you reply to an earlier message in the conversation that is not already in the bot's context window, that message is automatically surfaced as inline context so the bot can understand the full conversation thread.
|
|
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
|