You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
396 lines
14 KiB
396 lines
14 KiB
import re |
|
import json |
|
import os |
|
import openai |
|
from dotenv import load_dotenv |
|
import requests |
|
from bot_strings import botstrings |
|
|
|
# Call the load_dotenv function |
|
load_dotenv() |
|
|
|
from pprint import pprint |
|
|
|
|
|
class Report: |
|
def __init__(self): |
|
self.description = None |
|
self.incident = None |
|
self.name = None |
|
self.role = None |
|
self.location = None |
|
self.image = None |
|
self.size = None |
|
self.shape = None |
|
self.color = None |
|
self.organization = None |
|
self.type = None |
|
self.marked: bool = None |
|
self.blockage: bool = None |
|
self.area_description = None |
|
self.contact_person_phone_number = False |
|
self.contact_person_name = False |
|
|
|
self.info_groups = [ |
|
["description", "color", "size", "shape"], |
|
["image"], |
|
["location", "area_description"], |
|
["incident"], |
|
["blockage", "marked"], |
|
["contact_person_name", "contact_person_phone_number"], |
|
["organization", "name", "role"], |
|
] |
|
|
|
self.descriptions = { |
|
"description": "A general description of the object.", |
|
"incident": "Has there been an incident related to the object?", |
|
"name": "The name of the person reporting.", |
|
"role": "The role or occupation of the person reporting.", |
|
"location": "The location of the object.", # TODO Add more details |
|
"image": "A picture of the object.", |
|
"size": "The size of the object.", |
|
"shape": "The shape of the object.", |
|
"color": "The color of the object.", |
|
"organization": "The organization of the person reporting.", |
|
"type": "The type of the object.", |
|
"marked": "Is the object marked?", |
|
"blockage": "Is the object blocking anything?", |
|
"area_description": "A description of the area where the object is located.", |
|
"contact_person_phone_number": "The phone number of the contact person.", |
|
"contact_person_name": "The name of the contact person.", |
|
} |
|
|
|
self.looking_for: list = self.info_groups[0] |
|
|
|
|
|
class BaseBot: |
|
def __init__(self) -> None: |
|
# Fetch the OpenAI key from the environment variables |
|
self.OPEN_AI_KEY = os.getenv("OPEN_AI_KEY") |
|
# # Set the OpenAI key |
|
# openai.api_key = self.OPEN_AI_KEY |
|
self.url = "https://api.openai.com/v1/chat/completions" |
|
self.headers = { |
|
"Content-Type": "application/json", |
|
"Authorization": f"Bearer {self.OPEN_AI_KEY}", |
|
} |
|
|
|
self.memory: list[dict] = None |
|
|
|
def generate(self, message): |
|
|
|
message = re.sub(" +", " ", message) |
|
|
|
if self.memory: |
|
messages = self.memory |
|
if messages[-1]["content"] != message: |
|
messages.append({"role": "user", "content": message}) |
|
else: |
|
messages = [ |
|
{"role": "system", "content": self.system_prompt}, |
|
{"role": "user", "content": message}, |
|
] |
|
|
|
data = {"model": "gpt-3.5-turbo", "messages": messages} |
|
# Print the last message in yellow |
|
last_message = messages[-1]["content"] |
|
print("\n\033[94m" + last_message + "\033[0m\n") |
|
|
|
response = requests.post(self.url, headers=self.headers, json=data).json() |
|
answer = response["choices"][0]["message"] |
|
|
|
print("\033[95m" + answer["content"] + "\033[0m") |
|
|
|
return answer |
|
|
|
|
|
class Chatbot(BaseBot): |
|
def __init__(self) -> None: |
|
system_prompt = botstrings.chatbot_system_prompt |
|
super().__init__() # Inherit from BaseBot |
|
# TODO Check if there is an older conversation with the same number. |
|
# Set the system prompt and the first user message. |
|
self.first_instructions_sent = False |
|
self.informations_requested = False |
|
self.memory = [{"role": "system", "content": system_prompt}] |
|
|
|
def ask_for_info(self, report: Report, chatbot: "Chatbot"): |
|
# Ask for information |
|
looking_for = [] |
|
group_found = False |
|
for group in report.info_groups: |
|
if not group_found: |
|
for info in group: |
|
if getattr(report, info) is None: |
|
group_found = True |
|
looking_for.append(info) |
|
|
|
# Check so that the set of info is not already asked for without an answer |
|
if looking_for == report.looking_for: |
|
for info in looking_for: |
|
setattr(report, info, "Unanswered") |
|
looking_for = [] |
|
group_found = False |
|
|
|
if looking_for != []: |
|
report.looking_for = looking_for |
|
# Ask for the information |
|
general_bot.memory = [ |
|
{"role": "user", "content": 'Formulate a question asking for the following information: ["name", "age"]'}, |
|
{"role": "assistant", "content": "Can you tell me about the name and age of what you've found?"}, |
|
] |
|
if looking_for == ["image"]: |
|
question = 'image' |
|
else: |
|
prompt = f"Formulate a question asking for the following information: {looking_for}" |
|
general_bot.memory.append({"role": "user", "content": prompt}) |
|
question = general_bot.generate(prompt)["content"] |
|
return question |
|
|
|
else: |
|
return False, False |
|
|
|
|
|
class CheckerBot(BaseBot): |
|
def __init__(self) -> None: |
|
self.system_prompt = botstrings.checker_bot_system_prompt |
|
super().__init__() # Inherit from BaseBot |
|
self.history = [] |
|
|
|
def check_message_type(self, message): |
|
prompt = f"""What kind of message is this? |
|
'''{message}''' |
|
Don't care about the content, only the type of message. \ |
|
Answer with any of the following: ["greeting", "question", "information", "statement"]\ |
|
""" |
|
|
|
result = self.generate(prompt)["content"] |
|
if "greeting" in result.lower(): |
|
message_type = "greeting" |
|
elif "question" in result.lower(): |
|
message_type = "question" |
|
elif "information" in result.lower() or "statement" in result.lower(): #TODO should "statement" be here? |
|
message_type = "information" |
|
|
|
return message_type |
|
|
|
def check_answer(self, answer, question, chatbot: Chatbot): |
|
return True # TODO We need to fins a good way to check if the answer is an answer to the question. |
|
question = question.replace("\n", " ") |
|
answer = answer.replace("\n", " ") |
|
if question == botstrings.first_instructions: |
|
question = "What have you found?" |
|
|
|
prompt = f''' |
|
Have a look at this conversation:\n |
|
""" |
|
{chat2string(chatbot.memory[-4:]).strip()} |
|
"""\n |
|
Is the last message a resonable answer to the last question ({question})? |
|
Answer ONLY with any of the following: "True", "False", "Unclear" |
|
'''.strip() |
|
result = self.generate(prompt)["content"] |
|
|
|
if "unclear" in result.lower(): |
|
prompt = f""" |
|
A user is having a conversation with an assistant. This is the conversation:\n'''{chatbot.memory}'''\ |
|
Is the last message ('''{answer}''') an answer to the question ('{question}')? |
|
Answer ONLY with any of the following: "True", "False", "Unclear"\ |
|
""" |
|
self.memory.append({"role": "user", "content": prompt}) |
|
|
|
result = self.generate(prompt) |
|
|
|
if "true" in result.lower(): |
|
answered = True |
|
|
|
elif "false" in result.lower() or "unclear" in result.lower(): |
|
answered = False |
|
|
|
return answered |
|
|
|
def check_for_info(self, user_message, report: Report, looking_for: list, n_try=0): |
|
info_dict = {} |
|
for info in looking_for: |
|
if getattr(report, info) is None: |
|
info_dict[info] = report.descriptions[info] |
|
prompt = f"""" |
|
This is a message from a user: '''{user_message}'''\n |
|
This is a dict describing what info I want:\n\n{info_dict} \n\n\ |
|
Take a look at the message and create a dictionary with the information that is requested.\ |
|
There might not be information available in the mesasge for all fields. \ |
|
If you can't find information for a certain field, fill that with a python null value ("None" or "null"). |
|
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.\ |
|
""" |
|
|
|
n_try += 1 |
|
result = self.generate(prompt) |
|
|
|
try: |
|
json_content = json.loads( |
|
result["content"] |
|
) # Parse the string back into a dictionary |
|
for key, value in json_content.items(): |
|
if value not in ["null", "None", "", "Unknown"]: |
|
setattr(report, key, value) |
|
except: |
|
try: |
|
result_content = result_content[ |
|
result_content.find("{") : result_content.rfind("}") + 1 |
|
] |
|
json_content = json.loads( |
|
result["content"] |
|
) # Parse the string back into a dictionary |
|
for key, value in json_content.items(): |
|
if value not in ["null", "None", "", "Unknown"]: |
|
setattr(report, key, value) |
|
except: |
|
if n_try > 3: |
|
return False |
|
self.check_for_info(user_message, report, looking_for, n_try=n_try) |
|
|
|
return json_content |
|
|
|
def check_for_tips(self, bot_message): |
|
# Check if the message is a tip |
|
prompt = f""" |
|
An assistant wants to send this message to a user: '''{bot_message}'''\ |
|
Is the message a tip or recommendation on how to handle a suspicious object? |
|
Answer ONLY with any of the following: "True", "False"\ |
|
""" |
|
result = self.generate(prompt)["content"] |
|
print(result) |
|
if "true" in result.lower(): |
|
is_tip = True |
|
elif "false" in result.lower(): |
|
is_tip = False |
|
|
|
return is_tip |
|
|
|
|
|
class GeneralBot(BaseBot): |
|
def __init__(self) -> None: |
|
self.system_prompt = botstrings.general_bot_system_prompt |
|
super().__init__() |
|
self.memory = [] |
|
|
|
|
|
def send(message, check=True): |
|
|
|
# Check if the message is a tip. |
|
if check: |
|
tip = checker_bot.check_for_tips(message) |
|
else: |
|
tip = False |
|
|
|
if tip: |
|
return False |
|
else: |
|
print(message) # TODO Send the message to the user |
|
return True |
|
|
|
|
|
def chat2string(chat: list): |
|
chat_string = "" |
|
for message in chat: |
|
chat_string += f"{message['role']}: {message['content']}\n" |
|
return chat_string |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
#! For testing |
|
user_input = "I have found a strange looking thing on my lawn." |
|
|
|
# Initiate the chatbot. |
|
chatbot = Chatbot( |
|
first_message=user_input, |
|
system_prompt="You are an assistant chatting with a user.", |
|
) # TODO ? Add phone number, etc. |
|
checker_bot = CheckerBot() |
|
general_bot = GeneralBot() |
|
report = Report() |
|
|
|
while True: |
|
# Check the message type |
|
message_type = checker_bot.check_message_type(message=user_input) |
|
print("Message type:", message_type) |
|
|
|
if "greeting" in message_type.lower(): |
|
# Answer the greeting |
|
bot_answer = chatbot.generate() |
|
if not chatbot.first_instructions_sent: |
|
# Give instructions for how to use the bot |
|
chatbot.instructions_sent = True |
|
chatbot.informations_requested = True |
|
chatbot.memory.append( |
|
{"role": "system", "content": botstrings.first_instructions} |
|
) |
|
else: |
|
chatbot.memory.append(bot_answer) |
|
|
|
elif "question" in message_type.lower(): |
|
if not chatbot.first_instructions_sent: |
|
# Give instructions for how to use the bot |
|
chatbot.first_instructions_sent = True |
|
chatbot.memory.append( |
|
{"role": "system", "content": botstrings.first_instructions} |
|
) |
|
|
|
bot_answer = chatbot.generate() # TODO How to handle questions? |
|
|
|
# Check if the answer is an answer to the question. |
|
answered = checker_bot.check_answer( |
|
bot_answer["content"], user_input, chatbot |
|
) |
|
if not answered: |
|
bot_answer = "I could not understand your question. Please ask again." |
|
send(bot_answer) |
|
chatbot.memory.append({"role": "system", "content": bot_answer}) |
|
|
|
elif "information" in message_type.lower(): |
|
if not chatbot.first_instructions_sent: |
|
# Give instructions for how to use the bot |
|
chatbot.first_instructions_sent = True |
|
chatbot.informations_requested = True |
|
chatbot.memory.append( |
|
{"role": "system", "content": botstrings.first_instructions} |
|
) |
|
send(botstrings.first_instructions, check=False) |
|
else: |
|
if chatbot.informations_requested: |
|
answered = checker_bot.check_answer( |
|
user_input, chatbot.memory[-1]["content"], chatbot |
|
) |
|
if answered: |
|
# Ask for information and send that message to the report |
|
result = checker_bot.check_for_info( |
|
user_input, report, report.looking_for |
|
) |
|
if result: |
|
pprint(result) |
|
else: |
|
send( |
|
"I could not understand your message. Please try again.", |
|
check=False, |
|
) |
|
looking_for = chatbot.ask_for_info(report) |
|
|
|
else: |
|
send( |
|
"I could not understand your message. Please try again.", |
|
check=False, |
|
) |
|
|
|
else: |
|
print("Unknown message type") |
|
|
|
user_input = input(">>> ") |
|
|
|
if os.path.isfile(user_input): |
|
|
|
report.image = user_input |
|
else: |
|
print("User input is not a valid image file.") |
|
|
|
general_bot = GeneralBot()
|
|
|