gotta archive this lmao

This commit is contained in:
ForeverPyrite
2025-08-12 11:50:18 -05:00
parent 72c2126cd1
commit 32ac4622f9
11 changed files with 964 additions and 838 deletions

378
ai_logic.py Normal file → Executable file
View 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}")