TeLLMgramBot 2.1.2__tar.gz → 2.3.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-2.1.2 → tellmgrambot-2.3.0}/LICENSE +1 -1
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/PKG-INFO +7 -11
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/README.md +4 -8
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot/TeLLMgramBot.py +168 -101
- tellmgrambot-2.3.0/TeLLMgramBot/conversation.py +177 -0
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot/initialize.py +52 -54
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot/message_handlers.py +17 -13
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot/openai_singleton.py +1 -1
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot/tokenGPT.py +18 -10
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot/utils.py +21 -23
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot/web_utils.py +14 -18
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot.egg-info/PKG-INFO +7 -11
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot.egg-info/requires.txt +1 -1
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/setup.py +3 -3
- tellmgrambot-2.1.2/TeLLMgramBot/conversation.py +0 -171
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-2.1.2 → tellmgrambot-2.3.0}/setup.cfg +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: OpenAI GPT, driven by Telegram
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
7
7
|
Author-email: ronin.atx@gmail.com
|
|
8
8
|
License: MIT
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: openai>2.0
|
|
@@ -16,7 +16,7 @@ Requires-Dist: beautifulsoup4
|
|
|
16
16
|
Requires-Dist: typing
|
|
17
17
|
Requires-Dist: validators
|
|
18
18
|
Requires-Dist: tiktoken>=0.12
|
|
19
|
-
Requires-Dist: python-telegram-bot
|
|
19
|
+
Requires-Dist: python-telegram-bot>20.0
|
|
20
20
|
Dynamic: author
|
|
21
21
|
Dynamic: author-email
|
|
22
22
|
Dynamic: description
|
|
@@ -54,8 +54,6 @@ Using Telegram as the interface not only solves "exposing" the interface, but gi
|
|
|
54
54
|
## Directories
|
|
55
55
|
When initializing TeLLMgramBot, the following directories get created:
|
|
56
56
|
* `configs` - Contains bot configuration files.
|
|
57
|
-
* `commands.txt`
|
|
58
|
-
* Users can type `/help` interacting with the to see this file's text get displayed in Telegram.
|
|
59
57
|
* `config.yaml` (can be a different name)
|
|
60
58
|
* This file sets main OpenAI parameters like naming and GPT models to process.
|
|
61
59
|
* The parameter `url_model` is to read URL content, different than `chat_model` that the bot normally uses to interact with the user.
|
|
@@ -64,7 +62,7 @@ When initializing TeLLMgramBot, the following directories get created:
|
|
|
64
62
|
* This important YAML file contains token size parameters for supported OpenAI models.
|
|
65
63
|
* If the first time, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4o`, and `gpt-4o-mini` get populated, but the user can specify more models with token size parameters as needed.
|
|
66
64
|
* `prompts` - Contains prompt files for how the bot interacts with any user.
|
|
67
|
-
* `test_personality.prmpt`
|
|
65
|
+
* `test_personality.prmpt` (can be a different name)
|
|
68
66
|
* This is a sample prompt file as a basis to test this library.
|
|
69
67
|
* The user can create more prompt files as needed for different personalities. See [OpenAI Playground](https://platform.openai.com/playground) to test some ideas.
|
|
70
68
|
* `url_analysis.prmpt`
|
|
@@ -122,12 +120,11 @@ Each file with the associated API key will update its respective environment var
|
|
|
122
120
|
This library includes an example script `test_local.py`, which uses files from the folders `configs` and `prompts` for TeLLMgramBot to process. The bot communicates with OpenAI via the **Responses API**, which replaces the older Chat Completions endpoint.
|
|
123
121
|
1. Ensure the previous sections are followed with the proper API keys and your Telegram bot set.
|
|
124
122
|
2. Install this library via PIP (`pip install TeLLMgramBot`) and then import into your project.
|
|
125
|
-
3. Instantiate the bot by passing in various configuration pieces needed below
|
|
123
|
+
3. Instantiate the bot by passing in various configuration pieces needed below.
|
|
124
|
+
Note the Telegram bot's full name and username auto-populate before startup.
|
|
126
125
|
```
|
|
127
126
|
telegram_bot = TeLLMgramBot.TelegramBot(
|
|
128
|
-
bot_username = <Bot username like 'friendly_bot'>,
|
|
129
127
|
bot_owner = <Bot owner's Telegram username>,
|
|
130
|
-
bot_name = <Bot name like 'Friendly Bot'>,
|
|
131
128
|
bot_nickname = <Bot nickname like 'Botty'>,
|
|
132
129
|
bot_initials = <Bot initials like 'FB'>,
|
|
133
130
|
chat_model = <Conversation model like 'gpt-4o-mini'>,
|
|
@@ -142,8 +139,7 @@ This library includes an example script `test_local.py`, which uses files from t
|
|
|
142
139
|
telegram_bot.start_polling()
|
|
143
140
|
```
|
|
144
141
|
Once you see `TeLLMgramBot polling...`, the bot is online in Telegram.
|
|
145
|
-
5.
|
|
146
|
-
6. Only as owner, type `/start` directly to the bot to initiate user conversations.
|
|
142
|
+
5. Converse! Type `/help` for all available commands.
|
|
147
143
|
|
|
148
144
|
## Resources
|
|
149
145
|
* GitHub repository [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) has guides to create a Telegram bot.
|
|
@@ -24,8 +24,6 @@ Using Telegram as the interface not only solves "exposing" the interface, but gi
|
|
|
24
24
|
## Directories
|
|
25
25
|
When initializing TeLLMgramBot, the following directories get created:
|
|
26
26
|
* `configs` - Contains bot configuration files.
|
|
27
|
-
* `commands.txt`
|
|
28
|
-
* Users can type `/help` interacting with the to see this file's text get displayed in Telegram.
|
|
29
27
|
* `config.yaml` (can be a different name)
|
|
30
28
|
* This file sets main OpenAI parameters like naming and GPT models to process.
|
|
31
29
|
* The parameter `url_model` is to read URL content, different than `chat_model` that the bot normally uses to interact with the user.
|
|
@@ -34,7 +32,7 @@ When initializing TeLLMgramBot, the following directories get created:
|
|
|
34
32
|
* This important YAML file contains token size parameters for supported OpenAI models.
|
|
35
33
|
* If the first time, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4o`, and `gpt-4o-mini` get populated, but the user can specify more models with token size parameters as needed.
|
|
36
34
|
* `prompts` - Contains prompt files for how the bot interacts with any user.
|
|
37
|
-
* `test_personality.prmpt`
|
|
35
|
+
* `test_personality.prmpt` (can be a different name)
|
|
38
36
|
* This is a sample prompt file as a basis to test this library.
|
|
39
37
|
* The user can create more prompt files as needed for different personalities. See [OpenAI Playground](https://platform.openai.com/playground) to test some ideas.
|
|
40
38
|
* `url_analysis.prmpt`
|
|
@@ -92,12 +90,11 @@ Each file with the associated API key will update its respective environment var
|
|
|
92
90
|
This library includes an example script `test_local.py`, which uses files from the folders `configs` and `prompts` for TeLLMgramBot to process. The bot communicates with OpenAI via the **Responses API**, which replaces the older Chat Completions endpoint.
|
|
93
91
|
1. Ensure the previous sections are followed with the proper API keys and your Telegram bot set.
|
|
94
92
|
2. Install this library via PIP (`pip install TeLLMgramBot`) and then import into your project.
|
|
95
|
-
3. Instantiate the bot by passing in various configuration pieces needed below
|
|
93
|
+
3. Instantiate the bot by passing in various configuration pieces needed below.
|
|
94
|
+
Note the Telegram bot's full name and username auto-populate before startup.
|
|
96
95
|
```
|
|
97
96
|
telegram_bot = TeLLMgramBot.TelegramBot(
|
|
98
|
-
bot_username = <Bot username like 'friendly_bot'>,
|
|
99
97
|
bot_owner = <Bot owner's Telegram username>,
|
|
100
|
-
bot_name = <Bot name like 'Friendly Bot'>,
|
|
101
98
|
bot_nickname = <Bot nickname like 'Botty'>,
|
|
102
99
|
bot_initials = <Bot initials like 'FB'>,
|
|
103
100
|
chat_model = <Conversation model like 'gpt-4o-mini'>,
|
|
@@ -112,8 +109,7 @@ This library includes an example script `test_local.py`, which uses files from t
|
|
|
112
109
|
telegram_bot.start_polling()
|
|
113
110
|
```
|
|
114
111
|
Once you see `TeLLMgramBot polling...`, the bot is online in Telegram.
|
|
115
|
-
5.
|
|
116
|
-
6. Only as owner, type `/start` directly to the bot to initiate user conversations.
|
|
112
|
+
5. Converse! Type `/help` for all available commands.
|
|
117
113
|
|
|
118
114
|
## Resources
|
|
119
115
|
* GitHub repository [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) has guides to create a Telegram bot.
|
|
@@ -2,63 +2,93 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import json
|
|
5
|
+
import asyncio
|
|
5
6
|
from tempfile import gettempdir
|
|
6
7
|
from math import floor
|
|
7
|
-
from telegram import Update
|
|
8
|
+
from telegram import Bot, Update, Message, Chat, User
|
|
9
|
+
from telegram.constants import MessageLimit
|
|
8
10
|
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
|
9
11
|
import openai
|
|
10
12
|
|
|
11
13
|
from .openai_singleton import OpenAIClientSingleton
|
|
12
|
-
from .initialize import INIT_BOT_CONFIG, init_structure, init_bot_config, init_bot_prompt
|
|
14
|
+
from .initialize import INIT_BOT_CONFIG, init_structure, init_bot_config, init_bot_prompt
|
|
13
15
|
from .conversation import Conversation
|
|
14
16
|
from .tokenGPT import TokenGPT
|
|
15
17
|
from .message_handlers import handle_greetings, handle_common_queries, handle_url_ask
|
|
16
18
|
from .utils import read_yaml, read_text, exact_word_match, generate_error_path, log_error
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
20
20
|
class TelegramBot:
|
|
21
|
-
|
|
21
|
+
"""
|
|
22
|
+
The bridge between the Telegram interface and Large Langage Model (LLM) using OpenAI's GPT models.
|
|
23
|
+
- Tokens help retain conversation messages between the Telegram bot assistant and the user.
|
|
24
|
+
- URLs in [square brackets] can be supplied on how the bot should interpret it.
|
|
25
|
+
- For more information, see README or: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
26
|
+
"""
|
|
27
|
+
async def _tele_info(self):
|
|
28
|
+
"""Fetch the Telegram bot's information before running application."""
|
|
29
|
+
try:
|
|
30
|
+
bot = await Bot(token=os.environ['TELLMGRAMBOT_TELEGRAM_API_KEY']).get_me()
|
|
31
|
+
self.telegram['username'] = bot.username
|
|
32
|
+
self.telegram['name'] = bot.full_name
|
|
33
|
+
except Exception as e:
|
|
34
|
+
# Fail fast if the bot's identity cannot be fetched to avoid
|
|
35
|
+
# continuing with an uninitialized self.telegram['username'].
|
|
36
|
+
log_error(e, error_type='Telegram', error_filename=self.error_log)
|
|
37
|
+
raise
|
|
38
|
+
|
|
22
39
|
async def tele_commands(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
"""Show all the commands available when the user sends `/help` to the bot."""
|
|
41
|
+
await update.message.reply_text("TeLLMgramBot commands:\n"
|
|
42
|
+
"/nick - Set your nickname (e.g. \"/nick B0b #2\").\n"
|
|
43
|
+
"/forget - Clear all of your conversations.\n"
|
|
44
|
+
"/help - Display this usage information.\n\n"
|
|
45
|
+
"You must have a username to converse (for now).\n"
|
|
46
|
+
"Chat history will still show in your Telegram client.\n\n"
|
|
47
|
+
"Administrator-only commands:\n"
|
|
48
|
+
"/start - Go online to receive new responses (default).\n"
|
|
49
|
+
"/stop - Go offline to prevent new responses.\n"
|
|
50
|
+
)
|
|
28
51
|
|
|
29
|
-
# Start command only runs by the 'bot_owner' specified in configuration file
|
|
30
52
|
async def tele_start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
53
|
+
"""A bot_owner-only command to go online and handle any new messages."""
|
|
31
54
|
uname = update.message.from_user.username
|
|
32
55
|
if uname == self.telegram['owner']:
|
|
56
|
+
self._online = True
|
|
33
57
|
greeting_text = f"Oh, hello {update.message.from_user.first_name}! Let me get to work!"
|
|
34
58
|
await update.message.reply_text(greeting_text)
|
|
35
|
-
self.started = True
|
|
36
|
-
self.GPTOnline = True
|
|
37
59
|
else:
|
|
38
|
-
await update.message.reply_text("Sorry,
|
|
60
|
+
await update.message.reply_text("Sorry, I can't do that for you.")
|
|
39
61
|
|
|
40
|
-
# Stop command formally closes out the polling loop
|
|
41
62
|
async def tele_stop_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
63
|
+
"""A bot_owner-only command to go offline and not handle any new messages."""
|
|
42
64
|
uname = update.message.from_user.username
|
|
43
65
|
if uname == self.telegram['owner']:
|
|
44
|
-
self.
|
|
66
|
+
self._online = False
|
|
45
67
|
await update.message.reply_text("Sure thing boss, cutting out!")
|
|
46
68
|
else:
|
|
47
69
|
await update.message.reply_text("Sorry, I can't do that for you.")
|
|
48
70
|
|
|
49
|
-
# Let bot know to call the user by a different name other than the Telegram user name
|
|
50
|
-
# noinspection RegExpRedundantEscape,PyArgumentList
|
|
51
71
|
async def tele_nick_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
"""
|
|
73
|
+
Let bot assistant know to call the user by a different name other than the Telegram username.
|
|
74
|
+
Must follow the format of "/nick(@<bot name>)? <nickname>", checked by a regular expression.
|
|
75
|
+
"""
|
|
76
|
+
validated = await self.tele_validate(update)
|
|
77
|
+
if not validated:
|
|
78
|
+
return
|
|
79
|
+
(msg, chat, user) = validated
|
|
80
|
+
|
|
81
|
+
# If it's a group conversation, need to handle commands directly to bot with @<bot name> also
|
|
82
|
+
pattern = re.compile(fr"^\s*/nick(?:@{re.escape(self.telegram['username'])})?\s+(.+?)\s*$", re.IGNORECASE)
|
|
83
|
+
result = pattern.search(msg.text or "")
|
|
84
|
+
if result:
|
|
55
85
|
prompt = f"Please refer to me by my nickname, {result.group(1).strip()}, rather than my user name."
|
|
56
|
-
await
|
|
86
|
+
await msg.reply_text(await self.tele_handle_response(prompt, msg))
|
|
57
87
|
else:
|
|
58
|
-
await
|
|
88
|
+
await msg.reply_text("Please provide a valid nickname after the command like \"/nick B0b #2\".")
|
|
59
89
|
|
|
60
|
-
# Remove the whole conversation including past session logs for peace of mind
|
|
61
90
|
async def tele_forget_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
91
|
+
"""Remove the whole conversation including past session logs for peace of mind."""
|
|
62
92
|
uname = update.message.from_user.username
|
|
63
93
|
if uname in self.conversations:
|
|
64
94
|
self.conversations[uname].clear_interaction()
|
|
@@ -70,17 +100,24 @@ class TelegramBot:
|
|
|
70
100
|
" already asked me to forget about you. Either way, nice to meet you!"
|
|
71
101
|
)
|
|
72
102
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
async def tele_handle_response(self, text: str, msg: Message) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Primary function for handling any response including Generative AI, ensuring:
|
|
106
|
+
- The owner started up the bot assistant for user interactions.
|
|
107
|
+
- User has conversed with the bot assistant before.
|
|
108
|
+
- Depending on regular interaction or URL request, formulate message based
|
|
109
|
+
on the Generative AI model (e.g. OpenAI GPT) and token count checks.
|
|
110
|
+
"""
|
|
76
111
|
# Starting ensures we get some kind of user account details for logging
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
112
|
+
if not self._online:
|
|
113
|
+
return "I'd love to chat, but I am offline at the moment!"
|
|
114
|
+
|
|
115
|
+
# Print information of the user sending message for conversation
|
|
116
|
+
uname = msg.from_user.username
|
|
117
|
+
print(f"User {uname} in {msg.chat.type} chat ID {str(msg.chat.id)}")
|
|
80
118
|
|
|
81
119
|
# For a new session, track if the user has conversed with the Telegram bot before
|
|
82
120
|
# Username is consistent across restarts and different conversation instances
|
|
83
|
-
uname = update.message.from_user.username
|
|
84
121
|
if uname not in self.conversations:
|
|
85
122
|
self.conversations[uname] = Conversation(
|
|
86
123
|
uname,
|
|
@@ -101,16 +138,16 @@ class TelegramBot:
|
|
|
101
138
|
|
|
102
139
|
# Form the assistant's message based on low level easy stuff or send to GPT
|
|
103
140
|
# OpenAI's Responses API relies on the maximum number of tokens the selected model can support
|
|
104
|
-
reply =
|
|
141
|
+
reply = "Sorry, I couldn't process your message! Please contact my creator."
|
|
105
142
|
if handle_greetings(text):
|
|
106
143
|
reply = handle_greetings(text)
|
|
107
144
|
elif handle_common_queries(text):
|
|
108
145
|
reply = handle_common_queries(text)
|
|
109
146
|
elif url_match:
|
|
110
147
|
# URL content is passed into another model to summarize (GPT-4 preferred)
|
|
111
|
-
await
|
|
148
|
+
await msg.reply_text("Sure, give me a moment to look at that URL...")
|
|
112
149
|
reply = await handle_url_ask(text, self.chatgpt['url_model'])
|
|
113
|
-
elif self.
|
|
150
|
+
elif self._online:
|
|
114
151
|
# This is essentially the transition point between quick Telegram replies and GPT
|
|
115
152
|
response = await self.gpt_completion(uname)
|
|
116
153
|
reply = response.choices[0].message.content
|
|
@@ -121,10 +158,10 @@ class TelegramBot:
|
|
|
121
158
|
# If the user is getting closer to the bot's token threshold, warn the first time
|
|
122
159
|
if token_count > self.chatgpt['prune_back_to'] and uname not in self.token_warning:
|
|
123
160
|
reply += ("\n\n"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
161
|
+
"By the way, our conversation will soon reach my token limit, so I may"
|
|
162
|
+
" start forgetting some of our older exchanges. Would you like me to"
|
|
163
|
+
" summarize our conversation so far to keep the main points alive?"
|
|
164
|
+
)
|
|
128
165
|
self.token_warning[uname] = True
|
|
129
166
|
|
|
130
167
|
# Add assistant's message to the user's conversation
|
|
@@ -136,53 +173,81 @@ class TelegramBot:
|
|
|
136
173
|
|
|
137
174
|
return reply
|
|
138
175
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
176
|
+
async def tele_handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
177
|
+
"""Handles the Telegram side of the message, discerning between Private and Group conversation."""
|
|
178
|
+
validated = await self.tele_validate(update)
|
|
179
|
+
if not validated:
|
|
180
|
+
return
|
|
181
|
+
(msg, chat, user) = validated
|
|
144
182
|
|
|
145
183
|
# If it's a group text, only reply if the bot is named
|
|
146
184
|
# The real magic of how the bot behaves is in tele_handle_response()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
185
|
+
response = "Sorry, I couldn't process your message! Please contact my creator."
|
|
186
|
+
if chat.type == 'supergroup' or chat.type == 'group':
|
|
187
|
+
if exact_word_match(self.telegram['username'], msg.text):
|
|
188
|
+
pattern = r'@?\b' + re.escape(self.telegram['username']) + r'\b'
|
|
189
|
+
new_text = re.sub(pattern, '', msg.text).strip()
|
|
190
|
+
response = await self.tele_handle_response(new_text, msg)
|
|
152
191
|
elif (
|
|
153
|
-
exact_word_match(self.telegram['nickname'],
|
|
154
|
-
exact_word_match(self.telegram['initials'],
|
|
192
|
+
exact_word_match(self.telegram['nickname'], msg.text) or
|
|
193
|
+
exact_word_match(self.telegram['initials'], msg.text)
|
|
155
194
|
):
|
|
156
|
-
|
|
157
|
-
response: str = await self.tele_handle_response(text=message_text, update=update)
|
|
195
|
+
response = await self.tele_handle_response(msg.text, msg)
|
|
158
196
|
else:
|
|
159
197
|
return
|
|
160
|
-
elif
|
|
161
|
-
|
|
162
|
-
response: str = await self.tele_handle_response(text=message_text, update=update)
|
|
198
|
+
elif chat.type == 'private':
|
|
199
|
+
response = await self.tele_handle_response(msg.text, msg)
|
|
163
200
|
else:
|
|
164
201
|
return
|
|
165
|
-
await update.message.reply_text(response)
|
|
166
202
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
await
|
|
203
|
+
# Split into smaller chunks since Telegram messages have a maximum text length (likely 4096)
|
|
204
|
+
chunk_length = MessageLimit.MAX_TEXT_LENGTH - 1
|
|
205
|
+
for chunk in [response[i:i+chunk_length] for i in range(0, len(response), chunk_length)]:
|
|
206
|
+
await msg.reply_text(chunk)
|
|
207
|
+
await asyncio.sleep(0.12) # small pause to reduce risk of rate limiting
|
|
208
|
+
|
|
209
|
+
async def tele_validate(self, update: Update) -> tuple[Message, Chat, User] | None:
|
|
210
|
+
"""
|
|
211
|
+
Check if any update through Telegram has a complete message, chat, and user.
|
|
212
|
+
This uses python-telegram-bot's Update `effective` methods for all three.
|
|
213
|
+
"""
|
|
214
|
+
if update is None:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# Any python-telegram-bot 'effective' method helps ensure there are no "None" types
|
|
218
|
+
# Non-user updates like Inline queries or chat member updates makes either one undefined
|
|
219
|
+
msg = update.effective_message
|
|
220
|
+
chat = update.effective_chat
|
|
221
|
+
user = update.effective_user
|
|
222
|
+
|
|
223
|
+
# If there is no chat or valid message of the unique conversation, no reason to reply
|
|
224
|
+
if chat is None or msg is None:
|
|
225
|
+
return None
|
|
226
|
+
# chat.type => 'private', 'group', 'supergroup', 'channel'
|
|
227
|
+
# chat.id = user.id if 'private'
|
|
228
|
+
|
|
229
|
+
# Handle the user sending the message
|
|
230
|
+
if user:
|
|
231
|
+
# This ensures conversations don't overlap (for now, until user.id is implemented)
|
|
232
|
+
uname = getattr(user, "username", None)
|
|
233
|
+
if uname is None:
|
|
234
|
+
await msg.reply_text('Add a username to your Telegram account so that I can talk to you!')
|
|
235
|
+
return None
|
|
172
236
|
else:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
237
|
+
# If not the user chatting directly to the bot, don't bother
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
return (msg, chat, user)
|
|
241
|
+
|
|
242
|
+
async def tele_error(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
243
|
+
"""Handle any conversation error caused on the Telegram side."""
|
|
244
|
+
log_error(context.error, error_type='Telegram', error_filename=self.error_log)
|
|
245
|
+
msg = update.effective_message if update else None
|
|
246
|
+
if msg:
|
|
247
|
+
await msg.reply_text("Sorry, I ran into an error! Please contact my creator.")
|
|
248
|
+
|
|
185
249
|
async def gpt_completion(self, uname: str):
|
|
250
|
+
"""Get the OpenAI response based on bot configuration."""
|
|
186
251
|
try:
|
|
187
252
|
client = OpenAIClientSingleton.get_instance()
|
|
188
253
|
response = await client.chat.completions.create(
|
|
@@ -190,7 +255,6 @@ class TelegramBot:
|
|
|
190
255
|
messages=self.conversations[uname].messages,
|
|
191
256
|
)
|
|
192
257
|
return response
|
|
193
|
-
|
|
194
258
|
except openai.AuthenticationError as e:
|
|
195
259
|
# Handle authentication error
|
|
196
260
|
log_error(e, error_type='OpenAI-Authentication', error_filename=self.error_log)
|
|
@@ -211,47 +275,51 @@ class TelegramBot:
|
|
|
211
275
|
# Catch any other unexpected exceptions
|
|
212
276
|
log_error(e, error_type='Other', error_filename=self.error_log)
|
|
213
277
|
|
|
214
|
-
# The main polling "loop" the user interacts with via Telegram
|
|
215
278
|
def start_polling(self):
|
|
279
|
+
"""The main polling "loop" the user interacts with via Telegram."""
|
|
216
280
|
print(f"TeLLMgramBot {self.telegram['username']} polling...")
|
|
217
281
|
self.telegram['app'].run_polling(poll_interval=self.telegram['pollinterval'])
|
|
218
282
|
print(f"TeLLMgramBot {self.telegram['username']} polling ended.")
|
|
219
283
|
|
|
220
284
|
# Initialization
|
|
221
285
|
def __init__(self,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
286
|
+
bot_owner = INIT_BOT_CONFIG['bot_owner'],
|
|
287
|
+
bot_nickname = INIT_BOT_CONFIG['bot_nickname'],
|
|
288
|
+
bot_initials = INIT_BOT_CONFIG['bot_initials'],
|
|
289
|
+
chat_model = INIT_BOT_CONFIG['chat_model'],
|
|
290
|
+
url_model = INIT_BOT_CONFIG['url_model'],
|
|
291
|
+
token_limit = INIT_BOT_CONFIG['token_limit'],
|
|
292
|
+
persona_temp = INIT_BOT_CONFIG['persona_temp'],
|
|
293
|
+
persona_prompt = INIT_BOT_CONFIG['persona_prompt']
|
|
294
|
+
):
|
|
295
|
+
# Starting to initialize, not online yet
|
|
296
|
+
self._online = False
|
|
297
|
+
|
|
233
298
|
# First provide the main structure if not already there
|
|
234
299
|
init_structure()
|
|
235
300
|
|
|
236
|
-
#
|
|
301
|
+
# Initialize some variables
|
|
237
302
|
self.error_log = generate_error_path()
|
|
238
|
-
self.
|
|
239
|
-
self.GPTOnline = False
|
|
240
|
-
self.token_warning = {} # Determines whether user has reached token limit by OpenAI model
|
|
303
|
+
self.token_warning = {} # Determines whether user has reached token limit by AI model
|
|
241
304
|
self.conversations = {} # Provides Conversation class per user based on bot response
|
|
242
|
-
|
|
243
|
-
# Get Telegram Spun Up
|
|
244
|
-
# noinspection PyDictCreation
|
|
245
305
|
self.telegram = {
|
|
246
306
|
'owner' : bot_owner,
|
|
247
|
-
'username' : bot_username,
|
|
248
307
|
'nickname' : bot_nickname,
|
|
249
308
|
'initials' : bot_initials,
|
|
250
309
|
'pollinterval' : 3
|
|
251
310
|
}
|
|
252
|
-
|
|
311
|
+
# Check for event running loops before getting the bot's information
|
|
312
|
+
try:
|
|
313
|
+
loop = asyncio.get_running_loop()
|
|
314
|
+
except RuntimeError:
|
|
315
|
+
# No running event loop; safe to run synchronously
|
|
316
|
+
asyncio.run(self._tele_info())
|
|
317
|
+
else:
|
|
318
|
+
# Already in an event loop; schedule as a background task
|
|
319
|
+
loop.create_task(self._tele_info())
|
|
253
320
|
|
|
254
|
-
#
|
|
321
|
+
# Build our application with handlers for Commands, Messages, and Errors
|
|
322
|
+
self.telegram['app'] = Application.builder().token(os.environ['TELLMGRAMBOT_TELEGRAM_API_KEY']).build()
|
|
255
323
|
self.telegram['app'].add_handler(CommandHandler('help', self.tele_commands))
|
|
256
324
|
self.telegram['app'].add_handler(CommandHandler('start', self.tele_start_command))
|
|
257
325
|
self.telegram['app'].add_handler(CommandHandler('stop', self.tele_stop_command))
|
|
@@ -263,7 +331,6 @@ class TelegramBot:
|
|
|
263
331
|
# Get our LLM spun up with defaults if not defined by user input
|
|
264
332
|
# Tokens as integers measure the length of conversation messages
|
|
265
333
|
self.chatgpt = {
|
|
266
|
-
'name' : bot_name,
|
|
267
334
|
'prompt' : persona_prompt,
|
|
268
335
|
'chat_model' : chat_model,
|
|
269
336
|
'url_model' : url_model,
|
|
@@ -276,9 +343,11 @@ class TelegramBot:
|
|
|
276
343
|
self.chatgpt['prune_threshold'] = floor(0.95 * self.chatgpt['token_limit'])
|
|
277
344
|
self.chatgpt['prune_back_to'] = max(0, self.chatgpt['prune_threshold'] - 500)
|
|
278
345
|
|
|
279
|
-
|
|
280
|
-
|
|
346
|
+
# Bot is now ready and active by default
|
|
347
|
+
self._online = True
|
|
348
|
+
|
|
281
349
|
def set(config_file='config.yaml', prompt_file='test_personality.prmpt'):
|
|
350
|
+
"""Set TeLLMgramBot object based on its YAML configuration and prompt files."""
|
|
282
351
|
# First provide the main structure if not already there
|
|
283
352
|
init_structure()
|
|
284
353
|
|
|
@@ -301,9 +370,7 @@ class TelegramBot:
|
|
|
301
370
|
|
|
302
371
|
# Apply parameters to bot:
|
|
303
372
|
return TelegramBot(
|
|
304
|
-
bot_username = config['bot_username'],
|
|
305
373
|
bot_owner = config['bot_owner'],
|
|
306
|
-
bot_name = config['bot_name'],
|
|
307
374
|
bot_nickname = config['bot_nickname'],
|
|
308
375
|
bot_initials = config['bot_initials'],
|
|
309
376
|
chat_model = config['chat_model'],
|