2023-05-11 09:03:03 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module DiscourseAi
|
|
|
|
module AiBot
|
|
|
|
class OpenAiBot < Bot
|
|
|
|
def self.can_reply_as?(bot_user)
|
|
|
|
open_ai_bot_ids = [
|
|
|
|
DiscourseAi::AiBot::EntryPoint::GPT4_ID,
|
|
|
|
DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID,
|
|
|
|
]
|
|
|
|
|
|
|
|
open_ai_bot_ids.include?(bot_user.id)
|
|
|
|
end
|
|
|
|
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
def prompt_limit(allow_commands:)
|
2023-09-28 01:32:22 -04:00
|
|
|
# provide a buffer of 120 tokens - our function counting is not
|
|
|
|
# 100% accurate and getting numbers to align exactly is very hard
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
buffer = reply_params[:max_tokens] + 50
|
|
|
|
|
|
|
|
if allow_commands
|
|
|
|
# note this is about 100 tokens over, OpenAI have a more optimal representation
|
|
|
|
@function_size ||= tokenize(available_functions.to_json.to_s).length
|
|
|
|
buffer += @function_size
|
|
|
|
end
|
2023-08-21 18:36:41 -04:00
|
|
|
|
2023-05-20 03:45:54 -04:00
|
|
|
if bot_user.id == DiscourseAi::AiBot::EntryPoint::GPT4_ID
|
2023-08-21 18:36:41 -04:00
|
|
|
8192 - buffer
|
2023-05-20 03:45:54 -04:00
|
|
|
else
|
2023-08-21 18:36:41 -04:00
|
|
|
16_384 - buffer
|
2023-05-20 03:45:54 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def reply_params
|
2023-06-22 20:02:04 -04:00
|
|
|
# technically we could allow GPT-3.5 16k more tokens
|
|
|
|
# but lets just keep it here for now
|
|
|
|
{ temperature: 0.4, top_p: 0.9, max_tokens: 2500 }
|
2023-05-11 09:03:03 -04:00
|
|
|
end
|
|
|
|
|
2023-08-21 18:36:41 -04:00
|
|
|
def extra_tokens_per_message
|
|
|
|
# open ai defines about 4 tokens per message of overhead
|
|
|
|
4
|
|
|
|
end
|
|
|
|
|
2023-05-21 22:09:14 -04:00
|
|
|
def submit_prompt(
|
|
|
|
prompt,
|
|
|
|
prefer_low_cost: false,
|
2023-10-31 17:41:31 -04:00
|
|
|
post: nil,
|
2023-05-21 22:09:14 -04:00
|
|
|
temperature: nil,
|
|
|
|
top_p: nil,
|
|
|
|
max_tokens: nil,
|
|
|
|
&blk
|
|
|
|
)
|
|
|
|
params =
|
|
|
|
reply_params.merge(
|
|
|
|
temperature: temperature,
|
|
|
|
top_p: top_p,
|
|
|
|
max_tokens: max_tokens,
|
|
|
|
) { |key, old_value, new_value| new_value.nil? ? old_value : new_value }
|
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
model = model_for(low_cost: prefer_low_cost)
|
|
|
|
|
2023-06-21 03:10:30 -04:00
|
|
|
params[:functions] = available_functions if available_functions.present?
|
|
|
|
|
2023-10-31 17:41:31 -04:00
|
|
|
DiscourseAi::Inference::OpenAiCompletions.perform!(
|
|
|
|
prompt,
|
|
|
|
model,
|
|
|
|
**params,
|
|
|
|
post: post,
|
|
|
|
&blk
|
|
|
|
)
|
2023-05-21 22:09:14 -04:00
|
|
|
end
|
|
|
|
|
2023-08-28 20:43:58 -04:00
|
|
|
def tokenizer
|
|
|
|
DiscourseAi::Tokenizer::OpenAiTokenizer
|
2023-05-21 22:09:14 -04:00
|
|
|
end
|
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
def model_for(low_cost: false)
|
2023-08-16 21:00:11 -04:00
|
|
|
return "gpt-4" if bot_user.id == DiscourseAi::AiBot::EntryPoint::GPT4_ID && !low_cost
|
2023-06-19 18:45:31 -04:00
|
|
|
"gpt-3.5-turbo-16k"
|
2023-05-31 19:10:33 -04:00
|
|
|
end
|
|
|
|
|
2023-06-20 01:44:03 -04:00
|
|
|
def clean_username(username)
|
|
|
|
if username.match?(/\0[a-zA-Z0-9_-]{1,64}\z/)
|
|
|
|
username
|
|
|
|
else
|
|
|
|
# not the best in the world, but this is what we have to work with
|
|
|
|
# if sites enable unicode usernames this can get messy
|
|
|
|
username.gsub(/[^a-zA-Z0-9_-]/, "_")[0..63]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-08-22 17:49:36 -04:00
|
|
|
def include_function_instructions_in_system_prompt?
|
|
|
|
# open ai uses a bespoke system for function calls
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2023-05-11 09:03:03 -04:00
|
|
|
private
|
|
|
|
|
2023-11-23 14:39:56 -05:00
|
|
|
def populate_functions(partial:, reply:, functions:, done:, current_delta:)
|
2023-08-22 17:49:36 -04:00
|
|
|
return if !partial
|
2023-06-19 18:45:31 -04:00
|
|
|
fn = partial.dig(:choices, 0, :delta, :function_call)
|
|
|
|
if fn
|
|
|
|
functions.add_function(fn[:name]) if fn[:name].present?
|
2023-09-14 02:46:56 -04:00
|
|
|
functions.add_argument_fragment(fn[:arguments]) if !fn[:arguments].nil?
|
2023-11-23 14:39:56 -05:00
|
|
|
functions.custom = true
|
2023-06-19 18:45:31 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_message(poster_username, content, function: false, system: false)
|
2023-05-20 03:45:54 -04:00
|
|
|
is_bot = poster_username == bot_user.username
|
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
if function
|
|
|
|
role = "function"
|
|
|
|
elsif system
|
2023-05-20 03:45:54 -04:00
|
|
|
role = "system"
|
|
|
|
else
|
|
|
|
role = is_bot ? "assistant" : "user"
|
|
|
|
end
|
2023-05-11 09:03:03 -04:00
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
result = { role: role, content: content }
|
|
|
|
|
|
|
|
if function
|
|
|
|
result[:name] = poster_username
|
2023-06-20 01:44:03 -04:00
|
|
|
elsif !system && poster_username != bot_user.username && poster_username.present?
|
2023-06-19 18:45:31 -04:00
|
|
|
# Open AI restrict name to 64 chars and only A-Za-z._ (work around)
|
2023-06-20 01:44:03 -04:00
|
|
|
result[:name] = clean_username(poster_username)
|
2023-06-19 18:45:31 -04:00
|
|
|
end
|
2023-05-11 09:03:03 -04:00
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
result
|
2023-05-11 09:03:03 -04:00
|
|
|
end
|
|
|
|
|
2023-05-23 09:08:17 -04:00
|
|
|
def get_delta(partial, _context)
|
|
|
|
partial.dig(:choices, 0, :delta, :content).to_s
|
2023-05-11 09:03:03 -04:00
|
|
|
end
|
|
|
|
|
2023-05-16 13:38:21 -04:00
|
|
|
def get_updated_title(prompt)
|
|
|
|
DiscourseAi::Inference::OpenAiCompletions.perform!(
|
|
|
|
prompt,
|
|
|
|
model_for,
|
|
|
|
temperature: 0.7,
|
|
|
|
top_p: 0.9,
|
|
|
|
max_tokens: 40,
|
|
|
|
).dig(:choices, 0, :message, :content)
|
|
|
|
end
|
2023-05-11 09:03:03 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|