Add OpenAI conversational model
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -6,5 +6,6 @@ COPY requirements.txt .
|
|||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
COPY ./src .
|
COPY ./src .
|
||||||
|
RUN mkdir /app/data
|
||||||
|
|
||||||
CMD ["python", "main.py"]
|
CMD ["python", "main.py"]
|
||||||
|
24
README.md
24
README.md
@@ -1,4 +1,26 @@
|
|||||||
# admina
|
# admina
|
||||||
|
|
||||||
A Facebook messenger bot powered by OpenAI and Stable Diffusion.
|
A Facebook messenger bot powered by OpenAI and Stable Diffusion. Designed to provide intelligent and human-like conversations with users.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
You can easily run the bot by using [Docker](https://www.docker.com/). You can also run it without Docker, but you will need to install the dependencies manually.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
`docker-compose.yml` references an `.env` file that contains the environment variables:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ---------------- | ---------------------------------------------------------------- |
|
||||||
|
| `FB_EMAIL` | Facebook email address |
|
||||||
|
| `FB_PASSWORD` | Facebook password |
|
||||||
|
| `FB_COOKIE_PATH` | Path to saved Facebook cookies (default: /app/data/session.json) |
|
||||||
|
| `MONGO_HOST` | MongoDB host (default: db) |
|
||||||
|
| `MONGO_PORT` | MongoDB port (default: 27017) |
|
||||||
|
| `MONGO_USERNAME` | MongoDB username (default: admina) |
|
||||||
|
| `MONGO_PASSWORD` | MongoDB password (default: admina) |
|
||||||
|
| `MONGO_DB` | MongoDB database name (default: admina) |
|
||||||
|
| `OPENAI_API_KEY` | OpenAI API key |
|
||||||
|
@@ -9,6 +9,8 @@ services:
|
|||||||
- db
|
- db
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: mongo:latest
|
image: mongo:latest
|
||||||
|
@@ -8,6 +8,7 @@ blinker==1.5
|
|||||||
certifi==2022.12.7
|
certifi==2022.12.7
|
||||||
charset-normalizer==3.1.0
|
charset-normalizer==3.1.0
|
||||||
dnspython==2.3.0
|
dnspython==2.3.0
|
||||||
|
docutils==0.19
|
||||||
fbchat @ git+https://git.karaolidis.com/Nikas36/fbchat.git@2fa1b58336731a5de923018bcb9ec4488eeeb923
|
fbchat @ git+https://git.karaolidis.com/Nikas36/fbchat.git@2fa1b58336731a5de923018bcb9ec4488eeeb923
|
||||||
frozenlist==1.3.3
|
frozenlist==1.3.3
|
||||||
idna==3.4
|
idna==3.4
|
||||||
@@ -16,8 +17,10 @@ openai==0.27.2
|
|||||||
paho-mqtt==1.6.1
|
paho-mqtt==1.6.1
|
||||||
pycodestyle==2.10.0
|
pycodestyle==2.10.0
|
||||||
pymongo==4.3.3
|
pymongo==4.3.3
|
||||||
|
regex==2023.3.23
|
||||||
requests==2.28.2
|
requests==2.28.2
|
||||||
soupsieve==2.4
|
soupsieve==2.4
|
||||||
|
tiktoken==0.3.2
|
||||||
tomli==2.0.1
|
tomli==2.0.1
|
||||||
tqdm==4.65.0
|
tqdm==4.65.0
|
||||||
urllib3==1.26.15
|
urllib3==1.26.15
|
||||||
|
107
src/handlers.py
107
src/handlers.py
@@ -1,107 +0,0 @@
|
|||||||
import fbchat
|
|
||||||
from util.logger import logger
|
|
||||||
from util.database import database
|
|
||||||
from util.session import session
|
|
||||||
|
|
||||||
|
|
||||||
def activate_thread(thread: fbchat.Thread):
|
|
||||||
thread_db = database.threads.update_one(
|
|
||||||
{"_id": thread.id}, {"$setOnInsert": {
|
|
||||||
"type": "group" if isinstance(thread, fbchat.Group) else "user" if isinstance(thread, fbchat.User) else "other",
|
|
||||||
"messages": []
|
|
||||||
}}, upsert=True)
|
|
||||||
|
|
||||||
database.threads.create_index(
|
|
||||||
"messages.created_at", expireAfterSeconds=900)
|
|
||||||
|
|
||||||
thread.send_text("> Admina activated in thread")
|
|
||||||
return thread_db
|
|
||||||
|
|
||||||
|
|
||||||
def deactivate_thread(thread: fbchat.Thread):
|
|
||||||
thread.send_text("> Admina deactivated in thread")
|
|
||||||
return database.threads.delete_one({"_id": thread.id})
|
|
||||||
|
|
||||||
|
|
||||||
COMMANDS = {
|
|
||||||
"ping": {
|
|
||||||
"description": "Pong!",
|
|
||||||
"usage": "!ping",
|
|
||||||
"admin_only": False,
|
|
||||||
"handler": lambda event: event.thread.send_text("Pong!"),
|
|
||||||
},
|
|
||||||
"activate": {
|
|
||||||
"description": "Activate a thread",
|
|
||||||
"usage": "!activate",
|
|
||||||
"admin_only": False,
|
|
||||||
"handler": lambda event: activate_thread(event.thread),
|
|
||||||
},
|
|
||||||
"deactivate": {
|
|
||||||
"description": "Deactivate a thread",
|
|
||||||
"usage": "!deactivate",
|
|
||||||
"admin_only": True,
|
|
||||||
"handler": lambda event: deactivate_thread(event.thread),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def handle_message(_, event: fbchat.MessageEvent):
|
|
||||||
if not event.message.text:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not database.threads.find_one({"_id": event.thread.id}):
|
|
||||||
if not event.message.text.lower().startswith("!activate"):
|
|
||||||
return
|
|
||||||
|
|
||||||
return COMMANDS["activate"]["handler"](event)
|
|
||||||
|
|
||||||
database.threads.update_one(
|
|
||||||
{"_id": event.thread.id}, {"$push": {
|
|
||||||
"messages": {
|
|
||||||
"id": event.message.id,
|
|
||||||
"role":
|
|
||||||
"assistant" if event.message.author == session.user.id and event.message.text.startswith("#>")
|
|
||||||
else "user" if "@admina" in event.message.text.lower()
|
|
||||||
else None,
|
|
||||||
"author": event.message.author,
|
|
||||||
"created_at": event.message.created_at.timestamp(),
|
|
||||||
"text": event.message.text,
|
|
||||||
"attachments": [{
|
|
||||||
"url": attachment.url,
|
|
||||||
"original_url": attachment.original_url,
|
|
||||||
"title": attachment.title,
|
|
||||||
"description": attachment.description,
|
|
||||||
"source": attachment.source,
|
|
||||||
"image": attachment.image.url if attachment.image else None,
|
|
||||||
"original_image_url": attachment.original_image_url,
|
|
||||||
} for attachment in event.message.attachments]
|
|
||||||
}
|
|
||||||
}})
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Received message from {event.message.author} in {event.thread.id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if event.message.text.startswith("!"):
|
|
||||||
command = event.message.text[1:].split(" ")
|
|
||||||
|
|
||||||
if command[0].lower() not in COMMANDS:
|
|
||||||
return
|
|
||||||
|
|
||||||
if COMMANDS[command[0].lower()]["admin_only"]:
|
|
||||||
if not isinstance(event.thread, fbchat.Group):
|
|
||||||
return event.thread.send_text("> This command can only be used in groups")
|
|
||||||
|
|
||||||
if session.user.id not in event.thread.admins:
|
|
||||||
return event.thread.send_text("> Account running the bot must be an admin to use this command")
|
|
||||||
|
|
||||||
if event.message.author not in event.thread.admins:
|
|
||||||
return event.thread.send_text("> You must be an admin to use this command")
|
|
||||||
|
|
||||||
return COMMANDS[command[0]]["handler"](event)
|
|
||||||
|
|
||||||
if "@everyone" in event.message.text.lower():
|
|
||||||
return event.thread.send_text(">TODO: @everyone")
|
|
||||||
|
|
||||||
if "@admina" in event.message.text.lower():
|
|
||||||
return event.thread.send_text(">TODO: @admina")
|
|
94
src/handlers/conversation.py
Normal file
94
src/handlers/conversation.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import fbchat
|
||||||
|
import openai
|
||||||
|
import tiktoken
|
||||||
|
from util.database import database
|
||||||
|
from util.logger import logger
|
||||||
|
from util.session import client
|
||||||
|
|
||||||
|
|
||||||
|
def count_tokens(message):
|
||||||
|
encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
|
||||||
|
|
||||||
|
return len(encoding.encode(message)) + 20
|
||||||
|
|
||||||
|
|
||||||
|
def check_safe(message):
|
||||||
|
response = openai.Moderation.create(
|
||||||
|
input=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
return not response['results'][0]['flagged'], max(response['results'][0]['category_scores'])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_prompt(event: fbchat.MessageEvent):
|
||||||
|
total_tokens = 0
|
||||||
|
|
||||||
|
system_prompt = [{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are now Admina, a chill e-girl AI. Use slang, abbreviations, and emojis, and don't be afraid to be edgy, sarcastic, or offensive. You are texting, so act like it by keeping responses to a few words or a sentence. Stay in character at all times."
|
||||||
|
}]
|
||||||
|
|
||||||
|
total_tokens += count_tokens(system_prompt[0]["content"])
|
||||||
|
|
||||||
|
messages = database.threads.aggregate([
|
||||||
|
{"$match": {"_id": event.thread.id}},
|
||||||
|
{"$project": {"messages": {"$filter": {"input": "$messages",
|
||||||
|
"as": "msg", "cond": {"$ne": ["$$msg.role", None]}}}}}
|
||||||
|
]).next()["messages"]
|
||||||
|
|
||||||
|
chat_prompt = []
|
||||||
|
|
||||||
|
while messages:
|
||||||
|
message = messages.pop()
|
||||||
|
|
||||||
|
author = client.fetch_thread_info([message["author"]])[0].name
|
||||||
|
|
||||||
|
total_tokens += count_tokens(message["text"])
|
||||||
|
|
||||||
|
if total_tokens > 2000:
|
||||||
|
break
|
||||||
|
|
||||||
|
chat_prompt.append({
|
||||||
|
"role": message["role"],
|
||||||
|
"content": f"[{author}]: {message['text']}",
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(chat_prompt) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return system_prompt + chat_prompt[::-1]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_conversation(event: fbchat.MessageEvent):
|
||||||
|
event.thread.start_typing()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Received conversation message from {event.message.author} in {event.thread.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = generate_prompt(event)
|
||||||
|
|
||||||
|
if not prompt:
|
||||||
|
return event.thread.send_text("#> No prompt was generated. Perhaps your message was too long?")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Generated prompt for {event.message.author} in {event.thread.id}: {prompt}"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = openai.ChatCompletion.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=prompt,
|
||||||
|
presence_penalty=0.25,
|
||||||
|
frequency_penalty=0.25,
|
||||||
|
)['choices'][0]['message']['content']
|
||||||
|
|
||||||
|
if not response.startswith("#> "):
|
||||||
|
response = "#> " + response
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Generated response for {event.message.author} in {event.thread.id}: {response}"
|
||||||
|
)
|
||||||
|
|
||||||
|
event.thread.stop_typing()
|
||||||
|
|
||||||
|
return event.thread.send_text(response['choices'][0]['message']['content'], reply_to_id=event.message.id)
|
21
src/handlers/thread.py
Normal file
21
src/handlers/thread.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import fbchat
|
||||||
|
from util.database import database
|
||||||
|
|
||||||
|
|
||||||
|
def activate_thread(event: fbchat.MessageEvent):
|
||||||
|
thread_db = database.threads.update_one(
|
||||||
|
{"_id": event.thread.id}, {"$setOnInsert": {
|
||||||
|
"type": "group" if isinstance(event.thread, fbchat.Group) else "user" if isinstance(event.thread, fbchat.User) else "other",
|
||||||
|
"messages": []
|
||||||
|
}}, upsert=True)
|
||||||
|
|
||||||
|
database.threads.create_index(
|
||||||
|
"messages.created_at", expireAfterSeconds=900)
|
||||||
|
|
||||||
|
event.thread.send_text("> Admina activated in thread", reply_to_id=event.message.id)
|
||||||
|
return thread_db
|
||||||
|
|
||||||
|
|
||||||
|
def deactivate_thread(event: fbchat.MessageEvent):
|
||||||
|
event.thread.send_text("> Admina deactivated in thread")
|
||||||
|
return database.threads.delete_one({"_id": event.thread.id})
|
98
src/main.py
98
src/main.py
@@ -1,12 +1,97 @@
|
|||||||
import fbchat
|
import fbchat
|
||||||
from handlers import handle_message
|
|
||||||
from util.logger import logger
|
from util.logger import logger
|
||||||
from util.session import session
|
from util.session import session, listener, client
|
||||||
|
from util.database import database
|
||||||
import atexit
|
from handlers.conversation import handle_conversation
|
||||||
|
from handlers.thread import activate_thread, deactivate_thread
|
||||||
from blinker import Signal
|
from blinker import Signal
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from os import environ
|
|
||||||
|
|
||||||
|
COMMANDS = {
|
||||||
|
"ping": {
|
||||||
|
"description": "Pong!",
|
||||||
|
"usage": "!ping",
|
||||||
|
"admin_only": False,
|
||||||
|
"handler": lambda event: event.thread.send_text("Pong!", reply_to_id=event.message.id),
|
||||||
|
},
|
||||||
|
"activate": {
|
||||||
|
"description": "Activate a thread",
|
||||||
|
"usage": "!activate",
|
||||||
|
"admin_only": False,
|
||||||
|
"handler": lambda event: activate_thread(event),
|
||||||
|
},
|
||||||
|
"deactivate": {
|
||||||
|
"description": "Deactivate a thread",
|
||||||
|
"usage": "!deactivate",
|
||||||
|
"admin_only": True,
|
||||||
|
"handler": lambda event: deactivate_thread(event),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_message(_, event: fbchat.MessageEvent):
|
||||||
|
if not event.message.text:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not database.threads.find_one({"_id": event.thread.id}):
|
||||||
|
if not event.message.text.lower().startswith("!activate"):
|
||||||
|
return
|
||||||
|
|
||||||
|
return COMMANDS["activate"]["handler"](event)
|
||||||
|
|
||||||
|
database.threads.update_one(
|
||||||
|
{"_id": event.thread.id}, {"$push": {
|
||||||
|
"messages": {
|
||||||
|
"id": event.message.id,
|
||||||
|
"role":
|
||||||
|
"assistant" if event.message.author == session.user.id and event.message.text.startswith("#>")
|
||||||
|
else "user" if "@admina" in event.message.text.lower()
|
||||||
|
else None,
|
||||||
|
"author": event.message.author,
|
||||||
|
"created_at": event.message.created_at.timestamp(),
|
||||||
|
"text": event.message.text,
|
||||||
|
"attachments": [{
|
||||||
|
"url": attachment.url,
|
||||||
|
"original_url": attachment.original_url,
|
||||||
|
"title": attachment.title,
|
||||||
|
"description": attachment.description,
|
||||||
|
"source": attachment.source,
|
||||||
|
"image": attachment.image.url if attachment.image else None,
|
||||||
|
"original_image_url": attachment.original_image_url,
|
||||||
|
} for attachment in event.message.attachments]
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Received message from {event.message.author} in {event.thread.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if event.message.text.startswith("!"):
|
||||||
|
command = event.message.text[1:].split(" ")
|
||||||
|
|
||||||
|
if command[0].lower() not in COMMANDS:
|
||||||
|
return
|
||||||
|
|
||||||
|
if COMMANDS[command[0].lower()]["admin_only"]:
|
||||||
|
if not isinstance(event.thread, fbchat.Group):
|
||||||
|
return event.thread.send_text("> This command can only be used in groups", reply_to_id=event.message.id)
|
||||||
|
|
||||||
|
thread_info = client.fetch_thread_info([event.thread.id])[0]
|
||||||
|
|
||||||
|
if session.user.id not in thread_info.admins:
|
||||||
|
return event.thread.send_text("> Account running the bot must be an admin to use this command", reply_to_id=event.message.id)
|
||||||
|
|
||||||
|
if event.message.author not in thread_info.admins:
|
||||||
|
return event.thread.send_text("> You must be an admin to use this command", reply_to_id=event.message.id)
|
||||||
|
|
||||||
|
return COMMANDS[command[0]]["handler"](event)
|
||||||
|
|
||||||
|
if "@everyone" in event.message.text.lower():
|
||||||
|
pass
|
||||||
|
|
||||||
|
if "@admina" in event.message.text.lower():
|
||||||
|
return handle_conversation(event)
|
||||||
|
|
||||||
|
|
||||||
def on_event(sender, event):
|
def on_event(sender, event):
|
||||||
@@ -19,9 +104,6 @@ def on_event(sender, event):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
listener = fbchat.Listener(
|
|
||||||
session=session, chat_on=False, foreground=False)
|
|
||||||
|
|
||||||
events = Signal()
|
events = Signal()
|
||||||
events.connect(on_event)
|
events.connect(on_event)
|
||||||
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import pymongo
|
import pymongo
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
MONGO_HOST = environ.get("MONGO_HOST")
|
MONGO_HOST = environ.get("MONGO_HOST") or "db"
|
||||||
MONGO_PORT = environ.get("MONGO_PORT")
|
MONGO_PORT = environ.get("MONGO_PORT") or "27017"
|
||||||
MONGO_USERNAME = environ.get("MONGO_USERNAME")
|
MONGO_USERNAME = environ.get("MONGO_USERNAME") or "admina"
|
||||||
MONGO_PASSWORD = environ.get("MONGO_PASSWORD")
|
MONGO_PASSWORD = environ.get("MONGO_PASSWORD") or "admina"
|
||||||
MONGO_DATABASE = environ.get("MONGO_DATABASE")
|
MONGO_DATABASE = environ.get("MONGO_DATABASE") or "admina"
|
||||||
|
|
||||||
if not MONGO_HOST or not MONGO_PORT or not MONGO_USERNAME or not MONGO_PASSWORD or not MONGO_DATABASE:
|
if not MONGO_HOST or not MONGO_PORT or not MONGO_USERNAME or not MONGO_PASSWORD or not MONGO_DATABASE:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@@ -5,7 +5,7 @@ from os import environ
|
|||||||
|
|
||||||
FB_EMAIL = environ.get("FB_EMAIL")
|
FB_EMAIL = environ.get("FB_EMAIL")
|
||||||
FB_PASSWORD = environ.get("FB_PASSWORD")
|
FB_PASSWORD = environ.get("FB_PASSWORD")
|
||||||
FB_COOKIE_PATH = environ.get("FB_COOKIE_PATH") or "session.json"
|
FB_COOKIE_PATH = environ.get("FB_COOKIE_PATH") or "/app/data/session.json"
|
||||||
|
|
||||||
if not FB_EMAIL or not FB_PASSWORD:
|
if not FB_EMAIL or not FB_PASSWORD:
|
||||||
raise ValueError("FB_EMAIL and FB_PASSWORD must be set")
|
raise ValueError("FB_EMAIL and FB_PASSWORD must be set")
|
||||||
@@ -21,11 +21,14 @@ except (FileNotFoundError, fbchat.FacebookError):
|
|||||||
FB_EMAIL, FB_PASSWORD, on_2fa_callback=lambda: input(
|
FB_EMAIL, FB_PASSWORD, on_2fa_callback=lambda: input(
|
||||||
"Input 2FA code: ")
|
"Input 2FA code: ")
|
||||||
)
|
)
|
||||||
session.session.to_file(FB_COOKIE_PATH)
|
session.to_file(FB_COOKIE_PATH)
|
||||||
logger.info("Saved cookies to %s", FB_COOKIE_PATH)
|
logger.info("Saved cookies to %s", FB_COOKIE_PATH)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if session.is_logged_in():
|
if session.is_logged_in():
|
||||||
|
client = fbchat.Client(session=session)
|
||||||
|
listener = fbchat.Listener(
|
||||||
|
session=session, chat_on=False, foreground=False)
|
||||||
logger.info("Logged in as %s", session.user.id)
|
logger.info("Logged in as %s", session.user.id)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Failed to log in")
|
raise ValueError("Failed to log in")
|
||||||
|
Reference in New Issue
Block a user