Skip to content

Instantly share code, notes, and snippets.

@drusepth
Created November 4, 2023 02:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drusepth/23fb43ca5a325853a6abef5bfebaeed6 to your computer and use it in GitHub Desktop.
Save drusepth/23fb43ca5a325853a6abef5bfebaeed6 to your computer and use it in GitHub Desktop.
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