mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-11-13 20:58:45 +00:00
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>
122 lines
4.0 KiB
Ruby
122 lines
4.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe DiscourseAi::Summarization::Models::OpenAi do
|
|
subject(:model) { described_class.new(model_name, max_tokens: max_tokens) }
|
|
|
|
let(:model_name) { "gpt-3.5-turbo" }
|
|
let(:max_tokens) { 720 }
|
|
|
|
let(:content) do
|
|
{
|
|
resource_path: "/t/1/POST_NUMBER",
|
|
content_title: "This is a title",
|
|
contents: [{ poster: "asd", id: 1, text: "This is a text" }],
|
|
}
|
|
end
|
|
|
|
def as_chunk(item)
|
|
{ ids: [item[:id]], summary: "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }
|
|
end
|
|
|
|
def expected_messages(contents, opts)
|
|
base_prompt = <<~TEXT
|
|
You are a summarization bot.
|
|
You effectively summarise any text and reply ONLY with ONLY the summarized text.
|
|
You condense it into a shorter version.
|
|
You understand and generate Discourse forum Markdown.
|
|
You format the response, including links, using markdown.
|
|
Try generating links as well the format is #{opts[:resource_path]}. eg: [ref](#{opts[:resource_path]}/77)
|
|
The discussion title is: #{opts[:content_title]}.
|
|
TEXT
|
|
|
|
messages = [{ role: "system", content: base_prompt }]
|
|
|
|
text =
|
|
contents.reduce("") do |memo, item|
|
|
memo += "(#{item[:id]} #{item[:poster]} said: #{item[:text]} "
|
|
end
|
|
|
|
messages << {
|
|
role: "user",
|
|
content:
|
|
"Summarize the following in 400 words. Keep the summary in the same language used in the text below.\n#{text}",
|
|
}
|
|
end
|
|
|
|
describe "#summarize_in_chunks" do
|
|
context "when the content fits in a single chunk" do
|
|
it "performs a request to summarize" do
|
|
opts = content.except(:contents)
|
|
|
|
OpenAiCompletionsInferenceStubs.stub_response(
|
|
expected_messages(content[:contents], opts),
|
|
"This is summary 1",
|
|
)
|
|
|
|
chunks = content[:contents].map { |c| as_chunk(c) }
|
|
summarized_chunks = model.summarize_in_chunks(chunks, opts).map { |c| c[:summary] }
|
|
|
|
expect(summarized_chunks).to contain_exactly("This is summary 1")
|
|
end
|
|
end
|
|
|
|
context "when the content fits in multiple chunks" do
|
|
it "performs a request for each one to summarize" do
|
|
content[:contents] << {
|
|
poster: "asd2",
|
|
id: 2,
|
|
text: "This is a different text to summarize",
|
|
}
|
|
opts = content.except(:contents)
|
|
|
|
content[:contents].each_with_index do |item, idx|
|
|
OpenAiCompletionsInferenceStubs.stub_response(
|
|
expected_messages([item], opts),
|
|
"This is summary #{idx + 1}",
|
|
)
|
|
end
|
|
|
|
chunks = content[:contents].map { |c| as_chunk(c) }
|
|
summarized_chunks = model.summarize_in_chunks(chunks, opts).map { |c| c[:summary] }
|
|
|
|
expect(summarized_chunks).to contain_exactly("This is summary 1", "This is summary 2")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#concatenate_summaries" do
|
|
it "combines all the different summaries into a single one" do
|
|
messages = [
|
|
{ role: "system", content: "You are a helpful bot" },
|
|
{
|
|
role: "user",
|
|
content:
|
|
"Concatenate these disjoint summaries, creating a cohesive narrative. Keep the summary in the same language used in the text below.\nsummary 1\nsummary 2",
|
|
},
|
|
]
|
|
|
|
OpenAiCompletionsInferenceStubs.stub_response(messages, "concatenated summary")
|
|
|
|
expect(model.concatenate_summaries(["summary 1", "summary 2"])).to eq("concatenated summary")
|
|
end
|
|
end
|
|
|
|
describe "#summarize_with_truncation" do
|
|
let(:max_tokens) { 709 }
|
|
|
|
it "truncates the context to meet the token limit" do
|
|
opts = content.except(:contents)
|
|
|
|
truncated_version = expected_messages(content[:contents], opts)
|
|
|
|
truncated_version.last[
|
|
:content
|
|
] = "Summarize the following in 400 words. Keep the summary in the same language used in the text below.\n(1 asd said: This is a"
|
|
|
|
OpenAiCompletionsInferenceStubs.stub_response(truncated_version, "truncated summary")
|
|
|
|
expect(model.summarize_with_truncation(content[:contents], opts)).to eq("truncated summary")
|
|
end
|
|
end
|
|
end
|