Initial working version

This commit is contained in:
Kumi 2023-04-16 14:08:57 +00:00
commit 67779b8335
Signed by: kumi
GPG key ID: ECBCC9082395383F
6 changed files with 324 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
*.db
config.ini
venv/
*.pyc
__pycache__/

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2023 Kumi Mitterer <gptbot@kumi.email>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

32
README.md Normal file
View file

@ -0,0 +1,32 @@
# GPTbot
GPTbot is a simple bot that uses the [OpenAI ChatCompletion API](https://platform.openai.com/docs/guides/chat)
to generate responses to messages in a Matrix room.
It will also save a log of the spent tokens to a sqlite database
(token_usage.db in the working directory).
## Installation
Simply clone this repository and install the requirements.
### Requirements
* Python 3.8 or later
* Requirements from `requirements.txt` (install with `pip install -r requirements.txt` in a venv)
### Configuration
The bot requires a configuration file to be present in the working directory.
Copy the provided `config.dist.ini` to `config.ini` and edit it to your needs.
## Running
The bot can be run with `python -m gptbot`. If required, activate a venv first.
You may want to run the bot in a screen or tmux session, or use a process
manager like systemd.
## License
This project is licensed under the terms of the MIT license.

8
config.dist.ini Normal file
View file

@ -0,0 +1,8 @@
[OpenAI]
Model = gpt-3.5-turbo
APIKey = sk-yoursecretkey
[Matrix]
Homeserver = https://matrix.local
AccessToken = syt_yoursynapsetoken
UserID = @gptbot:matrix.local

256
gptbot.py Normal file
View file

@ -0,0 +1,256 @@
import sqlite3
import os
import inspect
import openai
import asyncio
import markdown2
import tiktoken
from nio import AsyncClient, RoomMessageText, MatrixRoom, Event, InviteEvent
from nio.api import MessageDirection
from nio.responses import RoomMessagesError, SyncResponse
from configparser import ConfigParser
from datetime import datetime
config = ConfigParser()
config.read("config.ini")
# Set up GPT API
openai.api_key = config["OpenAI"]["APIKey"]
MODEL = config["OpenAI"].get("Model", "gpt-3.5-turbo")
# Set up Matrix client
MATRIX_HOMESERVER = config["Matrix"]["Homeserver"]
MATRIX_ACCESS_TOKEN = config["Matrix"]["AccessToken"]
BOT_USER_ID = config["Matrix"]["UserID"]
client = AsyncClient(MATRIX_HOMESERVER, BOT_USER_ID)
SYNC_TOKEN = None
# Set up SQLite3 database
conn = sqlite3.connect("token_usage.db")
cursor = conn.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS token_usage (
room_id TEXT NOT NULL,
tokens INTEGER NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.commit()
# Define the system message and max token limit
SYSTEM_MESSAGE = "You are a helpful assistant."
MAX_TOKENS = 3000
def logging(message, log_level="info"):
caller = inspect.currentframe().f_back.f_code.co_name
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S:%f")
print(f"[{timestamp} - {caller}] [{log_level.upper()}] {message}")
async def gpt_query(messages):
logging(f"Querying GPT with {len(messages)} messages")
try:
response = openai.ChatCompletion.create(
model=MODEL,
messages=messages
)
result_text = response.choices[0].message['content']
tokens_used = response.usage["total_tokens"]
logging(f"Used {tokens_used} tokens")
return result_text, tokens_used
except Exception as e:
logging(f"Error during GPT API call: {e}", "error")
return None, 0
async def fetch_last_n_messages(room_id, n=20):
# Fetch the last n messages from the room
room = await client.join(room_id)
messages = []
logging(f"Fetching last {n} messages from room {room_id}...")
response = await client.room_messages(
room_id=room_id,
start=SYNC_TOKEN,
limit=n,
)
if isinstance(response, RoomMessagesError):
logging(
f"Error fetching messages: {response.message} (status code {response.status_code})", "error")
return []
for event in response.chunk:
if isinstance(event, RoomMessageText):
messages.append(event)
logging(f"Found {len(messages)} messages")
# Reverse the list so that messages are in chronological order
return messages[::-1]
def truncate_messages_to_fit_tokens(messages, max_tokens=MAX_TOKENS):
encoding = tiktoken.encoding_for_model(MODEL)
total_tokens = 0
system_message_tokens = len(encoding.encode(SYSTEM_MESSAGE)) + 1
if system_message_tokens > max_tokens:
logging(
f"System message is too long to fit within token limit ({system_message_tokens} tokens) - cannot proceed", "error")
return []
total_tokens += system_message_tokens
total_tokens = len(SYSTEM_MESSAGE) + 1
truncated_messages = []
for message in messages[0] + reversed(messages[1:]):
content = message["content"]
tokens = len(encoding.encode(content)) + 1
if total_tokens + tokens > max_tokens:
break
total_tokens += tokens
truncated_messages.append(message)
return list(truncated_messages[0] + reversed(truncated_messages[1:]))
async def message_callback(room: MatrixRoom, event: RoomMessageText):
logging(f"Received message from {event.sender} in room {room.room_id}")
if event.sender == BOT_USER_ID:
logging("Message is from bot - ignoring")
return
await client.room_typing(room.room_id, True)
await client.room_read_markers(room.room_id, event.event_id)
last_messages = await fetch_last_n_messages(room.room_id, 20)
if not last_messages or all(message.sender == BOT_USER_ID for message in last_messages):
logging("No messages to respond to")
await client.room_typing(room.room_id, False)
return
chat_messages = [{"role": "system", "content": SYSTEM_MESSAGE}]
for message in last_messages:
role = "assistant" if message.sender == BOT_USER_ID else "user"
if not message.event_id == event.event_id:
chat_messages.append({"role": role, "content": message.body})
chat_messages.append({"role": "user", "content": event.body})
# Truncate messages to fit within the token limit
truncated_messages = truncate_messages_to_fit_tokens(
chat_messages, MAX_TOKENS - 1)
response, tokens_used = await gpt_query(truncated_messages)
if response:
# Send the response to the room
logging(f"Sending response to room {room.room_id}...")
markdowner = markdown2.Markdown(extras=["fenced-code-blocks"])
formatted_body = markdowner.convert(response)
await client.room_send(
room.room_id, "m.room.message", {"msgtype": "m.text", "body": response,
"format": "org.matrix.custom.html", "formatted_body": formatted_body}
)
logging("Logging tokens used...")
cursor.execute(
"INSERT INTO token_usage (room_id, tokens) VALUES (?, ?)", (room.room_id, tokens_used))
conn.commit()
else:
# Send a notice to the room if there was an error
logging("Error during GPT API call - sending notice to room")
await client.room_send(
room.room_id, "m.room.message", {
"msgtype": "m.notice", "body": "Sorry, I'm having trouble connecting to the GPT API right now. Please try again later."}
)
print("No response from GPT API")
await client.room_typing(room.room_id, False)
async def room_invite_callback(room: MatrixRoom, event):
logging(f"Received invite to room {room.room_id} - joining...")
await client.join(room.room_id)
await client.room_send(
room.room_id,
"m.room.message",
{"msgtype": "m.text",
"body": "Hello! I'm a helpful assistant. How can I help you today?"}
)
async def accept_pending_invites():
logging("Accepting pending invites...")
for room_id in list(client.invited_rooms.keys()):
logging(f"Joining room {room_id}...")
await client.join(room_id)
await client.room_send(
room_id,
"m.room.message",
{"msgtype": "m.text",
"body": "Hello! I'm a helpful assistant. How can I help you today?"}
)
async def sync_cb(response):
logging(f"Sync response received (next batch: {response.next_batch})")
SYNC_TOKEN = response.next_batch
async def main():
logging("Starting bot...")
client.access_token = MATRIX_ACCESS_TOKEN # Set the access token directly
client.user_id = BOT_USER_ID # Set the user_id directly
client.add_response_callback(sync_cb, SyncResponse)
logging("Syncing...")
await client.sync(timeout=30000)
client.add_event_callback(message_callback, RoomMessageText)
client.add_event_callback(room_invite_callback, InviteEvent)
await accept_pending_invites() # Accept pending invites
logging("Bot started")
try:
await client.sync_forever(timeout=30000) # Continue syncing events
finally:
await client.close() # Properly close the aiohttp client session
logging("Bot stopped")
if __name__ == "__main__":
try:
asyncio.run(main())
finally:
conn.close()

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
openai
matrix-nio[e2e]
markdown2[all]
tiktoken