I am developing a Facebook Bot for my brothers mobile auto repair that's supposed to be a live support agent essentially to try to figure out what the issue is before he jumps in. The issue I'm having is this -- I originally had it responding to every message. But that leads to circumstances with people like myself that type quickly and in multiple messages. So I made a MessageQueue class that stores all messages per conversation locally in a JSON file for now (will move to a database once I fix this). The issue is that once the user sends X messages in a time span without more messages (right now about 10 seconds), it's supposed to process the messages, and then return a response and save that response to the conversation history.
But because it's asynchronous and sometimes there's multiple tasks, it runs the final function multiple times, appending the bots message multiple times to the conversation history. Does anyone have any advice on how to fix this? Here's my code
Also here's the response I get for the following image
the issue is basically it should just have the messages the user is sending + the bots messages in the conversation history, and when I send multiple replies e.g. here it's supposed to only append the users messages and a "welcome message" from the bot, but instead it sends
{
"6764367863592357": {
"messages": [
{
"conv_id": "6764367863592357",
"time": 1684887807029,
"sender_id": "113272841265752",
"text": "okay here we go"
},
{
"conv_id": "6764367863592357",
"time": 1684887812237,
"sender_id": "113272841265752",
"text": "seeing if this writes properly to a txt file"
},
{
"conv_id": "6764367863592357",
"time": 1684887822.4400299,
"sender_id": "bot",
"text": "Welcome to Drew's Mobile Auto Repair, my name is Otto and I'm your personal support agent to get you started. I can help you diagnose your problems, provide general customer support, or help schedule an appointment. To begin diagnosing, please type \"diagnose\", otherwise type away!"
},
{
"conv_id": "6764367863592357",
"time": 1684887822.4400299,
"sender_id": "bot",
"text": "Welcome to Drew's Mobile Auto Repair, my name is Otto and I'm your personal support agent to get you started. I can help you diagnose your problems, provide general customer support, or help schedule an appointment. To begin diagnosing, please type \"diagnose\", otherwise type away!"
},
{
"conv_id": "6764367863592357",
"time": 1684945914885,
"sender_id": "113272841265752",
"text": "Hey, this is a test message"
},
{
"conv_id": "6764367863592357",
"time": 1684945918581,
"sender_id": "113272841265752",
"text": "I am adding a second one"
},
{
"conv_id": "6764367863592357",
"time": 1684945925218,
"sender_id": "113272841265752",
"text": "to see if you add three messages to the conversation"
},
{
"conv_id": "6764367863592357",
"time": 1684945935.4484422,
"sender_id": "bot",
"text": "I'm still learning, this response isn't programmed yet!"
},
{
"conv_id": "6764367863592357",
"time": 1684945935.4484422,
"sender_id": "bot",
"text": "I'm still learning, this response isn't programmed yet!"
}
]
}
}
@app.route("/", methods=["POST"])
async def handle_incoming_messages():
data = await request.get_json()
print(f"Data received: {data}")
if data["object"] == "page":
# Get the message data and send it to incoming_message
if data["entry"][0]:
text: str = data["entry"][0]["messaging"][0]["message"]["text"]
sender_id = data["entry"][0]["messaging"][0]["sender"]["id"]
conv_id = data["entry"][0]["id"]
time = data["entry"][0]["time"]
message = Message(sender_id, time, conv_id, text)
app.add_background_task(incoming_message, sender_id, message, conversations)
return "ok", 200
import json
import os
class Message:
def __init__(
self,
conv_id: str,
time: float,
sender_id: str,
text: str,
user_message: bool = True,
):
self.conv_id = conv_id
self.time = time
self.sender_id = sender_id
self.text = text
self.user_message = user_message
def to_dict(self):
if self.user_message:
return {
"conv_id": self.conv_id,
"time": self.time,
"sender_id": self.sender_id,
"text": self.text,
}
else:
return {
"conv_id": self.conv_id,
"time": self.time,
"sender_id": "bot",
"text": self.text,
}
def to_ai_format(self):
if self.user_message:
return f"User @ {self.time}: {self.text}"
else:
return f"Otto @ {self.time}: {self.text}"
class Conversation:
def __init__(self, conv_id: str, messages: list[Message]):
self.conv_id = conv_id
self.messages = messages
def to_dict(self):
return {
self.conv_id: {
"messages": [message.to_dict() for message in self.messages],
}
}
def to_ai_format(self):
return "\n".join([message.to_ai_format() for message in self.messages])
class Conversations:
file_name = "data/conversations.json"
ai_file_name = "data/ai_conversations.txt"
conversations: dict[str, Conversation] = {}
def __init__(self):
self.conversations = self.load_conversations()
def load_conversations(self):
if os.path.getsize(self.file_name) == 0:
# Supposed to mean it's an empty file
conversations = {}
else:
with open(self.file_name, "r+") as f:
dict_conversations = json.load(f)
conversations = {}
for conv in dict_conversations:
messages = []
for message in dict_conversations[conv]["messages"]:
messages.append(
Message(
message["conv_id"],
message["time"],
message["sender_id"],
message["text"],
)
)
conversations[conv] = Conversation(conv, messages)
self.write_conversations()
return conversations
def write_conversations(self):
with open(self.file_name, "w+") as f:
for conv_id in self.conversations:
f.write(json.dumps(self.conversations[conv_id].to_dict()))
with open(self.ai_file_name, "w+") as f:
for conv_id in self.conversations:
f.write(self.conversations[conv_id].to_ai_format())
f.write("\n\n")
def update_conversation(self, conversation_id: str, messages: list[Message]):
first_message = False
if conversation_id not in self.conversations:
self.conversations[conversation_id] = Conversation(
conversation_id, messages
)
first_message = True
else:
for message in messages:
self.conversations[conversation_id].messages.append(message)
self.write_conversations()
return first_message, self.conversations[conversation_id]
@dataclass
class MessageQueue:
"""Class for keeping track of messages sent within the last TIMEOUT seconds of a message, to ensure bot doesn't respond 25 times to 25 messages over TIMEOUT seconds"""
conv_id: str
conversations: Conversations
task: asyncio.Task | None = None
messages: list[Message] = field(default_factory=list)
last_message_time: float = 0.0
async def handle_message(self, message: Message):
# in theory each message should have a unique timestamp...
found_message = False
for msg in self.messages:
if msg.time == message.time:
found_message = True
if not found_message:
self.messages.append(message)
self.last_message_time = time.time()
# Now we check if the task is None, and if it is we create it
if not self.task:
self.task = asyncio.create_task(self.process_messages())
# If the task is not done and we receive another message, we cancel the task and create a new one
elif not self.task.done():
self.task.cancel()
self.task = asyncio.create_task(self.process_messages())
elif self.task.done():
self.task = asyncio.create_task(self.process_messages())
return self.task
async def process_messages(self):
# Send all messages to the AI and backend
# In theory this will wait TIMEOUT seconds from the last message received, then update with all the *current* messages
# Unsure if this will work, we'll try it out
await asyncio.sleep(TIMEOUT)
if time.time() - self.last_message_time > TIMEOUT:
# All messages have been received within the last 3 seconds
# Add all messages to the conversation which we need a reference to somehow
# self.conversations.update_conversation(self.conv_id, self.messages)
print(
f"Should be {TIMEOUT} seconds since last message, and we would be responding here"
)
return True
return False
# Dictionary of MessageQueue objects, one for each conversation, that will hold all messages for that
# conversation in a queue until the person is perceived as done
MESSAGE_QUEUES: dict[str, MessageQueue] = {}
# def process_messages():
def send_message(recipient_id, message: Message):
data = {
"recipient": {"id": recipient_id},
"message": {"text": message.text},
"messaging_type": "RESPONSE",
"access_token": PAGE_ACCESS_TOKEN,
}
url = f"https://graph.facebook.com/v17.0/{PAGE_ID}/messages"
response = requests.post(url, json=data)
print(f"Response: {response.json()}")
return response.json()
async def incoming_message(
sender_id: str, message: Message, conversations: Conversations
):
# Create MessageQueue if it doesn't exist for the conv ID, then call handle_message on every message
if not message.conv_id in MESSAGE_QUEUES:
MESSAGE_QUEUES[message.conv_id] = MessageQueue(message.conv_id, conversations)
task = await MESSAGE_QUEUES[message.conv_id].handle_message(message)
if task is not None:
shouldUpdateMessages = await task
if shouldUpdateMessages:
# Update the conversation with the messages and get if this is the start of the conversation (first time they messaged)
# Also get the user_conversation which has all the messages in their conversation
first_message, user_conversation = conversations.update_conversation(
message.conv_id, MESSAGE_QUEUES[message.conv_id].messages
)
# After we empty the message queue as they have all been added
MESSAGE_QUEUES[message.conv_id].messages = []
# If the first message in the conversation, we send the welcome message
if first_message:
# We need to add a classifier to the Message object to see if it's sent by AI or not
# Last bool is True if sent by a user (default), False if sent by bot
# We need to add AI generation to the message object in the else below
# TODO: See how to stop this running twice, check if the message is already sent by the bot
if (
user_conversation.messages[
len(user_conversation.messages) - 1
].sender_id
is not "bot"
):
message = Message(
message.conv_id, time.time(), "bot", WELCOME_MESSAGE, False
)
user_conversation.messages.append(message)
conversations.update_conversation(message.conv_id, [message])
response = send_message(sender_id, message)
print(f"Response: {json.dumps(response, indent=2)}")
else:
# TODO: Add AI generation here with conversation context
# TODO: Format the conversational history to a readable format for the AI
# TODO: See how to stop this running twice, check if the message is already sent by the bot
if (
user_conversation.messages[
len(user_conversation.messages) - 1
].sender_id
is not "bot"
):
message = Message(
message.conv_id,
time.time(),
"bot",
"I'm still learning, this response isn't programmed yet!",
False,
)
user_conversation.messages.append(message)
conversations.update_conversation(message.conv_id, [message])
response = send_message(sender_id, message)
print(f"Response: {json.dumps(response, indent=2)}")
else:
print(f"Task is None for {message.conv_id}")
If anyone has any advice I'd greatly appreciate it, thank you!!
- Zach