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.
 
 

527 lines
19 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.object = None
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 = None
self.blockage = None
self.area_description = None
self.contact_person_phone_number = None
self.contact_person_name = None
self.at_the_location: bool = None
self.coordinates = None
self.woreda = None
self.info_groups = [
["description"],
["color", "size", "shape"],
["at_the_location"],
#['coordinates'],
["image"],
["woreda"],
["location"],
["area_description"],
["incident"],
["blockage", "marked"],
]
self.info_explicit = [
"name",
"contact_person_name",
"contact_person_phone_number",
]
self.info_professional = ["organization", "role", "type"]
self.descriptions = {
"description": "A general description of the object.",
"at_the_location": "If the person reporting is still where the object was found. (True or False)",
"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.",
"woreda": "The woreda where the object was found.",
"location": "The closest city or village to where the object was found.", # TODO Add more details
"coordinates": "The coordinates of the person reporting.",
"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.",
"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.",
"type": "The type of the object.",
}
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()
pprint(response)
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:
print("#", info, getattr(report, info))
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
if looking_for == ["image"]:
question = "image"
else:
description_dict = {}
for i in looking_for:
description_dict[i] = report.descriptions[i]
# Give the bot examles of how to ask for the information
general_bot.memory = [
{
"role": "user",
"content": """I'm looking for the information in this disctionary:
{'description': 'If the person reporting is still where the object was found?'}
Formulate a question asking for the information in the dictionary.""",
},
{
"role": "assistant",
"content": f"Are you still at the place where object was found?",
},
]
# If we know what the object is, we can ask about it.
if report.object is not None:
general_bot.memory.insert(
0,
{
"role": "system",
"content": botstrings.general_bot_system_prompt
+ f" The object found might be {report.object}.",
},
)
general_bot.memory[2]["content"] = f"Are you still at the place where the {report.object} was found?"
object_string = ''
if report.object is not None:
object_string = f" The object found is {report.object}."
prompt = f"""I'm looking for the information about an object. {object_string}. The information needed is described in this disctionary:
{description_dict}
Formulate a question asking for the information in the dictionary."""
question = general_bot.generate(prompt)["content"]
general_bot.memory = []
return question
else:
return "done"
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 #! We need to finf 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, question: str, looking_for: list, n_try=0) -> dict:
if report.object is None:
prompt = f"""
A user has sent a message:\n'''{user_message}'''
If the user is describing what he/she has found, describe the object with a single word or a very short phrase. Else answer "null".\
"""
messages = [
{
"role": "user",
"content": prompt.replace(user_message, "I think it's a bomb"),
},
{"role": "assistant", "content": "a bomb"},
{
"role": "user",
"content": prompt.replace(user_message, "I don'w know"),
},
{"role": "assistant", "content": "null"},
{
"role": "user",
"content": prompt.replace(
user_message, "something that looks weird"
),
},
{"role": "assistant", "content": "a weird thing"},
{"role": "user", "content": prompt},
]
self.memory = messages
result = self.generate(user_message)["content"]
self.memory = []
if result not in ["null", "None", "", "Unknown"]:
print("Object:", result)
report.object = result
info_dict = {}
for info in looking_for:
if getattr(report, info) is None:
info_dict[info] = report.descriptions[info]
# If the bot asks for a description, it should also check for color, size and shape.
if looking_for == ["description"]:
for i in ["color", "size", "shape"]:
info_dict[i] = report.descriptions[i]
prompt = f""""
A user was asked '''{question}'''
This is the answer from the user: '''{user_message}'''\n
This is a dictionary describing what info I want:\n\n{info_dict} \n\n\
Take a look at the message along with the question 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", None]:
setattr(report, key, value)
else: # Don't ask for these again.
if (
key in ["shape", "size", "color", "marked", "blockage"]
and key in looking_for
):
setattr(report, key, "Unknown")
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 = looking_for, question=question, 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
def check_image_answer(self, message: str, report: Report):
prompt = f"""
user has been asked to upload an image and answered with a message:
{message}
What is the meaning of the message? Answer ONLY with one of ["no, I can't", "yes, I can", "I don't know how to send it"]
"""
if "no" in self.generate(prompt)["content"].lower():
answer = "no"
report.image = "Cannot send image"
elif "yes" in self.generate(prompt)["content"].lower():
answer = "yes"
else:
answer = "help"
return answer
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:
question = ''
for i in chatbot.memory:
if i["role"] == "assistant":
question = i["content"]
print('QUESTION:', question)
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, looking_for=report.looking_for, question=question
)
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()