Skip to content

Commit

Permalink
update to the official openai APIs!
Browse files Browse the repository at this point in the history
  • Loading branch information
n3d1117 committed Mar 2, 2023
1 parent f173450 commit e0f2f86
Show file tree
Hide file tree
Showing 7 changed files with 594 additions and 191 deletions.
7 changes: 3 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
OPENAI_EMAIL="<YOUR_OPENAI_EMAIL>"
OPENAI_PASSWORD="<YOUR_OPENAI_PASSWORD>"
TELEGRAM_BOT_TOKEN="<YOUR_TELEGRAM_BOT_TOKEN>"
ALLOWED_TELEGRAM_USER_IDS="<USER_ID_1>,<USER_ID_2>,..."
OPENAI_API_KEY="XXX"
TELEGRAM_BOT_TOKEN="XXX"
ALLOWED_TELEGRAM_USER_IDS="USER_ID_1,<USER_ID_2" # comma separated list of telegram user ids, or * to allow all
4 changes: 2 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ name = "pypi"

[packages]
requests = "*"
python-telegram-bot = "==20.0a6"
revchatgpt = "==0.0.38.8"
python-telegram-bot = "==20.1"
openai = "==0.27.0"
python-dotenv = "*"

[dev-packages]
Expand Down
540 changes: 498 additions & 42 deletions Pipfile.lock

Large diffs are not rendered by default.

35 changes: 12 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
> **Warning**
> No longer works as of December 12th due to Cloudflare protections (see [#261](https://github.com/acheong08/ChatGPT/issues/261))
# ChatGPT Telegram Bot
![python-version](https://img.shields.io/badge/python-3.10-blue.svg)
[![revChatGPT-version](https://img.shields.io/badge/revChatGPT-0.0.38.8-green.svg)](https://github.com/acheong08/ChatGPT)
[![openai-version](https://img.shields.io/badge/openai-0.27.0-green.svg)](https://openai.com/)
[![license](https://img.shields.io/badge/License-GPL%202.0-brightgreen.svg)](LICENSE)

A [Telegram bot](https://core.telegram.org/bots/api) that integrates with OpenAI's [ChatGPT](https://openai.com/blog/chatgpt/) to provide answers. Ready to use with minimal configuration required. Based on [acheong08/ChatGPT](https://github.com/acheong08/ChatGPT)
A [Telegram bot](https://core.telegram.org/bots/api) that integrates with OpenAI's _official_ [ChatGPT](https://openai.com/blog/chatgpt/) APIs to provide answers. Ready to use with minimal configuration required.

## Screenshot
<img width="600" alt="Demo" src="https://user-images.githubusercontent.com/11541888/205654171-80359706-d2ef-4fac-8300-62fe448bfb55.png">
Expand All @@ -18,16 +15,17 @@ A [Telegram bot](https://core.telegram.org/bots/api) that integrates with OpenAI
- [x] Typing indicator while generating a response
- [x] Access can be restricted by specifying a list of allowed users
- [x] (NEW!) Docker support
- [x] (NEW!) Live answer updating as the bot types

## Coming soon
- [ ] Customizable initial prompt
- [ ] Customizable temperature
- [ ] Better handling of rate limiting errors
- [ ] See remaining tokens and current usage
- [ ] Multi-chat support
- [ ] Image generation using DALL·E APIs

## Additional Features - help needed!
- [ ] Multi-chat support (ongoing, see [pull/22](https://github.com/n3d1117/chatgpt-telegram-bot/pull/22))
- Idea: cache different instances of `ChatGPT3Bot`, one for every chat id (maybe even persist them), so that every user has their own private conversation
- [ ] Support group chats (ongoing, see [pull/17](https://github.com/n3d1117/chatgpt-telegram-bot/pull/17))
- Allow the bot to be used in group chats with specific commands
- [ ] Advanced commands
- With premade ad-hoc prompts
- [ ] Group chat support

PRs are always welcome!

Expand All @@ -41,24 +39,17 @@ PRs are always welcome!
### Configuration
Customize the configuration by copying `.env.example` and renaming it to `.env`, then editing the settings as desired:
```bash
OPENAI_EMAIL="<YOUR_OPENAI_EMAIL>"
OPENAI_PASSWORD="<YOUR_OPENAI_PASSWORD>"
OPENAI_API_KEY="<YOUR_OPENAI_API_KEY>"
TELEGRAM_BOT_TOKEN="<YOUR_TELEGRAM_BOT_TOKEN>"
```
* `OPENAI_EMAIL,OPENAI_PASSWORD`: Your OpenAI credentials (these are only sent to the OpenAI server to periodically refresh the access token and never shared). You can read more about it [here](https://github.com/acheong08/ChatGPT)
* `OPENAI_API_KEY`: Your OpenAI API key, get if from [here](https://platform.openai.com/account/api-keys)
* `TELEGRAM_BOT_TOKEN`: Your Telegram bot's token, obtained using [BotFather](http://t.me/botfather) (see [tutorial](https://core.telegram.org/bots/tutorial#obtain-your-bot-token))

Additional optional (but recommended) configuration values:
```bash
ALLOWED_TELEGRAM_USER_IDS="<USER_ID_1>,<USER_ID_2>,..." # Defaults to "*"
PROXY="<HTTP/HTTPS_PROXY>" # E.g. "http://localhost:8080", defaults to none
USE_STREAM=false # Defaults to true
DEBUG=false # Defaults to true
```
* `ALLOWED_TELEGRAM_USER_IDS`: A comma-separated list of Telegram user IDs that are allowed to interact with the bot (use [getidsbot](https://t.me/getidsbot) to find your user ID). **Important**: by default, *everyone* is allowed (`*`)
* `PROXY`: Proxy to be used when authenticating with OpenAI
* `USE_STREAM`: Streams the response as the bot types. Set to `false` to only answer once the response is fully generated
* `DEBUG`: Enable debug logging for the [revChatGpt](https://github.com/acheong08/ChatGPT) package

### Installing
1. Clone the repository and navigate to the project directory:
Expand Down Expand Up @@ -93,8 +84,6 @@ docker-compose up

## Credits
- [ChatGPT](https://chat.openai.com/chat) from [OpenAI](https://openai.com)
- [acheong08/ChatGPT](https://github.com/acheong08/ChatGPT) for reverse engineering ChatGPT APIs
- [rawandahmad698/PyChatGPT](https://github.com/rawandahmad698/PyChatGPT) for his work on the authentication protocol
- [python-telegram-bot](https://python-telegram-bot.org)

## Disclaimer
Expand Down
48 changes: 48 additions & 0 deletions gpt_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging
import openai


class GPTHelper:
"""
ChatGPT helper class.
"""

def __init__(self, config: dict):
"""
Initializes the GPT helper class with the given configuration.
:param config: A dictionary containing the GPT configuration
"""
openai.api_key = config['api_key']
self.prompt = "You are a helpful assistant. You answer with concise, straight-forward answers. You sometimes " \
"make jokes, if appropriate. You are never rude. You are always helpful."
self.history = [{"role": "system", "content": self.prompt}]

def get_response(self, query) -> str:
"""
Gets a response from the GPT-3 model.
:param query: The query to send to the model
:return: The answer from the model
"""
try:
self.history.append({"role": "user", "content": query})

response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=self.history
)

answer = response.choices[0]['message']['content']
self.history.append({"role": "assistant", "content": answer})
return answer
except openai.error.RateLimitError as e:
logging.exception(e)
return "OpenAI RateLimit exceed"
except Exception as e:
logging.exception(e)
return "Error"

def reset(self):
"""
Resets the conversation history.
"""
self.history = [{"role": "system", "content": self.prompt}]
23 changes: 8 additions & 15 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import os

from dotenv import load_dotenv
from revChatGPT.revChatGPT import AsyncChatbot as ChatGPT3Bot

from gpt_helper import GPTHelper
from telegram_bot import ChatGPT3TelegramBot


Expand All @@ -18,31 +18,24 @@ def main():
)

# Check if the required environment variables are set
required_values = ['TELEGRAM_BOT_TOKEN', 'OPENAI_EMAIL', 'OPENAI_PASSWORD']
required_values = ['TELEGRAM_BOT_TOKEN', 'OPENAI_API_KEY']
missing_values = [value for value in required_values if os.environ.get(value) is None]
if len(missing_values) > 0:
logging.error(f'The following environment values are missing in your .env: {", ".join(missing_values)}')
exit(1)

# Setup configuration
chatgpt_config = {
'email': os.environ['OPENAI_EMAIL'],
'password': os.environ['OPENAI_PASSWORD']
# Setup configurations
gpt_config = {
'api_key': os.environ['OPENAI_API_KEY']
}
telegram_config = {
'token': os.environ['TELEGRAM_BOT_TOKEN'],
'allowed_user_ids': os.environ.get('ALLOWED_TELEGRAM_USER_IDS', '*'),
'use_stream': os.environ.get('USE_STREAM', 'true').lower() == 'true'
'allowed_user_ids': os.environ.get('ALLOWED_TELEGRAM_USER_IDS', '*')
}

if os.environ.get('PROXY', None) is not None:
chatgpt_config.update({'proxy': os.environ.get('PROXY')})

debug = os.environ.get('DEBUG', 'true').lower() == 'true'

# Setup and run ChatGPT and Telegram bot
gpt3_bot = ChatGPT3Bot(config=chatgpt_config, debug=debug)
telegram_bot = ChatGPT3TelegramBot(config=telegram_config, gpt3_bot=gpt3_bot)
gpt = GPTHelper(config=gpt_config)
telegram_bot = ChatGPT3TelegramBot(config=telegram_config, gpt=gpt)
telegram_bot.run()


Expand Down
128 changes: 23 additions & 105 deletions telegram_bot.py
Original file line number Diff line number Diff line change
@@ -1,152 +1,71 @@
import asyncio
import logging

import telegram.constants as constants
from httpx import HTTPError
from revChatGPT.revChatGPT import AsyncChatbot as ChatGPT3Bot
from telegram import Update, Message
from telegram.error import RetryAfter, BadRequest
from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters

from gpt_helper import GPTHelper


class ChatGPT3TelegramBot:
"""
Class representing a Chat-GPT3 Telegram Bot.
"""

def __init__(self, config: dict, gpt3_bot: ChatGPT3Bot):
def __init__(self, config: dict, gpt: GPTHelper):
"""
Initializes the bot with the given configuration and GPT-3 bot object.
:param config: A dictionary containing the bot configuration
:param gpt3_bot: The GPT-3 bot object
:param gpt: GPTHelper object
"""
self.config = config
self.gpt3_bot = gpt3_bot
self.gpt = gpt
self.disallowed_message = "Sorry, you are not allowed to use this bot. You can check out the source code at " \
"https://github.com/n3d1117/chatgpt-telegram-bot"

async def help(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Shows the help menu.
"""
await update.message.reply_text("/start - Start the bot\n"
"/reset - Reset conversation\n"
await update.message.reply_text("/reset - Reset conversation\n"
"/help - Help menu\n\n"
"Open source at https://github.com/n3d1117/chatgpt-telegram-bot",
disable_web_page_preview=True)

async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Handles the /start command.
"""
if not self.is_allowed(update):
logging.info(f'User {update.message.from_user.name} is not allowed to start the bot')
await self.send_disallowed_message(update, context)
return

logging.info('Bot started')
await context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a Chat-GPT3 Bot, please talk to me!")

async def reset(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Resets the conversation.
"""
if not self.is_allowed(update):
logging.info(f'User {update.message.from_user.name} is not allowed to reset the bot')
logging.warning(f'User {update.message.from_user.name} is not allowed to reset the bot')
await self.send_disallowed_message(update, context)
return

logging.info('Resetting the conversation...')
self.gpt3_bot.reset_chat()
await context.bot.send_message(chat_id=update.effective_chat.id, text="Done!")

async def send_typing_periodically(self, update: Update, context: ContextTypes.DEFAULT_TYPE, every_seconds: float):
"""
Sends the typing action periodically to the chat
"""
while True:
await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=constants.ChatAction.TYPING)
await asyncio.sleep(every_seconds)
logging.info(f'Resetting the conversation for user {update.message.from_user.name}...')
self.gpt.reset()
response = self.gpt.get_response('hi')
await context.bot.send_message(chat_id=update.effective_chat.id, text=response)

async def prompt(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
React to incoming messages and respond accordingly.
"""
if not self.is_allowed(update):
logging.info(f'User {update.message.from_user.name} is not allowed to use the bot')
logging.warning(f'User {update.message.from_user.name} is not allowed to use the bot')
await self.send_disallowed_message(update, context)
return

logging.info(f'New message received from user {update.message.from_user.name}')

# Send "Typing..." action periodically every 4 seconds until the response is received
typing_task = context.application.create_task(
self.send_typing_periodically(update, context, every_seconds=4)
await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=constants.ChatAction.TYPING)
response = self.gpt.get_response(update.message.text)
await context.bot.send_message(
chat_id=update.effective_chat.id,
reply_to_message_id=update.message.message_id,
text=response,
parse_mode=constants.ParseMode.MARKDOWN
)

if self.config['use_stream']:
initial_message: Message or None = None
chunk_index, chunk_text = (0, '')

async def message_update(every_seconds: float):
"""
Edits the `initial_message` periodically with the updated text from the latest chunk
"""
while True:
try:
if initial_message is not None and chunk_text != initial_message.text:
await initial_message.edit_text(chunk_text)
except (BadRequest, HTTPError, RetryAfter):
# Ignore common errors while editing the message
pass
except Exception as e:
logging.info(f'Error while editing the message: {str(e)}')

await asyncio.sleep(every_seconds)

# Start task to update the initial message periodically every 0.5 seconds
# If you're frequently hitting rate limits, increase this interval
message_update_task = context.application.create_task(message_update(every_seconds=0.5))

# Stream the response
async for chunk in await self.gpt3_bot.get_chat_response(update.message.text, output='stream'):
if chunk_index == 0 and initial_message is None:
# Sends the initial message, to be edited later with updated text
initial_message = await context.bot.send_message(
chat_id=update.effective_chat.id,
reply_to_message_id=update.message.message_id,
text=chunk['message']
)
typing_task.cancel()
chunk_index, chunk_text = (chunk_index + 1, chunk['message'])

message_update_task.cancel()

# Final edit, including Markdown formatting
await initial_message.edit_text(chunk_text, parse_mode=constants.ParseMode.MARKDOWN)

else:
response = await self.get_chatgpt_response(update.message.text)
typing_task.cancel()

await context.bot.send_message(
chat_id=update.effective_chat.id,
reply_to_message_id=update.message.message_id,
text=response['message'],
parse_mode=constants.ParseMode.MARKDOWN
)

async def get_chatgpt_response(self, message) -> dict:
"""
Gets the response from the ChatGPT APIs.
"""
try:
response = await self.gpt3_bot.get_chat_response(message)
return response
except Exception as e:
logging.info(f'Error while getting the response: {str(e)}')
return {"message": "I'm having some trouble talking to you, please try again later."}

async def send_disallowed_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Sends the disallowed message to the user.
Expand Down Expand Up @@ -177,10 +96,9 @@ def run(self):
"""
application = ApplicationBuilder().token(self.config['token']).build()

application.add_handler(CommandHandler('start', self.start, block=False))
application.add_handler(CommandHandler('reset', self.reset, block=False))
application.add_handler(CommandHandler('help', self.help, block=False))
application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), self.prompt, block=False))
application.add_handler(CommandHandler('reset', self.reset))
application.add_handler(CommandHandler('help', self.help))
application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), self.prompt))

application.add_error_handler(self.error_handler)

Expand Down

0 comments on commit e0f2f86

Please sign in to comment.