Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ DISCORD_BOT_TOKEN=x
DISCORD_CLIENT_ID=x

ALLOWED_SERVER_IDS=1
SERVER_TO_MODERATION_CHANNEL=1:1

OPENAI_API_URL=https://api.openai.com/v1/chat/completions
OPENAI_MODEL=gpt-3.5-turbo

SYSTEM_MESSAGE="You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Knowledge cutoff: {knowledge_cutoff} Current date: {current_date}"
KNOWLEDGE_CUTOFF="2021-09"
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ Thank you!
---
# GPT Discord Bot

Example Discord bot written in Python that uses the [completions API](https://beta.openai.com/docs/api-reference/completions) to have conversations with the `text-davinci-003` model, and the [moderations API](https://beta.openai.com/docs/api-reference/moderations) to filter the messages.

**THIS IS NOT CHATGPT.**
Example Discord bot written in Python that uses the [completions API](https://beta.openai.com/docs/api-reference/completions) to have conversations with the `gpt-3.5-turbo` or `gpt-4` models.

This bot uses the [OpenAI Python Library](https://github.com/openai/openai-python) and [discord.py](https://discordpy.readthedocs.io/).

Expand Down Expand Up @@ -53,9 +51,9 @@ This bot uses the [OpenAI Python Library](https://github.com/openai/openai-pytho

# Optional configuration

1. If you want moderation messages, create and copy the channel id for each server that you want the moderation messages to send to in `SERVER_TO_MODERATION_CHANNEL`. This should be of the format: `server_id:channel_id,server_id_2:channel_id_2`
1. If you want to change the personality of the bot, go to `src/config.yaml` and edit the instructions
1. If you want to change the moderation settings for which messages get flagged or blocked, edit the values in `src/constants.py`. A lower value means less chance of it triggering.
- If you want to change the model used, you can do so in `OPENAI_MODEL`. Currently only `gpt-3.5-turbo` and `gpt-4` work with the present codebase.

- If you want to change the behavior/personality of the bot, change the system prompt in `SYSTEM_MESSAGE`, with optional variables enclosed in `{`curly braces`}`. Currently the only variables available are `current_date` and `knowledge_cutoff`, with the latter being equivalent to the environment variable of the same name. The former is always in ISO 8601 format.

# FAQ

Expand Down
5 changes: 4 additions & 1 deletion src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
OPENAI_API_URL = os.environ['OPENAI_API_URL']
OPENAI_MODEL = os.environ['OPENAI_MODEL']

SYSTEM_MESSAGE = os.environ["SYSTEM_MESSAGE"]
KNOWLEDGE_CUTOFF = os.environ["KNOWLEDGE_CUTOFF"]

ALLOWED_SERVER_IDS: List[int] = []
server_ids = os.environ["ALLOWED_SERVER_IDS"].split(",")
for s in server_ids:
Expand All @@ -36,5 +39,5 @@
ACTIVATE_THREAD_PREFX = "💬✅"
INACTIVATE_THREAD_PREFIX = "💬❌"
MAX_CHARS_PER_REPLY_MSG = (
1500 # discord has a 2k limit, we just break message into 1.5k
2000 # discord has a 2k limit
)
22 changes: 21 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import discord
from discord import Message as DiscordMessage
import logging
Expand All @@ -8,6 +9,8 @@
ACTIVATE_THREAD_PREFX,
MAX_THREAD_MESSAGES,
SECONDS_DELAY_RECEIVING_MSG,
SYSTEM_MESSAGE,
KNOWLEDGE_CUTOFF
)
import asyncio
from src.utils import (
Expand Down Expand Up @@ -78,8 +81,17 @@ async def chat_command(int: discord.Interaction, message: str):
auto_archive_duration=60,
)
async with thread.typing():
# prepare the initial system message
current_date = datetime.datetime.now().strftime("%Y-%m-%d")
system_message = SYSTEM_MESSAGE.format(
knowledge_cutoff=KNOWLEDGE_CUTOFF,
current_date=current_date
)
# fetch completion
messages = [Message(user='system', text=message)]
messages = [
Message(user='system', text=system_message),
Message(user='user', text=message)
]
response_data = await generate_completion_response(
messages=messages,
)
Expand Down Expand Up @@ -145,13 +157,21 @@ async def on_message(message: DiscordMessage):
f"Thread message to process - {message.author}: {message.content[:50]} - {thread.name} {thread.jump_url}"
)

# prepare the initial system message
current_date = datetime.datetime.now().strftime("%Y-%m-%d")
system_message = SYSTEM_MESSAGE.format(
knowledge_cutoff=KNOWLEDGE_CUTOFF,
current_date=current_date
)

channel_messages = [
discord_message_to_message(
message=message,
bot_name=client.user)
async for message in thread.history(limit=MAX_THREAD_MESSAGES)
]
channel_messages = [x for x in channel_messages if x is not None]
channel_messages.append(Message(user='system', text=system_message))
channel_messages.reverse()

# generate the response
Expand Down
69 changes: 52 additions & 17 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,65 @@ def discord_message_to_message(
logger.info(
f"field.name - {field.name}"
)
return Message(user="system", text=field.value)
return Message(user="user", text=field.value)
else:
if message.content:
user_name = "assistant" if message.author == bot_name else "user"
return Message(user=user_name, text=message.content)
return None


def split_into_shorter_messages(message: str) -> List[str]:
indices_object = re.finditer(
pattern='```',
string=message)

indices = [index.start() for index in indices_object]
indices[1::2] = [x + 4 for x in indices[1::2]]
indices.insert(0, 0)
indices.append(len(message))

result = []
for i in range(1, len(indices)):
result.append(message[indices[i-1]:indices[i]])

return result

def split_into_shorter_messages(text, limit=MAX_CHARS_PER_REPLY_MSG, code_block='```'):
def split_at_boundary(s, boundary):
parts = s.split(boundary)
result = []
for i, part in enumerate(parts):
if i % 2 == 1:
result.extend(split_code_block(part))
else:
result += split_substring(part)
return result

def split_substring(s):
if len(s) <= limit:
return [s]
for boundary in ('\n', ' '):
if boundary in s:
break
else:
return [s[:limit]] + split_substring(s[limit:])

pieces = s.split(boundary)
result = []
current_part = pieces[0]
for piece in pieces[1:]:
if len(current_part) + len(boundary) + len(piece) > limit:
result.append(current_part)
current_part = piece
else:
current_part += boundary + piece
result.append(current_part)
return result

def split_code_block(s):
if len(code_block + s + code_block) <= limit:
return [code_block + s + code_block]
else:
lines = s.split('\n')
result = [code_block]
for line in lines:
if len(result[-1] + '\n' + line) > limit:
result[-1] += code_block
result.append(code_block + line)
else:
result[-1] += '\n' + line
result[-1] += code_block
return result

if code_block in text:
return split_at_boundary(text, code_block)
else:
return split_substring(text)

def is_last_message_stale(
interaction_message: DiscordMessage, last_message: DiscordMessage, bot_id: str
Expand Down