Previous to this changeset we used a custom system for tools/command support for Anthropic. We defined commands by using !command as a signal to execute it Following Anthropic Claude 2.1, there is an official supported syntax (beta) for tools execution. eg: ``` + <function_calls> + <invoke> + <tool_name>image</tool_name> + <parameters> + <prompts> + [ + "an oil painting", + "a cute fluffy orange", + "3 apple's", + "a cat" + ] + </prompts> + </parameters> + </invoke> + </function_calls> ``` This implements the spec per Anthropic, it should be stable enough to also work on other LLMs. Keep in mind that OpenAI is not impacted here at all, as it has its own custom system for function calls. Additionally: - Fixes the title system prompt so it works with latest Anthropic - Uses new spec for "system" messages by Anthropic - Tweak forum helper persona to guide Anthropic a tiny be better Overall results are pretty awesome and Anthropic Claude performs really well now on Discourse
148 lines
4.1 KiB
Ruby
148 lines
4.1 KiB
Ruby
# 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
|
|
|
|
def prompt_limit(allow_commands:)
|
|
# provide a buffer of 120 tokens - our function counting is not
|
|
# 100% accurate and getting numbers to align exactly is very hard
|
|
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
|
|
|
|
if bot_user.id == DiscourseAi::AiBot::EntryPoint::GPT4_ID
|
|
8192 - buffer
|
|
else
|
|
16_384 - buffer
|
|
end
|
|
end
|
|
|
|
def reply_params
|
|
# 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 }
|
|
end
|
|
|
|
def extra_tokens_per_message
|
|
# open ai defines about 4 tokens per message of overhead
|
|
4
|
|
end
|
|
|
|
def submit_prompt(
|
|
prompt,
|
|
prefer_low_cost: false,
|
|
post: nil,
|
|
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 }
|
|
|
|
model = model_for(low_cost: prefer_low_cost)
|
|
|
|
params[:functions] = available_functions if available_functions.present?
|
|
|
|
DiscourseAi::Inference::OpenAiCompletions.perform!(
|
|
prompt,
|
|
model,
|
|
**params,
|
|
post: post,
|
|
&blk
|
|
)
|
|
end
|
|
|
|
def tokenizer
|
|
DiscourseAi::Tokenizer::OpenAiTokenizer
|
|
end
|
|
|
|
def model_for(low_cost: false)
|
|
return "gpt-4" if bot_user.id == DiscourseAi::AiBot::EntryPoint::GPT4_ID && !low_cost
|
|
"gpt-3.5-turbo-16k"
|
|
end
|
|
|
|
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
|
|
|
|
def include_function_instructions_in_system_prompt?
|
|
# open ai uses a bespoke system for function calls
|
|
false
|
|
end
|
|
|
|
private
|
|
|
|
def populate_functions(partial:, reply:, functions:, done:, current_delta:)
|
|
return if !partial
|
|
fn = partial.dig(:choices, 0, :delta, :function_call)
|
|
if fn
|
|
functions.add_function(fn[:name]) if fn[:name].present?
|
|
functions.add_argument_fragment(fn[:arguments]) if !fn[:arguments].nil?
|
|
functions.custom = true
|
|
end
|
|
end
|
|
|
|
def build_message(poster_username, content, function: false, system: false)
|
|
is_bot = poster_username == bot_user.username
|
|
|
|
if function
|
|
role = "function"
|
|
elsif system
|
|
role = "system"
|
|
else
|
|
role = is_bot ? "assistant" : "user"
|
|
end
|
|
|
|
result = { role: role, content: content }
|
|
|
|
if function
|
|
result[:name] = poster_username
|
|
elsif !system && poster_username != bot_user.username && poster_username.present?
|
|
# Open AI restrict name to 64 chars and only A-Za-z._ (work around)
|
|
result[:name] = clean_username(poster_username)
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def get_delta(partial, _context)
|
|
partial.dig(:choices, 0, :delta, :content).to_s
|
|
end
|
|
|
|
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
|
|
end
|
|
end
|
|
end
|