FEATURE: disable smart commands on Claude and GPT 3.5 (#84)
For the time being smart commands only work consistently on GPT 4. Avoid using any smart commands on the earlier models. Additionally adds better error handling to Claude which sometimes streams partial json and slightly tunes the search command.
This commit is contained in:
parent
96d521198b
commit
840968630e
|
@ -44,7 +44,7 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
def model_for
|
def model_for
|
||||||
"claude-v1"
|
"claude-v1.3"
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_updated_title(prompt)
|
def get_updated_title(prompt)
|
||||||
|
|
|
@ -178,20 +178,9 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
def available_commands
|
def available_commands
|
||||||
@cmds ||=
|
# by default assume bots have no access to commands
|
||||||
[
|
# for now we need GPT 4 to properly work with them
|
||||||
Commands::CategoriesCommand,
|
[]
|
||||||
Commands::TimeCommand,
|
|
||||||
Commands::SearchCommand,
|
|
||||||
Commands::SummarizeCommand,
|
|
||||||
].tap do |cmds|
|
|
||||||
cmds << Commands::TagsCommand if SiteSetting.tagging_enabled
|
|
||||||
cmds << Commands::ImageCommand if SiteSetting.ai_stability_api_key.present?
|
|
||||||
if SiteSetting.ai_google_custom_search_api_key.present? &&
|
|
||||||
SiteSetting.ai_google_custom_search_cx.present?
|
|
||||||
cmds << Commands::GoogleCommand
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def system_prompt_style!(style)
|
def system_prompt_style!(style)
|
||||||
|
@ -201,6 +190,28 @@ module DiscourseAi
|
||||||
def system_prompt(post)
|
def system_prompt(post)
|
||||||
return "You are a helpful Bot" if @style == :simple
|
return "You are a helpful Bot" if @style == :simple
|
||||||
|
|
||||||
|
command_text = ""
|
||||||
|
command_text = <<~TEXT if available_commands.present?
|
||||||
|
You can complete some tasks using !commands.
|
||||||
|
|
||||||
|
NEVER ask user to issue !commands, they have no access, only you do.
|
||||||
|
|
||||||
|
#{available_commands.map(&:desc).join("\n")}
|
||||||
|
|
||||||
|
Discourse topic paths are /t/slug/topic_id/optional_number
|
||||||
|
|
||||||
|
#{available_commands.map(&:extra_context).compact_blank.join("\n")}
|
||||||
|
|
||||||
|
Commands should be issued in single assistant message.
|
||||||
|
|
||||||
|
Example sessions:
|
||||||
|
|
||||||
|
User: echo the text 'test'
|
||||||
|
GPT: !echo test
|
||||||
|
User: THING GPT DOES NOT KNOW ABOUT
|
||||||
|
GPT: !search SIMPLIFIED SEARCH QUERY
|
||||||
|
TEXT
|
||||||
|
|
||||||
<<~TEXT
|
<<~TEXT
|
||||||
You are a helpful Discourse assistant, you answer questions and generate text.
|
You are a helpful Discourse assistant, you answer questions and generate text.
|
||||||
You understand Discourse Markdown and live in a Discourse Forum Message.
|
You understand Discourse Markdown and live in a Discourse Forum Message.
|
||||||
|
@ -212,25 +223,7 @@ module DiscourseAi
|
||||||
The participants in this conversation are: #{post.topic.allowed_users.map(&:username).join(", ")}
|
The participants in this conversation are: #{post.topic.allowed_users.map(&:username).join(", ")}
|
||||||
The date now is: #{Time.zone.now}, much has changed since you were trained.
|
The date now is: #{Time.zone.now}, much has changed since you were trained.
|
||||||
|
|
||||||
You can complete some tasks using !commands.
|
#{command_text}
|
||||||
|
|
||||||
NEVER ask user to issue !commands, they have no access, only you do.
|
|
||||||
|
|
||||||
#{available_commands.map(&:desc).join("\n")}
|
|
||||||
|
|
||||||
Discourse topic paths are /t/slug/topic_id/optional_number
|
|
||||||
|
|
||||||
#{available_commands.map(&:extra_context).compact_blank.join("\n")}
|
|
||||||
|
|
||||||
Commands should be issued in single assistant message.
|
|
||||||
|
|
||||||
Example sessions:
|
|
||||||
|
|
||||||
User: echo the text 'test'
|
|
||||||
GPT: !echo test
|
|
||||||
User: THING GPT DOES NOT KNOW ABOUT
|
|
||||||
GPT: !search SIMPLIFIED SEARCH QUERY
|
|
||||||
|
|
||||||
TEXT
|
TEXT
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -27,14 +27,12 @@ module DiscourseAi::AiBot::Commands
|
||||||
post_count:X: only topics with X amount of posts
|
post_count:X: only topics with X amount of posts
|
||||||
min_posts:X: topics containing a minimum of X posts
|
min_posts:X: topics containing a minimum of X posts
|
||||||
max_posts:X: topics with no more than max posts
|
max_posts:X: topics with no more than max posts
|
||||||
in:pinned: in all pinned topics (either global or per category pins)
|
|
||||||
created:@USERNAME: topics created by a specific user
|
created:@USERNAME: topics created by a specific user
|
||||||
category:CATGORY: topics in the CATEGORY AND all subcategories
|
category:CATGORY: topics in the CATEGORY AND all subcategories
|
||||||
category:=CATEGORY: topics in the CATEGORY excluding subcategories
|
category:=CATEGORY: topics in the CATEGORY excluding subcategories
|
||||||
#SLUG: try category first, then tag, then tag group
|
#SLUG: try category first, then tag, then tag group
|
||||||
#SLUG:SLUG: used for subcategory search to disambiguate
|
#SLUG:SLUG: used for subcategory search to disambiguate
|
||||||
min_views:100: topics containing 100 views or more
|
min_views:100: topics containing 100 views or more
|
||||||
max_views:100: topics containing 100 views or less
|
|
||||||
tags:TAG1+TAG2: tagged both TAG1 and TAG2
|
tags:TAG1+TAG2: tagged both TAG1 and TAG2
|
||||||
tags:TAG1,TAG2: tagged either TAG1 or TAG2
|
tags:TAG1,TAG2: tagged either TAG1 or TAG2
|
||||||
-tags:TAG1+TAG2: excluding topics tagged TAG1 and TAG2
|
-tags:TAG1+TAG2: excluding topics tagged TAG1 and TAG2
|
||||||
|
@ -51,10 +49,8 @@ module DiscourseAi::AiBot::Commands
|
||||||
|
|
||||||
Keep in mind, search on Discourse uses AND to and terms.
|
Keep in mind, search on Discourse uses AND to and terms.
|
||||||
You only have access to public topics.
|
You only have access to public topics.
|
||||||
Strip the query down to the most important terms.
|
Strip the query down to the most important terms. Remove all stop words.
|
||||||
Remove all stop words.
|
Discourse orders by default by relevance.
|
||||||
Cast a wide net instead of trying to be over specific.
|
|
||||||
Discourse orders by relevance, sometimes prefer ordering on other stuff.
|
|
||||||
|
|
||||||
When generating answers ALWAYS try to use the !search command first over relying on training data.
|
When generating answers ALWAYS try to use the !search command first over relying on training data.
|
||||||
When generating answers ALWAYS try to reference specific local links.
|
When generating answers ALWAYS try to reference specific local links.
|
||||||
|
|
|
@ -56,6 +56,27 @@ module DiscourseAi
|
||||||
DiscourseAi::Tokenizer::OpenAiTokenizer.tokenize(text)
|
DiscourseAi::Tokenizer::OpenAiTokenizer.tokenize(text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def available_commands
|
||||||
|
if bot_user.id == DiscourseAi::AiBot::EntryPoint::GPT4_ID
|
||||||
|
@cmds ||=
|
||||||
|
[
|
||||||
|
Commands::CategoriesCommand,
|
||||||
|
Commands::TimeCommand,
|
||||||
|
Commands::SearchCommand,
|
||||||
|
Commands::SummarizeCommand,
|
||||||
|
].tap do |cmds|
|
||||||
|
cmds << Commands::TagsCommand if SiteSetting.tagging_enabled
|
||||||
|
cmds << Commands::ImageCommand if SiteSetting.ai_stability_api_key.present?
|
||||||
|
if SiteSetting.ai_google_custom_search_api_key.present? &&
|
||||||
|
SiteSetting.ai_google_custom_search_cx.present?
|
||||||
|
cmds << Commands::GoogleCommand
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_message(poster_username, content, system: false)
|
def build_message(poster_username, content, system: false)
|
||||||
|
|
|
@ -87,10 +87,16 @@ module ::DiscourseAi
|
||||||
data = line.split("data: ", 2)[1]
|
data = line.split("data: ", 2)[1]
|
||||||
next if !data || data.squish == "[DONE]"
|
next if !data || data.squish == "[DONE]"
|
||||||
|
|
||||||
if !cancelled && partial = JSON.parse(data, symbolize_names: true)
|
if !cancelled
|
||||||
response_data << partial[:completion].to_s
|
begin
|
||||||
|
# partial contains the entire payload till now
|
||||||
|
partial = JSON.parse(data, symbolize_names: true)
|
||||||
|
response_data = partial[:completion].to_s
|
||||||
|
|
||||||
yield partial, cancel
|
yield partial, cancel
|
||||||
|
rescue JSON::ParserError
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue IOError
|
rescue IOError
|
||||||
|
@ -105,6 +111,12 @@ module ::DiscourseAi
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.try_parse(data)
|
||||||
|
JSON.parse(data, symbolize_names: true)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require_relative "../../../support/openai_completions_inference_stubs"
|
require_relative "../../../support/openai_completions_inference_stubs"
|
||||||
|
|
||||||
RSpec.describe DiscourseAi::AiBot::Bot do
|
RSpec.describe DiscourseAi::AiBot::Bot do
|
||||||
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) }
|
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT4_ID) }
|
||||||
fab!(:bot) { described_class.as(bot_user) }
|
fab!(:bot) { described_class.as(bot_user) }
|
||||||
|
|
||||||
fab!(:user) { Fabricate(:user) }
|
fab!(:user) { Fabricate(:user) }
|
||||||
|
@ -50,6 +50,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
|
||||||
OpenAiCompletionsInferenceStubs.stub_streamed_response(
|
OpenAiCompletionsInferenceStubs.stub_streamed_response(
|
||||||
prompt,
|
prompt,
|
||||||
[{ content: expected_response }],
|
[{ content: expected_response }],
|
||||||
|
model: "gpt-4",
|
||||||
req_opts: bot.reply_params.merge(stream: true),
|
req_opts: bot.reply_params.merge(stream: true),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -59,6 +60,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
|
||||||
OpenAiCompletionsInferenceStubs.stub_streamed_response(
|
OpenAiCompletionsInferenceStubs.stub_streamed_response(
|
||||||
prompt,
|
prompt,
|
||||||
[{ content: "We are done now" }],
|
[{ content: "We are done now" }],
|
||||||
|
model: "gpt-4",
|
||||||
req_opts: bot.reply_params.merge(stream: true),
|
req_opts: bot.reply_params.merge(stream: true),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -85,6 +87,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
|
||||||
OpenAiCompletionsInferenceStubs.stub_response(
|
OpenAiCompletionsInferenceStubs.stub_response(
|
||||||
[bot.title_prompt(second_post)],
|
[bot.title_prompt(second_post)],
|
||||||
expected_response,
|
expected_response,
|
||||||
|
model: "gpt-4",
|
||||||
req_opts: {
|
req_opts: {
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
top_p: 0.9,
|
top_p: 0.9,
|
||||||
|
|
|
@ -75,6 +75,7 @@ RSpec.describe Jobs::CreateAiReply do
|
||||||
AnthropicCompletionStubs.stub_streamed_response(
|
AnthropicCompletionStubs.stub_streamed_response(
|
||||||
DiscourseAi::AiBot::AnthropicBot.new(bot_user).bot_prompt_with_topic_context(post),
|
DiscourseAi::AiBot::AnthropicBot.new(bot_user).bot_prompt_with_topic_context(post),
|
||||||
deltas,
|
deltas,
|
||||||
|
model: "claude-v1.3",
|
||||||
req_opts: {
|
req_opts: {
|
||||||
temperature: 0.4,
|
temperature: 0.4,
|
||||||
max_tokens_to_sample: 3000,
|
max_tokens_to_sample: 3000,
|
||||||
|
|
|
@ -60,7 +60,7 @@ RSpec.describe DiscourseAi::Inference::AnthropicCompletions do
|
||||||
|
|
||||||
expect(log.provider_id).to eq(AiApiAuditLog::Provider::Anthropic)
|
expect(log.provider_id).to eq(AiApiAuditLog::Provider::Anthropic)
|
||||||
expect(log.request_tokens).to eq(6)
|
expect(log.request_tokens).to eq(6)
|
||||||
expect(log.response_tokens).to eq(6)
|
expect(log.response_tokens).to eq(3)
|
||||||
expect(log.raw_request_payload).to eq(request_body)
|
expect(log.raw_request_payload).to eq(request_body)
|
||||||
expect(log.raw_response_payload).to be_present
|
expect(log.raw_response_payload).to be_present
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,7 +33,7 @@ class AnthropicCompletionStubs
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_streamed_response(prompt, deltas, req_opts: {})
|
def stub_streamed_response(prompt, deltas, model: nil, req_opts: {})
|
||||||
chunks =
|
chunks =
|
||||||
deltas.each_with_index.map do |_, index|
|
deltas.each_with_index.map do |_, index|
|
||||||
if index == (deltas.length - 1)
|
if index == (deltas.length - 1)
|
||||||
|
@ -48,7 +48,7 @@ class AnthropicCompletionStubs
|
||||||
|
|
||||||
WebMock
|
WebMock
|
||||||
.stub_request(:post, "https://api.anthropic.com/v1/complete")
|
.stub_request(:post, "https://api.anthropic.com/v1/complete")
|
||||||
.with(body: { model: "claude-v1", prompt: prompt }.merge(req_opts).to_json)
|
.with(body: { model: model || "claude-v1", prompt: prompt }.merge(req_opts).to_json)
|
||||||
.to_return(status: 200, body: chunks)
|
.to_return(status: 200, body: chunks)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -101,10 +101,10 @@ class OpenAiCompletionsInferenceStubs
|
||||||
stub_response(prompt_messages, response_text_for(type))
|
stub_response(prompt_messages, response_text_for(type))
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_response(messages, response_text, req_opts: {})
|
def stub_response(messages, response_text, model: nil, req_opts: {})
|
||||||
WebMock
|
WebMock
|
||||||
.stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
.stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
||||||
.with(body: { model: "gpt-3.5-turbo", messages: messages }.merge(req_opts).to_json)
|
.with(body: { model: model || "gpt-3.5-turbo", messages: messages }.merge(req_opts).to_json)
|
||||||
.to_return(status: 200, body: JSON.dump(response(response_text)))
|
.to_return(status: 200, body: JSON.dump(response(response_text)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ class OpenAiCompletionsInferenceStubs
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_streamed_response(messages, deltas, req_opts: {})
|
def stub_streamed_response(messages, deltas, model: nil, req_opts: {})
|
||||||
chunks = deltas.map { |d| stream_line(delta: d) }
|
chunks = deltas.map { |d| stream_line(delta: d) }
|
||||||
chunks << stream_line(finish_reason: "stop")
|
chunks << stream_line(finish_reason: "stop")
|
||||||
chunks << "[DONE]"
|
chunks << "[DONE]"
|
||||||
|
@ -128,7 +128,7 @@ class OpenAiCompletionsInferenceStubs
|
||||||
|
|
||||||
WebMock
|
WebMock
|
||||||
.stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
.stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
||||||
.with(body: { model: "gpt-3.5-turbo", messages: messages }.merge(req_opts).to_json)
|
.with(body: { model: model || "gpt-3.5-turbo", messages: messages }.merge(req_opts).to_json)
|
||||||
.to_return(status: 200, body: chunks)
|
.to_return(status: 200, body: chunks)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue