Skip to content

Commit

Permalink
Merge pull request #107 from merefield/confirm_legal_non_post_urls
Browse files Browse the repository at this point in the history
FEATURE: reject LLM responses with hallucinated non-post urls
  • Loading branch information
merefield authored Jun 26, 2024
2 parents 0b4496f + e852a1e commit f05a70a
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 13 deletions.
24 changes: 21 additions & 3 deletions lib/discourse_chatbot/bots/open_ai_bot_rag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def get_response(prompt, opts)
@inner_thoughts = []
@posts_ids_found = []
@topic_ids_found = []
@non_post_urls_found = []

@chat_history += prompt

Expand Down Expand Up @@ -185,7 +186,7 @@ def generate_response(opts)

if (['stop','length'].include?(finish_reason) && tools_calls.nil? || @inner_thoughts.length > 7)
if iteration > 1 && SiteSetting.chatbot_url_integrity_check
if legal_urls?(res["choices"][0]["message"]["content"], @posts_ids_found, @topic_ids_found)
if legal_post_urls?(res["choices"][0]["message"]["content"], @posts_ids_found, @topic_ids_found) && legal_non_post_urls?(res["choices"][0]["message"]["content"], @non_post_urls_found)
return res
else
@inner_thoughts << { role: 'user', content: I18n.t("chatbot.prompt.system.rag.illegal_urls") }
Expand Down Expand Up @@ -248,9 +249,10 @@ def handle_function_call(res, opts)
tool_call_id = function_called["id"]
if func_name == "local_forum_search"
result_hash = call_function(func_name, args_str, opts)
result, post_ids_found, topic_ids_found = result_hash.values_at(:result, :post_ids_found, :topic_ids_found)
result, post_ids_found, topic_ids_found, non_post_urls_found = result_hash.values_at(:result, :post_ids_found, :topic_ids_found, :non_post_urls_found)
@posts_ids_found = (@posts_ids_found.to_set | post_ids_found.to_set).to_a
@topic_ids_found = (@topic_ids_found.to_set | topic_ids_found.to_set).to_a
@non_post_urls_found = (@non_post_urls_found.to_set | non_post_urls_found.to_set).to_a
else
result = call_function(func_name, args_str, opts)
end
Expand Down Expand Up @@ -282,7 +284,7 @@ def call_function(func_name, args_str, opts)
end
end

def legal_urls?(res, post_ids_found, topic_ids_found)
def legal_post_urls?(res, post_ids_found, topic_ids_found)
return true if res.blank?

post_url_regex = ::DiscourseChatbot::POST_URL_REGEX
Expand Down Expand Up @@ -311,6 +313,22 @@ def legal_urls?(res, post_ids_found, topic_ids_found)
true
end

def legal_non_post_urls?(res, non_post_urls_found)
return true if res.blank?
non_post_url_regex = ::DiscourseChatbot::NON_POST_URL_REGEX

urls_in_text = res.scan(non_post_url_regex)

urls_in_text = urls_in_text.reject { |url| url.include?('/t/') }

urls_in_text.each do |url|
if !non_post_urls_found.include?(url)
return false
end
end
true
end

private

def image_url?(string)
Expand Down
17 changes: 15 additions & 2 deletions lib/discourse_chatbot/functions/forum_search_function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def process(args)
top_topic_title_results = []
post_ids_found = []
topic_ids_found = []
non_post_urls_found = []
query = args[parameters[0][:name]]
number_of_posts = args[parameters[1][:name]].blank? ? 3 : args[parameters[1][:name]]
number_of_posts = number_of_posts > SiteSetting.chatbot_forum_search_function_max_results ? SiteSetting.chatbot_forum_search_function_max_results : number_of_posts
Expand Down Expand Up @@ -103,18 +104,21 @@ def process(args)
response += I18n.t("chatbot.prompt.function.forum_search.answer.topic.each.post", post_number: post_number, username: post.user.username, date: post.created_at, raw: post.raw)

topic_ids_in_raw_urls_found, post_ids_in_raw_urls_found = find_post_and_topic_ids_from_raw_urls(post.raw)
non_post_urls_found = non_post_urls_found | find_other_urls(post.raw)

topic_ids_found = topic_ids_found | topic_ids_in_raw_urls_found
post_ids_found = post_ids_found | post_ids_in_raw_urls_found

post_ids_found << post.id
topic_ids_found << post.topic_id
post_number += 1
end
end
else
response = I18n.t("chatbot.prompt.function.forum_search.answer.post.summary", number_of_posts: number_of_posts)
top_results.each_with_index do |result, index|
current_post = ::Post.find(result[:post_id].to_i)

score = result[:score]
url = "https://#{Discourse.current_hostname}/t/slug/#{current_post.topic_id}/#{current_post.post_number}"
raw = current_post.raw
Expand All @@ -127,10 +131,13 @@ def process(args)
topic_ids_found = topic_ids_found | topic_ids_in_raw_urls_found
post_ids_found = post_ids_found | post_ids_in_raw_urls_found

post_ids_found << current_post.id
non_post_urls_found = non_post_urls_found | find_other_urls(raw)

post_ids_found = post_ids_found | [current_post.id]
topic_ids_found = topic_ids_found | [current_post.topic_id]
end
end
{ result: response, topic_ids_found: topic_ids_found, post_ids_found: post_ids_found }
{ result: response, topic_ids_found: topic_ids_found, post_ids_found: post_ids_found, non_post_urls_found: non_post_urls_found}
rescue StandardError => e
Rails.logger.error("Chatbot: Error occurred while attempting to retrieve Forum Search results for query '#{query}': #{e.message}")
{ result: I18n.t("chatbot.prompt.function.forum_search.error", query: args[parameters[0][:name]]), topic_ids_found: [], post_ids_found: [] }
Expand Down Expand Up @@ -159,5 +166,11 @@ def find_post_and_topic_ids_from_raw_urls(raw)

[topic_ids_found, post_ids_found]
end

def find_other_urls(raw)
urls = raw.scan(::DiscourseChatbot::NON_POST_URL_REGEX)

urls.reject { |url| url.include?('/t/') }
end
end
end
3 changes: 2 additions & 1 deletion plugin.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# name: discourse-chatbot
# about: a plugin that allows you to have a conversation with a configurable chatbot in Discourse Chat, Topics and Private Messages
# version: 0.9.37
# version: 0.9.38
# authors: merefield
# url: https://github.com/merefield/discourse-chatbot

Expand Down Expand Up @@ -36,6 +36,7 @@ module ::DiscourseChatbot

TOPIC_URL_REGEX = %r{\/t/[^/]+/(\d+)(?!\d|\/)}
POST_URL_REGEX = %r{\/t/[^/]+/(\d+)/(\d+)(?!\d|\/)}
NON_POST_URL_REGEX = %r{\bhttps?:\/\/[^\s\/$.?#].[^\s)]*}

def progress_debug_message(message)
puts "Chatbot: #{message}" if SiteSetting.chatbot_enable_verbose_console_logging
Expand Down
18 changes: 13 additions & 5 deletions spec/lib/bot/open_ai_agent_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,34 @@
end

it "returns correct status for a response that includes and illegal topic id" do
result = rag.legal_urls?(res, post_ids_found, topic_ids_found)
result = rag.legal_post_urls?(res, post_ids_found, topic_ids_found)

expect(result).to eq(false)
end

it "returns correct status for a response that includes a legal post id" do
expect(post_1).to be_present
result = rag.legal_urls?(res_2, post_ids_found_2, topic_ids_found)
result = rag.legal_post_urls?(res_2, post_ids_found_2, topic_ids_found)
expect(result).to eq(true)
end

it "correctly identifies a legal post id in a url in a response" do
expect(described_class.new({}).legal_urls?("hello /t/slug/112/2", [post_1.id], [topic_1.id])).to eq(true)
expect(described_class.new({}).legal_post_urls?("hello /t/slug/112/2", [post_1.id], [topic_1.id])).to eq(true)
end

it "correctly skips a full url check if a response is blank" do
expect(described_class.new({}).legal_urls?("", [post_1.id], [topic_1.id])).to eq(true)
expect(described_class.new({}).legal_post_urls?("", [post_1.id], [topic_1.id])).to eq(true)
end

it "correctly identifies an illegal topic id in a url in a response" do
expect(described_class.new({}).legal_urls?("hello /t/slug/113/2", [post_1.id], [topic_1.id])).to eq(false)
expect(described_class.new({}).legal_post_urls?("hello /t/slug/113/2", [post_1.id], [topic_1.id])).to eq(false)
end

it "correctly identifies an illegal non-post url in a response" do
expect(described_class.new({}).legal_non_post_urls?("hello https://someplace.com/t/slug/113/2 try looking at https://notanexample.com it's great", ["https://example.com", "https://otherexample.com"])).to eq(false)
end

it "correctly identifies a legal non-post url in a response" do
expect(described_class.new({}).legal_non_post_urls?("hello https://someplace.com/t/slug/113/2 try looking at https://example.com it's great", ["https://example.com", "https://otherexample.com"])).to eq(true)
end
end
19 changes: 17 additions & 2 deletions spec/lib/functions/forum_search_function_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
expect(topic_1).not_to be_nil
expect(topic_2).not_to be_nil
expect(topic_3).not_to be_nil
expect(subject.process(args)[:topic_ids_found]).to eq([post_2.topic_id])
expect(subject.process(args)[:topic_ids_found]).to eq([post_3.topic_id, post_5.topic_id])
expect(subject.process(args)[:post_ids_found]).to include(post_5.id)
expect(subject.process(args)[:post_ids_found]).to include(post_3.id)
expect(subject.process(args)[:post_ids_found]).to include(post_2.id)
Expand Down Expand Up @@ -76,4 +76,19 @@
it "finds urls with a post id" do
expect(subject.find_post_and_topic_ids_from_raw_urls(post_5.raw)).to eq([[post_2.topic_id], [post_2.id]])
end
end

it "method finds urls that are not posts" do
text = <<~TEXT
Check out these links:
this one is good https://meta.discourse.org/t/user-specific-slow-mode/310081/1
wow https://example.com yeah
https://meta.discourse.org/t/another-topic/310082
http://another-example.org/path?query=string this is a good one
my my https://meta.discourse.org/t/yet-another-topic/310083/2
(https://www.sample.org/sample-path)
TEXT
result = subject.find_other_urls(text)
expect(result.length).to eq(3)
expect(result).to eq(["https://example.com", "http://another-example.org/path?query=string", "https://www.sample.org/sample-path"])
end
end

0 comments on commit f05a70a

Please sign in to comment.