0

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

image of chat

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
Zach Handley
  • 914
  • 1
  • 12
  • 25

0 Answers0