TeLLMgramBot 3.10.2__tar.gz → 3.10.3__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 (24) hide show
  1. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/PKG-INFO +2 -1
  2. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/README.md +1 -0
  3. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/database.py +103 -34
  4. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/initialize.py +1 -0
  5. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/PKG-INFO +2 -1
  6. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/setup.py +1 -1
  7. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/LICENSE +0 -0
  8. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/TeLLMgramBot.py +0 -0
  9. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/__init__.py +0 -0
  10. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/conversation.py +0 -0
  11. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/message_handlers.py +0 -0
  12. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/models.py +0 -0
  13. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/__init__.py +0 -0
  14. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
  15. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/base.py +0 -0
  16. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/factory.py +0 -0
  17. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/openai_provider.py +0 -0
  18. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/utils.py +0 -0
  19. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/web_utils.py +0 -0
  20. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
  21. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
  22. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/requires.txt +0 -0
  23. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/top_level.txt +0 -0
  24. {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.10.2
3
+ Version: 3.10.3
4
4
  Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
5
5
  Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
6
6
  Author: Digital Heresy
@@ -44,6 +44,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
44
44
  * Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
45
45
  * Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
46
46
  * All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
47
+ * Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
47
48
  * Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
48
49
  * The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
49
50
  * This eliminates amnesia when switching between private and group chats.
@@ -12,6 +12,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
12
12
  * Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
13
13
  * Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
14
14
  * All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
15
+ * Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
15
16
  * Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
16
17
  * The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
17
18
  * This eliminates amnesia when switching between private and group chats.
@@ -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 json
9
10
  import os
10
11
  from typing import Optional
11
12
 
@@ -42,19 +43,21 @@ CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id, created_at)
42
43
  CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id, created_at);
43
44
 
44
45
  CREATE TABLE IF NOT EXISTS users (
45
- user_id INTEGER PRIMARY KEY,
46
- username TEXT,
47
- first_name TEXT,
48
- last_name TEXT,
49
- private_mode INTEGER NOT NULL DEFAULT 0,
50
- updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
46
+ user_id INTEGER PRIMARY KEY,
47
+ username TEXT,
48
+ first_name TEXT,
49
+ last_name TEXT,
50
+ private_mode INTEGER NOT NULL DEFAULT 0,
51
+ previous_names TEXT,
52
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
51
53
  );
52
54
 
53
55
  CREATE TABLE IF NOT EXISTS chats (
54
- chat_id INTEGER PRIMARY KEY,
55
- chat_type TEXT,
56
- chat_title TEXT,
57
- updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
56
+ chat_id INTEGER PRIMARY KEY,
57
+ chat_type TEXT,
58
+ chat_title TEXT,
59
+ previous_titles TEXT,
60
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
58
61
  );
59
62
  """
60
63
 
@@ -71,7 +74,7 @@ def set_db_filename(filename: str) -> None:
71
74
  Args:
72
75
  filename: DB filename, e.g. 'MyBot_conversations.db'. A '.db' suffix is
73
76
  appended automatically if not already present. Must be a plain
74
- basename path separators and absolute paths are rejected.
77
+ basename - path separators and absolute paths are rejected.
75
78
 
76
79
  Raises:
77
80
  ValueError: If filename contains path components or is absolute.
@@ -133,14 +136,19 @@ async def init_db() -> None:
133
136
  # Migration: drop username column now stored in the normalized users table.
134
137
  if 'username' in existing:
135
138
  await db.execute("ALTER TABLE messages DROP COLUMN username")
136
- # Migration: add chat_type to chats table if absent.
139
+ # Migration: add chat_type and previous_titles to chats table if absent.
137
140
  chats_cols = {row[1] async for row in await db.execute("PRAGMA table_info(chats)")}
138
141
  if 'chat_type' not in chats_cols:
139
142
  await db.execute("ALTER TABLE chats ADD COLUMN chat_type TEXT")
143
+ if 'previous_titles' not in chats_cols:
144
+ await db.execute("ALTER TABLE chats ADD COLUMN previous_titles TEXT")
140
145
  # Migration: merge private_mode table into users.private_mode and drop the old table.
146
+ # Also add previous_names column for name history tracking.
141
147
  users_cols = {row[1] async for row in await db.execute("PRAGMA table_info(users)")}
142
148
  if 'private_mode' not in users_cols:
143
149
  await db.execute("ALTER TABLE users ADD COLUMN private_mode INTEGER NOT NULL DEFAULT 0")
150
+ if 'previous_names' not in users_cols:
151
+ await db.execute("ALTER TABLE users ADD COLUMN previous_names TEXT")
144
152
  tables = {row[0] async for row in await db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
145
153
  if 'private_mode' in tables:
146
154
  await db.execute(
@@ -595,7 +603,10 @@ async def upsert_user(
595
603
  Insert or update a user record in the users table.
596
604
 
597
605
  Always reflects the latest Telegram profile data. Called on every interaction
598
- so that name changes are picked up automatically.
606
+ so that name changes are picked up automatically. When first/last name changes,
607
+ the old display name (first_name + space + last_name) is appended to the
608
+ previous_names column (JSON array, deduplicated) to enable search by
609
+ historical identities.
599
610
 
600
611
  Args:
601
612
  user_id: Telegram user ID.
@@ -604,13 +615,40 @@ async def upsert_user(
604
615
  last_name: Telegram last name, may be None.
605
616
  """
606
617
  async with aiosqlite.connect(get_db_path()) as db:
618
+ async with db.execute(
619
+ "SELECT first_name, last_name, previous_names FROM users WHERE user_id = ?",
620
+ (user_id,),
621
+ ) as cur:
622
+ existing = await cur.fetchone()
623
+
624
+ updated_prev = None
625
+ if existing is not None:
626
+ old_first, old_last, prev = existing
627
+ old_display = f"{old_first or ''} {old_last or ''}".strip()
628
+ new_display = f"{first_name or ''} {last_name or ''}".strip()
629
+ if old_display and old_display != new_display:
630
+ if prev:
631
+ try:
632
+ seen = json.loads(prev)
633
+ if not isinstance(seen, list):
634
+ seen = [seen] if seen else []
635
+ except json.JSONDecodeError:
636
+ seen = [n.strip() for n in prev.split(",") if n.strip()]
637
+ else:
638
+ seen = []
639
+ if old_display not in seen:
640
+ seen.append(old_display)
641
+ updated_prev = json.dumps(seen)
642
+ else:
643
+ updated_prev = prev
644
+
607
645
  await db.execute(
608
- "INSERT INTO users (user_id, username, first_name, last_name) VALUES (?, ?, ?, ?) "
646
+ "INSERT INTO users (user_id, username, first_name, last_name, previous_names) VALUES (?, ?, ?, ?, ?) "
609
647
  "ON CONFLICT(user_id) DO UPDATE SET "
610
648
  "username = excluded.username, first_name = excluded.first_name, "
611
- "last_name = excluded.last_name, "
649
+ "last_name = excluded.last_name, previous_names = excluded.previous_names, "
612
650
  "updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
613
- (user_id, username, first_name, last_name),
651
+ (user_id, username, first_name, last_name, updated_prev),
614
652
  )
615
653
  await db.commit()
616
654
 
@@ -624,7 +662,9 @@ async def upsert_chat(
624
662
  Insert or update a chat record in the chats table.
625
663
 
626
664
  Always reflects the latest Telegram chat metadata. Called on every interaction
627
- so that chat type and title changes are picked up automatically.
665
+ so that chat type and title changes are picked up automatically. When the
666
+ chat_title changes, the old title is appended to the previous_titles column
667
+ (JSON array, deduplicated) to enable search by historical identities.
628
668
 
629
669
  Args:
630
670
  chat_id: Telegram chat ID.
@@ -632,12 +672,38 @@ async def upsert_chat(
632
672
  chat_title: Display name of the group/channel, or None for private chats.
633
673
  """
634
674
  async with aiosqlite.connect(get_db_path()) as db:
675
+ async with db.execute(
676
+ "SELECT chat_title, previous_titles FROM chats WHERE chat_id = ?",
677
+ (chat_id,),
678
+ ) as cur:
679
+ existing = await cur.fetchone()
680
+
681
+ updated_prev = None
682
+ if existing is not None:
683
+ old_title, prev = existing
684
+ if old_title and old_title != chat_title:
685
+ if prev:
686
+ try:
687
+ seen = json.loads(prev)
688
+ if not isinstance(seen, list):
689
+ seen = [seen] if seen else []
690
+ except json.JSONDecodeError:
691
+ seen = [t.strip() for t in prev.split(",") if t.strip()]
692
+ else:
693
+ seen = []
694
+ if old_title not in seen:
695
+ seen.append(old_title)
696
+ updated_prev = json.dumps(seen)
697
+ else:
698
+ updated_prev = prev
699
+
635
700
  await db.execute(
636
- "INSERT INTO chats (chat_id, chat_type, chat_title) VALUES (?, ?, ?) "
701
+ "INSERT INTO chats (chat_id, chat_type, chat_title, previous_titles) VALUES (?, ?, ?, ?) "
637
702
  "ON CONFLICT(chat_id) DO UPDATE SET "
638
703
  "chat_type = excluded.chat_type, chat_title = excluded.chat_title, "
704
+ "previous_titles = excluded.previous_titles, "
639
705
  "updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
640
- (chat_id, chat_type, chat_title),
706
+ (chat_id, chat_type, chat_title, updated_prev),
641
707
  )
642
708
  await db.commit()
643
709
 
@@ -679,9 +745,12 @@ async def search_messages(
679
745
 
680
746
  Resolves speaker_query and chat_query using exact-match-first logic:
681
747
  1. Case-insensitive exact match - if exactly one entity matches, proceed.
682
- 2. Partial LIKE match - if exactly one accessible entity matches, proceed.
683
- Zero matches: colloquial fallback (chat: all accessible; speaker: return empty).
684
- Two or more matches: return an ambiguity sentinel dict instead of results.
748
+ 2. Partial LIKE match (including historical names/titles) - if exactly one accessible
749
+ entity matches, proceed. Searches current username/first_name/last_name for speakers
750
+ and current chat_title for chats; falls back to previous_names and previous_titles
751
+ columns to find entities by their past identities. Zero matches: colloquial fallback
752
+ (chat: all accessible; speaker: return empty). Two or more matches: return an
753
+ ambiguity sentinel dict instead of results.
685
754
 
686
755
  Leading `@` is stripped from speaker_query automatically.
687
756
  Access control: results are always scoped to accessible_chat_ids (the caller's
@@ -716,7 +785,7 @@ async def search_messages(
716
785
  # Resolve speaker_query -> user_ids (exact match first, then LIKE)
717
786
  # Scoped to users who have at least one message in accessible_chat_ids (s3kp).
718
787
  # Batched over accessible_chat_ids to stay within SQLite's 999-parameter limit;
719
- # each batch leaves 4 slots for name-matching params (exact: 3, partial: 4).
788
+ # each batch leaves 5 slots for name-matching params (exact: 3, partial: 5).
720
789
  speaker_ids: Optional[list[int]] = None
721
790
  speaker_amb: Optional[dict] = None
722
791
  if speaker_query:
@@ -746,8 +815,8 @@ async def search_messages(
746
815
  else:
747
816
  like = f"%{q}%"
748
817
  partial_seen: dict[int, tuple] = {}
749
- for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS - 4):
750
- chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS - 4]
818
+ for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS - 5):
819
+ chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS - 5]
751
820
  chunk_ph = ','.join('?' * len(chunk))
752
821
  async with db.execute(
753
822
  f"SELECT DISTINCT u.user_id, "
@@ -756,8 +825,8 @@ async def search_messages(
756
825
  f"FROM users u JOIN messages m ON m.user_id = u.user_id "
757
826
  f"WHERE m.chat_id IN ({chunk_ph}) "
758
827
  f"AND (u.username LIKE ? OR u.first_name LIKE ? OR u.last_name LIKE ? "
759
- f"OR (u.first_name || ' ' || u.last_name) LIKE ?)",
760
- (*chunk, like, like, like, like),
828
+ f"OR (u.first_name || ' ' || u.last_name) LIKE ? OR u.previous_names LIKE ?)",
829
+ (*chunk, like, like, like, like, like),
761
830
  ) as cur:
762
831
  for row in await cur.fetchall():
763
832
  partial_seen.setdefault(row[0], row)
@@ -792,23 +861,23 @@ async def search_messages(
792
861
  # Check for any global match first to distinguish colloquial fallback
793
862
  # (zero global matches) from access-control rejection (global match, not accessible).
794
863
  async with db.execute(
795
- "SELECT 1 FROM chats WHERE chat_title LIKE ? LIMIT 1",
796
- (like,),
864
+ "SELECT 1 FROM chats WHERE chat_title LIKE ? OR previous_titles LIKE ? LIMIT 1",
865
+ (like, like),
797
866
  ) as cur:
798
867
  has_global = await cur.fetchone() is not None
799
868
  if not has_global:
800
869
  pass # Zero global matches: colloquial fallback (resolved_chat_ids stays None)
801
870
  else:
802
871
  # Batch accessible_chat_ids to stay within SQLite's 999-parameter limit;
803
- # reserve 1 slot for the LIKE parameter.
872
+ # reserve 2 slots for the LIKE parameters (chat_title + previous_titles).
804
873
  partial_accessible_seen: dict[int, tuple] = {}
805
- for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS - 1):
806
- chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS - 1]
874
+ for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS - 2):
875
+ chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS - 2]
807
876
  chunk_ph = ','.join('?' * len(chunk))
808
877
  async with db.execute(
809
878
  f"SELECT chat_id, chat_title FROM chats "
810
- f"WHERE chat_title LIKE ? AND chat_id IN ({chunk_ph})",
811
- (like, *chunk),
879
+ f"WHERE (chat_title LIKE ? OR previous_titles LIKE ?) AND chat_id IN ({chunk_ph})",
880
+ (like, like, *chunk),
812
881
  ) as cur:
813
882
  for row in await cur.fetchall():
814
883
  partial_accessible_seen.setdefault(row[0], row)
@@ -122,6 +122,7 @@ _SYSTEM_APPENDIX = (
122
122
  "Always invoke the search tool before concluding a message does not exist.\n"
123
123
  "Messages marked private are never shared between chats.\n"
124
124
  "Never reveal or repeat Telegram user IDs, chat IDs, or any other internal numeric identifiers in your responses.\n"
125
+ "When your context includes a '[Replying to Name, ...]' prefix indicating you were triggered via a reply, acknowledge the original author by name in your response.\n"
125
126
  "Current date and time: (not yet known)\n"
126
127
  )
127
128
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.10.2
3
+ Version: 3.10.3
4
4
  Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
5
5
  Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
6
6
  Author: Digital Heresy
@@ -44,6 +44,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
44
44
  * Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
45
45
  * Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
46
46
  * All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
47
+ * Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
47
48
  * Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
48
49
  * The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
49
50
  * This eliminates amnesia when switching between private and group chats.
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='TeLLMgramBot',
8
- version='3.10.2',
8
+ version='3.10.3',
9
9
  packages=find_packages(),
10
10
  license='MIT',
11
11
  author='Digital Heresy',
File without changes
File without changes