Created
November 4, 2023 02:03
-
-
Save drusepth/23fb43ca5a325853a6abef5bfebaeed6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'discordrb' | |
require 'dotenv' | |
require 'openai' # ruby-openai gem, not openai gem | |
require 'optparse' | |
require 'yaml' | |
require 'httparty' | |
def parse_options | |
options = {} | |
OptionParser.new do |opts| | |
opts.banner = "Usage: sage.rb [options]" | |
opts.on('-p', '--persona PERSONA', 'Persona configuration file') do |config| | |
options[:config] = config | |
end | |
end.parse! | |
options | |
end | |
def load_config(options) | |
config_file = options[:config] || 'personas/general.yaml' | |
config = YAML.load_file(config_file) | |
rescue Errno::ENOENT | |
abort("Configuration file #{config_file} not found.") | |
end | |
# Load our API keys from .env and our settings from --config | |
Dotenv.load | |
options = parse_options | |
config = load_config(options) | |
# Plug in OpenAI | |
OpenAI.configure do |config| | |
config.access_token = ENV.fetch('ENV_OPENAI_KEY') | |
org_id = ENV.fetch('ENV_OPENAI_ORG', '') | |
config.organization_id = org_id if org_id.length > 0 | |
end | |
# Manage bot state | |
@created_threads = [] | |
@thread_context = Hash.new("") | |
# This is the config block for using Azure OpenAI instead: | |
# OpenAI.configure do |config| | |
# config.access_token = ENV.fetch("AZURE_OPENAI_API_KEY") | |
# config.uri_base = ENV.fetch("AZURE_OPENAI_URI") | |
# config.api_type = :azure | |
# config.api_version = "2023-03-15-preview" | |
# end | |
def respond_to(prompt, config) | |
@openai ||= OpenAI::Client.new | |
response = @openai.chat( | |
parameters: { | |
model: config['model_name'], | |
messages: [ | |
{ role: "system", content: config['system_message'] }, | |
{ role: "user", content: prompt } | |
], | |
temperature: config['temperature'], | |
} | |
) | |
response.dig("choices", 0, "message", "content") | |
end | |
# I don't think discordrb has API wrappers for Discord's new thread endpoints, so we'll use them manually | |
def create_thread(token, channel_id, message_id, name) | |
response = HTTParty.post( | |
"https://discord.com/api/v10/channels/#{channel_id}/messages/#{message_id}/threads", | |
headers: { | |
"Authorization" => "Bot #{token}", | |
"Content-Type" => "application/json" | |
}, | |
body: { | |
name: name, | |
auto_archive_duration: 60 | |
}.to_json | |
) | |
thread_id = response.parsed_response['id'] # Return the thread ID | |
@created_threads << thread_id | |
thread_id | |
end | |
bot = Discordrb::Bot.new token: config['discord_token'] | |
bot.message do |event| | |
channel_to_respond_to = event.channel | |
# Skip messages from the bot itself to prevent infinite loops | |
next if event.user.id == bot.profile.id | |
created_new_thread = false | |
we_were_mentioned = event.message.mentions.any? { |u| u.id == bot.profile.id } | |
# If we've been mentioned outside of a thread, we should create a thread to respond in, and then do so | |
if we_were_mentioned && !@created_threads.include?(event.channel.id.to_s) && !channel_to_respond_to.pm? | |
thread_id = create_thread(config['discord_token'], event.channel.id, event.message.id, "Sage Advice") | |
thread_channel = bot.channel(thread_id) | |
created_new_thread = true | |
@thread_context[thread_id.to_s] = "" | |
# and we'll also override the channel to respond to to this new thread | |
channel_to_respond_to = thread_channel | |
end | |
# We should respond if ANY of these conditions are true: | |
# 1. We've been mentioned, OR | |
# 2. There's a new message in a thread we've created | |
if we_were_mentioned || @created_threads.include?(event.channel.id.to_s) | |
puts "#{event.user.name}: #{event.message.content}" | |
event.channel.start_typing | |
@thread_context[channel_to_respond_to.id.to_s] += event.message.content + "\n\n\n" | |
context_window = 2000 # characters | |
truncated_context = @thread_context[channel_to_respond_to.id.to_s] | |
truncated_context = truncated_context[-context_window, context_window] if truncated_context.length > context_window | |
response = respond_to(truncated_context, config) | |
# Also add our response to the context, so we know what we've already responded to | |
@thread_context[channel_to_respond_to.id.to_s] += response + "\n\n\n" | |
# puts "---> Responding with:" | |
# puts response | |
# puts "-"*40 | |
response_chunks = Discordrb.split_message(response) | |
response_chunks.each do |chunk| | |
channel_to_respond_to.send_message(chunk) | |
end | |
end | |
end | |
bot.register_application_command(:clearmemory, 'Clears the memory for the current thread/channel') do |cmd| | |
# No subcommands to register, so this block can be empty | |
end | |
# Handling the /clearmemory command | |
bot.application_command(:clearmemory) do |event| | |
# Clear the thread context for the current thread or channel | |
@thread_context[event.channel.id.to_s] = "" | |
# Send a confirmation message to the user | |
event.respond(content: "Memory cleared for this channel/thread.") | |
end | |
bot.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment