Compare commits

...
Author SHA1 Message Date
Sam Saffron d544f2101b more tweaks... 2023-05-31 17:15:32 +10:00
Sam Saffron 2d299900b9 we certainly do not need search through pinned topics for now...
This just causes bots to get confused.
2023-05-29 14:15:12 +10:00
Sam Saffron c7781c57fc Fix all specs
Also ensure triage is more consistent by reducing temp
2023-05-29 14:11:32 +10:00
Sam Saffron db11fe391d FEATURE: use triage command to attempt to ground models on GPT 3.5
Previous to this change our results were ungrounded and simpler models like
GPT 3.5 and claude had a real tough time figuring out how to run commands

This splits responses into 2 phases:

Phase 1: figure out if you need to run a command
Phase 2: respond to user

This seems to produce better results on both Claude and GPT 3.5 but still
needs a fair bit of tuning.
2023-05-26 17:07:52 +10:00
24 changed files with 334 additions and 243 deletions
+10 -17
View File
@@ -7,8 +7,8 @@ module DiscourseAi
bot_user.id == DiscourseAi::AiBot::EntryPoint::CLAUDE_V1_ID bot_user.id == DiscourseAi::AiBot::EntryPoint::CLAUDE_V1_ID
end end
def bot_prompt_with_topic_context(post) def bot_prompt_with_topic_context(post, triage: false)
super(post).join("\n\n") super(post, triage: triage).join("\n\n")
end end
def prompt_limit def prompt_limit
@@ -22,25 +22,18 @@ module DiscourseAi
delta = full[context[:pos]..-1] delta = full[context[:pos]..-1]
context[:pos] = full.length context[:pos] = full.length
if !context[:processed]
delta = ""
index = full.index("Assistant: ")
if index
delta = full[index + 11..-1]
context[:processed] = true
end
end
delta delta
end end
private private
def build_message(poster_username, content, system: false) def build_message(poster_username, content, system: false, last: false)
role = poster_username == bot_user.username ? "Assistant" : "Human" role = poster_username == bot_user.username ? "Assistant" : "Human"
"#{role}: #{content}" result = +"#{role}: #{content}"
result << "\nAssistant: " if last
result
end end
def model_for def model_for
@@ -56,12 +49,12 @@ module DiscourseAi
).dig(:completion) ).dig(:completion)
end end
def submit_prompt(prompt, prefer_low_cost: false, &blk) def submit_prompt(prompt, max_tokens: nil, temperature: nil, prefer_low_cost: false, &blk)
DiscourseAi::Inference::AnthropicCompletions.perform!( DiscourseAi::Inference::AnthropicCompletions.perform!(
prompt, prompt,
model_for, model_for,
temperature: 0.4, temperature: temperature || 0.4,
max_tokens: 3000, max_tokens: max_tokens || 3000,
&blk &blk
) )
end end
+168 -102
View File
File diff suppressed because it is too large Load Diff
@@ -13,14 +13,14 @@ module DiscourseAi::AiBot::Commands
end end
def result_name def result_name
"results" "Category list is"
end end
def description_args def description_args
{ count: @last_count || 0 } { count: @last_count || 0 }
end end
def process(_args) def process
columns = { columns = {
name: "Name", name: "Name",
slug: "Slug", slug: "Slug",
+10 -43
View File
@@ -9,7 +9,7 @@ module DiscourseAi
raise NotImplemented raise NotImplemented
end end
def invoked?(cmd_name) def should_invoke?(cmd_name)
cmd_name == name cmd_name == name
end end
@@ -22,25 +22,18 @@ module DiscourseAi
end end
end end
attr_reader :bot_user, :args attr_reader :bot_user, :args, :post
def initialize(bot_user, args) def initialize(bot_user, post, args)
@bot_user = bot_user @bot_user = bot_user
@args = args @args = args
@post = post
end end
def bot def bot
@bot ||= DiscourseAi::AiBot::Bot.as(bot_user) @bot ||= DiscourseAi::AiBot::Bot.as(bot_user)
end end
def standalone?
false
end
def low_cost?
false
end
def result_name def result_name
raise NotImplemented raise NotImplemented
end end
@@ -49,7 +42,7 @@ module DiscourseAi
raise NotImplemented raise NotImplemented
end end
def process(post) def process
raise NotImplemented raise NotImplemented
end end
@@ -57,45 +50,23 @@ module DiscourseAi
{} {}
end end
def custom_raw
end
def chain_next_response def chain_next_response
true true
end end
def invoke_and_attach_result_to(post) def pre_raw_details
post.post_custom_prompt ||= post.build_post_custom_prompt(custom_prompt: []) I18n.t("discourse_ai.ai_bot.command_summary.#{self.class.name}")
prompt = post.post_custom_prompt.custom_prompt || [] end
prompt << ["!#{self.class.name} #{args}", bot_user.username] def post_raw_details
prompt << [process(args), result_name] +<<~HTML
post.post_custom_prompt.update!(custom_prompt: prompt)
raw = +<<~HTML
<details> <details>
<summary>#{I18n.t("discourse_ai.ai_bot.command_summary.#{self.class.name}")}</summary> <summary>#{I18n.t("discourse_ai.ai_bot.command_summary.#{self.class.name}")}</summary>
<p> <p>
#{I18n.t("discourse_ai.ai_bot.command_description.#{self.class.name}", self.description_args)} #{I18n.t("discourse_ai.ai_bot.command_description.#{self.class.name}", self.description_args)}
</p> </p>
</details> </details>
HTML HTML
raw << custom_raw if custom_raw.present?
replacement = "!#{self.class.name} #{args}"
raw = post.raw.sub(replacement, raw) if post.raw.include?(replacement)
if chain_next_response
post.raw = raw
post.save!(validate: false)
else
post.revise(bot_user, { raw: raw }, skip_validations: true, skip_revision: true)
end
chain_next_response
end end
def format_results(rows, column_names = nil) def format_results(rows, column_names = nil)
@@ -132,10 +103,6 @@ module DiscourseAi
formatted formatted
end end
protected
attr_reader :bot_user, :args
end end
end end
end end
@@ -13,7 +13,7 @@ module DiscourseAi::AiBot::Commands
end end
def result_name def result_name
"results" "Google Results"
end end
def description_args def description_args
@@ -24,11 +24,11 @@ module DiscourseAi::AiBot::Commands
} }
end end
def process(search_string) def process
@last_query = search_string @last_query = @args
api_key = SiteSetting.ai_google_custom_search_api_key api_key = SiteSetting.ai_google_custom_search_api_key
cx = SiteSetting.ai_google_custom_search_cx cx = SiteSetting.ai_google_custom_search_cx
query = CGI.escape(search_string) query = CGI.escape(@args)
uri = uri =
URI("https://www.googleapis.com/customsearch/v1?key=#{api_key}&cx=#{cx}&q=#{query}&num=10") URI("https://www.googleapis.com/customsearch/v1?key=#{api_key}&cx=#{cx}&q=#{query}&num=10")
body = Net::HTTP.get(uri) body = Net::HTTP.get(uri)
+11 -8
View File
@@ -8,7 +8,7 @@ module DiscourseAi::AiBot::Commands
end end
def desc def desc
"!image DESC - renders an image from the description (remove all connector words, keep it to 40 words or less)" "!image DESC - renders an image from the description (remove all connector words, keep it to 40 words or less, be creative)"
end end
end end
@@ -17,19 +17,20 @@ module DiscourseAi::AiBot::Commands
end end
def description_args def description_args
{ prompt: @last_prompt || 0 } { prompt: @args || 0 }
end
def custom_raw
@last_custom_raw
end end
def chain_next_response def chain_next_response
false false
end end
def process(prompt) def post_raw_details
@last_prompt = prompt "#{super}\n\n#{@last_custom_raw}"
end
def process
prompt = @args
results = DiscourseAi::Inference::StabilityGenerator.perform!(prompt) results = DiscourseAi::Inference::StabilityGenerator.perform!(prompt)
uploads = [] uploads = []
@@ -47,6 +48,8 @@ module DiscourseAi::AiBot::Commands
uploads uploads
.map { |upload| "![#{prompt.gsub(/\|\'\"/, "")}|512x512, 50%](#{upload.short_url})" } .map { |upload| "![#{prompt.gsub(/\|\'\"/, "")}|512x512, 50%](#{upload.short_url})" }
.join(" ") .join(" ")
nil
end end
end end
end end
@@ -0,0 +1,27 @@
#frozen_string_literal: true
module DiscourseAi::AiBot::Commands
class NoopCommand < Command
class << self
def name
"noop"
end
def desc
"!noop - does nothing, catch all command when you do not know how to triage"
end
end
def post_raw_details
nil
end
def pre_raw_details
nil
end
def process
nil
end
end
end
@@ -22,13 +22,10 @@ module DiscourseAi::AiBot::Commands
status:open: not closed or archived status:open: not closed or archived
status:closed: closed status:closed: closed
status:archived: archived status:archived: archived
status:noreplies: post count is 1
status:single_user: only a single user posted on the topic status:single_user: only a single user posted on the topic
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
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
@@ -46,6 +43,7 @@ module DiscourseAi::AiBot::Commands
order:likes: order by post like count - most liked posts first order:likes: order by post like count - most liked posts first
after:YYYY-MM-DD: only topics created after a specific date after:YYYY-MM-DD: only topics created after a specific date
before:YYYY-MM-DD: only topics created before a specific date before:YYYY-MM-DD: only topics created before a specific date
limit:X: limit the number of results to X
Example: !search @user in:tagged #support order:latest_topic Example: !search @user in:tagged #support order:latest_topic
@@ -53,13 +51,12 @@ module DiscourseAi::AiBot::Commands
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.
Cast a wide net instead of trying to be over specific. Remove connector words.
Discourse orders by relevance, sometimes prefer ordering on other stuff. 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.
Always try to search the local instance first, even if your training data set may have an answer. It may be wrong. Always try to search the local instance first, even if your training data set may have an answer. It may be wrong.
Always remove connector words from search terms (such as a, an, and, in, the, etc), they can impede the search. Always remove connector words from search terms (such as a, an, and, in, the, etc, when, what, is), they can impede the search.
YOUR LOCAL INFORMATION IS OUT OF DATE, YOU ARE TRAINED ON OLD DATA. Always try local search first. YOUR LOCAL INFORMATION IS OUT OF DATE, YOU ARE TRAINED ON OLD DATA. Always try local search first.
TEXT TEXT
@@ -67,7 +64,7 @@ module DiscourseAi::AiBot::Commands
end end
def result_name def result_name
"results" "search results for #{@args}"
end end
def description_args def description_args
@@ -78,8 +75,9 @@ module DiscourseAi::AiBot::Commands
} }
end end
def process(search_string) def process
limit = nil limit = nil
search_string = @args
search_string = search_string =
search_string search_string
@@ -8,7 +8,7 @@ module DiscourseAi::AiBot::Commands
end end
def desc def desc
"!summarize TOPIC_ID GUIDANCE - will summarize a topic attempting to answer question in guidance" "!summarize TOPIC_ID OPTIONAL_GUIDANCE - will summarize a topic attempting to answer question in guidance, if provided"
end end
end end
@@ -28,8 +28,8 @@ module DiscourseAi::AiBot::Commands
{ url: "#{Discourse.base_path}/t/-/#{@last_topic_id}", title: @last_topic_title || "" } { url: "#{Discourse.base_path}/t/-/#{@last_topic_id}", title: @last_topic_title || "" }
end end
def process(instructions) def process
topic_id, guidance = instructions.split(" ", 2) topic_id, guidance = @args.split(" ", 2)
@last_topic_id = topic_id @last_topic_id = topic_id
+6 -2
View File
@@ -20,7 +20,7 @@ module DiscourseAi::AiBot::Commands
{ count: @last_count || 0 } { count: @last_count || 0 }
end end
def process(_args) def process
column_names = { name: "Name", public_topic_count: "Topic Count" } column_names = { name: "Name", public_topic_count: "Topic Count" }
tags = tags =
@@ -32,7 +32,11 @@ module DiscourseAi::AiBot::Commands
@last_count = tags.length @last_count = tags.length
format_results(tags, column_names.values) if @last_count == 0
"This Discourse instance has no tags"
else
format_results(tags, column_names.values)
end
end end
end end
end end
+2 -2
View File
@@ -20,10 +20,10 @@ module DiscourseAi::AiBot::Commands
{ timezone: @last_timezone, time: @last_time } { timezone: @last_timezone, time: @last_time }
end end
def process(timezone) def process
time = time =
begin begin
Time.now.in_time_zone(timezone) Time.now.in_time_zone(@args)
rescue StandardError rescue StandardError
nil nil
end end
+1
View File
@@ -39,6 +39,7 @@ module DiscourseAi
require_relative "commands/summarize_command" require_relative "commands/summarize_command"
require_relative "commands/image_command" require_relative "commands/image_command"
require_relative "commands/google_command" require_relative "commands/google_command"
require_relative "commands/noop_command"
end end
def inject_into(plugin) def inject_into(plugin)
+5 -13
View File
@@ -30,21 +30,13 @@ module DiscourseAi
1500 1500
end end
{ temperature: 0.4, top_p: 0.9, max_tokens: max_tokens } { temperature: 0.4, max_tokens: max_tokens }
end end
def submit_prompt( def submit_prompt(prompt, prefer_low_cost: false, temperature: nil, max_tokens: nil, &blk)
prompt,
prefer_low_cost: false,
temperature: nil,
top_p: nil,
max_tokens: nil,
&blk
)
params = params =
reply_params.merge( reply_params.merge(
temperature: temperature, temperature: temperature,
top_p: top_p,
max_tokens: max_tokens, max_tokens: max_tokens,
) { |key, old_value, new_value| new_value.nil? ? old_value : new_value } ) { |key, old_value, new_value| new_value.nil? ? old_value : new_value }
@@ -56,9 +48,7 @@ module DiscourseAi
DiscourseAi::Tokenizer::OpenAiTokenizer.tokenize(text) DiscourseAi::Tokenizer::OpenAiTokenizer.tokenize(text)
end end
private def build_message(poster_username, content, system: false, last: false)
def build_message(poster_username, content, system: false)
is_bot = poster_username == bot_user.username is_bot = poster_username == bot_user.username
if system if system
@@ -70,6 +60,8 @@ module DiscourseAi
{ role: role, content: is_bot ? content : "#{poster_username}: #{content}" } { role: role, content: is_bot ? content : "#{poster_username}: #{content}" }
end end
private
def model_for def model_for
return "gpt-4" if bot_user.id == DiscourseAi::AiBot::EntryPoint::GPT4_ID return "gpt-4" if bot_user.id == DiscourseAi::AiBot::EntryPoint::GPT4_ID
"gpt-3.5-turbo" "gpt-3.5-turbo"
+12 -4
View File
@@ -87,10 +87,18 @@ 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 # anthropic sends a bunch of partial JSON that can not be parsed
partial =
yield partial, cancel begin
JSON.parse(data, symbolize_names: true)
rescue JSON::ParserError
nil
end
if partial
response_data << partial[:completion].to_s
yield partial, cancel
end
end end
end end
rescue IOError rescue IOError
+12 -6
View File
@@ -13,14 +13,20 @@ RSpec.describe DiscourseAi::AiBot::AnthropicBot do
context = {} context = {}
reply = +"" reply = +""
reply << subject.get_delta({ completion: "\n\nAssist" }, context) full = +"test"
expect(reply).to eq("")
reply << subject.get_delta({ completion: "\n\nAssistant: test" }, context) reply << subject.get_delta({ completion: full }, context)
expect(reply).to eq("test") expect(reply).to eq(full)
reply << subject.get_delta({ completion: "\n\nAssistant: test\nworld" }, context) full << "test2"
expect(reply).to eq("test\nworld")
reply << subject.get_delta({ completion: full }, context)
expect(reply).to eq(full)
full << "test3"
reply << subject.get_delta({ completion: full }, context)
expect(reply).to eq(full)
end end
end end
end end
+12 -11
View File
@@ -25,12 +25,10 @@ RSpec.describe DiscourseAi::AiBot::Bot do
describe "#system_prompt" do describe "#system_prompt" do
it "includes relevant context in system prompt" do it "includes relevant context in system prompt" do
bot.system_prompt_style!(:standard)
SiteSetting.title = "My Forum" SiteSetting.title = "My Forum"
SiteSetting.site_description = "My Forum Description" SiteSetting.site_description = "My Forum Description"
system_prompt = bot.system_prompt(second_post) system_prompt = bot.system_prompt(second_post, triage: false)
expect(system_prompt).to include(SiteSetting.title) expect(system_prompt).to include(SiteSetting.title)
expect(system_prompt).to include(SiteSetting.site_description) expect(system_prompt).to include(SiteSetting.site_description)
@@ -41,20 +39,23 @@ RSpec.describe DiscourseAi::AiBot::Bot do
describe "#reply_to" do describe "#reply_to" do
it "can respond to !search" do it "can respond to !search" do
bot.system_prompt_style!(:simple) expected_response = "!search test search"
expected_response = "ok, searching...\n!search test search" prompt = bot.bot_prompt_with_topic_context(second_post, triage: true)
prompt = bot.bot_prompt_with_topic_context(second_post)
OpenAiCompletionsInferenceStubs.stub_streamed_response( OpenAiCompletionsInferenceStubs.stub_streamed_response(
prompt, prompt,
[{ content: expected_response }], [{ content: expected_response }],
req_opts: bot.reply_params.merge(stream: true), req_opts: bot.triage_params.merge(stream: true),
) )
prompt << { role: "assistant", content: "!search test search" } # second post will contain the search instruction
prompt << { role: "user", content: "results: No results found" } prompt = bot.bot_prompt_with_topic_context(first_post)
command = DiscourseAi::AiBot::Commands::SearchCommand.new(nil, nil, SecureRandom.hex(10))
content, username = bot.context_prompt(second_post, command.process, command.result_name)
prompt << bot.build_message(username, content)
OpenAiCompletionsInferenceStubs.stub_streamed_response( OpenAiCompletionsInferenceStubs.stub_streamed_response(
prompt, prompt,
@@ -69,10 +70,10 @@ RSpec.describe DiscourseAi::AiBot::Bot do
expect(last.raw).to include("<details>") expect(last.raw).to include("<details>")
expect(last.raw).to include("<summary>Search</summary>") expect(last.raw).to include("<summary>Search</summary>")
expect(last.raw).not_to include("translation missing") expect(last.raw).not_to include("translation missing")
expect(last.raw).to include("ok, searching...")
expect(last.raw).to include("We are done now") expect(last.raw).to include("We are done now")
expect(last.post_custom_prompt.custom_prompt.to_s).to include("We are done now") expect(last.post_custom_prompt.custom_prompt.to_s).to include("We are done now")
expect(last.post_custom_prompt.custom_prompt.to_s).not_to include("details")
end end
end end
@@ -7,7 +7,7 @@ RSpec.describe DiscourseAi::AiBot::Commands::CategoriesCommand do
it "can generate correct info" do it "can generate correct info" do
Fabricate(:category, name: "america", posts_year: 999) Fabricate(:category, name: "america", posts_year: 999)
info = DiscourseAi::AiBot::Commands::CategoriesCommand.new(nil, nil).process(nil) info = DiscourseAi::AiBot::Commands::CategoriesCommand.new(nil, nil, nil).process
expect(info).to include("america") expect(info).to include("america")
expect(info).to include("999") expect(info).to include("999")
end end
@@ -4,7 +4,7 @@ require_relative "../../../../support/openai_completions_inference_stubs"
RSpec.describe DiscourseAi::AiBot::Commands::Command do RSpec.describe DiscourseAi::AiBot::Commands::Command do
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) } fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) }
let(:command) { DiscourseAi::AiBot::Commands::Command.new(bot_user, nil) } let(:command) { DiscourseAi::AiBot::Commands::Command.new(bot_user, nil, nil) }
describe "#format_results" do describe "#format_results" do
it "can generate efficient tables of data" do it "can generate efficient tables of data" do
@@ -32,8 +32,8 @@ RSpec.describe DiscourseAi::AiBot::Commands::GoogleCommand do
"https://www.googleapis.com/customsearch/v1?cx=cx&key=abc&num=10&q=some%20search%20term", "https://www.googleapis.com/customsearch/v1?cx=cx&key=abc&num=10&q=some%20search%20term",
).to_return(status: 200, body: json_text, headers: {}) ).to_return(status: 200, body: json_text, headers: {})
google = described_class.new(bot_user, post) google = described_class.new(bot_user, post, "some search term")
info = google.process("some search term") info = google.process
expect(google.description_args[:count]).to eq(1) expect(google.description_args[:count]).to eq(1)
expect(info).to include("title1") expect(info).to include("title1")
@@ -11,9 +11,9 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do
describe "#process" do describe "#process" do
it "can handle no results" do it "can handle no results" do
post1 = Fabricate(:post) post1 = Fabricate(:post)
search = described_class.new(bot_user, post1) search = described_class.new(bot_user, post1, "order:fake ABDDCDCEDGDG")
results = search.process("order:fake ABDDCDCEDGDG") results = search.process
expect(results).to eq("No results found") expect(results).to eq("No results found")
end end
@@ -23,15 +23,15 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do
_post3 = Fabricate(:post, user: post1.user) _post3 = Fabricate(:post, user: post1.user)
# search has no built in support for limit: so handle it from the outside # search has no built in support for limit: so handle it from the outside
search = described_class.new(bot_user, post1) search = described_class.new(bot_user, post1, "@#{post1.user.username} limit:2")
results = search.process("@#{post1.user.username} limit:2") results = search.process
# title + 2 rows # title + 2 rows
expect(results.split("\n").length).to eq(3) expect(results.split("\n").length).to eq(3)
# just searching for everything search = described_class.new(bot_user, post1, "order:latest_topic")
results = search.process("order:latest_topic") results = search.process
expect(results.split("\n").length).to be > 1 expect(results.split("\n").length).to be > 1
end end
end end
@@ -14,8 +14,8 @@ RSpec.describe DiscourseAi::AiBot::Commands::SummarizeCommand do
body: JSON.dump({ choices: [{ message: { content: "summary stuff" } }] }), body: JSON.dump({ choices: [{ message: { content: "summary stuff" } }] }),
) )
summarizer = described_class.new(bot_user, post) summarizer = described_class.new(bot_user, post, "#{post.topic_id} why did it happen?")
info = summarizer.process("#{post.topic_id} why did it happen?") info = summarizer.process
expect(info).to include("Topic summarized") expect(info).to include("Topic summarized")
expect(summarizer.custom_raw).to include("summary stuff") expect(summarizer.custom_raw).to include("summary stuff")
@@ -30,8 +30,8 @@ RSpec.describe DiscourseAi::AiBot::Commands::SummarizeCommand do
topic = Fabricate(:topic, category_id: category.id) topic = Fabricate(:topic, category_id: category.id)
post = Fabricate(:post, topic: topic) post = Fabricate(:post, topic: topic)
summarizer = described_class.new(bot_user, post) summarizer = described_class.new(bot_user, post, "#{post.topic_id} why did it happen?")
info = summarizer.process("#{post.topic_id} why did it happen?") info = summarizer.process
expect(info).not_to include(post.raw) expect(info).not_to include(post.raw)
@@ -10,7 +10,7 @@ RSpec.describe DiscourseAi::AiBot::Commands::TagsCommand do
Fabricate(:tag, name: "america", public_topic_count: 100) Fabricate(:tag, name: "america", public_topic_count: 100)
Fabricate(:tag, name: "not_here", public_topic_count: 0) Fabricate(:tag, name: "not_here", public_topic_count: 0)
info = DiscourseAi::AiBot::Commands::TagsCommand.new(nil, nil).process(nil) info = DiscourseAi::AiBot::Commands::TagsCommand.new(nil, nil, nil).process
expect(info).to include("america") expect(info).to include("america")
expect(info).not_to include("not_here") expect(info).not_to include("not_here")
@@ -23,12 +23,24 @@ RSpec.describe Jobs::CreateAiReply do
# time needs to be frozen so time in prompt does not drift # time needs to be frozen so time in prompt does not drift
freeze_time freeze_time
OpenAiCompletionsInferenceStubs.stub_streamed_response(
DiscourseAi::AiBot::OpenAiBot.new(bot_user).bot_prompt_with_topic_context(
post,
triage: true,
),
[{ content: "!noop" }],
req_opts: {
temperature: 0.1,
max_tokens: 100,
stream: true,
},
)
OpenAiCompletionsInferenceStubs.stub_streamed_response( OpenAiCompletionsInferenceStubs.stub_streamed_response(
DiscourseAi::AiBot::OpenAiBot.new(bot_user).bot_prompt_with_topic_context(post), DiscourseAi::AiBot::OpenAiBot.new(bot_user).bot_prompt_with_topic_context(post),
deltas, deltas,
req_opts: { req_opts: {
temperature: 0.4, temperature: 0.4,
top_p: 0.9,
max_tokens: 1500, max_tokens: 1500,
stream: true, stream: true,
}, },
@@ -66,12 +78,25 @@ RSpec.describe Jobs::CreateAiReply do
end end
context "when chatting with Claude from Anthropic" do context "when chatting with Claude from Anthropic" do
let(:claude_response) { "Assistant: #{expected_response}" } let(:claude_response) { "#{expected_response}" }
let(:deltas) { claude_response.split(" ").map { |w| "#{w} " } } let(:deltas) { claude_response.split(" ").map { |w| "#{w} " } }
before do before do
bot_user = User.find(DiscourseAi::AiBot::EntryPoint::CLAUDE_V1_ID) bot_user = User.find(DiscourseAi::AiBot::EntryPoint::CLAUDE_V1_ID)
AnthropicCompletionStubs.stub_streamed_response(
DiscourseAi::AiBot::AnthropicBot.new(bot_user).bot_prompt_with_topic_context(
post,
triage: true,
),
[{ content: "!noop" }],
req_opts: {
temperature: 0.1,
max_tokens_to_sample: 100,
stream: true,
},
)
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,
+1 -1
View File
@@ -33,7 +33,7 @@ RSpec.describe DiscourseAi::AiBot::OpenAiBot do
it "trims the prompt" do it "trims the prompt" do
prompt_messages = subject.bot_prompt_with_topic_context(post_1) prompt_messages = subject.bot_prompt_with_topic_context(post_1)
expect(prompt_messages[-2][:role]).to eq("assistant") expect(prompt_messages[-2][:role]).to eq("system")
expect(prompt_messages[-1][:role]).to eq("user") expect(prompt_messages[-1][:role]).to eq("user")
# trimming is tricky... it needs to account for system message as # trimming is tricky... it needs to account for system message as
# well... just make sure we trim for now # well... just make sure we trim for now