FEATURE: search command now support searching in context of user (#610)
This optional feature allows search to be performed in the context of the user that executed it. By default we do not allow this behavior cause it means llm gets access to potentially secure data.
This commit is contained in:
parent
514823daca
commit
61890b667c
|
@ -1,17 +1,44 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
import { Input } from "@ember/component";
|
import { Input } from "@ember/component";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
const AiPersonaCommandOptionEditor = <template>
|
export default class AiPersonaCommandOptionEditor extends Component {
|
||||||
|
get isBoolean() {
|
||||||
|
return this.args.option.type === "boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedValue() {
|
||||||
|
return this.args.option.value.value === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onCheckboxChange(event) {
|
||||||
|
this.args.option.value.value = event.target.checked ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
<div class="control-group ai-persona-command-option-editor">
|
<div class="control-group ai-persona-command-option-editor">
|
||||||
<label>
|
<label>
|
||||||
{{@option.name}}
|
{{@option.name}}
|
||||||
</label>
|
</label>
|
||||||
<div class="">
|
<div class="">
|
||||||
|
{{#if this.isBoolean}}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={{this.selectedValue}}
|
||||||
|
{{on "click" this.onCheckboxChange}}
|
||||||
|
/>
|
||||||
|
{{@option.description}}
|
||||||
|
{{else}}
|
||||||
<Input @value={{@option.value.value}} />
|
<Input @value={{@option.value.value}} />
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
{{#unless this.isBoolean}}
|
||||||
<div class="ai-persona-command-option-editor__instructions">
|
<div class="ai-persona-command-option-editor__instructions">
|
||||||
{{@option.description}}
|
{{@option.description}}
|
||||||
</div>
|
</div>
|
||||||
|
{{/unless}}
|
||||||
</div>
|
</div>
|
||||||
</template>;
|
</template>
|
||||||
|
}
|
||||||
export default AiPersonaCommandOptionEditor;
|
|
||||||
|
|
|
@ -205,6 +205,9 @@ en:
|
||||||
searching: "Searching for: '%{query}'"
|
searching: "Searching for: '%{query}'"
|
||||||
command_options:
|
command_options:
|
||||||
search:
|
search:
|
||||||
|
search_private:
|
||||||
|
name: "Search Private"
|
||||||
|
description: "Include all topics user has access to in search results (by default only public topics are included)"
|
||||||
max_results:
|
max_results:
|
||||||
name: "Maximum number of results"
|
name: "Maximum number of results"
|
||||||
description: "Maximum number of results to include in the search - if empty default rules will be used and count will be scaled depending on model used. Highest value is 100."
|
description: "Maximum number of results to include in the search - if empty default rules will be used and count will be scaled depending on model used. Highest value is 100."
|
||||||
|
|
|
@ -83,7 +83,11 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
def accepted_options
|
def accepted_options
|
||||||
[option(:base_query, type: :string), option(:max_results, type: :integer)]
|
[
|
||||||
|
option(:base_query, type: :string),
|
||||||
|
option(:max_results, type: :integer),
|
||||||
|
option(:search_private, type: :boolean),
|
||||||
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -106,12 +110,18 @@ module DiscourseAi
|
||||||
search_string = "#{search_string} #{options[:base_query]}"
|
search_string = "#{search_string} #{options[:base_query]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
safe_search_string = search_string.to_s
|
||||||
|
guardian = nil
|
||||||
|
|
||||||
|
if options[:search_private] && context[:user]
|
||||||
|
guardian = Guardian.new(context[:user])
|
||||||
|
else
|
||||||
|
guardian = Guardian.new
|
||||||
|
safe_search_string += " status:public"
|
||||||
|
end
|
||||||
|
|
||||||
results =
|
results =
|
||||||
::Search.execute(
|
::Search.execute(safe_search_string, search_type: :full_page, guardian: guardian)
|
||||||
search_string.to_s + " status:public",
|
|
||||||
search_type: :full_page,
|
|
||||||
guardian: Guardian.new(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# let's be frugal with tokens, 50 results is too much and stuff gets cut off
|
# let's be frugal with tokens, 50 results is too much and stuff gets cut off
|
||||||
max_results = calculate_max_results(llm)
|
max_results = calculate_max_results(llm)
|
||||||
|
@ -129,10 +139,10 @@ module DiscourseAi
|
||||||
posts = posts[0..results_limit.to_i - 1]
|
posts = posts[0..results_limit.to_i - 1]
|
||||||
|
|
||||||
if should_try_semantic_search
|
if should_try_semantic_search
|
||||||
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(Guardian.new())
|
semantic_search = DiscourseAi::Embeddings::SemanticSearch.new(guardian)
|
||||||
topic_ids = Set.new(posts.map(&:topic_id))
|
topic_ids = Set.new(posts.map(&:topic_id))
|
||||||
|
|
||||||
search = ::Search.new(search_string, guardian: Guardian.new)
|
search = ::Search.new(search_string, guardian: guardian)
|
||||||
|
|
||||||
results = nil
|
results = nil
|
||||||
begin
|
begin
|
||||||
|
|
|
@ -66,14 +66,20 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
def options
|
def options
|
||||||
self
|
result = HashWithIndifferentAccess.new
|
||||||
.class
|
self.class.accepted_options.each do |option|
|
||||||
.accepted_options
|
|
||||||
.reduce(HashWithIndifferentAccess.new) do |memo, option|
|
|
||||||
val = @persona_options[option.name]
|
val = @persona_options[option.name]
|
||||||
memo[option.name] = val if val
|
if val
|
||||||
memo
|
case option.type
|
||||||
|
when :boolean
|
||||||
|
val = val == "true"
|
||||||
|
when :integer
|
||||||
|
val = val.to_i
|
||||||
end
|
end
|
||||||
|
result[option.name] = val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def chain_next_response?
|
def chain_next_response?
|
||||||
|
|
|
@ -29,13 +29,29 @@ RSpec.describe DiscourseAi::AiBot::Tools::Search do
|
||||||
Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden])
|
Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden])
|
||||||
end
|
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 }
|
before { SiteSetting.ai_bot_enabled = true }
|
||||||
|
|
||||||
describe "#invoke" do
|
describe "#invoke" do
|
||||||
it "can retrieve options from persona correctly" do
|
it "can retrieve options from persona correctly" do
|
||||||
persona_options = { "base_query" => "#funny" }
|
persona_options = {
|
||||||
|
"base_query" => "#funny",
|
||||||
|
"search_private" => "true",
|
||||||
|
"max_results" => "10",
|
||||||
|
}
|
||||||
|
|
||||||
search_post = Fabricate(:post, topic: topic_with_tags)
|
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)
|
_bot_post = Fabricate(:post)
|
||||||
|
|
||||||
|
@ -46,18 +62,28 @@ RSpec.describe DiscourseAi::AiBot::Tools::Search do
|
||||||
bot_user: bot_user,
|
bot_user: bot_user,
|
||||||
llm: llm,
|
llm: llm,
|
||||||
context: {
|
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)
|
results = search.invoke(&progress_blk)
|
||||||
expect(results[:rows].length).to eq(1)
|
expect(results[:rows].length).to eq(1)
|
||||||
|
|
||||||
|
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.tags = []
|
||||||
search_post.topic.save!
|
search_post.topic.save!
|
||||||
|
|
||||||
# no longer has the tag funny
|
# no longer has the tag funny, but secure one does
|
||||||
results = search.invoke(&progress_blk)
|
results = search.invoke(&progress_blk)
|
||||||
expect(results[:rows].length).to eq(0)
|
expect(results[:rows].length).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can handle no results" do
|
it "can handle no results" do
|
||||||
|
|
|
@ -75,6 +75,12 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||||
"description" =>
|
"description" =>
|
||||||
I18n.t("discourse_ai.ai_bot.command_options.search.max_results.description"),
|
I18n.t("discourse_ai.ai_bot.command_options.search.max_results.description"),
|
||||||
},
|
},
|
||||||
|
"search_private" => {
|
||||||
|
"type" => "boolean",
|
||||||
|
"name" => I18n.t("discourse_ai.ai_bot.command_options.search.search_private.name"),
|
||||||
|
"description" =>
|
||||||
|
I18n.t("discourse_ai.ai_bot.command_options.search.search_private.description"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue