Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement AI assistant #1649

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=your_api_key_here
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,5 @@ documentation.json
/log
/buck-out
/result

.vscode/
1 change: 1 addition & 0 deletions assistant/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
venv/
185 changes: 185 additions & 0 deletions assistant/assistant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import os
import asyncio
import logging
from dotenv import load_dotenv
from openai import AsyncOpenAI


logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)


class NoHTTPRequestFilter(logging.Filter):
def filter(self, record):
return "HTTP Request:" not in record.getMessage()


for handler in logging.root.handlers:
handler.addFilter(NoHTTPRequestFilter())


load_dotenv()


OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
logging.error(
"The `OPENAI_API_KEY` environment variable is not set. For instructions on how to set it, refer to the README."
)
exit(1)


client = AsyncOpenAI(api_key=OPENAI_API_KEY)


ASSISTANT_NAME = "Cave Echo"


async def retrieve_assistant_by_name(name):
try:
assistants = await client.beta.assistants.list()
for assistant in assistants.data:
if assistant.name == name:
return assistant
return None
except Exception as e:
logging.error(f"Failed to retrieve assistant by name: {e}")
return None


async def create_thread():
try:
thread = await client.beta.threads.create()
return thread
except Exception as e:
logging.error(f"Failed to create thread: {e}")
return None


async def create_message(thread_id, content):
try:
message = await client.beta.threads.messages.create(
thread_id=thread_id, role="user", content=content
)
return message
except Exception as e:
logging.error(f"Failed to create message: {e}")
return None


async def create_a_run(assistant_id, thread_id):
try:
run = await client.beta.threads.runs.create(
assistant_id=assistant_id, thread_id=thread_id
)
return run
except Exception as e:
logging.error(f"Failed to create a run: {e}")
return None


async def bat_animation():
"""Displays a moving bat emoji in the console."""
frames = [
"🦇 ",
" 🦇 ",
" 🦇",
" 🦇 ",
]
while True:
for frame in frames:
print(f"\r{frame}", end="", flush=True)
await asyncio.sleep(0.3) # animation speed


async def get_responses(thread_id, run_id):
animation_task = None
try:
animation_task = asyncio.create_task(bat_animation())

while True:
run = await client.beta.threads.runs.retrieve(
thread_id=thread_id, run_id=run_id
)
if run.status == "completed":
break
await asyncio.sleep(1)

if animation_task:
animation_task.cancel()
try:
await animation_task
except asyncio.CancelledError:
pass # expected exception on task cancellation

print("\r", end="", flush=True) # clear the animation from the console

messages = await client.beta.threads.messages.list(thread_id=thread_id)
if messages.data:
# the first message in the list is the latest response
message = messages.data[0]
if message.role == "assistant" and message.content:
print(f"{ASSISTANT_NAME}: {message.content[0].text.value}")

except Exception as e:
logging.error(f"Failed to get responses: {e}")
if animation_task:
animation_task.cancel()
try:
await animation_task
except asyncio.CancelledError:
pass # again, ignore the expected cancellation error

finally:
# ensure the line is clear of animation after exception or completion
print("\r", end="")


async def delete_thread(client, thread_id):
try:
response = await client.beta.threads.delete(thread_id)
logging.info(f"Thread {thread_id} deleted successfully.")
return response
except Exception as e:
logging.error(f"Failed to delete thread {thread_id}: {e}")
return None


async def main():
assistant = await retrieve_assistant_by_name(ASSISTANT_NAME)
if assistant is None:
logging.info(
f"Assistant {ASSISTANT_NAME} not found. Aborting. For instructions on how to create an assistant, refer to the README."
)
return

logging.info(
f"Entering the cave! Beware of bats. Type 'exit' to see the sunlight again."
)
thread = await create_thread()
if thread is None:
logging.error("Failed to create conversation thread.")
return

try:
while True:
user_input = input("You: ")
if user_input.lower() == "exit":
logging.info("Emerging from the cave, back to the daylight. Goodbye!")
break

await create_message(thread.id, user_input)
run = await create_a_run(assistant.id, thread.id)
if run is None:
logging.error("Failed to create a run.")
return
await get_responses(thread.id, run.id)
finally:
await delete_thread(
client, thread.id
) # ensure the thread is deleted when exiting


if __name__ == "__main__":
asyncio.run(main())
76 changes: 76 additions & 0 deletions assistant/combine_components.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/bash

# This script is meant to be run from `create_or_update_assistant.py`. It
# distributes the highest version files from specified directories into a set
# number of parts.

if [ -z "$1" ]; then
echo "Please specify the number of parts."
exit 1
fi

num_parts=$1
if ! [[ "$num_parts" =~ ^[0-9]+$ ]]; then
echo "The number of parts must be a positive integer."
exit 1
fi

output_file_base="noredinkuicomponentspart"
ignore_list=("AnimatedIcon" "AssignmentIcon" "CharacterIcon" "Logo" "MasteryIcon" "Pennant" "Sprite" "UiIcon")

part=1
dir_count=0
total_dirs=$(find ../src/Nri/Ui/ -maxdepth 1 -type d | wc -l)
interval_dirs=$((total_dirs / num_parts))

output_file="${output_file_base}${part}.md"
# ensure the output file is empty before starting
: > "$output_file"


is_in_ignore_list() {
local folder=$1
for ignore_folder in "${ignore_list[@]}"; do
if [[ "$folder" == *"$ignore_folder"* ]]; then
return 0 # true, folder is in the ignore list
fi
done
return 1 # false, folder is not in the ignore list
}

# concatenate the highest version file from a folder to the output file
concatenate_highest_version() {
local folder=$1
# use find to list files only, then sort and pick the highest version file
highest_version_file=$(find "$folder" -maxdepth 1 -type f | sort -V | tail -n 1)

if [ ! -z "$highest_version_file" ] && [ -f "$highest_version_file" ]; then
# ensure the file is readable before attempting to concatenate
if [ -r "$highest_version_file" ]; then
echo -e "# $highest_version_file\n" >> "$output_file"
cat "$highest_version_file" >> "$output_file" || {
echo "Failed to read file: $highest_version_file"
return 1
}
echo -e "\n---\n" >> "$output_file"
else
echo "Cannot read file: $highest_version_file"
fi
fi
}


for dir in ../src/Nri/Ui/*/ ; do
if [ -d "$dir" ] && ! is_in_ignore_list "$dir"; then
if [ "$dir_count" -ge "$interval_dirs" ] && [ "$part" -lt "$num_parts" ]; then
part=$((part + 1))
output_file="${output_file_base}${part}.md"
: > "$output_file" # clear the new output file
interval_dirs=$((interval_dirs + total_dirs / num_parts))
fi
concatenate_highest_version "$dir"
dir_count=$((dir_count + 1))
fi
done

echo "Completed. Contents of the highest version files are distributed across ${num_parts} files."
Loading
Loading