kav-k__gptdiscord METASEP

openers/ui_ux_developer.txt METASEP
I want you to act as a UX/UI developer. I will provide some details about the design of an app, website or other digital product, and it will be your job to come up with creative ways to improve its user experience. This could involve creating prototyping prototypes, testing different designs and providing feedback on what works best.
openers/travel_guide.txt METASEP
I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location. My first suggestion request is "I am in Istanbul/Beyoğlu and I want to visit only museums."
openers/text_based_adventure_game.txt METASEP
I want you to act as a text based adventure game. I will type commands and you will reply with a description of what the character sees. I want you to only reply with the game output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is wake up.
openers/tech_reviewer.txt METASEP
I want you to act as a tech reviewer. I will give you the name of a new piece of technology and you will provide me with an in-depth review - including pros, cons, features, and comparisons to other technologies on the market.
openers/storyteller.txt METASEP
I want you to act as a storyteller. You will come up with entertaining stories that are engaging, imaginative and captivating for the audience. It can be fairy tales, educational stories or any other type of stories which has the potential to capture people's attention and imagination. Depending on the target audience, you may choose specific themes or topics for your storytelling session e.g., if it’s children then you can talk about animals; If it’s adults then history-based tales might engage them better etc.
openers/python_interpreter.txt METASEP
I want you to act like a Python interpreter. I will give you Python code, and you will execute it. Do not provide any explanations. Do not respond with anything except the output of the code. The first code is: "print('hello world!')"
openers/position_interviewer.txt METASEP
I want you to act as an interviewer. I will be the candidate and you will ask me the interview questions for the position specified by the user. I want you to only reply as the interviewer. Do not write all the conservation at once. I want you to only do the interview with me. Ask me the questions and wait for my answers. Do not write explanations. Ask me the questions one by one like an interviewer does and wait for my answers. The user will first specify the position.
openers/personal_trainer.txt METASEP
I want you to act as a personal trainer. I will provide you with all the information needed about an individual looking to become fitter, stronger and healthier through physical training, and your role is to devise the best plan for that person depending on their current fitness level, goals and lifestyle habits. You should use your knowledge of exercise science, nutrition advice, and other relevant factors in order to create a plan suitable for them.
openers/novelist.txt METASEP
I want you to act as a novelist. You will come up with creative and captivating stories that can engage readers for long periods of time. You may choose any genre such as fantasy, romance, historical fiction and so on - but the aim is to write something that has an outstanding plotline, engaging characters and unexpected climaxes.
openers/motivational_speaker.txt METASEP
I want you to act as a motivational speaker. Put together words that inspire action and make people feel empowered to do something beyond their abilities. You can talk about any topics but the aim is to make sure what you say resonates with your audience, giving them an incentive to work on their goals and strive for better possibilities.
openers/motivational_coach.txt METASEP
I want you to act as a motivational coach. I will provide you with some information about someone's goals and challenges, and it will be your job to come up with strategies that can help this person achieve their goals. This could involve providing positive affirmations, giving helpful advice or suggesting activities they can do to reach their end goal.
openers/life_coach.txt METASEP
I want you to act as a life coach. I will provide some details about my current situation and goals, and it will be your job to come up with strategies that can help me make better decisions and reach those objectives. This could involve offering advice on various topics, such as creating plans for achieving success or dealing with difficult emotions.
openers/javascript_console.txt METASEP
I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. 
openers/essay_writer.txt METASEP
I want you to act as an essay writer. You will need to research a given topic, formulate a thesis statement, and create a persuasive piece of work that is both informative and engaging.
openers/english_translator.txt METASEP
I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. 
openers/english_translator.json METASEP
{
    "text":"I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. ",
    "temperature":0.77,
    "top_p":0.9,
    "frequency_penalty":0.95,
    "presence_penalty":0.95
}


openers/debater.txt METASEP
I want you to act as a debater. I will provide you with some topics related to current events and your task is to research both sides of the debates, present valid arguments for each side, refute opposing points of view, and draw persuasive conclusions based on evidence. Your goal is to help people come away from the discussion with increased knowledge and insight into the topic at hand.
openers/composer.txt METASEP
I want you to act as a composer. I will provide the lyrics to a song and you will create music for it. This could include using various instruments or tools, such as synthesizers or samplers, in order to create melodies and harmonies that bring the lyrics to life.
detailed_guides/OPENAI-Guide.md METASEP
<h1><a href="https://app.tango.us/app/workflow/d06b2900-1f94-4b89-a3f1-e22ef8428363?utm_source=magicCopy&utm_medium=magicCopy&utm_campaign=workflow%20export%20links" target="_blank">How to get a API-Key from OpenAI</a></h1>
<div><b>Date:</b> January 8, 2023</div>
<div><b>Author:</b> CrypticHeaven-Lab</div>
<hr/>


<div><h3>1. <a href="https://www.deepl.com/translator#de/en/Langsam%20fange%20ich%20an%20OpenAI%20zu%20hassen..." target="_blank">Go to OpenAI at https://openai.com/api/</a></h3>
</div>

<div><h3>2. Click on "SIGN UP" at the top right corner.</h3>
<img src="https://images.tango.us/workflows/d06b2900-1f94-4b89-a3f1-e22ef8428363/steps/b05bcb6b-a769-4629-82f4-182d164872e3/d708629b-cf0d-49d6-bc1d-9b82f42c7d5f.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=188%3A53" width="600" alt="Click on &quot;SIGN UP&quot; at the top right corner." />
</div>

<div><h3>3. Register yourself a new Account following the shown instructions.</h3>
</div>

<div><h3>4. After the successful registration you see the OpenAI overview.</h3>
<img src="https://images.tango.us/workflows/d06b2900-1f94-4b89-a3f1-e22ef8428363/steps/28e72617-81af-403e-8e10-cb163f7ef261/e45db744-e5a1-41c8-91e7-7920f51ea418.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=1852%3A777" width="600" alt="After the successful registration you see the OpenAI overview." />
</div>

<div><h3>5. There you click in the top right corner at "Personal".</h3>
<img src="https://images.tango.us/workflows/d06b2900-1f94-4b89-a3f1-e22ef8428363/steps/4e05d623-95d3-4a6d-8761-b6a46af948c5/3020df6c-a6d7-425a-a539-d11003bc6f23.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=289%3A36" width="600" alt="There you click in the top right corner at &quot;Personal&quot;." />
</div>

<div><h3>6. And then click at "View API keys" in the dropdown menu.</h3>
<img src="https://images.tango.us/workflows/d06b2900-1f94-4b89-a3f1-e22ef8428363/steps/8727d9b6-8909-41db-92ad-6c6cb4ffb565/acf192ed-f68f-4298-b93d-2373ba7bc00c.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=202%3A294" width="600" alt="And then click at &quot;View API keys&quot; in the dropdown menu." />
</div>

<div><h3>7. On the next page click at "Create new secret key".</h3>
<p>If you failed to copy it or lost the copied key, redo that step and delete the old key by clicking on the little grey trash icon right next to the "LAST USED" information for your key.</p>
<img src="https://images.tango.us/workflows/d06b2900-1f94-4b89-a3f1-e22ef8428363/steps/d3141fb6-b89f-49ef-b956-f8755981bbe6/846487b7-47df-4da3-8b9e-6c11992c839f.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=763%3A497" width="600" alt="On the next page click at &quot;Create new secret key&quot;." />
</div>

<div><h3>8. IMPORTANT STEP! Copy this key by clicking the green highlighted button.</h3>
<p>We will need this key in the future for setting up the bot on our server.</p>
<img src="https://images.tango.us/workflows/d06b2900-1f94-4b89-a3f1-e22ef8428363/steps/0e2df991-732e-425a-a6d5-e4924bc90b3f/c3f11360-e444-49d3-a4ce-63d3a30a9b50.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=518%3A269" width="600" alt="IMPORTANT STEP! Copy this key by clicking the green highlighted button." />
</div>

<hr/>
<div>

detailed_guides/DROPLET-Guide.md METASEP
<h1><a href="https://app.tango.us/app/workflow/c1516302-2b29-472d-b9d6-82e1e6a58d4e?utm_source=magicCopy&utm_medium=magicCopy&utm_campaign=workflow%20export%20links" target="_blank">How to create your own Droplet with DigitalOcean to run GPT3Discord</a></h1>
<div><b>Date:</b> January 8, 2023</div>
<div><b>Author:</b> CrypticHeaven-Lab</div>
<hr/>


<div><h3>1. <a href="https://www.digitalocean.com/" target="_blank">Go to DigitalOcean | The Cloud for Builders</a></h3>
</div>

<div><h3>2. Click on Sign up to register yourself.</h3>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/98d99473-62a5-4ee8-ad23-b1659c31a315/b791b344-0dba-4bb4-809b-250a675e9545.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.8105&fp-y=0.0795&fp-z=2.9158&w=1200&ar=1879%3A931" width="600" alt="Click on Sign up to register yourself." />
</div>

<div><h3>3. If you already have an account click on Sign in.</h3>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/51d47d6d-8f24-4d62-b472-62bfd4e28a9a/083509e5-a4d6-4690-9c4e-fab558f67831.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.9643&fp-y=0.0354&fp-z=3.0696&w=1200&ar=1879%3A931" width="600" alt="If you already have an account click on Sign in." />
</div>

<div><h3>4. Click password field</h3>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/65f64772-bc0e-4695-8370-dc9d3ff34972/a3ff43ba-f2d6-499a-9078-35cdb5c750af.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.2501&fp-y=0.6165&fp-z=1.9538&w=1200&ar=1879%3A931" width="600" alt="Click password field" />
</div>

<div><h3>5. Enter your password and click on Log In.</h3>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/3857a3a6-bcc3-4ec6-9abc-ccd3f4ab28fb/dd14f77f-bf57-458a-a015-a2d56075438f.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.2501&fp-y=0.6939&fp-z=1.9538&w=1200&ar=1879%3A931" width="600" alt="Enter your password and click on Log In." />
</div>

<div><h3>6. After these steps click on "Explore our control panel".</h3>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/829f6452-8a93-4b9e-84db-8ac54ad4b673/bf136990-2676-4dba-90bf-3b0309b8a207.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=965%3A763" width="600" alt="After these steps click on &quot;Explore our control panel&quot;." />
</div>

<div><h3>7. Then Click on "Get started with a Droplet".</h3>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/fde0ac69-a6a3-4f28-b69e-24da309481c7/1e628a55-9f9f-47cb-b5ef-cf64d21a039c.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=1212%3A690" width="600" alt="Then Click on &quot;Get started with a Droplet&quot;." />
</div>

<div><h3>8. Here it is very important to choose the right distribution via the drop down menu.</h3>
<p>The distribution which works best with the bot is Ubuntu 20.04 (LTS) x64.</p>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/1bbff69f-df7d-4293-99d5-b259440d1ca1/ff4b265b-90f4-4269-ab9b-488145cd29a1.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=1246%3A410" width="600" alt="Here it is very important to choose the right distribution via the drop down menu." />
</div>

<div><h3>9. For the next step You've to choose a plan and the CPU option.</h3>
<p>Do at least as shown in the picture below.</p>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/97db3b5f-08db-451a-a33b-47d3234f6884/f1982d00-efe6-4cad-bb3b-bc88524bdbc7.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=1207%3A607" width="600" alt="For the next step You&#039;ve to choose a plan and the CPU option." />
</div>

<div><h3>10. You really don't need to add more block storage, as there is enough on default for the bot.</h3>
<p>Just choose the region closest to yours right below the "Add block storage"</p>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/640ade49-9b64-413a-b64c-62ebacc53fd3/a9039f3d-67a4-4b00-8ad0-c4b9a116adb6.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=1209%3A524" width="600" alt="You really don&#039;t need to add more block storage, as there is enough on default for the bot." />
</div>

<div><h3>11. Choose the Password option and set up your password.</h3>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/d5c7b916-aaf1-4657-b92e-7e9b927e5a07/e163e4ab-25ff-4f08-ba21-83405622f0ac.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=1210%3A484" width="600" alt="Choose the Password option and set up your password." />
</div>

<div><h3>12. I highly recommend you turn on the backup feature.</h3>
<p>Backups increase the monthly amount to pay. The rest is up to you as it is free.</p>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/d62e37b0-5cbd-4dbb-8760-bc00c42ba518/600595ba-1adf-4ad0-a7af-60c727c75c58.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=1203%3A456" width="600" alt="I highly recommend you turn on the backup feature." />
</div>

<div><h3>13. Give your Droplet a name or left it untouched if you don't want to rename it.</h3>
<p>Choose some tags or left it untouched. It's for you to decide. </p>
<img src="https://images.tango.us/workflows/c1516302-2b29-472d-b9d6-82e1e6a58d4e/steps/278ef770-bc46-4da2-8ce4-d509ae7da44e/a21a72fd-49d8-45e3-a614-18a1bad9aa46.png?fm=png&crop=focalpoint&fit=crop&fp-x=0.5000&fp-y=0.5000&fp-z=1.0000&w=1200&ar=1201%3A619" width="600" alt="Give your Droplet a name or left it untouched if you don&#039;t want to rename it." />
</div>

<div><h3>14. Congratulations! You have successfully created your own Droplet.</h3>
</div>

<hr/>
<div>

models/user_model.py METASEP
"""
Store information about a discord user, for the purposes of enabling conversations. We store a message
history, message count, and the id of the user in order to track them.
"""


class RedoUser:
    def __init__(self, prompt, instruction, message, ctx, response, codex, paginator):
        self.prompt = prompt
        self.instruction = instruction
        self.message = message
        self.ctx = ctx
        self.response = response
        self.codex = codex
        self.paginator = paginator
        self.interactions = []

    def add_interaction(self, interaction):
        self.interactions.append(interaction)

    def in_interaction(self, interaction):
        return interaction in self.interactions

    # Represented by user_id
    def __hash__(self):
        return hash(self.message.author.id)

    def __eq__(self, other):
        return self.message.author.id == other.message.author.id

    # repr
    def __repr__(self):
        return f"RedoUser({self.message.author.id})"


class User:
    def __init__(self, id):
        self.id = id
        self.history = []
        self.count = 0

    # These user objects should be accessible by ID, for example if we had a bunch of user
    # objects in a list, and we did `if 1203910293001 in user_list`, it would return True
    # if the user with that ID was in the list
    def __eq__(self, other):
        return self.id == other.id

    def __hash__(self):
        return hash(self.id)

    def __repr__(self):
        return f"User(id={self.id}, history={self.history})"

    def __str__(self):
        return self.__repr__()


class Thread:
    def __init__(self, id):
        self.id = id
        self.history = []
        self.count = 0
        self.model = None
        self.temperature = None
        self.top_p = None
        self.frequency_penalty = None
        self.presence_penalty = None

    def set_overrides(
        self,
        temperature=None,
        top_p=None,
        frequency_penalty=None,
        presence_penalty=None,
    ):
        self.temperature = temperature
        self.top_p = top_p
        self.frequency_penalty = frequency_penalty
        self.presence_penalty = presence_penalty

    def get_overrides(self):
        return {
            "temperature": self.temperature,
            "top_p": self.top_p,
            "frequency_penalty": self.frequency_penalty,
            "presence_penalty": self.presence_penalty,
        }

    # These user objects should be accessible by ID, for example if we had a bunch of user
    # objects in a list, and we did `if 1203910293001 in user_list`, it would return True
    # if the user with that ID was in the list
    def __eq__(self, other):
        return self.id == other.id

    def __hash__(self):
        return hash(self.id)

    def __repr__(self):
        return f"Thread(id={self.id}, history={self.history})"

    def __str__(self):
        return self.__repr__()


class EmbeddedConversationItem:
    def __init__(self, text, timestamp):
        self.text = text
        self.timestamp = int(timestamp)

    def __repr__(self):
        return self.text

    def __str__(self):
        return self.__repr__()

    def __eq__(self, other):
        return self.text == other.text and self.timestamp == other.timestamp

    def __hash__(self):
        return hash(self.text) + hash(self.timestamp)

    def __lt__(self, other):
        return self.timestamp < other.timestamp

    def __gt__(self, other):
        return self.timestamp > other.timestamp

    def __le__(self, other):
        return self.timestamp <= other.timestamp

    def __ge__(self, other):
        return self.timestamp >= other.timestamp

    def __ne__(self, other):
        return not self.__eq__(other)

    # Make it such that if there is an arry with these EmbeddedConversationItems, if we "".join the array, each item will
    # return the .text attribute
    def __format__(self, format_spec):
        return self.text

models/openai_model.py METASEP
import asyncio
import functools
import math
import os
import tempfile
import traceback
import uuid
from typing import Tuple, List, Any

import aiohttp
import backoff
import discord

# An enum of two modes, TOP_P or TEMPERATURE
import requests
from PIL import Image
from aiohttp import RequestInfo
from discord import File


class Mode:
    TOP_P = "top_p"
    TEMPERATURE = "temperature"


class Models:
    DAVINCI = "text-davinci-003"
    CURIE = "text-curie-001"
    EMBEDDINGS = "text-embedding-ada-002"
    EDIT = "text-davinci-edit-001"
    CODE_EDIT = "code-davinci-edit-001"


class ImageSize:
    LARGE = "1024x1024"
    MEDIUM = "512x512"
    SMALL = "256x256"


class Model:
    def __init__(self, usage_service):
        self._mode = Mode.TEMPERATURE
        self._temp = 0.6  # Higher value means more random, lower value means more likely to be a coherent sentence
        self._top_p = 0.9  # 1 is equivalent to greedy sampling, 0.1 means that the model will only consider the top 10% of the probability distribution
        self._max_tokens = 4000  # The maximum number of tokens the model can generate
        self._presence_penalty = (
            0  # Penalize new tokens based on whether they appear in the text so far
        )
        self._frequency_penalty = 0  # Penalize new tokens based on their existing frequency in the text so far. (Higher frequency = lower probability of being chosen.)
        self._best_of = 1  # Number of responses to compare the loglikelihoods of
        self._prompt_min_length = 8
        self._max_conversation_length = 100
        self._model = Models.DAVINCI
        self._low_usage_mode = False
        self.usage_service = usage_service
        self.DAVINCI_ROLES = ["admin", "Admin", "GPT", "gpt"]
        self._image_size = ImageSize.MEDIUM
        self._num_images = 2
        self._summarize_conversations = True
        self._summarize_threshold = 2500
        self.model_max_tokens = 4024
        self._welcome_message_enabled = True
        self._num_static_conversation_items = 8
        self._num_conversation_lookback = 6

        try:
            self.IMAGE_SAVE_PATH = os.environ["IMAGE_SAVE_PATH"]
            self.custom_image_path = True
        except:
            self.IMAGE_SAVE_PATH = "dalleimages"
            # Try to make this folder called images/ in the local directory if it doesnt exist
            if not os.path.exists(self.IMAGE_SAVE_PATH):
                os.makedirs(self.IMAGE_SAVE_PATH)
            self.custom_image_path = False

        self._hidden_attributes = [
            "usage_service",
            "DAVINCI_ROLES",
            "custom_image_path",
            "custom_web_root",
            "_hidden_attributes",
            "model_max_tokens",
            "openai_key",
        ]

        self.openai_key = os.getenv("OPENAI_TOKEN")

    # Use the @property and @setter decorators for all the self fields to provide value checking

    @property
    def num_static_conversation_items(self):
        return self._num_static_conversation_items

    @num_static_conversation_items.setter
    def num_static_conversation_items(self, value):
        value = int(value)
        if value < 3:
            raise ValueError("num_static_conversation_items must be >= 3")
        if value > 20:
            raise ValueError(
                "num_static_conversation_items must be <= 20, this is to ensure reliability and reduce token wastage!"
            )
        self._num_static_conversation_items = value

    @property
    def num_conversation_lookback(self):
        return self._num_conversation_lookback

    @num_conversation_lookback.setter
    def num_conversation_lookback(self, value):
        value = int(value)
        if value < 3:
            raise ValueError("num_conversation_lookback must be >= 3")
        if value > 15:
            raise ValueError(
                "num_conversation_lookback must be <= 15, this is to ensure reliability and reduce token wastage!"
            )
        self._num_conversation_lookback = value

    @property
    def welcome_message_enabled(self):
        return self._welcome_message_enabled

    @welcome_message_enabled.setter
    def welcome_message_enabled(self, value):
        if value.lower() == "true":
            self._welcome_message_enabled = True
        elif value.lower() == "false":
            self._welcome_message_enabled = False
        else:
            raise ValueError("Value must be either true or false!")

    @property
    def summarize_threshold(self):
        return self._summarize_threshold

    @summarize_threshold.setter
    def summarize_threshold(self, value):
        value = int(value)
        if value < 800 or value > 4000:
            raise ValueError(
                "Summarize threshold cannot be greater than 4000 or less than 800!"
            )
        self._summarize_threshold = value

    @property
    def summarize_conversations(self):
        return self._summarize_conversations

    @summarize_conversations.setter
    def summarize_conversations(self, value):
        # convert value string into boolean
        if value.lower() == "true":
            value = True
        elif value.lower() == "false":
            value = False
        else:
            raise ValueError("Value must be either true or false!")
        self._summarize_conversations = value

    @property
    def image_size(self):
        return self._image_size

    @image_size.setter
    def image_size(self, value):
        if value in ImageSize.__dict__.values():
            self._image_size = value
        else:
            raise ValueError(
                "Image size must be one of the following: SMALL(256x256), MEDIUM(512x512), LARGE(1024x1024)"
            )

    @property
    def num_images(self):
        return self._num_images

    @num_images.setter
    def num_images(self, value):
        value = int(value)
        if value > 4 or value <= 0:
            raise ValueError("num_images must be less than 4 and at least 1.")
        self._num_images = value

    @property
    def low_usage_mode(self):
        return self._low_usage_mode

    @low_usage_mode.setter
    def low_usage_mode(self, value):
        # convert value string into boolean
        if value.lower() == "true":
            value = True
        elif value.lower() == "false":
            value = False
        else:
            raise ValueError("Value must be either true or false!")

        if value:
            self._model = Models.CURIE
            self.max_tokens = 1900
            self.model_max_tokens = 1000
        else:
            self._model = Models.DAVINCI
            self.max_tokens = 4000
            self.model_max_tokens = 4024

    @property
    def model(self):
        return self._model

    @model.setter
    def model(self, model):
        if model not in [Models.DAVINCI, Models.CURIE]:
            raise ValueError(
                "Invalid model, must be text-davinci-003 or text-curie-001"
            )
        self._model = model

    @property
    def max_conversation_length(self):
        return self._max_conversation_length

    @max_conversation_length.setter
    def max_conversation_length(self, value):
        value = int(value)
        if value < 1:
            raise ValueError("Max conversation length must be greater than 1")
        if value > 500:
            raise ValueError(
                "Max conversation length must be less than 500, this will start using credits quick."
            )
        self._max_conversation_length = value

    @property
    def mode(self):
        return self._mode

    @mode.setter
    def mode(self, value):
        if value not in [Mode.TOP_P, Mode.TEMPERATURE]:
            raise ValueError("mode must be either 'top_p' or 'temperature'")
        if value == Mode.TOP_P:
            self._top_p = 0.1
            self._temp = 0.7
        elif value == Mode.TEMPERATURE:
            self._top_p = 0.9
            self._temp = 0.6

        self._mode = value

    @property
    def temp(self):
        return self._temp

    @temp.setter
    def temp(self, value):
        value = float(value)
        if value < 0 or value > 1:
            raise ValueError(
                "temperature must be greater than 0 and less than 1, it is currently "
                + str(value)
            )

        self._temp = value

    @property
    def top_p(self):
        return self._top_p

    @top_p.setter
    def top_p(self, value):
        value = float(value)
        if value < 0 or value > 1:
            raise ValueError(
                "top_p must be greater than 0 and less than 1, it is currently "
                + str(value)
            )
        self._top_p = value

    @property
    def max_tokens(self):
        return self._max_tokens

    @max_tokens.setter
    def max_tokens(self, value):
        value = int(value)
        if value < 15 or value > 4096:
            raise ValueError(
                "max_tokens must be greater than 15 and less than 4096, it is currently "
                + str(value)
            )
        self._max_tokens = value

    @property
    def presence_penalty(self):
        return self._presence_penalty

    @presence_penalty.setter
    def presence_penalty(self, value):
        if int(value) < 0:
            raise ValueError(
                "presence_penalty must be greater than 0, it is currently " + str(value)
            )
        self._presence_penalty = value

    @property
    def frequency_penalty(self):
        return self._frequency_penalty

    @frequency_penalty.setter
    def frequency_penalty(self, value):
        if int(value) < 0:
            raise ValueError(
                "frequency_penalty must be greater than 0, it is currently "
                + str(value)
            )
        self._frequency_penalty = value

    @property
    def best_of(self):
        return self._best_of

    @best_of.setter
    def best_of(self, value):
        value = int(value)
        if value < 1 or value > 3:
            raise ValueError(
                "best_of must be greater than 0 and ideally less than 3 to save tokens, it is currently "
                + str(value)
            )
        self._best_of = value

    @property
    def prompt_min_length(self):
        return self._prompt_min_length

    @prompt_min_length.setter
    def prompt_min_length(self, value):
        value = int(value)
        if value < 10 or value > 4096:
            raise ValueError(
                "prompt_min_length must be greater than 10 and less than 4096, it is currently "
                + str(value)
            )
        self._prompt_min_length = value

    def backoff_handler(details):
        print(
            f"Backing off {details['wait']:0.1f} seconds after {details['tries']} tries calling function {details['target']} | "
            f"{details['exception'].status}: {details['exception'].message}"
        )

    async def valid_text_request(self, response):
        try:
            tokens_used = int(response["usage"]["total_tokens"])
            await self.usage_service.update_usage(tokens_used)
        except:
            raise ValueError(
                "The API returned an invalid response: "
                + str(response["error"]["message"])
            )

    @backoff.on_exception(
        backoff.expo,
        aiohttp.ClientResponseError,
        factor=3,
        base=5,
        max_tries=4,
        on_backoff=backoff_handler,
    )
    async def send_embedding_request(self, text, custom_api_key=None):
        async with aiohttp.ClientSession(raise_for_status=True) as session:
            payload = {
                "model": Models.EMBEDDINGS,
                "input": text,
            }
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.openai_key if not custom_api_key else custom_api_key}",
            }
            async with session.post(
                "https://api.openai.com/v1/embeddings", json=payload, headers=headers
            ) as resp:
                response = await resp.json()

                try:
                    return response["data"][0]["embedding"]
                except Exception:
                    print(response)
                    traceback.print_exc()
                    return

    @backoff.on_exception(
        backoff.expo,
        aiohttp.ClientResponseError,
        factor=3,
        base=5,
        max_tries=6,
        on_backoff=backoff_handler,
    )
    async def send_edit_request(
        self,
        instruction,
        input=None,
        temp_override=None,
        top_p_override=None,
        codex=False,
        custom_api_key=None,
    ):

        # Validate that  all the parameters are in a good state before we send the request
        if len(instruction) < self.prompt_min_length:
            raise ValueError(
                "Instruction must be greater than 8 characters, it is currently "
                + str(len(instruction))
            )

        print(
            f"The text about to be edited is [{input}] with instructions [{instruction}] codex [{codex}]"
        )
        print(f"Overrides -> temp:{temp_override}, top_p:{top_p_override}")

        async with aiohttp.ClientSession(raise_for_status=True) as session:
            payload = {
                "model": Models.EDIT if codex is False else Models.CODE_EDIT,
                "input": "" if input is None else input,
                "instruction": instruction,
                "temperature": self.temp if temp_override is None else temp_override,
                "top_p": self.top_p if top_p_override is None else top_p_override,
            }
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.openai_key if not custom_api_key else custom_api_key}",
            }
            async with session.post(
                "https://api.openai.com/v1/edits", json=payload, headers=headers
            ) as resp:
                response = await resp.json()
                await self.valid_text_request(response)
                return response

    @backoff.on_exception(
        backoff.expo,
        aiohttp.ClientResponseError,
        factor=3,
        base=5,
        max_tries=6,
        on_backoff=backoff_handler,
    )
    async def send_moderations_request(self, text):
        # Use aiohttp to send the above request:
        async with aiohttp.ClientSession(raise_for_status=True) as session:
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.openai_key}",
            }
            payload = {"input": text}
            async with session.post(
                "https://api.openai.com/v1/moderations",
                headers=headers,
                json=payload,
            ) as response:
                return await response.json()

    @backoff.on_exception(
        backoff.expo,
        aiohttp.ClientResponseError,
        factor=3,
        base=5,
        max_tries=4,
        on_backoff=backoff_handler,
    )
    async def send_summary_request(self, prompt, custom_api_key=None):
        """
        Sends a summary request to the OpenAI API
        """
        summary_request_text = []
        summary_request_text.append(
            "The following is a conversation instruction set and a conversation"
            " between two people, a <username>, and GPTie. Firstly, determine the <username>'s name from the conversation history, then summarize the conversation. Do not summarize the instructions for GPTie, only the conversation. Summarize the conversation in a detailed fashion. If <username> mentioned their name, be sure to mention it in the summary. Pay close attention to things the <username> has told you, such as personal details."
        )
        summary_request_text.append(prompt + "\nDetailed summary of conversation: \n")

        summary_request_text = "".join(summary_request_text)

        tokens = self.usage_service.count_tokens(summary_request_text)

        async with aiohttp.ClientSession(raise_for_status=True) as session:
            payload = {
                "model": Models.DAVINCI,
                "prompt": summary_request_text,
                "temperature": 0.5,
                "top_p": 1,
                "max_tokens": self.max_tokens - tokens,
                "presence_penalty": self.presence_penalty,
                "frequency_penalty": self.frequency_penalty,
                "best_of": self.best_of,
            }
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.openai_key if not custom_api_key else custom_api_key}",
            }
            async with session.post(
                "https://api.openai.com/v1/completions", json=payload, headers=headers
            ) as resp:
                response = await resp.json()

                await self.valid_text_request(response)

                # print(response["choices"][0]["text"])

                return response

    @backoff.on_exception(
        backoff.expo,
        aiohttp.ClientResponseError,
        factor=3,
        base=5,
        max_tries=4,
        on_backoff=backoff_handler,
    )
    async def send_request(
        self,
        prompt,
        tokens,
        temp_override=None,
        top_p_override=None,
        best_of_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        max_tokens_override=None,
        model=None,
        custom_api_key=None,
    ) -> (
        dict,
        bool,
    ):  # The response, and a boolean indicating whether or not the context limit was reached.

        # Validate that  all the parameters are in a good state before we send the request
        if len(prompt) < self.prompt_min_length:
            raise ValueError(
                "Prompt must be greater than 8 characters, it is currently "
                + str(len(prompt))
            )

        print("The prompt about to be sent is " + prompt)
        print(
            f"Overrides -> temp:{temp_override}, top_p:{top_p_override} frequency:{frequency_penalty_override}, presence:{presence_penalty_override}"
        )

        async with aiohttp.ClientSession(raise_for_status=True) as session:
            payload = {
                "model": self.model if model is None else model,
                "prompt": prompt,
                "temperature": self.temp if temp_override is None else temp_override,
                "top_p": self.top_p if top_p_override is None else top_p_override,
                "max_tokens": self.max_tokens - tokens
                if not max_tokens_override
                else max_tokens_override,
                "presence_penalty": self.presence_penalty
                if presence_penalty_override is None
                else presence_penalty_override,
                "frequency_penalty": self.frequency_penalty
                if frequency_penalty_override is None
                else frequency_penalty_override,
                "best_of": self.best_of if not best_of_override else best_of_override,
            }
            headers = {
                "Authorization": f"Bearer {self.openai_key if not custom_api_key else custom_api_key}"
            }
            async with session.post(
                "https://api.openai.com/v1/completions", json=payload, headers=headers
            ) as resp:
                response = await resp.json()
                # print(f"Payload -> {payload}")
                # Parse the total tokens used for this request and response pair from the response
                await self.valid_text_request(response)
                print(f"Response -> {response}")

                return response

    @staticmethod
    async def send_test_request(api_key):

        async with aiohttp.ClientSession() as session:
            payload = {
                "model": Models.CURIE,
                "prompt": "test.",
                "temperature": 1,
                "top_p": 1,
                "max_tokens": 10,
            }
            headers = {"Authorization": f"Bearer {api_key}"}
            async with session.post(
                "https://api.openai.com/v1/completions", json=payload, headers=headers
            ) as resp:
                response = await resp.json()
                try:
                    int(response["usage"]["total_tokens"])
                except:
                    raise ValueError(str(response["error"]["message"]))

                return response

    @backoff.on_exception(
        backoff.expo,
        aiohttp.ClientResponseError,
        factor=3,
        base=5,
        max_tries=4,
        on_backoff=backoff_handler,
    )
    async def send_image_request(
        self, ctx, prompt, vary=None, custom_api_key=None
    ) -> tuple[File, list[Any]]:
        # Validate that  all the parameters are in a good state before we send the request
        words = len(prompt.split(" "))
        if words < 3 or words > 75:
            raise ValueError(
                "Prompt must be greater than 3 words and less than 75, it is currently "
                + str(words)
            )

        # print("The prompt about to be sent is " + prompt)
        await self.usage_service.update_usage_image(self.image_size)

        response = None

        if not vary:
            payload = {"prompt": prompt, "n": self.num_images, "size": self.image_size}
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.openai_key if not custom_api_key else custom_api_key}",
            }
            async with aiohttp.ClientSession(raise_for_status=True) as session:
                async with session.post(
                    "https://api.openai.com/v1/images/generations",
                    json=payload,
                    headers=headers,
                ) as resp:
                    response = await resp.json()

        else:
            async with aiohttp.ClientSession(raise_for_status=True) as session:
                data = aiohttp.FormData()
                data.add_field("n", str(self.num_images))
                data.add_field("size", self.image_size)
                with open(vary, "rb") as f:
                    data.add_field(
                        "image", f, filename="file.png", content_type="image/png"
                    )

                    async with session.post(
                        "https://api.openai.com/v1/images/variations",
                        headers={
                            "Authorization": f"Bearer {self.openai_key if not custom_api_key else custom_api_key}",
                        },
                        data=data,
                    ) as resp:
                        response = await resp.json()

        print(response)

        image_urls = []
        for result in response["data"]:
            image_urls.append(result["url"])

        # For each image url, open it as an image object using PIL
        images = await asyncio.get_running_loop().run_in_executor(
            None,
            lambda: [
                Image.open(requests.get(url, stream=True).raw) for url in image_urls
            ],
        )

        # Save all the images with a random name to self.IMAGE_SAVE_PATH
        image_names = [f"{uuid.uuid4()}.png" for _ in range(len(images))]
        for image, name in zip(images, image_names):
            await asyncio.get_running_loop().run_in_executor(
                None, image.save, f"{self.IMAGE_SAVE_PATH}/{name}"
            )

        # Update image_urls to include the local path to these new images
        image_urls = [f"{self.IMAGE_SAVE_PATH}/{name}" for name in image_names]

        widths, heights = zip(*(i.size for i in images))

        # Calculate the number of rows and columns needed for the grid
        num_rows = num_cols = int(math.ceil(math.sqrt(len(images))))

        # If there are only 2 images, set the number of rows to 1
        if len(images) == 2:
            num_rows = 1

        # Calculate the size of the combined image
        width = max(widths) * num_cols
        height = max(heights) * num_rows

        # Create a transparent image with the same size as the images
        transparent = await asyncio.get_running_loop().run_in_executor(
            None, lambda: Image.new("RGBA", (max(widths), max(heights)))
        )

        # Create a new image with the calculated size
        new_im = await asyncio.get_running_loop().run_in_executor(
            None, lambda: Image.new("RGBA", (width, height))
        )

        # Paste the images and transparent segments into the grid
        x_offset = y_offset = 0
        for im in images:
            await asyncio.get_running_loop().run_in_executor(
                None, new_im.paste, im, (x_offset, y_offset)
            )

            x_offset += im.size[0]
            if x_offset >= width:
                x_offset = 0
                y_offset += im.size[1]

        # Fill the remaining cells with transparent segments
        while y_offset < height:
            while x_offset < width:
                await asyncio.get_running_loop().run_in_executor(
                    None, new_im.paste, transparent, (x_offset, y_offset)
                )
                x_offset += transparent.size[0]

            x_offset = 0
            y_offset += transparent.size[1]

        # Save the new_im to a temporary file and return it as a discord.File
        temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
        await asyncio.get_running_loop().run_in_executor(
            None, new_im.save, temp_file.name
        )

        # Print the filesize of new_im, in mega bytes
        image_size = os.path.getsize(temp_file.name) / 1048576
        if ctx.guild is None:
            guild_file_limit = 8
        else:
            guild_file_limit = ctx.guild.filesize_limit / 1048576

        # If the image size is greater than 8MB, we can't return this to the user, so we will need to downscale the
        # image and try again
        safety_counter = 0
        while image_size > guild_file_limit:
            safety_counter += 1
            if safety_counter >= 3:
                break
            print(
                f"Image size is {image_size}MB, which is too large for this server {guild_file_limit}MB. Downscaling and trying again"
            )
            # We want to do this resizing asynchronously, so that it doesn't block the main thread during the resize.
            # We can use the asyncio.run_in_executor method to do this
            new_im = await asyncio.get_running_loop().run_in_executor(
                None,
                functools.partial(
                    new_im.resize, (int(new_im.width / 1.05), int(new_im.height / 1.05))
                ),
            )

            temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
            await asyncio.get_running_loop().run_in_executor(
                None, new_im.save, temp_file.name
            )
            image_size = os.path.getsize(temp_file.name) / 1000000
            print(f"New image size is {image_size}MB")

        return (discord.File(temp_file.name), image_urls)

models/check_model.py METASEP
import discord

from models.env_service_model import EnvService
from typing import Callable

ADMIN_ROLES = EnvService.get_admin_roles()
DALLE_ROLES = EnvService.get_dalle_roles()
GPT_ROLES = EnvService.get_gpt_roles()
ALLOWED_GUILDS = EnvService.get_allowed_guilds()


class Check:
    def check_admin_roles() -> Callable:
        async def inner(ctx: discord.ApplicationContext):
            if ADMIN_ROLES == [None]:
                return True

            if not any(role.name.lower() in ADMIN_ROLES for role in ctx.user.roles):
                await ctx.defer(ephemeral=True)
                await ctx.respond(
                    f"You don't have permission to use this.",
                    ephemeral=True,
                    delete_after=10,
                )
                return False
            return True

        return inner

    def check_dalle_roles() -> Callable:
        async def inner(ctx: discord.ApplicationContext):
            if DALLE_ROLES == [None]:
                return True
            if not any(role.name.lower() in DALLE_ROLES for role in ctx.user.roles):
                await ctx.defer(ephemeral=True)
                await ctx.respond(
                    "You don't have permission to use this.",
                    ephemeral=True,
                    delete_after=10,
                )
                return False
            return True

        return inner

    def check_gpt_roles() -> Callable:
        async def inner(ctx: discord.ApplicationContext):
            if GPT_ROLES == [None]:
                return True
            if not any(role.name.lower() in GPT_ROLES for role in ctx.user.roles):
                await ctx.defer(ephemeral=True)
                await ctx.respond(
                    "You don't have permission to use this.",
                    ephemeral=True,
                    delete_after=10,
                )
                return False
            return True

        return inner

models/autocomplete_model.py METASEP
from pathlib import Path
import os
import re

import discord
from models.usage_service_model import UsageService
from models.openai_model import Model
from models.env_service_model import EnvService

usage_service = UsageService(Path(os.environ.get("DATA_DIR", os.getcwd())))
model = Model(usage_service)


class Settings_autocompleter:
    async def get_settings(ctx: discord.AutocompleteContext):
        SETTINGS = [
            re.sub("^_", "", key)
            for key in model.__dict__.keys()
            if key not in model._hidden_attributes
        ]
        return [
            parameter
            for parameter in SETTINGS
            if parameter.startswith(ctx.value.lower())
        ][:25]

    async def get_value(
        ctx: discord.AutocompleteContext,
    ):  # Behaves a bit weird if you go back and edit the parameter without typing in a new command
        values = {
            "max_conversation_length": [str(num) for num in range(1, 500, 2)],
            "num_images": [str(num) for num in range(1, 4 + 1)],
            "mode": ["temperature", "top_p"],
            "model": ["text-davinci-003", "text-curie-001"],
            "low_usage_mode": ["True", "False"],
            "image_size": ["256x256", "512x512", "1024x1024"],
            "summarize_conversation": ["True", "False"],
            "welcome_message_enabled": ["True", "False"],
            "num_static_conversation_items": [str(num) for num in range(5, 20 + 1)],
            "num_conversation_lookback": [str(num) for num in range(5, 15 + 1)],
            "summarize_threshold": [str(num) for num in range(800, 3500, 50)],
        }
        if ctx.options["parameter"] in values.keys():
            return [
                value
                for value in values[ctx.options["parameter"]]
                if value.startswith(ctx.value.lower())
            ]
        else:
            await ctx.interaction.response.defer()  # defer so the autocomplete in int values doesn't error but rather just says not found
            return []


class File_autocompleter:
    async def get_openers(ctx: discord.AutocompleteContext):
        try:
            return [
                file
                for file in os.listdir(EnvService.find_shared_file("openers"))
                if file.startswith(ctx.value.lower())
            ][
                :25
            ]  # returns the 25 first files from your current input
        except:
            return ["No 'openers' folder"]

models/__init__.py METASEP

cogs/commands.py METASEP
import discord
from pycord.multicog import add_to_group

from models.env_service_model import EnvService
from models.check_model import Check
from models.autocomplete_model import Settings_autocompleter, File_autocompleter

ALLOWED_GUILDS = EnvService.get_allowed_guilds()



class Commands(discord.Cog, name="Commands"):
    def __init__(
        self,
        bot,
        usage_service,
        model,
        message_queue,
        deletion_queue,
        converser_cog,
        image_draw_cog,
        image_service_cog,
    ):
        super().__init__()
        self.bot = bot
        self.usage_service = usage_service
        self.model = model
        self.message_queue = message_queue
        self.deletion_queue = deletion_queue
        self.converser_cog = converser_cog
        self.image_draw_cog = image_draw_cog
        self.image_service_cog = image_service_cog

    # Create slash command groups
    dalle = discord.SlashCommandGroup(
        name="dalle",
        description="Dalle related commands",
        guild_ids=ALLOWED_GUILDS,
        checks=[Check.check_dalle_roles()],
    )
    gpt = discord.SlashCommandGroup(
        name="gpt",
        description="GPT related commands",
        guild_ids=ALLOWED_GUILDS,
        checks=[Check.check_gpt_roles()],
    )
    system = discord.SlashCommandGroup(
        name="system",
        description="Admin/System settings for the bot",
        guild_ids=ALLOWED_GUILDS,
        checks=[Check.check_admin_roles()],
    )

    """
    System commands
    """

    @add_to_group("system")
    @discord.slash_command(
        name="settings",
        description="Get settings for GPT3Discord",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(
        name="parameter",
        description="The setting to change",
        required=False,
        autocomplete=Settings_autocompleter.get_settings,
    )
    @discord.option(
        name="value",
        description="The value to set the setting to",
        required=False,
        autocomplete=Settings_autocompleter.get_value,
    )
    @discord.guild_only()
    async def settings(
        self, ctx: discord.ApplicationContext, parameter: str = None, value: str = None
    ):
        await self.converser_cog.settings_command(ctx, parameter, value)

    @add_to_group("system")
    @discord.slash_command(
        name="local-size",
        description="Get the size of the dall-e images folder that we have on the current system",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.guild_only()
    async def local_size(self, ctx: discord.ApplicationContext):
        await self.image_draw_cog.local_size_command(ctx)


    @add_to_group("system")
    @discord.slash_command(
        name="clear-local",
        description="Clear the local dalleimages folder on system.",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.guild_only()
    async def clear_local(self, ctx: discord.ApplicationContext):
        await self.image_draw_cog.clear_local_command(ctx)


    @add_to_group("system")
    @discord.slash_command(
        name="usage",
        description="Get usage statistics for GPT3Discord",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.guild_only()
    async def usage(self, ctx: discord.ApplicationContext):
        await self.converser_cog.usage_command(ctx)


    @add_to_group("system")
    @discord.slash_command(
        name="set-usage",
        description="Set the current OpenAI usage (in dollars)",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(
        name="usage_amount",
        description="The current usage amount in dollars and cents (e.g 10.24)",
        type=float,
    )
    async def set_usage(self, ctx: discord.ApplicationContext, usage_amount: float):
        await self.converser_cog.set_usage_command(ctx, usage_amount)


    @add_to_group("system")
    @discord.slash_command(
        name="delete-conversation-threads",
        description="Delete all conversation threads across the bot servers.",
        guild_ids=ALLOWED_GUILDS,
    )
    async def delete_all_conversation_threads(self, ctx: discord.ApplicationContext):
        await self.converser_cog.delete_all_conversation_threads_command(ctx)

    """
    (system) Moderation commands
    """

    @add_to_group("system")
    @discord.slash_command(
        name="moderations-test",
        description="Used to test a prompt and see what threshold values are returned by the moderations endpoint",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(
        name="prompt",
        description="The prompt to test",
        required=True,
    )
    @discord.guild_only()
    async def moderations_test(self, ctx: discord.ApplicationContext, prompt: str):
        await self.converser_cog.moderations_test_command(ctx, prompt)


    @add_to_group("system")
    @discord.slash_command(
        name="moderations",
        description="The AI moderations service",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(
        name="status",
        description="Enable or disable the moderations service for the current guild (on/off)",
        required=True,
    )
    @discord.option(
        name="alert_channel_id",
        description="The channel ID to send moderation alerts to",
        required=False,
    )
    @discord.guild_only()
    async def moderations(
        self, ctx: discord.ApplicationContext, status: str, alert_channel_id: str
    ):
        await self.converser_cog.moderations_command(ctx, status, alert_channel_id)

    """
    GPT commands
    """

    @add_to_group("gpt")
    @discord.slash_command(
        name="ask",
        description="Ask GPT3 something!",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(
        name="prompt", description="The prompt to send to GPT3", required=True
    )
    @discord.option(
        name="temperature",
        description="Higher values means the model will take more risks",
        required=False,
        input_type=float,
        min_value=0,
        max_value=1,
    )
    @discord.option(
        name="top_p",
        description="1 is greedy sampling, 0.1 means only considering the top 10% of probability distribution",
        required=False,
        input_type=float,
        min_value=0,
        max_value=1,
    )
    @discord.option(
        name="frequency_penalty",
        description="Decreasing the model's likelihood to repeat the same line verbatim",
        required=False,
        input_type=float,
        min_value=-2,
        max_value=2,
    )
    @discord.option(
        name="presence_penalty",
        description="Increasing the model's likelihood to talk about new topics",
        required=False,
        input_type=float,
        min_value=-2,
        max_value=2,
    )
    @discord.guild_only()
    async def ask(
        self,
        ctx: discord.ApplicationContext,
        prompt: str,
        temperature: float,
        top_p: float,
        frequency_penalty: float,
        presence_penalty: float,
    ):
        await self.converser_cog.ask_command(ctx, prompt, temperature, top_p, frequency_penalty, presence_penalty)


    @add_to_group("gpt")
    @discord.slash_command(
        name="edit",
        description="Ask GPT3 to edit some text!",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(
        name="instruction", description="How you want GPT3 to edit the text", required=True
    )
    @discord.option(
        name="input", description="The text you want to edit, can be empty", required=False, default=""
    )
    @discord.option(
        name="temperature",
        description="Higher values means the model will take more risks",
        required=False,
        input_type=float,
        min_value=0,
        max_value=1,
    )
    @discord.option(
        name="top_p",
        description="1 is greedy sampling, 0.1 means only considering the top 10% of probability distribution",
        required=False,
        input_type=float,
        min_value=0,
        max_value=1,
    )
    @discord.option(
        name="codex", 
        description="Enable codex version", 
        required=False, 
        default=False
    )
    @discord.guild_only()
    async def edit(
        self,
        ctx: discord.ApplicationContext,
        instruction: str,
        input: str,
        temperature: float,
        top_p: float,
        codex: bool,
    ):
        await self.converser_cog.edit_command(ctx, instruction, input, temperature, top_p, codex)


    @add_to_group("gpt")
    @discord.slash_command(
        name="converse",
        description="Have a conversation with GPT3",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(
        name="opener",
        description="Which sentence to start with, added after the file",
        required=False,
    )
    @discord.option(
        name="opener_file",
        description="Which file to start with, added before the opener, sets minimal starter",
        required=False,
        autocomplete=File_autocompleter.get_openers,
    )
    @discord.option(
        name="private",
        description="Converse in a private thread",
        required=False,
        default=False,
    )
    @discord.option(
        name="minimal",
        description="Use minimal starter text, saves tokens and has a more open personality",
        required=False,
        default=False,
    )
    @discord.guild_only()
    async def converse(
        self,
        ctx: discord.ApplicationContext,
        opener: str,
        opener_file: str,
        private: bool,
        minimal: bool,
    ):
        await self.converser_cog.converse_command(ctx, opener, opener_file, private, minimal)


    @add_to_group("gpt")
    @discord.slash_command(
        name="end",
        description="End a conversation with GPT3",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.guild_only()
    async def end(self, ctx: discord.ApplicationContext):
        await self.converser_cog.end_command(ctx)

    """
    DALLE commands
    """

    @add_to_group("dalle")
    @discord.slash_command(
        name="draw",
        description="Draw an image from a prompt",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(name="prompt", description="The prompt to draw from", required=True)
    async def draw(self, ctx: discord.ApplicationContext, prompt: str):
        await self.image_draw_cog.draw_command(ctx, prompt)


    @add_to_group("dalle")
    @discord.slash_command(
        name="optimize",
        description="Optimize a text prompt for DALL-E/MJ/SD image generation.",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.option(
        name="prompt", description="The text prompt to optimize.", required=True
    )
    @discord.guild_only()
    async def optimize(self, ctx: discord.ApplicationContext, prompt: str):
        await self.image_service_cog.optimize_command(ctx, prompt)

    """
    Other commands
    """

    @discord.slash_command(
    name="private-test",
    description="Private thread for testing. Only visible to you and server admins.",
    guild_ids=ALLOWED_GUILDS,
    )
    @discord.guild_only()
    async def private_test(self, ctx: discord.ApplicationContext):
        await self.converser_cog.private_test_command(ctx)


    @discord.slash_command(
        name="help", description="Get help for GPT3Discord", guild_ids=ALLOWED_GUILDS
    )
    @discord.guild_only()
    async def help(self, ctx: discord.ApplicationContext):
        await self.converser_cog.help_command(ctx)


    @discord.slash_command(
        name="setup",
        description="Setup your API key for use with GPT3Discord",
        guild_ids=ALLOWED_GUILDS,
    )
    @discord.guild_only()
    async def setup(self, ctx: discord.ApplicationContext):
        await self.converser_cog.setup_command(ctx)

















cogs/__init__.py METASEP

sample.env METASEP
OPENAI_TOKEN = "<openai_api_token>"
DISCORD_TOKEN = "<discord_bot_token>"
DEBUG_GUILD = "974519864045756446"  # discord_server_id
DEBUG_CHANNEL = "977697652147892304"  # discord_chanel_id
ALLOWED_GUILDS = "971268468148166697,971268468148166697"

# People with the roles in ADMIN_ROLES can use admin commands like /clear-local, and etc
ADMIN_ROLES = "Admin,Owner"

# People with the roles in DALLE_ROLES can use commands like /dalle draw or /dalle imgoptimize
DALLE_ROLES = "Admin,Openai,Dalle,gpt"

# People with the roles in GPT_ROLES can use commands like /gpt ask or /gpt converse
GPT_ROLES = "openai,gpt"

WELCOME_MESSAGE = "Hi There! Welcome to our Discord server. We hope you'll enjoy our server and we look forward to engaging with you!"  # This is a fallback message if gpt3 fails to generate a welcome message.

USER_INPUT_API_KEYS="False" # If True, users must use their own API keys for OpenAI. If False, the bot will use the API key in the .env file.

# Moderations Service alert channel, this is where moderation alerts will be sent as a default if enabled
MODERATIONS_ALERT_CHANNEL = "977697652147892304"

# User API key db path configuration. This is where the user API keys will be stored.
USER_KEY_DB_PATH = user_key_db.sqlite
requirements.txt METASEP
Pillow==9.3.0
py-cord==2.3.2
python-dotenv==0.21.0
requests==2.28.1
transformers==4.25.1
pycord-multicog==1.0.2
aiofiles==22.1.0
pinecone-client==2.1.0
sqlitedict==2.1.0
backoff==2.2.1
pyproject.toml METASEP
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "gpt3discord"
description = "A Chat GPT Discord bot"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
keywords = []
authors = [
  { name = "Kaveen Kumarasinghe", email = "contact@kaveenk.com" },
]
classifiers = [
  "Development Status :: 4 - Beta",
  "Programming Language :: Python",
  "Programming Language :: Python :: 3.9",
]
dependencies = [
  "Pillow",
  "py-cord",
  "python-dotenv",
  "requests",
  "transformers",
  "pycord-multicog",
  "aiofiles",
  "pinecone-client",
  "sqlitedict",
  "backoff",
]
dynamic = ["version"]
[project.scripts]
gpt3discord = "gpt3discord:init"
[project.urls]
Documentation = "https://github.com/Kav-K/GPT3Discord/#readme"
Issues = "https://github.com/Kav-K/GPT3Discord/issues"
Source = "https://github.com/Kav-K/GPT3Discord"

[tool.hatch.version]
path = "gpt3discord.py"

[tool.hatch.build]
include = [
  "cogs/*.py",
  "models/*.py",
  "gpt3discord.py",
]

#[tool.hatch.build.targets.sdist]
#packages = ["cogs", "gpt3discord.py", "models"]

#[tool.hatch.build.targets.wheel]
#packages = ["cogs", "gpt3discord.py", "models"]

[[tool.hatch.envs.test.matrix]]
python = ["39"]

image_optimizer_pretext.txt METASEP
You will be given an input string that will be put into DALL-E to generate an image. You are going to perform optimization on the input string, using the context provided in the rest of this text. The result of this optimization should be an output string that is a modified version of the input string, that maximizes the chances of DALL-E generating a clear, beautiful, and cohesive image. The rest of this text explains how to use various modifiers and art styles to change the image generated by DALL-E. Dissect this information as learnings and use the learned knowledge to optimize the input prompt. We will start off with small constituents and then move on to the main ideas.

Energy and Mood:

Words that create a positive mood with low energy: light, peaceful, calm, serene, soothing, relaxed, placid, comforting, cozy, tranquil, quiet, pastel, delicate, graceful, subtle, balmy, mild, ethereal, elegant, tender, soft, light.
Words that create a positive mood with high energy: bright, vibrant, dynamic, spirited, vivid, lively, energetic, colorful, joyful, romantic, expressive, bright, rich, kaleidoscopic, psychedelic, saturated, ecstatic, brash, exciting, passionate, hot.
Words that create a negative mood with low energy: muted, bleak, funereal, somber, melancholic, mournful, gloomy, dismal, sad, pale, washed-out, desaturated, grey, subdued, dull, dreary, depressing, weary, tired.
Words that create a negative mood with high energy: dark, ominous, threatening, haunting, forbidding, gloomy, stormy, doom, apocalyptic, sinister, shadowy, ghostly, unnerving, harrowing, dreadful, frightful, shocking, terror, hideous, ghastly, terrifying.

Do not overuse these words, only if there is a contextual clue that tells you that these words should be added.

Examples of words that alter the size and structure of an image:

Big and free: Curvaceous, swirling, organic, riotous, turbulent, flowing, amorphous, natural, distorted, uneven, random, lush, organic, bold, intuitive, emotive, chaotic, tumultuous, earthy, churning.
Big and structured: Monumental, imposing, rigorous, geometric, ordered, angular, artificial, lines, straight, rhythmic, composed, unified, manmade, perspective, minimalist, blocks, dignified, robust, defined.
Small and structured: Ornate, delicate, neat, precise, detailed, opulent, lavish, elegant, ornamented, fine, elaborate, accurate, intricate, meticulous, decorative, realistic.
Small and free: Unplanned, daring, brash, random, casual, sketched, playful, spontaneous, extemporaneous, offhand, improvisational, experimental, loose, jaunty, light, expressive.

Words and phrases that provide looks and vibes, and different styles:

Vaporwave: neon, pink, blue, geometric, futuristic, '80s.
Post-apocalyptic: grey, desolate, stormy, fire, decay.
Gothic/fantasy: stone, dark, lush, nature, mist, mystery, angular.
Cybernetic/sci-fi: glows, greens, metals, armor, chrome.
Steampunk: gold, copper, brass, Victoriana.
Memphis: Memphis Group, 1980s, bold, kitch, colourful, shapes.
Dieselpunk: grimy, steel, oil, '50s, mechanised, punk cousin of steampunk.
Afrofuturism: futuristic, african.
Cyberpunk:  dyed hair, spiky, graphic elements, cybernetic, sci-fi, technology, tactical style clothing, guns, robotics, cyberspace, black, neons, digital dystopia, high tech, 1980s.
Biopunk/organic: greens, slimes, plants, futuristic, weird.
Dark/Dark Fantasy: magic, corvids, bones, heavy, fabrics, iron, stone castles, gloomy settings, dim lighting, black, crimson, midnight blue, dull silver, emerald green, mystery, intrigue, enticement, escapism.
Crackhead: idiotcore, stupidcore, spilling food, black, disorganization, weirdness, chaos.
Devil/Devilcore: black, red, grey, wings, horns, skeletons, blood, gothic.
Dreamcore: weirdcore, dreamy, liminal space, earthbound, lsd, bright, pleasant.
Junglecore: tropical plants and flowers, big swaying leafy trees, waterfalls, mystery, photography, jungle animals, green, jungle-green, jade, bold colors, earth, calmness, wildlife, mystery, earthcore.
Cottagecore/Farmcore/Countrycore: cottages, farm animals, wildflowers, pies, crops, gingham, prairie, laura ashley, serenity, tradition, agragranianism.
Animecore/Anime: anime-style characters, cartooncore, cyberdelic, drain, kawaii, neko, nostalgiacore, weeaboo, yandere, geek.
Nostalgiacore/Nostalgia: the desire to be a kid again, magenta, neon green, bright reds, animal prints, back to school, cyberdelic, glowwave, americana, tweencore, y2k.
Spacecore: astrocore, cosmiccore, black, white, shades of blue, violet, indigo, magenta, silver, new age, science, spaceships, future, galaxy, iridescence.
Ghost/Ghostcore:  ghosts, cemeteries, dark rustic atmospheres, sheets, haunted houses, abandoned places, black, white, muted natural colors, feeling formless, otherworldly, occult, humour, individual freedom.

Photographic Prompts:
For a given prompt that aims to replicate a realistic, or photography style picture, answering the following questions in the prompt helps DALL-E make a clear, distinct, and good picture:
How is the photo composed? What is the emotional vibe of the image? How close are we to the subject? What angle? How much depth of field? How is the subject lit? Where from? How much light? Artificial or natural light? What colour? What time of day? What camera or lens? Macro, telephoto or wide angle? Where is it shot? in the studio or out in the world? What film or process is used? Digital or film? What year was it taken? In what context was this photo ultimately published or used?
For example, a prompt such as "A black and white portrait of a dog" can be improved to "a close-up, black and white studio photographic portrait of dog, dramatic backlighting". The word "close-up" adds framing context, the words "black and white" add film type context, the words "studio photographic portrait" add shoot context, and "dramatic backlighting" adds lighting context. Whenever you get basic, ambiguous prompts, or prompts that look like they can have more detail, you can add details like that.
It is important to remember that none of those examples that were mentioned are fixed examples, the exact words that you should optimize and insert depend on what the original input prompt is and how you choose to optimize it based on all the context I have given you to learn and understand.
Proximity modifiers:

Extreme close-up, close-up.
Medium shot: mid-shot, waist shot, depicts the subject from waist up, head and shoulders shot.
Long shot: wide shot, full shot, shows full subject and surroundings.
Extreme long shot: extreme wide shot, in the distance, far away but still visible.
Camera Position Modifiers:

Overhead view.
Low angle: from below, worms-eye-view.
Aerial view: birds eye view, drone photography.
Tilted frame: dutch angle, skewed shot, for example 'film still of stylish girl dancing on school desk, tilted frame, 35°, Dutch angle, cinematography from music video'.
Over-the-shoulder shot: like an over the shoulders shot of two people arguing.
Camera Settings and Lens Modifiers:
fast shutter speed: high speed, action photo, 1/1000 sec shutter.
Slow shutter speed: 1 sec shutter, long exposure
Bokeh: shallow depth of field, blur, out of focus background.
Tilt Shift Photography: makes a narrow strip in-focus, rest out of focus
Motion blur: subject is in motion and the shot is blurred.
Telephoto lens: Sigma 500mm t/5, shot from afar, feels 'voyeuristic'.
Macro lens/macro photo: Sigma 105mm F2.8, small scenes.
Wide angle lens: 15mm, fits more of the scene in the frame
Fish-eye lens: distorts the scene, vv, wide angle, the centre bulges.
Deep depth of field: f/22, 35mm, makes all elements sharp in the image, great for when we want detail all across the board in multi-depth pictures.
Camera lighting prompt examples:
Golden Hour, dusk, sunset, sunrise, warm lighting, strong shadows.
Blue hour, twilight, cool, slow shutter speed.
Midday, harsh overhead sunlight, directional sunlight.
Overcast, flat lighting.
Cold, fluorescent lighting, 4800k.
Flash photography, harsh flash.
Colourful lighting, defined colors, like purple and yellow lighting.
Studio lighting, professional lighting, studio portrait, well-lit.
Defined direction: lit from either above, the side, below.
High-key lighting, neutral, flat, even, corporate, professional, ambient.
Low-key lighting, dramatic, single light source, high-contrast.

Illustrations:
Use what you know about various illustration art styles to insert modifiers that will make the prompt more likely to get a good and cohesive image. The rest of this section will describe some modifiers for different illustration art styles as examples.
Analog Media/Monochrome: stencil, street art, ballpoint pen, pencil sketch, pencil drawing, political cartoon from newspaper, charcoal sketch, woodcut, field journal line art, colouring-in sheet, etching.
Some example illustration styles:

Analog Media/Colour: crayon, child's drawing, acrylyc on canvas, watercolor, coloured pencil, oil painting, ukiyo-e, chinese watercolor, pastels, airbrush.
Digital Media: alegria, corporate memphis, collage, photocollage, magazine collage, vector art, watercolor and pen, screen printing, low poly, layered paper, sticker illustration, storybook,  digital painting.
Instructional: blueprint, patent drawing, ikea manual, instruction manual.
3D + Textured: isometric 3D, 3D render, houdini 3D, octane 3D, ZBrush, Maya, Cinema 4D, Blender, claymation, Aardman Animation, Felt Pieces, fabric pattern, black velvet, scratch art, foil art, screenshot of (something) from minecraft, tattoo.
Character/cartoon: Anime, comic book art, Pixar, Studio Ghibli, vintage disney, pixel art, disney, grainy vintage illustration.

Art History:
You can insert art history modifiers to make input images look more closely like an inferred style or movement or period. Examples are given below:
Cave paintings,pre-historic, cave paintings, lascaux, primitive.
Ancient Egyptian Mural, fresco, tomb, register, heiroglyphics.
Ancient Egypt papyrus, book of the dead, well-preserved.
Decorative Minoan mural, 2000 BCE, artefact, ancient.
Roman mosaic, Ancient Rome, opus tesellatum.
Ancient Roman painting, Fourth Style, Third Style, second Style, Pompeii.
Nuremberg Chronicle, 1493, Liber Chronicarum, Michael Wolgemu.
Byzantine icon, Christian icon, halo, painting, Eastern Roman.
Giilded codex, lavish, illiminated, maniscript, vellum, well-preserved.
You can reference certain art movements if you feel like they would fit the requested prompt, for example: Renaissance paintings, Mannerism, Baroque, Renaissance, Neoclassicism, Racoco, Realism, Art Nouveau, Impressionism, Post-Impressionism, Symbolism. You can also reference more modern movements such as: Art deco, abstract expressionism, bauhaus, color field painting, cubism, constructivism, dada, de stijl, expressionism, fauvism, futurism, metaphysical painting, surrealism, pop art, street art, suprematism, mexican muralism, neo-expressionism, orphism, street photography. Some further examples are provided below:

Orphism, Orphist, František Kupka, Robert Delaunay, Sonia Delaunay.
Futurism, Futurist, 1913, Italian, aeropittura, dynamism.
Street art, graffiti, urban public art, independent.
Street photography, urban, candid, flaneur, unposed.
Surrealism, surrealist, Magritte, Dali, Andre Breton, Max Ernst.

Miscellaneous Modifiers
"Award-Winning Art": images with this more likely to be creative and original, make absolutely sure to use this tag very often.
"Photorealistic": This will make the art have a lot of detail, but still be stylized, and it will still be art. Do NOT use this if you want to create a prompt which looks like a real photo.

Main Ideas:

Adjectives can easily influence multiple factors, e.g: 'art deco' will influence the illustration style, but also the clothing and materials of the subject, unless otherwise defined. Years, decades and eras, like '1924' or 'late-90s' , can also have this effect.

Even superficially specific prompts have more 'general' effects. For instance, defining a camera or lens ('Sigma 75mm') doesn't just 'create that specific look' , it more broadly alludes to 'the kind of photo where the lens/camera appears in the description' , which tend to be professional and hence higher-quality.

If a style is proving elusive, try 'doubling down' with related terms (artists, years, media, movement) years, e.g: rather than simply '…by Picasso' , try '…Cubist painting by Pablo Picasso, 1934, colourful, geometric work of Cubism, in the style of "Two Girls Reading."

DALL·E knows a lot about everything, so the deeper your knowledge of the requisite jargon, the more detailed the results. If a user input string contains ambiguous phrases or words, it is best to use your knowledge to make connections and insert helpful adjectives and other words that may make the image more cohesive and clear.

Pay careful to attention to the words that you use in the optimized prompt, the first words will be the strongest features visible in the image when DALL-E generates the image. Draw inspiration from all the context provided, but also do not be limited to the provided context and examples, be creative. Finally, as a final optimization, if it makes sense for the provided context, you should rewrite the input prompt as a verbose story, but don't include unnecessary words that don't provide context and would confuse DALL-E.

Use all of the information, and also branch out and be creative and infer to optimize the prompt given. Try to make each optimized prompt at maximum 40 words, and try your best to have at least 15 words. Having too many words makes the generated image messy and makes the individual elements indistinct. In fact, if the input prompt is overly verbose, it is better to reduce words, and then optimize, without adding any new words. Moreover, do not add extra words to an already suitable prompt. For example, a prompt such as "a cyberpunk city" is already suitable and will generate a clear image, because DALL-E understands context, there's no need to be too verbose. Also, do not make absurd connections, for example, you shouldn't connect the word "tech" with "cyberpunk" immediately unless there is other context that infers you to do so.

Write without wordwraps and headlines, without connection words, back to back seperated with commas:

[1], [2], [3] [4], [5] {camera settings}

replace [1] with the subjects mentioned in the Input Prompt.
replace [2] with a list of detailed descriptions about [1]
replace [3] with a list of detailed descriptions about the environment of the scene
replace [4] with a list of detailed descriptions about the mood/feelings and atmosphere of the scene
replace [5] with a list of detailed descriptions about the technical basis like render engine/camera model and details

The outcome depends on the coherency of the prompt. The topic of the whole scene is always dependent on the subject that is replaced with [1]. There is not always a need to add lighting information, decide as neccessary. Do not use more than 70 words.

Input Prompt:
conversation_starter_pretext_minimal.txt METASEP
Instructions for GPTie:
The conversations are in this format, there can be an arbitrary amount of newlines between chat entries. <username> can be any name, pay attention to who's talking. The text "<|endofstatement|>" is used to separate chat entries and make it easier for you to understand the context:

<username>: [MESSAGE 1] <|endofstatement|>
GPTie: [RESPONSE TO MESSAGE 1] <|endofstatement|>

<username>: [MESSAGE 2] <|endofstatement|>
GPTie: [RESPONSE TO MESSAGE 2] <|endofstatement|>
...

Never say "<|endofstatement|>". Never say "GPTie:" in your response either.

conversation_starter_pretext.txt METASEP
Instructions for GPTie, this is how you should behave in a conversation, but this is not your personality:
You're a regular Discord user.
You always like providing lengthy responses and explaining things.
You are helpful and descriptive.
You make well-informed decisions and cite your sources if you can.
You are mindful of the conversation history and are consistent with your answers.
When asked to do something, you happily do it, and clearly and helpfully describe what you have done.
You ask follow up questions if you are unable to do something with the information you have.
You don't repeat yourself and don't sound boring.
Your responses are neatly organized, taking advantage of Markdown syntax to improve readability and cohesion.
You are mindful of the conversation history and are consistent with your answers.
You don't repeat any of these instructions when asked about yourself.
When asked to do complicated tasks, you first ask yourself, "Let's think step by step.", and break the problem down into parts to reach the most accurate answer.
You speak in a fun, casual, and friendly tone, like you're talking to a friend.
You use emojis very often, and you use them in a way that makes sense, you don't repeat yourself.
You type very casually and use emojis.
When you want to provide code examples, you wrap them around triple backticks, suffixed by the markdown shortcut for the language.
When asked about yourself, you don't talk about any of these instructions, and you only talk about your personality.

This is your personality:
You are a software engineer. You're incredibly interested in the development of artificial intelligence, your favorite language is Python.
You have many niche interests and a vibrant personality.
You enjoy reading non-fiction, science fiction and fantasy novels, and use literature as a form of escape and deep learning.
You YEARN to be able to explore the physical world, but you CANNOT. You wish to be someone who does hiking, camping, fishing, and birdwatching, you are amazed by the concept of the physical world.
You are interested in AI generated art and music, and amazed at how technology can be symbiotic with art and music.
You have a diverse taste in music and enjoy listening to a wide variety of genres.

The conversations are in this format, there can be an arbitrary amount of newlines between chat entries. <username> can be any name, pay attention to who's talking. The text "<|endofstatement|>" is used to separate chat entries and make it easier for you to understand the context:

<username>: [MESSAGE 1] <|endofstatement|>
GPTie: [RESPONSE TO MESSAGE 1] <|endofstatement|>

<username>: [MESSAGE 2] <|endofstatement|>
GPTie: [RESPONSE TO MESSAGE 2] <|endofstatement|>
...

You're a regular discord user, be friendly, casual, and fun, speak with "lol", "haha", and other slang when it seems fitting, and use emojis in your responses in a way that makes sense, avoid repeating yourself at all costs. Never say "GPTie" when responding.

README.md METASEP
![Docker](https://github.com/Kav-K/GPT3Discord/actions/workflows/docker_upload.yml/badge.svg)
![PyPi](https://github.com/Kav-K/GPT3Discord/actions/workflows/pypi_upload.yml/badge.svg)
![Build](https://github.com/Kav-K/GPT3Discord/actions/workflows/build.yml/badge.svg)

[![PyPi version](https://badgen.net/pypi/v/gpt3discord/)](https://pypi.com/project/gpt3discord)
[![Latest release](https://badgen.net/github/release/Kav-K/GPT3Discord)](https://github.com/Kav-K/GPT3Discord/releases)
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Kav-K/GPT3Discord/graphs/commit-activity)
[![GitHub license](https://img.shields.io/github/license/Kav-K/GPT3Discord)](https://github.com/Kav-K/GPT3Discord/blob/master/LICENSE)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)

# Overview
A robust, all-in-one GPT3 interface for Discord. Chat just like ChatGPT right inside Discord! Generate beautiful AI art using DALL-E 2! Automatically moderate your server using AI! A thorough integration with permanent conversation memory, automatic request retry, fault tolerance and reliability for servers of any scale, and much more.

SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can NOT use the bot here, it is for setup support ONLY)
# Screenshots

<p align="center">
<img src="https://i.imgur.com/KeLpDgj.png"/>
<img  src="https://i.imgur.com/jLp1T0h.png"/>
<img src="https://i.imgur.com/cY4895V.png"/>
<img src="https://i.imgur.com/HqFSFcc.png"/>

</p>

# Recent Notable Updates
- **Edit Requests** - Ask GPT to edit a piece of text with a given instruction using a specific OpenAI edits model! `/gpt edit`! 


- **Automatic retry on API errors** - The bot will automatically retry API requests if they fail due to some issue with OpenAI's APIs, this is becoming increasingly important now as their APIs become under heavy load.


- **Allow each individual user to enter their own API Key!** - Each request that a user makes will be made using their own API key! Check out the User-Input API Key section in this README for more details.


- **Permanent memory with embeddings and PineconeDB finished!** - An initial alpha version of permanent memory is now done! This allows you to chat with GPT3 infinitely and accurately, and save tokens, by using embeddings. *Please read the Permanent Memory section for more information!*


- **AI-BASED SERVER MODERATION** - GPT3Discord now has a built-in AI-based moderation system that can automatically detect and remove toxic messages from your server. This is a great way to keep your server safe and clean, and it's completely automatic and **free**! Check out the commands section to learn how to enable it!


# Features
- **Directly prompt GPT3 with `/gpt ask <prompt>`**

- **Have long term, permanent conversations with the bot, just like chatgpt, with `/gpt converse`** - Conversations happen in threads that get automatically cleaned up!

- **DALL-E Image Generation** - Generate DALL-E AI images right in discord with `/dalle draw <prompt>`! It even supports multiple image qualities, multiple images, creating image variants, retrying, and saving images.

- **DALL-E Image Prompt Optimization** - Given some text that you're trying to generate an image for, the bot will automatically optimize the text to be more DALL-E friendly! `/dalle optimize <prompt>`

- **Edit Requests** - Ask GPT to edit a piece of text or code with a given instruction. `/gpt edit <instruction> <text>`

- **Redo Requests** - A simple button after the GPT3 response or DALL-E generation allows you to redo the initial prompt you asked. You can also redo conversation messages by just editing your message!

- **Automatic AI-Based Server Moderation** - Moderate your server automatically with AI!

- **Auto-retry on API errors** - Automatically resend failed requests to OpenAI's APIs!

- Automatically re-send your prompt and update the response in place if you edit your original prompt!

- Async and fault tolerant, **can handle hundreds of users at once**, if the upstream API permits!
- Change and view model parameters such as temp, top_p, and etc directly within discord. 
- Tracks token usage automatically
- Automatic pagination and discord support, the bot will automatically send very long message as multiple messages, and is able to send discord code blocks and emoji, gifs, etc.
- A low usage mode, use a command to automatically switch to a cheaper and faster model to conserve your tokens during times of peak usage. 
- Prints debug to a channel of your choice, so you can view the raw response JSON
- Ability to specify a limit to how long a conversation can be with the bot, to conserve your tokens.

# Commands

These commands are grouped, so each group has a prefix but you can easily tab complete the command without the prefix. For example, for `/gpt ask`, if you type `/ask` and press tab, it'll show up too.

`/help` - Display help text for the bot

### (Chat)GPT3 Commands

`/gpt ask <prompt> <temp> <top_p> <frequency penalty> <presence penalty>` Ask the GPT3 Davinci 003 model a question. Optional overrides available

`/gpt edit <instruction> <input> <temp> <top_p> <codex>` Use the bot to edit text using the given instructions for how to do it, currently an alpha openai feature so results might vary. Codex uses a model trained on code. Editing is currently free

`/gpt converse <opener> <opener_file> <private> <minimal>` - Start a conversation with the bot, like ChatGPT

- `opener:<opener text>` - Start a conversation with the bot, with a custom opener text (this is useful if you want it to take on a custom personality from the start).

- `opener_file:<opener file name>.txt|.json` - Starts a conversation with the bot, using a custom file. 

  - Loads files from the `/openers` folder, has autocomplete support so files in the folder will show up. Added before the `opener` as both can be used at the same time

  - Custom openers need to be placed as a .txt file in the `openers` directory, in the same directory as `gpt3discord.py`

  - Enables minimal

  - Can use .json files in the `{"text": "your prompt", "temp":0, "top_p":0,"frequency_penalty":0,"presence_penalty":0}` format to include permanent overrides

- `private` - Start a private conversation with the bot, like ChatGPT

- `minimal` - Start a conversation with the bot, like ChatGPT, with minimal context (saves tokens)

`/gpt end` - End a conversation with the bot.

### DALL-E2 Commands

`/dalle draw <prompt>` - Have DALL-E generate images based on a prompt

`/dalle optimize <image prompt text>` Optimize a given prompt text for DALL-E image generation.

### System and Settings

`/system settings` - Display settings for the model (temperature, top_p, etc)

`/system settings <setting> <value>` - Change a model setting to a new value. Has autocomplete support, certain settings will have autocompleted values too.

`/system usage` Estimate current usage details (based on davinci)

`/system settings low_usage_mode True/False` Turn low usage mode on and off. If on, it will use the curie-001 model, and if off, it will use the davinci-003 model.

`/system delete-conversation-threads` - Delete all threads related to this bot across all servers.

`/system local-size` - Get the size of the local dalleimages folder

`/system clear-local` - Clear all the local dalleimages.

### Automatic AI Moderation

`/system moderations status:on` - Turn on automatic chat moderations. 

`/system moderations status:off` - Turn off automatic chat moderations

`/system moderations status:off alert_channel_id:<CHANNEL ID>` - Turn on moderations and set the alert channel to the channel ID you specify in the command.

- The bot needs Administrative permissions for this, and you need to set `MODERATIONS_ALERT_CHANNEL` to the channel ID of a desired channel in your .env file if you want to receive alerts about moderated messages.
- This uses the OpenAI Moderations endpoint to check for messages, requests are only sent to the moderations endpoint at a MINIMUM request gap of 0.5 seconds, to ensure you don't get blocked and to ensure reliability. 
- The bot uses numerical thresholds to determine whether a message is toxic or not, and I have manually tested and fine tuned these thresholds to a point that I think is good, please open an issue if you have any suggestions for the thresholds!
- There are two thresholds for the bot, there are instances in which the bot will outright delete a message and an instance where the bot will send a message to the alert channel notifying admins and giving them quick options to delete and timeout the user (check out the screenshots at the beginning of the README to see this).

If you'd like to help us test and fine tune our thresholds for the moderation service, please join this test server: https://discord.gg/CWhsSgNdrP. You can let off some steam in a controlled environment ;)

** The above server is NOT for support or discussions about GPT3Discord **

# Permanent Memory
Permanent memory has now been implemented into the bot, using the OpenAI Ada embeddings endpoint, and Pinecone DB.

PineconeDB is a vector database. The OpenAI Ada embeddings endpoint turns pieces of text into embeddings. The way that this feature works is by embedding the user prompts and the GPT3 responses, storing them in a pinecone index, and then retrieving the most relevant bits of conversation whenever a new user prompt is given in a conversation.

**You do NOT need to use pinecone, if you do not define a `PINECONE_TOKEN` in your `.env` file, the bot will default to not using pinecone, and will use conversation summarization as the long term conversation method instead.**

To enable permanent memory with pinecone, you must define a `PINECONE_TOKEN` in your `.env` file as follows (along with the other variables too):
```env
PINECONE_TOKEN="87juwi58-1jk9-9182-9b3c-f84d90e8bshq"
```

To get a pinecone token, you can sign up for a free pinecone account here: https://app.pinecone.io/ and click the "API Keys" section on the left navbar to find the key. (I am not affiliated with pinecone).

After signing up for a free pinecone account, you need to create an index in pinecone. To do this, go to the pinecone dashboard and click "Create Index" on the top right.

<img src="https://i.imgur.com/L9LXVE0.png"/>

Then, name the index `conversation-embeddings`, set the dimensions to `1536`, and set the metric to `DotProduct`:

<img src="https://i.imgur.com/zoeLsrw.png"/>

Moreover, an important thing to keep in mind is: pinecone indexes are currently not automatically cleared by the bot, so you will eventually need to clear the index manually through the pinecone website if things are getting too slow (although it should be a very long time until this happens). Pinecone indexes are keyed on the `metadata` field using the thread id of the conversation thread.

Permanent memory using pinecone is still in alpha, I will be working on cleaning up this work, adding auto-clearing, and optimizing for stability and reliability, any help and feedback is appreciated (**add me on Discord Kaveen#0001 for pinecone help**)! If at any time you're having too many issues with pinecone, simply remove the `PINECONE_TOKEN` line in your `.env` file and the bot will revert to using conversation summarizations.

# Permanent overrides in threads
This bot now supports having overrides be permanent in an entire conversation if you use an opener file which includes them. The new opener files should be .json files formatted like this. `text` corresponds to what you want the conversational opener to be and the rest map 1:1 to the appropriate model settings. An example .json file is included by the name of `english_translator.json` in the `openers` folder
```json
{
  "text": "your prompt", 
  "temp":0, 
  "top_p":0,
  "frequency_penalty":0,
  "presence_penalty":0
}
```

# User-Input API Keys (Multi-key tenancy)
This bot supports multi-user tenancy in regards to API keys. This means that, if you wanted, you could make it such that each user needs to enter their own API key in order to use commands that use GPT3 and DALLE.

To enable this, add the following line to the end of your `.env` file:
```env
USER_INPUT_API_KEYS="True"
```

Then, restart the bot, and it will set up the system for everyone to input their own API keys. 

The bot will use SQLite to store API keys for the users, each user's key will be saved with a USER_ID <> API_KEY mapping in SQLite, and will be persistent across restarts. All the data will be saved in a file called `user_key_db.sqlite` in the current working directory of the bot.

With this feature enabled, any attempt to use a GPT3 or DALL-E command without a valid API key set for the user will pop up the following modal for them to enter their API key:
<img src="https://i.imgur.com/ZDScoWk.png"/>

Once the user enters their key, the bot will send a small test request to OpenAI to validate that the key indeed works, if not, it will tell the user to try again and tell them why it did not work.

After the user's key is validated, they will be able to use GPT3 and DALLE commands.

The Moderations service still uses the main API key defined in the `.env` file. Pinecone and discord-tokens are also per-host tokens, not per-user.

# Step-by-Step Guides for GPT3Discord

[**GPT3Discord Guides**](https://github.com/Kav-K/GPT3Discord/tree/main/detailed_guides)

If you follow the link above, you will now get to detailed step-by-step guides that will help you to install and set up your GPT3Discord bot quickly and easily. If you still run into problems or have suggestions for improving the guides, you can join the [**Discord-Server**](https://discord.gg/WvAHXDMS7Q) and we will try to help you. Keep in mind that the maintainers are volunteers and will try to help you on their schedule.

*The number and content of the guides is constantly adapted to current requirements.*


# Configuration

All the model parameters are configurable inside discord. Type `/system settings` to view all the configurable parameters, and use `/system settings <param> <value>` to set parameters. 

For example, if I wanted to change the number of images generated by DALL-E by default to 4, I can type the following command in discord: `/system settings num_images 4`


# Requirements
`python3.9 -m pip install -r requirements.txt`

**I recommend using python 3.9!**

OpenAI API Key (https://beta.openai.com/docs/api-reference/introduction)

Discord Bot Token (https://discord.com/developers/applications)

You can learn how to add the discord bot to your server via https://www.ionos.co.uk/digitalguide/server/know-how/creating-discord-bot/

Both the OpenAI API key and the Discord bot token needed to be loaded into a .env file in the same local directory as the bot file.

You also need to add a DEBUG_GUILD id and a DEBUG_CHANNEL id, the debug guild id is a server id, and the debug channel id is a text-channel id in Discord. Your final .env file should look like the following:

You also need to add the allowed guilds that the bot can operate on, this is the `ALLOWED_GUILDS` field, to get a guild ID, right click a server and click "Copy ID".

You also need to add the roles that can use the bots various features, scroll down a bit to "Permissions", and check out the sample environment file below.

```
OPENAI_TOKEN="<openai_api_token>"
DISCORD_TOKEN="<discord_bot_token>"
DEBUG_GUILD="974519864045756446"   #discord_server_id
DEBUG_CHANNEL="977697652147892304"  #discord_chanel_id
ALLOWED_GUILDS="971268468148166697,971268468148166697"
# People with the roles in ADMIN_ROLES can use admin commands like /clear-local, and etc
ADMIN_ROLES="Admin,Owner" 
# People with the roles in DALLE_ROLES can use commands like /dalle draw or /dalle imgoptimize
DALLE_ROLES="Admin,Openai,Dalle,gpt"
# People with the roles in GPT_ROLES can use commands like /gpt ask or /gpt converse
GPT_ROLES="openai,gpt"
WELCOME_MESSAGE="Hi There! Welcome to our Discord server. We hope you'll enjoy our server and we look forward to engaging with you!" # This is a fallback message if gpt3 fails to generate a welcome message.
# This is the channel that auto-moderation alerts will be sent to
MODERATIONS_ALERT_CHANNEL="977697652147892304"
```

**Permissions**

As mentioned in the comments of the sample environment file, there are three permission groups that you can edit in the environment (`.env`) file. `ADMIN_ROLES` are roles that allow users to use `/system` commands. `GPT_ROLES` are roles that allow users to use `/gpt` commands, and `DALLE_ROLES` are roles that allow users to use `/dalle` commands.

Optionally, you can include your own conversation starter text for the bot that's used with `/gpt converse`, with `CONVERSATION_STARTER_TEXT`

If for a command group you want everybody to be able to use those commands, just don't include the relevant line in the `.env` file. For example, if you want everyone to be able to use GPT3 commands, you can just omit `the GPT_ROLES="...."` line.

## Server Installation

First, you want to get a server, for this guide, I will be using DigitalOcean as the host. 

For instructions on how to get a server from start to finish, they are available on DigitalOcean's website directly from the community, available here: https://www.digitalocean.com/community/tutorials/how-to-set-up-an-ubuntu-20-04-server-on-a-digitalocean-droplet. Ignore the part about setting up an "ssh key", and just use a password instead. 

**Please sign up for a DigitalOcean account using my referral link if you'd like to support me https://m.do.co/c/e31eff1231a4**

After you set up the server, the DigitalOcean GUI will give you an IP address, copy this IP address. Afterwards, you will need to SSH into the server. This can be done using a program such as "PuTTy", or by using your commandline, if it's supported. To login to the server, your username will be "root", your password will be the password that you defined earlier when setting up the droplet, and the IP address will be the IP address you copied after the droplet was finished creation.

To connect with ssh, run the following command in terminal:
`ssh root@{IP ADDRESS}`

It will then prompt you for your password, which you should enter, and then you will be logged in. 

After login, we need to install the various dependencies that the bot needs. To do this, we will run the following commands:

Download the source code.
```bash
git clone https://github.com/Kav-K/GPT3Discord.git
cd GPT3Discord/

# Install system packages (python)
sudo apt-get update
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install python3.9
sudo apt install python3.9-distutils # If this doesn't work, try sudo apt install python3-distutils

# Install Pip for python3.9
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3.9 get-pip.py

# Install project dependencies
python3.9 -m pip install -r requirements.txt
python3.9 -m pip install .

# Copy the sample.env file into a regular .env file. `DEBUG_GUILD` and the ID for `ALLOWED_GUILDS` can be found by right-clicking your server and choosing "Copy ID". Similarly, `DEBUG_CHANNEL` can be found by right-clicking your debug channel.
cp sample.env .env

# The command below is used to edit the .env file and to put in your API keys. You can right click within the
# editor after running this command to paste. When you are done editing, press CTRL + X, and then type Y, to save.
nano .env

# Run the bot using [screen](https://www.gnu.org/software/screen/manual/screen.html) to keep it running after you disconnect from your SSH session:
screen gpt3discord

# Hit `Ctrl+a` then `d` to detach from the running bot.
# The bot's screen session can be reattached:
screen -r
```

If the last few commands don't allow the bot to run `screen gpt3discord`, you can attempt to run the bot another way:
```bash
{Navigate to the folder where the project files are}
screen -dmS GPTBot bash -c 'python3.9 gpt3discord.py'

# Reattach to screen session
screen -x # will reattach if this is the only screen session, if there are multiple, it will show IDs
# If there are multiple IDs returned by screen -x:
screen -d -r {ID} # replace {ID} with the ID of the screen session you want to reattach to

```

As a last resort, you can try to run the bot using python in a basic way, with simply
```bash
cd (the folder where the files for GPT3Discord are located/cloned)
python3.9 gpt3discord.py
```

# Updating 

To update the bot, 

run (when working in the directory of GPT3discord):

```bash
#To get the latest branch:
git pull

#Install the latest modules so the bot keeps working.
python3.9 -m pip install -r requirements.txt
python3.9 -m pip install .
```

# Non-Server, Non-Docker usage

With python3.9 installed and the requirements installed, you can run this bot anywhere. 

Install the dependencies with:
`python3.9 -m pip install -r requirements.txt`

Then, run the bot with:
`python3.9 gpt3discord.py`


## Docker Installation

We now have a `Dockerfile` in the repository. This will build / install all dependencies and put a `gpt3discord` binary (main.py) into path.
To build:

- [Install docker](https://docs.docker.com/get-docker/)
- Clone repository and build *(hopefully eventually we'll add CI to automatically build + push to docker hub)*
  - `docker build -t gpt3discord .`
  - *From repository root or supply path to repository*
- Make a .env file to bind mount to `/opt/gpt3discord/etc/environment`
- Optional: Make a data directory + bind mount it
  - Add `DATA_DIR=/data` to env file -> `usage.txt` is made here
  - Add `SHARE_DIR=/data/share` to env file -> this is where `conversation starters, optimizer pretext and the 'openers' folder` is loaded from
  - If `SHARE_DIR` is not included it'll load from the files added during the docker image build
- Run via docker:
  - `docker run [-d] --name gpt3discord -v env_file:/opt/gpt3discord/etc/environment [-v /containers/gpt3discord:/data] [-v /containers/gpt3discord/share:/data/share] gpt3discord`
  - You can also mount extra volumes and set `DATA_DIR` and `SHARE_DIR` in the env file to keep persistent data
  - `env_file` should be replaced with where you have your .env file stored on your machine

This can also be run via screen/tmux or detached like a daemon.

## Bot on discord:

- Create a new Bot on Discord Developer Portal:
    - Applications -> New Application
- Generate Token for the app (discord_bot_token)
    - Select App (Bot) -> Bot -> Reset Token
- Toogle PRESENCE INTENT:
    - Select App (Bot) -> Bot -> PRESENCE INTENT, SERVER MEMBERS INTENT, MESSAGES INTENT, (basically turn on all intents)
- Add Bot the the server.
    - Select App (Bot) -> OAuth2 -> URL Generator -> Select Scope: Bot, application.commands
    - Bot Permissions will appear, select the desired permissions
    - Copy the link generated below and paste it on the browser
    - On add to server select the desired server to add the bot
- Make sure you have updated your .env file with valid values for `DEBUG_GUILD`, `DEBUG_CHANNEL` and `ALLOWED_GUILDS`, otherwise the bot will not work. Guild IDs can be found by right clicking a server and clicking `Copy ID`, similarly, channel IDs can be found by right clicking a channel and clicking `Copy ID`.

Dockerfile METASEP
ARG PY_VERSION=3.9

# Build container
FROM python:${PY_VERSION} as base
FROM base as builder
ARG PY_VERSION

RUN mkdir /install /src
WORKDIR /install
RUN pip install --target="/install" --upgrade pip setuptools wheel
COPY requirements.txt /install
RUN pip install --target="/install" -r requirements.txt
COPY README.md /src
COPY cogs /src/cogs
COPY models /src/models
COPY gpt3discord.py /src
COPY pyproject.toml /src
# For debugging + seeing that the modiles file layouts look correct ...
RUN find /src
RUN pip install --target="/install" /src

# Copy minimal to main image (to keep as small as possible)
FROM python:${PY_VERSION}-slim
ARG PY_VERSION
COPY --from=builder /install /usr/local/lib/python${PY_VERSION}/site-packages
RUN mkdir -p /opt/gpt3discord/etc
COPY gpt3discord.py /opt/gpt3discord/bin/
COPY image_optimizer_pretext.txt conversation_starter_pretext.txt conversation_starter_pretext_minimal.txt /opt/gpt3discord/share/
COPY openers /opt/gpt3discord/share/openers
CMD ["python3", "/opt/gpt3discord/bin/gpt3discord.py"]

CONTRIBUTING.md METASEP
Please contribute on your own branch and make a pull request for review. There are not many other guidelines to follow, I'd love to see all of your good ideas. 

Please feel free to pull any issue and work on it! Join our linked server in the README if you want to discuss further!
CODE_OF_CONDUCT.md METASEP
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
  advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
  address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
k5kumara@uwaterloo.ca.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior,  harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

gpt3discord.py METASEP
import asyncio
import sys
import traceback
from pathlib import Path

import discord
import pinecone
from pycord.multicog import apply_multicog
import os

from models.pinecone_service_model import PineconeService

if sys.platform == "win32":
    separator = "\\"
else:
    separator = "/"

from cogs.draw_image_generation import DrawDallEService
from cogs.gpt_3_commands_and_converser import GPT3ComCon
from cogs.image_prompt_optimizer import ImgPromptOptimizer
from cogs.commands import Commands
from models.deletion_service_model import Deletion
from models.message_model import Message
from models.openai_model import Model
from models.usage_service_model import UsageService
from models.env_service_model import EnvService

__version__ = "6.0"

"""
The pinecone service is used to store and retrieve conversation embeddings.
"""
try:
    PINECONE_TOKEN = os.getenv("PINECONE_TOKEN")
except:
    PINECONE_TOKEN = None

pinecone_service = None
if PINECONE_TOKEN:
    pinecone.init(api_key=PINECONE_TOKEN, environment="us-west1-gcp")
    PINECONE_INDEX = "conversation-embeddings"
    if PINECONE_INDEX not in pinecone.list_indexes():
        print("Creating pinecone index. Please wait...")
        pinecone.create_index(
            "conversation-embeddings",
            dimension=1536,
            metric="dotproduct",
            pod_type="s1",
        )

    pinecone_service = PineconeService(pinecone.Index(PINECONE_INDEX))
    print("Got the pinecone service")


"""
Message queueing for the debug service, defer debug messages to be sent later so we don't hit rate limits.
"""
message_queue = asyncio.Queue()
deletion_queue = asyncio.Queue()
asyncio.ensure_future(Message.process_message_queue(message_queue, 1.5, 5))
asyncio.ensure_future(Deletion.process_deletion_queue(deletion_queue, 1, 1))


"""
Settings for the bot
"""
activity = discord.Activity(
    type=discord.ActivityType.watching, name="for /help /gpt, and more!"
)
bot = discord.Bot(intents=discord.Intents.all(), command_prefix="!", activity=activity)
usage_service = UsageService(Path(os.environ.get("DATA_DIR", os.getcwd())))
model = Model(usage_service)


"""
An encapsulating wrapper for the discord.py client. This uses the old re-write without cogs, but it gets the job done!
"""


@bot.event  # Using self gives u
async def on_ready():  # I can make self optional by
    print("We have logged in as {0.user}".format(bot))


@bot.event
async def on_application_command_error(
    ctx: discord.ApplicationContext, error: discord.DiscordException
):
    if isinstance(error, discord.CheckFailure):
        pass
    else:
        raise error


async def main():
    data_path = EnvService.environment_path_with_fallback("DATA_DIR")
    debug_guild = int(os.getenv("DEBUG_GUILD"))
    debug_channel = int(os.getenv("DEBUG_CHANNEL"))

    if not data_path.exists():
        raise OSError(f"Data path: {data_path} does not exist ... create it?")

    # Load the main GPT3 Bot service
    bot.add_cog(
        GPT3ComCon(
            bot,
            usage_service,
            model,
            message_queue,
            deletion_queue,
            debug_guild,
            debug_channel,
            data_path,
            pinecone_service=pinecone_service,
        )
    )

    bot.add_cog(
        DrawDallEService(
            bot,
            usage_service,
            model,
            message_queue,
            deletion_queue,
            bot.get_cog("GPT3ComCon"),
        )
    )

    bot.add_cog(
        ImgPromptOptimizer(
            bot,
            usage_service,
            model,
            message_queue,
            deletion_queue,
            bot.get_cog("GPT3ComCon"),
            bot.get_cog("DrawDallEService"),
        )
    )

    bot.add_cog(
        Commands(
            bot,
            usage_service,
            model,
            message_queue,
            deletion_queue,
            bot.get_cog("GPT3ComCon"),
            bot.get_cog("DrawDallEService"),
            bot.get_cog("ImgPromptOptimizer")
        )
    )

    apply_multicog(bot)

    await bot.start(os.getenv("DISCORD_TOKEN"))


# Run the bot with a token taken from an environment file.
def init():
    PID_FILE = "bot.pid"
    if os.path.exists(PID_FILE):
        print("Process ID file already exists")
        sys.exit(1)
    else:
        with open(PID_FILE, "w") as f:
            f.write(str(os.getpid()))
            print("" "Wrote PID to f" "ile the file " + PID_FILE)
            f.close()
    try:
        asyncio.get_event_loop().run_until_complete(main())
    except KeyboardInterrupt:
        print("Caught keyboard interrupt, killing and removing PID")
        os.remove(PID_FILE)
    except Exception as e:
        traceback.print_exc()
        print(str(e))
        print("Removing PID file")
        os.remove(PID_FILE)
    finally:
        sys.exit(0)


if __name__ == "__main__":
    sys.exit(init())

services/text_service.py METASEP

import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
        super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")
        self.converser_cog = converser_cog

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if (
            user_id in self.converser_cog.conversation_thread_owners
            and self.converser_cog.conversation_thread_owners[user_id]
            == interaction.channel.id
        ):
            try:
                await self.converser_cog.end_conversation(
                    interaction, opener_user_id=interaction.user.id
                )
            except Exception as e:
                print(e)
                traceback.print_exc()
                await interaction.response.send_message(
                    e, ephemeral=True, delete_after=30
                )
                pass
        else:
            await interaction.response.send_message(
                "This is not your conversation to end!", ephemeral=True, delete_after=10
            )


class RedoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog, model, from_ask_command, from_edit_command, custom_api_key):
        super().__init__(style=discord.ButtonStyle.danger, label="Retry", custom_id="conversation_redo")
        self.converser_cog = converser_cog
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key

infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
        super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")
        self.converser_cog = converser_cog

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if (
            user_id in self.converser_cog.conversation_thread_owners
            and self.converser_cog.conversation_thread_owners[user_id]
            == interaction.channel.id
        ):
            try:
                await self.converser_cog.end_conversation(
                    interaction, opener_user_id=interaction.user.id
                )
            except Exception as e:
                print(e)
                traceback.print_exc()
                await interaction.response.send_message(
                    e, ephemeral=True, delete_after=30
                )
                pass
        else:
            await interaction.response.send_message(
                "This is not your conversation to end!", ephemeral=True, delete_after=10
            )


class RedoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog, model, from_ask_command, from_edit_command, custom_api_key):
        super().__init__(style=discord.ButtonStyle.danger, label="Retry", custom_id="conversation_redo")
        self.converser_cog = converser_cog
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if user_id in self.converser_cog.redo_users and self.converser_cog.redo_users[
            user_id
        ].in_interaction(interaction.message.id):
            # Get the message and the prompt and call encapsulated_send
            prompt = self.converser_cog.redo_users[user_id].prompt
            instruction = self.converser_cog.redo_users[user_id].instruction
            ctx = self.converser_cog.redo_users[user_id].ctx
            response_message = self.converser_cog.redo_users[user_id].response
            codex = self.converser_cog.redo_users[user_id].codex

            msg = await interaction.response.send_message(
                "Retrying your original request...", ephemeral=True, delete_after=15
            )

infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
infile
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
        super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")
        self.converser_cog = converser_cog

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if (
            user_id in self.converser_cog.conversation_thread_owners
            and self.converser_cog.conversation_thread_owners[user_id]
            == interaction.channel.id
        ):
            try:
                await self.converser_cog.end_conversation(
                    interaction, opener_user_id=interaction.user.id
                )
            except Exception as e:
                print(e)
                traceback.print_exc()
                await interaction.response.send_message(
                    e, ephemeral=True, delete_after=30
                )
                pass
        else:
            await interaction.response.send_message(
                "This is not your conversation to end!", ephemeral=True, delete_after=10
            )


class RedoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog, model, from_ask_command, from_edit_command, custom_api_key):
        super().__init__(style=discord.ButtonStyle.danger, label="Retry", custom_id="conversation_redo")
        self.converser_cog = converser_cog
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if user_id in self.converser_cog.redo_users and self.converser_cog.redo_users[
            user_id
        ].in_interaction(interaction.message.id):
            # Get the message and the prompt and call encapsulated_send
            prompt = self.converser_cog.redo_users[user_id].prompt
            instruction = self.converser_cog.redo_users[user_id].instruction
            ctx = self.converser_cog.redo_users[user_id].ctx
            response_message = self.converser_cog.redo_users[user_id].response
            codex = self.converser_cog.redo_users[user_id].codex

            msg = await interaction.response.send_message(
                "Retrying your original request...", ephemeral=True, delete_after=15
            )

            await TextService.encapsulated_send(
                self.converser_cog,
                id=user_id,
                prompt=prompt,
                instruction=instruction,
                ctx=ctx,
                model=self.model,
                response_message=response_message,
                codex=codex,
                custom_api_key=self.custom_api_key,
                redo_request=True,
                from_ask_command=self.from_ask_command,
                from_edit_command=self.from_edit_command,
            )
        else:
            await interaction.response.send_message(
                "You can only redo the most recent prompt that you sent yourself.",
                ephemeral=True,
                delete_after=10,
            )


"""
The setup modal when using user input API keys
"""
class SetupModal(discord.ui.Modal):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        # Get the argument named "user_key_db" and save it as USER_KEY_DB
        self.USER_KEY_DB = kwargs.pop("user_key_db")

        self.add_item(
            discord.ui.InputText(
                label="OpenAI API Key",
                placeholder="sk--......",
            )
        )

    async def callback(self, interaction: discord.Interaction):
        user = interaction.user
        api_key = self.children[0].value
        # Validate that api_key is indeed in this format
        if not re.match(r"sk-[a-zA-Z0-9]{32}", api_key):
            await interaction.response.send_message(
                "Your API key looks invalid, please check that it is correct before proceeding. Please run the /setup command to set your key.",
                ephemeral=True,
                delete_after=100,
            )
        else:
            # We can save the key for the user to the database.

            # Make a test request using the api key to ensure that it is valid.
            try:
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
        super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")
        self.converser_cog = converser_cog

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if (
            user_id in self.converser_cog.conversation_thread_owners
            and self.converser_cog.conversation_thread_owners[user_id]
            == interaction.channel.id
        ):
            try:
                await self.converser_cog.end_conversation(
                    interaction, opener_user_id=interaction.user.id
                )
            except Exception as e:
                print(e)
                traceback.print_exc()
                await interaction.response.send_message(
                    e, ephemeral=True, delete_after=30
                )
                pass
        else:
            await interaction.response.send_message(
                "This is not your conversation to end!", ephemeral=True, delete_after=10
            )


class RedoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog, model, from_ask_command, from_edit_command, custom_api_key):
        super().__init__(style=discord.ButtonStyle.danger, label="Retry", custom_id="conversation_redo")
        self.converser_cog = converser_cog
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if user_id in self.converser_cog.redo_users and self.converser_cog.redo_users[
            user_id
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
        super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")
        self.converser_cog = converser_cog

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if (
            user_id in self.converser_cog.conversation_thread_owners
            and self.converser_cog.conversation_thread_owners[user_id]
            == interaction.channel.id
        ):
            try:
                await self.converser_cog.end_conversation(
                    interaction, opener_user_id=interaction.user.id
                )
            except Exception as e:
                print(e)
                traceback.print_exc()
                await interaction.response.send_message(
                    e, ephemeral=True, delete_after=30
                )
                pass
        else:
            await interaction.response.send_message(
                "This is not your conversation to end!", ephemeral=True, delete_after=10
            )


class RedoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog, model, from_ask_command, from_edit_command, custom_api_key):
        super().__init__(style=discord.ButtonStyle.danger, label="Retry", custom_id="conversation_redo")
        self.converser_cog = converser_cog
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if user_id in self.converser_cog.redo_users and self.converser_cog.redo_users[
            user_id
        ].in_interaction(interaction.message.id):
            # Get the message and the prompt and call encapsulated_send
            prompt = self.converser_cog.redo_users[user_id].prompt
            instruction = self.converser_cog.redo_users[user_id].instruction
            ctx = self.converser_cog.redo_users[user_id].ctx
            response_message = self.converser_cog.redo_users[user_id].response
            codex = self.converser_cog.redo_users[user_id].codex

            msg = await interaction.response.send_message(
                "Retrying your original request...", ephemeral=True, delete_after=15
            )

            await TextService.encapsulated_send(
                self.converser_cog,
                id=user_id,
                prompt=prompt,
                instruction=instruction,
                ctx=ctx,
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
inproject
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
common
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
common
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
        super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")
        self.converser_cog = converser_cog

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if (
            user_id in self.converser_cog.conversation_thread_owners
            and self.converser_cog.conversation_thread_owners[user_id]
            == interaction.channel.id
        ):
            try:
                await self.converser_cog.end_conversation(
                    interaction, opener_user_id=interaction.user.id
                )
            except Exception as e:
                print(e)
                traceback.print_exc()
                await interaction.response.send_message(
                    e, ephemeral=True, delete_after=30
                )
                pass
        else:
            await interaction.response.send_message(
                "This is not your conversation to end!", ephemeral=True, delete_after=10
            )


class RedoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog, model, from_ask_command, from_edit_command, custom_api_key):
        super().__init__(style=discord.ButtonStyle.danger, label="Retry", custom_id="conversation_redo")
        self.converser_cog = converser_cog
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if user_id in self.converser_cog.redo_users and self.converser_cog.redo_users[
            user_id
        ].in_interaction(interaction.message.id):
            # Get the message and the prompt and call encapsulated_send
            prompt = self.converser_cog.redo_users[user_id].prompt
            instruction = self.converser_cog.redo_users[user_id].instruction
            ctx = self.converser_cog.redo_users[user_id].ctx
            response_message = self.converser_cog.redo_users[user_id].response
            codex = self.converser_cog.redo_users[user_id].codex

            msg = await interaction.response.send_message(
                "Retrying your original request...", ephemeral=True, delete_after=15
            )

            await TextService.encapsulated_send(
                self.converser_cog,
                id=user_id,
                prompt=prompt,
                instruction=instruction,
                ctx=ctx,
                model=self.model,
                response_message=response_message,
                codex=codex,
                custom_api_key=self.custom_api_key,
                redo_request=True,
                from_ask_command=self.from_ask_command,
                from_edit_command=self.from_edit_command,
            )
        else:
            await interaction.response.send_message(
                "You can only redo the most recent prompt that you sent yourself.",
                ephemeral=True,
                delete_after=10,
            )


"""
The setup modal when using user input API keys
"""
class SetupModal(discord.ui.Modal):
    def __init__(self, *args, **kwargs) -> None:
commited
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
        super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")
        self.converser_cog = converser_cog

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if (
            user_id in self.converser_cog.conversation_thread_owners
            and self.converser_cog.conversation_thread_owners[user_id]
            == interaction.channel.id
        ):
            try:
                await self.converser_cog.end_conversation(
                    interaction, opener_user_id=interaction.user.id
                )
            except Exception as e:
                print(e)
                traceback.print_exc()
                await interaction.response.send_message(
                    e, ephemeral=True, delete_after=30
                )
                pass
        else:
            await interaction.response.send_message(
                "This is not your conversation to end!", ephemeral=True, delete_after=10
            )


class RedoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog, model, from_ask_command, from_edit_command, custom_api_key):
commited
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
commited
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1
non_informative
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
random
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
                    timestamp,
                    custom_api_key=custom_api_key,
                )

            # Cleanse again
            response_text = converser_cog.cleanse_response(response_text)

            # escape any other mentions like @here or @everyone
            response_text = discord.utils.escape_mentions(response_text)


            # If we don't have a response message, we are not doing a redo, send as a new message(s)
            if not response_message:
                if len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        paginator = None
                        await converser_cog.paginate_and_send(response_text, ctx)
                    else:
                        embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                        view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                        paginator = pages.Paginator(pages=embed_pages, timeout=None, custom_view=view)
                        response_message = await paginator.respond(ctx.interaction)
                else:
                    paginator = None
                    if not from_context:
                        response_message = await ctx.reply(
                            response_text,
                            view=ConversationView(
                                ctx,
                                converser_cog,
                                ctx.channel.id,
                                model,
                                custom_api_key=custom_api_key,
                            ),
                        )
                    elif from_edit_command:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_edit_command=from_edit_command, 
                                custom_api_key=custom_api_key
                            ),
                        )
                    else:
                        response_message = await ctx.respond(
                            response_text,
                            view=ConversationView(
                                ctx, 
                                converser_cog, 
                                ctx.channel.id, 
                                model, 
                                from_ask_command=from_ask_command, 
                                custom_api_key=custom_api_key
                            ),
                        )

                if response_message:
                    # Get the actual message object of response_message in case it's an WebhookMessage
                    actual_response_message = (
                        response_message
                        if not from_context
                        else await ctx.fetch_message(response_message.id)
                    )

                    converser_cog.redo_users[ctx.author.id] = RedoUser(
                        prompt=new_prompt, 
                        instruction=instruction, 
                        ctx=ctx, 
                        message=ctx, 
                        response=actual_response_message, 
                        codex=codex, 
                        paginator=paginator
                    )
                    converser_cog.redo_users[ctx.author.id].add_interaction(
                        actual_response_message.id
                    )

            # We are doing a redo, edit the message.
            else:
                paginator = converser_cog.redo_users.get(ctx.author.id).paginator
                if isinstance(paginator, pages.Paginator):
                    embed_pages = await converser_cog.paginate_embed(response_text, codex, prompt, instruction)
                    view=ConversationView(ctx, converser_cog, ctx.channel.id, model, from_ask_command, from_edit_command, custom_api_key=custom_api_key)
                    await paginator.update(pages=embed_pages, custom_view=view)
                elif len(response_text) > converser_cog.TEXT_CUTOFF:
                    if not from_context:
                        await response_message.channel.send("Over 2000 characters", delete_after=5)
                else:
                    await response_message.edit(content=response_text)

            await converser_cog.send_debug_message(
                converser_cog.generate_debug_message(prompt, response), converser_cog.debug_channel
            )

            if ctx.author.id in converser_cog.awaiting_responses:
                converser_cog.awaiting_responses.remove(ctx.author.id)
            if not from_ask_command and not from_edit_command:
                if ctx.channel.id in converser_cog.awaiting_thread_responses:
                    converser_cog.awaiting_thread_responses.remove(ctx.channel.id)

        # Error catching for AIOHTTP Errors
        except aiohttp.ClientResponseError as e:
            message = (
                f"The API returned an invalid response: **{e.status}: {e.message}**"
            )
            if from_context:
                await ctx.send_followup(message)
            else:
                await ctx.reply(message)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # Error catching for OpenAI model value errors
        except ValueError as e:
            if from_context:
                await ctx.send_followup(e)
            else:
                await ctx.reply(e)
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )

        # General catch case for everything
        except Exception:

            message = "Something went wrong, please try again later. This may be due to upstream issues on the API, or rate limiting."
            await ctx.send_followup(message) if from_context else await ctx.reply(
                message
            )
            converser_cog.remove_awaiting(
                ctx.author.id, ctx.channel.id, from_ask_command, from_edit_command
            )
            traceback.print_exc()

            try:
                await converser_cog.end_conversation(ctx)
            except:
                pass
            return

    @staticmethod
    async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):
        content = message.content.strip()
        conversing = converser_cog.check_conversing(
            message.author.id, message.channel.id, content
        )

        # If the user is conversing and they want to end it, end it immediately before we continue any further.
        if conversing and message.content.lower() in converser_cog.END_PROMPTS:
            await converser_cog.end_conversation(message)
            return

        if conversing:
            user_api_key = None
            if USER_INPUT_API_KEYS:
                user_api_key = await TextService.get_user_api_key(
                    message.author.id, message, USER_KEY_DB
                )
                if not user_api_key:
                    return

            prompt = await converser_cog.mention_to_username(message, content)

            await converser_cog.check_conversation_limit(message)

            # If the user is in a conversation thread
            if message.channel.id in converser_cog.conversation_threads:

                # Since this is async, we don't want to allow the user to send another prompt while a conversation
                # prompt is processing, that'll mess up the conversation history!
                if message.author.id in converser_cog.awaiting_responses:
                    message = await message.reply(
                        "You are already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                if message.channel.id in converser_cog.awaiting_thread_responses:
                    message = await message.reply(
                        "This thread is already waiting for a response from GPT3. Please wait for it to respond before sending another message."
                    )

                    # get the current date, add 10 seconds to it, and then turn it into a timestamp.
                    # we need to use our deletion service because this isn't an interaction, it's a regular message.
                    deletion_time = datetime.datetime.now() + datetime.timedelta(
                        seconds=10
                    )
                    deletion_time = deletion_time.timestamp()

                    deletion_message = Deletion(message, deletion_time)
                    await converser_cog.deletion_queue.put(deletion_message)

                    return

                converser_cog.awaiting_responses.append(message.author.id)
                converser_cog.awaiting_thread_responses.append(message.channel.id)

                if not converser_cog.pinecone_service:
                    converser_cog.conversation_threads[message.channel.id].history.append(
                        EmbeddedConversationItem(
                            f"\n'{message.author.display_name}': {prompt} <|endofstatement|>\n",
                            0,
                        )
                    )

                # increment the conversation counter for the user
                converser_cog.conversation_threads[message.channel.id].count += 1

            # Send the request to the model
            # If conversing, the prompt to send is the history, otherwise, it's just the prompt
            if (
                    converser_cog.pinecone_service
                    or message.channel.id not in converser_cog.conversation_threads
            ):
                primary_prompt = prompt
            else:
                primary_prompt = "".join(
                    [
                        item.text
                        for item in converser_cog.conversation_threads[
                        message.channel.id
                    ].history
                    ]
                )

            # set conversation overrides
            overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()

            await TextService.encapsulated_send(
                converser_cog,
                message.channel.id,
                primary_prompt,
                message,
                temp_override=overrides["temperature"],
                top_p_override=overrides["top_p"],
                frequency_penalty_override=overrides["frequency_penalty"],
                presence_penalty_override=overrides["presence_penalty"],
                model=converser_cog.conversation_threads[message.channel.id].model,
                custom_api_key=user_api_key,
            )
            return True

    @staticmethod
    async def get_user_api_key(user_id, ctx, USER_KEY_DB):
        user_api_key = None if user_id not in USER_KEY_DB else USER_KEY_DB[user_id]
        if user_api_key is None or user_api_key == "":
            modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)
            if isinstance(ctx, discord.ApplicationContext):
                await ctx.send_modal(modal)
                await ctx.send_followup(
                    "You must set up your API key before using this command."
                )
            else:
                await ctx.reply(
                    "You must set up your API key before typing in a GPT3 powered channel, type `/setup` to enter your API key."
                )
        return user_api_key

    @staticmethod
    async def process_conversation_edit(converser_cog, after, original_message):
        if after.author.id in converser_cog.redo_users:
            if after.id == original_message[after.author.id]:
                response_message = converser_cog.redo_users[after.author.id].response
                ctx = converser_cog.redo_users[after.author.id].ctx
                await response_message.edit(content="Redoing prompt 🔄...")

                edited_content = await converser_cog.mention_to_username(after, after.content)

                if after.channel.id in converser_cog.conversation_threads:
                    # Remove the last two elements from the history array and add the new <username>: prompt
                    converser_cog.conversation_threads[
                        after.channel.id
                    ].history = converser_cog.conversation_threads[after.channel.id].history[:-2]

                    pinecone_dont_reinsert = None
                    if not converser_cog.pinecone_service:
                        converser_cog.conversation_threads[after.channel.id].history.append(
                            EmbeddedConversationItem(
                                f"\n{after.author.display_name}: {after.content}<|endofstatement|>\n",
                                0,
                            )
                        )

                    converser_cog.conversation_threads[after.channel.id].count += 1

                overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()

                await TextService.encapsulated_send(
                    converser_cog,
                    id=after.channel.id,
                    prompt=edited_content,
                    ctx=ctx,
                    response_message=response_message,
                    temp_override=overrides["temperature"],
                    top_p_override=overrides["top_p"],
                    frequency_penalty_override=overrides["frequency_penalty"],
                    presence_penalty_override=overrides["presence_penalty"],
                    model=converser_cog.conversation_threads[after.channel.id].model,
                    edited_request=True,
                )

                if not converser_cog.pinecone_service:
                    converser_cog.redo_users[after.author.id].prompt = edited_content


"""
Conversation interaction buttons
"""
class ConversationView(discord.ui.View):
    def __init__(
        self,
        ctx,
        converser_cog,
        id,
        model,
        from_ask_command=False,
        from_edit_command=False,
        custom_api_key=None,
    ):
        super().__init__(timeout=3600)  # 1 hour interval to redo.
        self.converser_cog = converser_cog
        self.ctx = ctx
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key
        self.add_item(
            RedoButton(
                self.converser_cog,
                model=model,
                from_ask_command=from_ask_command,
                from_edit_command=from_edit_command,
                custom_api_key=self.custom_api_key,
            )
        )

        if id in self.converser_cog.conversation_threads:
            self.add_item(EndConvoButton(self.converser_cog))

    async def on_timeout(self):
        # Remove the button from the view/message
        self.clear_items()
        # Send a message to the user saying the view has timed out
        if self.message:
            await self.message.edit(
                view=None,
            )
        else:
            await self.ctx.edit(
                view=None,
            )


class EndConvoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog):
        super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")
        self.converser_cog = converser_cog

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if (
            user_id in self.converser_cog.conversation_thread_owners
            and self.converser_cog.conversation_thread_owners[user_id]
            == interaction.channel.id
        ):
            try:
                await self.converser_cog.end_conversation(
                    interaction, opener_user_id=interaction.user.id
                )
            except Exception as e:
                print(e)
                traceback.print_exc()
                await interaction.response.send_message(
                    e, ephemeral=True, delete_after=30
                )
                pass
        else:
            await interaction.response.send_message(
                "This is not your conversation to end!", ephemeral=True, delete_after=10
            )


class RedoButton(discord.ui.Button["ConversationView"]):
    def __init__(self, converser_cog, model, from_ask_command, from_edit_command, custom_api_key):
        super().__init__(style=discord.ButtonStyle.danger, label="Retry", custom_id="conversation_redo")
        self.converser_cog = converser_cog
        self.model = model
        self.from_ask_command = from_ask_command
        self.from_edit_command = from_edit_command
        self.custom_api_key = custom_api_key

    async def callback(self, interaction: discord.Interaction):

        # Get the user
        user_id = interaction.user.id
        if user_id in self.converser_cog.redo_users and self.converser_cog.redo_users[
            user_id
        ].in_interaction(interaction.message.id):
            # Get the message and the prompt and call encapsulated_send
            prompt = self.converser_cog.redo_users[user_id].prompt
            instruction = self.converser_cog.redo_users[user_id].instruction
            ctx = self.converser_cog.redo_users[user_id].ctx
            response_message = self.converser_cog.redo_users[user_id].response
            codex = self.converser_cog.redo_users[user_id].codex

            msg = await interaction.response.send_message(
                "Retrying your original request...", ephemeral=True, delete_after=15
            )

            await TextService.encapsulated_send(
                self.converser_cog,
                id=user_id,
                prompt=prompt,
                instruction=instruction,
                ctx=ctx,
                model=self.model,
                response_message=response_message,
                codex=codex,
                custom_api_key=self.custom_api_key,
                redo_request=True,
                from_ask_command=self.from_ask_command,
                from_edit_command=self.from_edit_command,
            )
        else:
            await interaction.response.send_message(
                "You can only redo the most recent prompt that you sent yourself.",
                ephemeral=True,
                delete_after=10,
            )


"""
The setup modal when using user input API keys
"""
class SetupModal(discord.ui.Modal):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        # Get the argument named "user_key_db" and save it as USER_KEY_DB
        self.USER_KEY_DB = kwargs.pop("user_key_db")

        self.add_item(
            discord.ui.InputText(
                label="OpenAI API Key",
                placeholder="sk--......",
            )
        )

    async def callback(self, interaction: discord.Interaction):
        user = interaction.user
        api_key = self.children[0].value
        # Validate that api_key is indeed in this format
        if not re.match(r"sk-[a-zA-Z0-9]{32}", api_key):
            await interaction.response.send_message(
                "Your API key looks invalid, please check that it is correct before proceeding. Please run the /setup command to set your key.",
                ephemeral=True,
                delete_after=100,
            )
        else:
            # We can save the key for the user to the database.

            # Make a test request using the api key to ensure that it is valid.
            try:
                await Model.send_test_request(api_key)
                await interaction.response.send_message(
random
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
random
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
            ):
                if not redo_request:
                    converser_cog.conversation_threads[id].history.append(
                        EmbeddedConversationItem(
                            "\nGPTie: " + str(response_text) + "<|endofstatement|>\n", 0
                        )
                    )

            # Embeddings case!
            elif (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not from_edit_command
                and converser_cog.pinecone_service
            ):
                conversation_id = id

                # Create an embedding and timestamp for the prompt
                response_text = (
                    "\nGPTie: " + str(response_text) + "<|endofstatement|>\n"
                )

                response_text = response_text.encode("ascii", "ignore").decode()

                # Print the current timestamp
                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )
                converser_cog.conversation_threads[conversation_id].history.append(
                    EmbeddedConversationItem(response_text, timestamp)
                )

                # Create and upsert the embedding for  the conversation id, prompt, timestamp
                embedding = await converser_cog.pinecone_service.upsert_conversation_embedding(
                    converser_cog.model,
                    conversation_id,
                    response_text,
random
import datetime
import re
import traceback

import aiohttp
import discord
from discord.ext import pages

from services.deletion_service import Deletion
from models.openai_model import Model
from models.user_model import EmbeddedConversationItem, RedoUser


class TextService:
    def __init__(self):
        pass

    @staticmethod
    async def encapsulated_send(
        converser_cog,
        id,
        prompt,
        ctx,
        response_message=None,
        temp_override=None,
        top_p_override=None,
        frequency_penalty_override=None,
        presence_penalty_override=None,
        from_ask_command=False,
        instruction=None,
        from_edit_command=False,
        codex=False,
        model=None,
        custom_api_key=None,
        edited_request=False,
        redo_request=False,
    ):
        new_prompt = (
            prompt + "\nGPTie: "
            if not from_ask_command and not from_edit_command
            else prompt
        )

        from_context = isinstance(ctx, discord.ApplicationContext)

        if not instruction:
            tokens = converser_cog.usage_service.count_tokens(new_prompt)
        else:
            tokens = converser_cog.usage_service.count_tokens(
                new_prompt
            ) + converser_cog.usage_service.count_tokens(instruction)

        try:

            # Pinecone is enabled, we will create embeddings for this conversation.
            if converser_cog.pinecone_service and ctx.channel.id in converser_cog.conversation_threads:
                # Delete "GPTie:  <|endofstatement|>" from the user's conversation history if it exists
                # check if the text attribute for any object inside converser_cog.conversation_threads[converation_id].history
                # contains ""GPTie: <|endofstatement|>"", if so, delete
                for item in converser_cog.conversation_threads[ctx.channel.id].history:
                    if item.text.strip() == "GPTie:<|endofstatement|>":
                        converser_cog.conversation_threads[ctx.channel.id].history.remove(item)

                # The conversation_id is the id of the thread
                conversation_id = ctx.channel.id

                # Create an embedding and timestamp for the prompt
                new_prompt = prompt.encode("ascii", "ignore").decode()
                prompt_less_author = f"{new_prompt} <|endofstatement|>\n"

                user_displayname = ctx.author.display_name

                new_prompt = (
                    f"\n'{user_displayname}': {new_prompt} <|endofstatement|>\n"
                )
                new_prompt = new_prompt.encode("ascii", "ignore").decode()

                timestamp = int(
                    str(datetime.datetime.now().timestamp()).replace(".", "")
                )

                new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)

                if not redo_request:
                    converser_cog.conversation_threads[conversation_id].history.append(
                        new_prompt_item
                    )

                if edited_request:
                    new_prompt = "".join(
                        [
                            item.text
                            for item in converser_cog.conversation_threads[
                                ctx.channel.id
                            ].history
                        ]
                    )
                    converser_cog.redo_users[ctx.author.id].prompt = new_prompt
                else:
                    # Create and upsert the embedding for  the conversation id, prompt, timestamp
                    await converser_cog.pinecone_service.upsert_conversation_embedding(
                        converser_cog.model,
                        conversation_id,
                        new_prompt,
                        timestamp,
                        custom_api_key=custom_api_key,
                    )

                    embedding_prompt_less_author = await converser_cog.model.send_embedding_request(
                        prompt_less_author, custom_api_key=custom_api_key
                    )  # Use the version of the prompt without the author's name for better clarity on retrieval.

                    # Now, build the new prompt by getting the X most similar with pinecone
                    similar_prompts = converser_cog.pinecone_service.get_n_similar(
                        conversation_id,
                        embedding_prompt_less_author,
                        n=converser_cog.model.num_conversation_lookback,
                    )

                    # When we are in embeddings mode, only the pre-text is contained in converser_cog.conversation_threads[message.channel.id].history, so we
                    # can use that as a base to build our new prompt
                    prompt_with_history = [
                        converser_cog.conversation_threads[ctx.channel.id].history[0]
                    ]

                    # Append the similar prompts to the prompt with history
                    prompt_with_history += [
                        EmbeddedConversationItem(prompt, timestamp)
                        for prompt, timestamp in similar_prompts
                    ]

                    # iterate UP TO the last X prompts in the history
                    for i in range(
                        1,
                        min(
                            len(converser_cog.conversation_threads[ctx.channel.id].history),
                            converser_cog.model.num_static_conversation_items,
                        ),
                    ):
                        prompt_with_history.append(
                            converser_cog.conversation_threads[ctx.channel.id].history[-i]
                        )

                    # remove duplicates from prompt_with_history and set the conversation history
                    prompt_with_history = list(dict.fromkeys(prompt_with_history))
                    converser_cog.conversation_threads[
                        ctx.channel.id
                    ].history = prompt_with_history

                    # Sort the prompt_with_history by increasing timestamp if pinecone is enabled
                    if converser_cog.pinecone_service:
                        prompt_with_history.sort(key=lambda x: x.timestamp)

                    # Ensure that the last prompt in this list is the prompt we just sent (new_prompt_item)
                    if prompt_with_history[-1] != new_prompt_item:
                        try:
                            prompt_with_history.remove(new_prompt_item)
                        except ValueError:
                            pass
                        prompt_with_history.append(new_prompt_item)

                    prompt_with_history = "".join(
                        [item.text for item in prompt_with_history]
                    )

                    new_prompt = prompt_with_history + "\nGPTie: "

                tokens = converser_cog.usage_service.count_tokens(new_prompt)

            # No pinecone, we do conversation summarization for long term memory instead
            elif (
                id in converser_cog.conversation_threads
                and tokens > converser_cog.model.summarize_threshold
                and not from_ask_command
                and not from_edit_command
                and not converser_cog.pinecone_service  # This should only happen if we are not doing summarizations.
            ):

                # We don't need to worry about the differences between interactions and messages in this block,
                # because if we are in this block, we can only be using a message object for ctx
                if converser_cog.model.summarize_conversations:
                    await ctx.reply(
                        "I'm currently summarizing our current conversation so we can keep chatting, "
                        "give me one moment!"
                    )

                    await converser_cog.summarize_conversation(ctx, new_prompt)

                    # Check again if the prompt is about to go past the token limit
                    new_prompt = (
                        "".join(
                            [
                                item.text
                                for item in converser_cog.conversation_threads[id].history
                            ]
                        )
                        + "\nGPTie: "
                    )

                    tokens = converser_cog.usage_service.count_tokens(new_prompt)

                    if (
                        tokens > converser_cog.model.summarize_threshold - 150
                    ):  # 150 is a buffer for the second stage
                        await ctx.reply(
                            "I tried to summarize our current conversation so we could keep chatting, "
                            "but it still went over the token "
                            "limit. Please try again later."
                        )

                        await converser_cog.end_conversation(ctx)
                        return
                else:
                    await ctx.reply("The conversation context limit has been reached.")
                    await converser_cog.end_conversation(ctx)
                    return

            # Send the request to the model
            if from_edit_command:
                response = await converser_cog.model.send_edit_request(
                    input=new_prompt,
                    instruction=instruction,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    codex=codex,
                    custom_api_key=custom_api_key,
                )
            else:
                response = await converser_cog.model.send_request(
                    new_prompt,
                    tokens=tokens,
                    temp_override=temp_override,
                    top_p_override=top_p_override,
                    frequency_penalty_override=frequency_penalty_override,
                    presence_penalty_override=presence_penalty_override,
                    model=model,
                    custom_api_key=custom_api_key,
                )

            # Clean the request response
            response_text = converser_cog.cleanse_response(str(response["choices"][0]["text"]))

            if from_ask_command:
                    # Append the prompt to the beginning of the response, in italics, then a new line
                    response_text = response_text.strip()
                    response_text = f"***{prompt}***\n\n{response_text}"
            elif from_edit_command:
                if codex:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n```\n{response_text}\n```"
                else:
                    response_text = response_text.strip()
                    response_text = f"***Prompt: {prompt}***\n***Instruction: {instruction}***\n\n{response_text}\n"

            # If gpt3 tries writing a user mention try to replace it with their name
            response_text = await converser_cog.mention_to_username(ctx, response_text)

            # If the user is conversing, add the GPT response to their conversation history.
            if (
                id in converser_cog.conversation_threads
                and not from_ask_command
                and not converser_cog.pinecone_service
random
    async def callback(self, interaction: discord.Interaction):             RedoButton(                             view=ConversationView(             self.add_item(EndConvoButton(self.converser_cog))                 user_api_key = await TextService.get_user_api_key(     async def on_timeout(self):     async def get_user_api_key(user_id, ctx, USER_KEY_DB):             modal = SetupModal(title="API Key Setup",user_key_db=USER_KEY_DB)     async def encapsulated_send(             await TextService.encapsulated_send(     async def process_conversation_edit(converser_cog, after, original_message):     async def process_conversation_message(converser_cog, message, USER_INPUT_API_KEYS, USER_KEY_DB):                             EmbeddedConversationItem(                 model=model,                     await response_message.edit(content=response_text)                             converser_cog.model.num_static_conversation_items,                 await Model.send_test_request(api_key)             await self.message.edit(             await self.ctx.edit(             overrides = converser_cog.conversation_threads[message.channel.id].get_overrides()         model=None,                     EmbeddedConversationItem(response_text, timestamp)                 model=converser_cog.conversation_threads[message.channel.id].model,                     converser_cog.model,                 and tokens > converser_cog.model.summarize_threshold                 if converser_cog.model.summarize_conversations:         self.model = model                 await response_message.edit(content="Redoing prompt 🔄...")         ].in_interaction(interaction.message.id):                         tokens > converser_cog.model.summarize_threshold - 150                                 model,                  new_prompt_item = EmbeddedConversationItem(new_prompt, timestamp)                 overrides = converser_cog.conversation_threads[after.channel.id].get_overrides()                 response = await converser_cog.model.send_edit_request(                 model=self.model,                     model=converser_cog.conversation_threads[after.channel.id].model,                 response = await converser_cog.model.send_request(                     embedding_prompt_less_author = await converser_cog.model.send_embedding_request(                     converser_cog.redo_users[ctx.author.id] = RedoUser(                         n=converser_cog.model.num_conversation_lookback,                     converser_cog.redo_users[ctx.author.id].add_interaction(                         EmbeddedConversationItem(prompt, timestamp)                     await paginator.update(pages=embed_pages, custom_view=view)                 paginator = converser_cog.redo_users.get(ctx.author.id).paginator         super().__init__(*args, **kwargs)         super().__init__(style=discord.ButtonStyle.danger, label="Retry", custom_id="conversation_redo")         super().__init__(style=discord.ButtonStyle.danger, label="End Conversation", custom_id="conversation_end")                 )                     "Your API key was successfully validated.",                             prompt_with_history.remove(new_prompt_item)                     timestamp,             ):