mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-07-05 05:52:16 +00:00
* FIX: Llm selector / forced tools / search tool This fixes a few issues: 1. When search was not finding any semantic results we would break the tool 2. Gemin / Anthropic models did not implement forced tools previously despite it being an API option 3. Mechanics around displaying llm selector were not right. If you disabled LLM selector server side persona PM did not work correctly. 4. Disabling native tools for anthropic model moved out of a site setting. This deliberately does not migrate cause this feature is really rare to need now, people who had it set probably did not need it. 5. Updates anthropic model names to latest release * linting * fix a couple of tests I missed * clean up conditional
223 lines
6.9 KiB
Ruby
223 lines
6.9 KiB
Ruby
#frozen_string_literal: true
|
|
|
|
RSpec.describe DiscourseAi::AiBot::Tools::Search do
|
|
before { SearchIndexer.enable }
|
|
after { SearchIndexer.disable }
|
|
|
|
fab!(:llm_model)
|
|
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
|
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
|
let(:progress_blk) { Proc.new {} }
|
|
|
|
fab!(:admin)
|
|
fab!(:parent_category) { Fabricate(:category, name: "animals") }
|
|
fab!(:category) { Fabricate(:category, parent_category: parent_category, name: "amazing-cat") }
|
|
fab!(:tag_funny) { Fabricate(:tag, name: "funny") }
|
|
fab!(:tag_sad) { Fabricate(:tag, name: "sad") }
|
|
fab!(:tag_hidden) { Fabricate(:tag, name: "hidden") }
|
|
fab!(:staff_tag_group) do
|
|
tag_group = Fabricate.build(:tag_group, name: "Staff only", tag_names: ["hidden"])
|
|
|
|
tag_group.permissions = [
|
|
[Group::AUTO_GROUPS[:staff], TagGroupPermission.permission_types[:full]],
|
|
]
|
|
tag_group.save!
|
|
tag_group
|
|
end
|
|
fab!(:topic_with_tags) do
|
|
Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden])
|
|
end
|
|
|
|
fab!(:user)
|
|
fab!(:group)
|
|
fab!(:private_category) do
|
|
c = Fabricate(:category_with_definition)
|
|
c.set_permissions(group => :readonly)
|
|
c.save
|
|
c
|
|
end
|
|
|
|
before { SiteSetting.ai_bot_enabled = true }
|
|
|
|
describe "#invoke" do
|
|
it "can retrieve options from persona correctly" do
|
|
persona_options = {
|
|
"base_query" => "#funny",
|
|
"search_private" => "true",
|
|
"max_results" => "10",
|
|
}
|
|
|
|
search_post = Fabricate(:post, topic: topic_with_tags)
|
|
private_search_post = Fabricate(:post, topic: Fabricate(:topic, category: private_category))
|
|
private_search_post.topic.tags = [tag_funny]
|
|
private_search_post.topic.save!
|
|
|
|
_bot_post = Fabricate(:post)
|
|
|
|
search =
|
|
described_class.new(
|
|
{ order: "latest" },
|
|
persona_options: persona_options,
|
|
bot_user: bot_user,
|
|
llm: llm,
|
|
context: {
|
|
user: user,
|
|
},
|
|
)
|
|
|
|
expect(search.options[:base_query]).to eq("#funny")
|
|
expect(search.options[:search_private]).to eq(true)
|
|
expect(search.options[:max_results]).to eq(10)
|
|
|
|
results = search.invoke(&progress_blk)
|
|
expect(results[:rows].length).to eq(1)
|
|
|
|
expect(search.last_query).to eq("#funny order:latest")
|
|
|
|
GroupUser.create!(group: group, user: user)
|
|
|
|
results = search.invoke(&progress_blk)
|
|
expect(results[:rows].length).to eq(2)
|
|
|
|
search_post.topic.tags = []
|
|
search_post.topic.save!
|
|
|
|
# no longer has the tag funny, but secure one does
|
|
results = search.invoke(&progress_blk)
|
|
expect(results[:rows].length).to eq(1)
|
|
end
|
|
|
|
it "can handle no results" do
|
|
_post1 = Fabricate(:post, topic: topic_with_tags)
|
|
search =
|
|
described_class.new(
|
|
{ search_query: "ABDDCDCEDGDG", order: "fake" },
|
|
bot_user: bot_user,
|
|
llm: llm,
|
|
)
|
|
|
|
results = search.invoke(&progress_blk)
|
|
|
|
expect(results[:args]).to eq({ search_query: "ABDDCDCEDGDG", order: "fake" })
|
|
expect(results[:rows]).to eq([])
|
|
end
|
|
|
|
describe "semantic search" do
|
|
let(:query) { "this is an expanded search" }
|
|
after { DiscourseAi::Embeddings::SemanticSearch.clear_cache_for(query) }
|
|
|
|
it "supports semantic search when enabled" do
|
|
assign_fake_provider_to(:ai_embeddings_semantic_search_hyde_model)
|
|
SiteSetting.ai_embeddings_semantic_search_enabled = true
|
|
SiteSetting.ai_embeddings_discourse_service_api_endpoint = "http://test.com"
|
|
|
|
hyde_embedding = [0.049382, 0.9999]
|
|
EmbeddingsGenerationStubs.discourse_service(
|
|
SiteSetting.ai_embeddings_model,
|
|
query,
|
|
hyde_embedding,
|
|
)
|
|
|
|
post1 = Fabricate(:post, topic: topic_with_tags)
|
|
search =
|
|
described_class.new(
|
|
{ search_query: "hello world, sam", status: "public" },
|
|
llm: llm,
|
|
bot_user: bot_user,
|
|
)
|
|
|
|
DiscourseAi::Embeddings::VectorRepresentations::BgeLargeEn
|
|
.any_instance
|
|
.expects(:asymmetric_topics_similarity_search)
|
|
.returns([post1.topic_id])
|
|
|
|
results =
|
|
DiscourseAi::Completions::Llm.with_prepared_responses(["<ai>#{query}</ai>"]) do
|
|
search.invoke(&progress_blk)
|
|
end
|
|
|
|
expect(results[:args]).to eq({ search_query: "hello world, sam", status: "public" })
|
|
expect(results[:rows].length).to eq(1)
|
|
|
|
# it also works with no query
|
|
search =
|
|
described_class.new(
|
|
{ order: "likes", user: "sam", status: "public", search_query: "a" },
|
|
llm: llm,
|
|
bot_user: bot_user,
|
|
)
|
|
|
|
# results will be expanded by semantic search, but it will find nothing
|
|
results =
|
|
DiscourseAi::Completions::Llm.with_prepared_responses(["<ai>#{query}</ai>"]) do
|
|
search.invoke(&progress_blk)
|
|
end
|
|
|
|
expect(results[:rows].length).to eq(0)
|
|
end
|
|
end
|
|
|
|
it "supports subfolder properly" do
|
|
Discourse.stubs(:base_path).returns("/subfolder")
|
|
|
|
post1 = Fabricate(:post, topic: topic_with_tags)
|
|
|
|
search =
|
|
described_class.new({ limit: 1, user: post1.user.username }, bot_user: bot_user, llm: llm)
|
|
|
|
results = search.invoke(&progress_blk)
|
|
expect(results[:rows].to_s).to include("/subfolder" + post1.url)
|
|
end
|
|
|
|
it "passes on all search params" do
|
|
params =
|
|
described_class.signature[:parameters]
|
|
.map do |param|
|
|
if param[:type] == "integer"
|
|
[param[:name], 1]
|
|
else
|
|
[param[:name], "test"]
|
|
end
|
|
end
|
|
.to_h
|
|
.symbolize_keys
|
|
|
|
search = described_class.new(params, bot_user: bot_user, llm: llm)
|
|
results = search.invoke(&progress_blk)
|
|
|
|
expect(results[:args]).to eq(params)
|
|
end
|
|
|
|
it "returns rich topic information" do
|
|
post1 = Fabricate(:post, like_count: 1, topic: topic_with_tags)
|
|
search = described_class.new({ user: post1.user.username }, bot_user: bot_user, llm: llm)
|
|
post1.topic.update!(views: 100, posts_count: 2, like_count: 10)
|
|
|
|
results = search.invoke(&progress_blk)
|
|
|
|
row = results[:rows].first
|
|
category = row[results[:column_names].index("category")]
|
|
|
|
expect(category).to eq("animals > amazing-cat")
|
|
|
|
tags = row[results[:column_names].index("tags")]
|
|
expect(tags).to eq("funny, sad")
|
|
|
|
likes = row[results[:column_names].index("likes")]
|
|
expect(likes).to eq(1)
|
|
|
|
username = row[results[:column_names].index("username")]
|
|
expect(username).to eq(post1.user.username)
|
|
|
|
likes = row[results[:column_names].index("topic_likes")]
|
|
expect(likes).to eq(10)
|
|
|
|
views = row[results[:column_names].index("topic_views")]
|
|
expect(views).to eq(100)
|
|
|
|
replies = row[results[:column_names].index("topic_replies")]
|
|
expect(replies).to eq(1)
|
|
end
|
|
end
|
|
end
|