gotta archive this lmao
This commit is contained in:
378
ai_logic.py
Normal file → Executable file
378
ai_logic.py
Normal file → Executable file
@@ -1,182 +1,196 @@
|
||||
import logging
|
||||
import os
|
||||
import asyncio
|
||||
import discord # For type hintings
|
||||
from json import loads as load_json
|
||||
|
||||
from openai import OpenAI
|
||||
from openai.types.beta import Assistant, Thread
|
||||
from openai.types.beta.threads import Run, Message, RequiredActionFunctionToolCall
|
||||
from quotes import QUOTES
|
||||
from students import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OPENAI_KEY = os.getenv('OPENAI_KEY')
|
||||
|
||||
# OpenAI & Assistant configuration
|
||||
client: OpenAI = OpenAI(
|
||||
api_key=OPENAI_KEY,
|
||||
project="proj_vUQ7duhKKc1jxIqllRhy6DVJ",
|
||||
)
|
||||
|
||||
mr_jacobs: Assistant = client.beta.assistants.retrieve("asst_KdPdwqNAKijujfyCRrJCOgJN")
|
||||
base_instructions: str = mr_jacobs.instructions
|
||||
logger.info(f"Base instructions: {base_instructions}")
|
||||
instructions: str = base_instructions + f"\n\nHere is a dictionary containing some of your most well known quotes, organized by their frequency. DO NOT USE THE QUOTES FREQUENTLY! JUST UTILIZE THEM TO KNOW THE TYPES OF THINGS YOU SAY! \n{QUOTES}\n\n"
|
||||
|
||||
def get_run_status(run: Run) -> str:
|
||||
"""
|
||||
Retrieves the status of a run.
|
||||
|
||||
Args:
|
||||
run: The run object.
|
||||
|
||||
Returns:
|
||||
str: The status of the run.
|
||||
"""
|
||||
status: str = client.beta.threads.runs.retrieve(
|
||||
run.id,
|
||||
thread_id=run.thread_id
|
||||
).status
|
||||
logger.info(f"Status of run {run.id} is {status}")
|
||||
return status
|
||||
|
||||
def get_instructions(context: discord.abc.GuildChannel | discord.Message = None) -> str:
|
||||
"""
|
||||
Retrieves the instructions for the AI based on the context.
|
||||
|
||||
Args:
|
||||
context (discord.abc.GuildChannel | discord.Message | None): The context from which to derive additional instructions.
|
||||
If it's a GuildChannel, includes channel name and category.
|
||||
If it's a Message, includes channel name and category from the message.
|
||||
|
||||
Returns:
|
||||
str: The instructions for the AI.
|
||||
"""
|
||||
tuned_instructions: str = instructions
|
||||
|
||||
if context:
|
||||
tuned_instructions += "\n\nNow I will give you the context of the channel that the message is sent in, keep it in mind. Please try to remain conversational depending on the topic of the channel, and don't be worshiping Cisco so religiously outside of on-topic. It's okay to joke about it, but you still wanna goof around with the students sometimes. However, if they have an essay, you should definately get on them about it outside of on-topic, so that you can still be helpful when they are talking about things on-topic, but can scold them to do their essay in any other channel. You should take the channel info fairly seriously, please. I really hate having to make such extreme channel descriptions to see a slight change in behavior from the Cisco worshipping. :\n"
|
||||
if isinstance(context, discord.Message):
|
||||
context = context.channel
|
||||
if not isinstance(context, discord.abc.Messageable):
|
||||
logger.critical(f"Literally how are we going to send a message in a channel that is not messageable? (bro how did we get here, wtf was passed as context? oh: {context}")
|
||||
return tuned_instructions
|
||||
if isinstance(context, discord.TextChannel):
|
||||
tuned_instructions += f"\nChannel Category: {context.category.name}\nChannel Name:{context.name}\nChannel Description: {context.topic if isinstance(context, discord.TextChannel) else 'No Description'}"
|
||||
elif isinstance(context, discord.DMChannel):
|
||||
logger.warning(f"DM Channel detected, adding context to instructions.\nDM channel with {ID_TO_NAME.get(context.recipient.id, context.recipient.name)}")
|
||||
tuned_instructions += f"\nThis is a direct message with {STUDENTS.get(context.recipient.id) if context.recipient.id in STUDENTS else context.recipient.name + 'No extra known info on user.'}"
|
||||
|
||||
return tuned_instructions
|
||||
|
||||
async def handle_action(run: Run) -> bool:
|
||||
# Define the list to store tool outputs
|
||||
tool_outputs = []
|
||||
tool: RequiredActionFunctionToolCall
|
||||
# Loop through each tool in the required action section
|
||||
try:
|
||||
logger.debug(f"Run.require_action is currently: {run.required_action} and tool calls are {run.required_action.submit_tool_outputs.tool_calls}")
|
||||
for tool in run.required_action.submit_tool_outputs.tool_calls:
|
||||
logger.info(f"Handling action for tool {tool.id}, function {tool.function.name}")
|
||||
if tool.function.name == "assign_essay":
|
||||
logger.debug(f"Handling action for assign_essay tool. Received arguments {tool.function.arguments}")
|
||||
args = load_json(tool.function.arguments)
|
||||
tool_outputs.append({
|
||||
"tool_call_id": tool.id,
|
||||
"output": assign_essay(int(run.metadata.get("user_id"))) if not tool.function.arguments else assign_essay(int(run.metadata.get("user_id")), args["topic"])
|
||||
})
|
||||
elif tool.function.name == "clear_essay":
|
||||
logger.debug(f"Clearing the essay for student {run.metadata.get('user_id')}")
|
||||
clear_essay(int(run.metadata.get("user_id")))
|
||||
tool_outputs.append({
|
||||
"tool_call_id": tool.id,
|
||||
"output": "Essay cleared sucessfully, feel free to still give the student feedback on their essay because it couldn't have been perfect."
|
||||
})
|
||||
except AttributeError as e:
|
||||
logger.error(f"Failed to handle action: {e}")
|
||||
except Exception as e:
|
||||
logger.critical(f"An unexpected error occurred while handling action: {e}")
|
||||
finally:
|
||||
logger.info(f"Tool outputs: {tool_outputs}")
|
||||
if tool_outputs:
|
||||
try:
|
||||
run = client.beta.threads.runs.submit_tool_outputs_and_poll(
|
||||
thread_id=run.thread_id,
|
||||
run_id=run.id,
|
||||
tool_outputs=tool_outputs
|
||||
)
|
||||
logger.info("Tool outputs submitted successfully.")
|
||||
except Exception as e:
|
||||
logger.error("Failed to submit tool outputs:", e)
|
||||
else:
|
||||
logger.warning("No tool outputs to submit.")
|
||||
|
||||
|
||||
async def handle_run(run: Run) -> bool:
|
||||
while True:
|
||||
run = client.beta.threads.runs.retrieve(
|
||||
run.id,
|
||||
thread_id=run.thread_id
|
||||
)
|
||||
match run.status:
|
||||
case "completed":
|
||||
return True
|
||||
case "failed":
|
||||
logger.error(f"Run {run.id} failed.")
|
||||
return False
|
||||
case "requires_action":
|
||||
logger.info(f"Run {run.id} requires action.")
|
||||
await handle_action(run)
|
||||
case _:
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
async def run(messages: list[dict], instructions: str = instructions, user_id : int = None) -> str:
|
||||
"""
|
||||
Runs the AI with the given messages and instructions.
|
||||
|
||||
Args:
|
||||
messages (list[dict]): The list of messages.
|
||||
instructions (str): The instructions for the AI.
|
||||
user_id (int): The Discord ID of the user who initiated the run.
|
||||
|
||||
Returns:
|
||||
str: The response from the AI.
|
||||
"""
|
||||
logger.debug(f"Running AI assistant with the following parameters:\nmessages={messages}\ninstructions={instructions}\nmetadata={str(user_id if user_id else -1)}")
|
||||
run = client.beta.threads.create_and_run(
|
||||
assistant_id=mr_jacobs.id,
|
||||
instructions=instructions,
|
||||
thread={
|
||||
"messages": messages
|
||||
},
|
||||
metadata={
|
||||
"user_id": str(user_id if user_id else -1)
|
||||
}
|
||||
)
|
||||
response = await run_message(run)
|
||||
return response
|
||||
|
||||
|
||||
async def run_message(run) -> str:
|
||||
"""
|
||||
Retrieves the response message from a run.
|
||||
|
||||
Args:
|
||||
run: The run object.
|
||||
|
||||
Returns:
|
||||
str: The response message.
|
||||
"""
|
||||
logger.debug(f"Retrieving response message for run ID {run.id}")
|
||||
# ew but Python doesn't have a do while and it's less ugly than the same statement twice.
|
||||
await handle_run(run)
|
||||
thread_messages = client.beta.threads.messages.list(run.thread_id)
|
||||
for msg_ob in thread_messages.data:
|
||||
if msg_ob.id == thread_messages.first_id:
|
||||
response = msg_ob.content[0].text.value
|
||||
return response
|
||||
logger.critical(f"Couldn't find the msg that matched with the first message ID:\nThread Messages List:\n{thread_messages}")
|
||||
# ----- Configuration & Setup -----
|
||||
import logging
|
||||
import os
|
||||
import asyncio
|
||||
import discord # For type hintings
|
||||
from json import loads as load_json
|
||||
|
||||
from openai import OpenAI
|
||||
from openai.types.beta import Assistant, Thread
|
||||
from openai.types.beta.threads import Run, Message, RequiredActionFunctionToolCall
|
||||
from quotes import QUOTES
|
||||
from students import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OPENAI_KEY = os.getenv('OPENAI_KEY')
|
||||
|
||||
client: OpenAI = OpenAI(
|
||||
api_key=OPENAI_KEY,
|
||||
project="proj_vUQ7duhKKc1jxIqllRhy6DVJ",
|
||||
)
|
||||
|
||||
mr_jacobs: Assistant = client.beta.assistants.retrieve("asst_KdPdwqNAKijujfyCRrJCOgJN")
|
||||
base_instructions: str = mr_jacobs.instructions
|
||||
logger.info(f"Base instructions: {base_instructions}")
|
||||
instructions: str = base_instructions + f"\n\nHere is a dictionary containing some of your most well known quotes, organized by their frequency. DO NOT USE THE QUOTES FREQUENTLY! JUST UTILIZE THEM TO KNOW THE TYPES OF THINGS YOU SAY! \n{QUOTES}\n\n"
|
||||
|
||||
# ----- Helper Functions -----
|
||||
def get_run_status(run: Run) -> str:
|
||||
"""
|
||||
Retrieves the status of a run.
|
||||
|
||||
Args:
|
||||
run: The run object.
|
||||
|
||||
Returns:
|
||||
str: The status of the run.
|
||||
"""
|
||||
status: str = client.beta.threads.runs.retrieve(
|
||||
run.id,
|
||||
thread_id=run.thread_id
|
||||
).status
|
||||
logger.info(f"Status of run {run.id} is {status}")
|
||||
return status
|
||||
|
||||
def get_instructions(context: discord.abc.GuildChannel | discord.Message = None) -> str:
|
||||
"""
|
||||
Retrieves the instructions for the AI based on the context.
|
||||
|
||||
Args:
|
||||
context (discord.abc.GuildChannel | discord.Message | None): The context from which to derive additional instructions.
|
||||
If it's a GuildChannel, includes channel name and category.
|
||||
If it's a Message, includes channel name and category from the message.
|
||||
|
||||
Returns:
|
||||
str: The instructions for the AI.
|
||||
"""
|
||||
tuned_instructions: str = instructions
|
||||
|
||||
if context:
|
||||
tuned_instructions += "\n\nNow I will give you the context of the channel that the message is sent in, keep it in mind. Please try to remain conversational depending on the topic of the channel, and don't be worshiping Cisco so religiously outside of on-topic. It's okay to joke about it, but you still wanna goof around with the students sometimes. However, if they have an essay, you should definately get on them about it outside of on-topic, so that you can still be helpful when they are talking about things on-topic, but can scold them to do their essay in any other channel. You should take the channel info fairly seriously, please. I really hate having to make such extreme channel descriptions to see a slight change in behavior from the Cisco worshipping. :\n"
|
||||
if isinstance(context, discord.Message):
|
||||
context = context.channel
|
||||
if not isinstance(context, discord.abc.Messageable):
|
||||
logger.critical(f"Literally how are we going to send a message in a channel that is not messageable? (bro how did we get here, wtf was passed as context? oh: {context}")
|
||||
return tuned_instructions
|
||||
if isinstance(context, discord.TextChannel):
|
||||
tuned_instructions += f"\nChannel Category: {context.category.name}\nChannel Name:{context.name}\nChannel Description: {context.topic if isinstance(context, discord.TextChannel) else 'No Description'}"
|
||||
elif isinstance(context, discord.DMChannel):
|
||||
logger.warning(f"DM Channel detected, adding context to instructions.\nDM channel with {ID_TO_NAME.get(context.recipient.id, context.recipient.name)}")
|
||||
tuned_instructions += f"\nThis is a direct message with {STUDENTS.get(context.recipient.id) if context.recipient.id in STUDENTS else context.recipient.name + 'No extra known info on user.'}"
|
||||
|
||||
return tuned_instructions
|
||||
|
||||
# ----- Action Handlers -----
|
||||
async def handle_action(run: Run) -> bool:
|
||||
# Define the list to store tool outputs
|
||||
tool_outputs = []
|
||||
tool: RequiredActionFunctionToolCall
|
||||
# Loop through each tool in the required action section
|
||||
try:
|
||||
logger.debug(f"Run.require_action is currently: {run.required_action} and tool calls are {run.required_action.submit_tool_outputs.tool_calls}")
|
||||
for tool in run.required_action.submit_tool_outputs.tool_calls:
|
||||
logger.info(f"Handling action for tool {tool.id}, function {tool.function.name}")
|
||||
if tool.function.name == "assign_essay":
|
||||
logger.debug(f"Handling action for assign_essay tool. Received arguments {tool.function.arguments}")
|
||||
args = load_json(tool.function.arguments)
|
||||
tool_outputs.append({
|
||||
"tool_call_id": tool.id,
|
||||
"output": assign_essay(int(run.metadata.get("user_id"))) if not tool.function.arguments else assign_essay(int(run.metadata.get("user_id")), args["topic"])
|
||||
})
|
||||
elif tool.function.name == "clear_essay":
|
||||
logger.debug(f"Clearing the essay for student {run.metadata.get('user_id')}")
|
||||
clear_essay(int(run.metadata.get("user_id")))
|
||||
tool_outputs.append({
|
||||
"tool_call_id": tool.id,
|
||||
"output": "Essay cleared sucessfully, feel free to still give the student feedback on their essay because it couldn't have been perfect."
|
||||
})
|
||||
except AttributeError as e:
|
||||
logger.error(f"Failed to handle action: {e}")
|
||||
except Exception as e:
|
||||
logger.critical(f"An unexpected error occurred while handling action: {e}")
|
||||
finally:
|
||||
logger.info(f"Tool outputs: {tool_outputs}")
|
||||
if tool_outputs:
|
||||
try:
|
||||
run = client.beta.threads.runs.submit_tool_outputs_and_poll(
|
||||
thread_id=run.thread_id,
|
||||
run_id=run.id,
|
||||
tool_outputs=tool_outputs
|
||||
)
|
||||
logger.info("Tool outputs submitted successfully.")
|
||||
except Exception as e:
|
||||
logger.error("Failed to submit tool outputs:", e)
|
||||
else:
|
||||
logger.warning("No tool outputs to submit.")
|
||||
|
||||
|
||||
# ----- Run Handlers -----
|
||||
async def handle_run(run: Run) -> bool:
|
||||
"""Manages a run and all it's different possible states.
|
||||
|
||||
Args:
|
||||
run (Run): The run to maintain.
|
||||
|
||||
Returns:
|
||||
bool: Whether or not the run completed successfully.
|
||||
"""
|
||||
while True:
|
||||
run = client.beta.threads.runs.retrieve(
|
||||
run.id,
|
||||
thread_id=run.thread_id
|
||||
)
|
||||
match run.status:
|
||||
case "completed":
|
||||
return True
|
||||
case "failed":
|
||||
logger.error(f"Run {run.id} failed.")
|
||||
return False
|
||||
case "requires_action":
|
||||
logger.info(f"Run {run.id} requires action.")
|
||||
await handle_action(run)
|
||||
case "expired":
|
||||
logger.error(f"Run {run.id} expired.")
|
||||
return False
|
||||
case _:
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
async def run(messages: list[dict], instructions: str = instructions, user_id : int = None) -> str:
|
||||
"""
|
||||
Runs the AI with the given messages and instructions.
|
||||
|
||||
Args:
|
||||
messages (list[dict]): The list of messages.
|
||||
instructions (str): The instructions for the AI.
|
||||
user_id (int): The Discord ID of the user who initiated the run.
|
||||
|
||||
Returns:
|
||||
str: The response from the AI.
|
||||
"""
|
||||
logger.debug(f"Running AI assistant with the following parameters:\nmessages={messages}\ninstructions={instructions}\nmetadata={str(user_id if user_id else -1)}")
|
||||
run = client.beta.threads.create_and_run(
|
||||
assistant_id=mr_jacobs.id,
|
||||
instructions=instructions,
|
||||
thread={
|
||||
"messages": messages
|
||||
},
|
||||
metadata={
|
||||
"user_id": str(user_id if user_id else -1)
|
||||
}
|
||||
)
|
||||
response = await run_message(run)
|
||||
return response
|
||||
|
||||
|
||||
async def run_message(run) -> str:
|
||||
"""
|
||||
Retrieves the response message from a run.
|
||||
|
||||
Args:
|
||||
run: The run object.
|
||||
|
||||
Returns:
|
||||
str: The response message.
|
||||
"""
|
||||
logger.debug(f"Retrieving response message for run ID {run.id}")
|
||||
# ew but Python doesn't have a do while and it's less ugly than the same statement twice.
|
||||
await handle_run(run)
|
||||
thread_messages = client.beta.threads.messages.list(run.thread_id)
|
||||
for msg_ob in thread_messages.data:
|
||||
if msg_ob.id == thread_messages.first_id:
|
||||
response = msg_ob.content[0].text.value
|
||||
return response
|
||||
logger.critical(f"Couldn't find the msg that matched with the first message ID:\nThread Messages List:\n{thread_messages}")
|
||||
|
||||
Reference in New Issue
Block a user