Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit bd0d533

Browse files
committed
Merge branch 'main' into dev-tool-editor-form-kit
2 parents eb71896 + 70248cc commit bd0d533

File tree

8 files changed

+72
-29
lines changed

8 files changed

+72
-29
lines changed

app/models/reviewable_ai_chat_message.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def build_action(
173173
# force_review :boolean default(FALSE), not null
174174
# reject_reason :text
175175
# potentially_illegal :boolean default(FALSE)
176+
# type_source :string default("unknown"), not null
176177
#
177178
# Indexes
178179
#

app/models/reviewable_ai_post.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ def build_action(
230230
# force_review :boolean default(FALSE), not null
231231
# reject_reason :text
232232
# potentially_illegal :boolean default(FALSE)
233+
# type_source :string default("unknown"), not null
233234
#
234235
# Indexes
235236
#

assets/javascripts/discourse/connectors/topic-map-expanded-after/ai-summary-trigger.gjs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ export default class AiSummaryTrigger extends Component {
1919

2020
<template>
2121
{{#if @outletArgs.topic.summarizable}}
22-
<DButton
23-
@label="summary.buttons.generate"
24-
@icon="discourse-sparkles"
25-
@action={{this.openAiSummaryModal}}
26-
class="btn-default ai-summarization-button"
27-
/>
22+
<section class="topic-map__additional-contents toggle-summary">
23+
<DButton
24+
@label="summary.buttons.generate"
25+
@icon="discourse-sparkles"
26+
@action={{this.openAiSummaryModal}}
27+
class="btn-default ai-summarization-button"
28+
/>
29+
</section>
2830
{{/if}}
2931
</template>
3032
}

evals/lib/boot.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
end
2525
end
2626

27-
discourse_path = File.expand_path(File.join(__dir__, "../../../.."))
27+
discourse_path = ENV["DISCOURSE_PATH"] || File.expand_path(File.join(__dir__, "../../../.."))
2828
# rubocop:disable Discourse/NoChdir
2929
Dir.chdir(discourse_path)
3030
# rubocop:enable Discourse/NoChdir
3131

32-
require "/home/sam/Source/discourse/config/environment"
32+
require "#{discourse_path}/config/environment"
3333

3434
ENV["DISCOURSE_AI_NO_DEBUG"] = "1"
3535
module DiscourseAi::Evals

lib/ai_helper/semantic_categorizer.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ class SemanticCategorizer
55
def initialize(input, user)
66
@user = user
77
@text = input[:text]
8+
@vector = DiscourseAi::Embeddings::Vector.instance
9+
@schema = DiscourseAi::Embeddings::Schema.for(Topic)
810
end
911

1012
def categories
1113
return [] if @text.blank?
1214
return [] if !DiscourseAi::Embeddings.enabled?
1315

14-
candidates = nearest_neighbors(limit: 100)
16+
candidates = nearest_neighbors
1517
return [] if candidates.empty?
1618

1719
candidate_ids = candidates.map(&:first)
@@ -40,6 +42,9 @@ def categories
4042
}
4143
end
4244
.map do |c|
45+
# Note: <#> returns the negative inner product since Postgres only supports ASC order index scans on operators
46+
c[:score] = (c[:score] + 1).abs if @vector.vdef.pg_function = "<#>"
47+
4348
c[:score] = 1 / (c[:score] + 1) # inverse of the distance
4449
c
4550
end
@@ -72,6 +77,9 @@ def tags
7277
.with_index { |tag_list, index| { tags: tag_list, score: candidates[index].last } }
7378
.flat_map { |c| c[:tags].map { |t| { name: t, score: c[:score] } } }
7479
.map do |c|
80+
# Note: <#> returns the negative inner product since Postgres only supports ASC order index scans on operators
81+
c[:score] = (c[:score] + 1).abs if @vector.vdef.pg_function = "<#>"
82+
7583
c[:score] = 1 / (c[:score] + 1) # inverse of the distance
7684
c
7785
end
@@ -91,11 +99,8 @@ def tags
9199

92100
private
93101

94-
def nearest_neighbors(limit: 100)
95-
vector = DiscourseAi::Embeddings::Vector.instance
96-
schema = DiscourseAi::Embeddings::Schema.for(Topic)
97-
98-
raw_vector = vector.vector_from(@text)
102+
def nearest_neighbors(limit: 50)
103+
raw_vector = @vector.vector_from(@text)
99104

100105
muted_category_ids = nil
101106
if @user.present?
@@ -106,7 +111,7 @@ def nearest_neighbors(limit: 100)
106111
).pluck(:category_id)
107112
end
108113

109-
schema
114+
@schema
110115
.asymmetric_similarity_search(raw_vector, limit: limit, offset: 0) do |builder|
111116
builder.join("topics t on t.id = topic_id")
112117
unless muted_category_ids.empty?

lib/embeddings/schema.rb

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class Schema
1515
EMBEDDING_TARGETS = %w[topics posts document_fragments]
1616
EMBEDDING_TABLES = [TOPICS_TABLE, POSTS_TABLE, RAG_DOCS_TABLE]
1717

18+
DEFAULT_HNSW_EF_SEARCH = 40
19+
1820
MissingEmbeddingError = Class.new(StandardError)
1921

2022
class << self
@@ -132,6 +134,8 @@ def find_by_target(target)
132134
end
133135

134136
def asymmetric_similarity_search(embedding, limit:, offset:)
137+
before_query = hnsw_search_workaround(limit)
138+
135139
builder = DB.build(<<~SQL)
136140
WITH candidates AS (
137141
SELECT
@@ -153,7 +157,7 @@ def asymmetric_similarity_search(embedding, limit:, offset:)
153157
ORDER BY
154158
embeddings::halfvec(#{dimensions}) #{pg_function} '[:query_embedding]'::halfvec(#{dimensions})
155159
LIMIT :limit
156-
OFFSET :offset
160+
OFFSET :offset;
157161
SQL
158162

159163
builder.where(
@@ -171,18 +175,24 @@ def asymmetric_similarity_search(embedding, limit:, offset:)
171175
candidates_limit = limit * 2
172176
end
173177

174-
builder.query(
175-
query_embedding: embedding,
176-
candidates_limit: candidates_limit,
177-
limit: limit,
178-
offset: offset,
179-
)
178+
ActiveRecord::Base.transaction do
179+
DB.exec(before_query) if before_query.present?
180+
builder.query(
181+
query_embedding: embedding,
182+
candidates_limit: candidates_limit,
183+
limit: limit,
184+
offset: offset,
185+
)
186+
end
180187
rescue PG::Error => e
181188
Rails.logger.error("Error #{e} querying embeddings for model #{vector_def.display_name}")
182189
raise MissingEmbeddingError
183190
end
184191

185192
def symmetric_similarity_search(record)
193+
limit = 200
194+
before_query = hnsw_search_workaround(limit)
195+
186196
builder = DB.build(<<~SQL)
187197
WITH le_target AS (
188198
SELECT
@@ -210,7 +220,7 @@ def symmetric_similarity_search(record)
210220
le_target
211221
LIMIT 1
212222
)
213-
LIMIT 200
223+
LIMIT #{limit}
214224
) AS widenet
215225
ORDER BY
216226
embeddings::halfvec(#{dimensions}) #{pg_function} (
@@ -220,14 +230,17 @@ def symmetric_similarity_search(record)
220230
le_target
221231
LIMIT 1
222232
)
223-
LIMIT 100;
233+
LIMIT #{limit / 2};
224234
SQL
225235

226236
builder.where("model_id = :vid AND strategy_id = :vsid")
227237

228238
yield(builder) if block_given?
229239

230-
builder.query(vid: vector_def.id, vsid: vector_def.strategy_id, target_id: record.id)
240+
ActiveRecord::Base.transaction do
241+
DB.exec(before_query) if before_query.present?
242+
builder.query(vid: vector_def.id, vsid: vector_def.strategy_id, target_id: record.id)
243+
end
231244
rescue PG::Error => e
232245
Rails.logger.error("Error #{e} querying embeddings for model #{vector_def.display_name}")
233246
raise MissingEmbeddingError
@@ -259,6 +272,13 @@ def store(record, embedding, digest)
259272

260273
private
261274

275+
def hnsw_search_workaround(limit)
276+
threshold = limit * 2
277+
278+
return "" if threshold < DEFAULT_HNSW_EF_SEARCH
279+
"SET LOCAL hnsw.ef_search = #{threshold};"
280+
end
281+
262282
delegate :dimensions, :pg_function, to: :vector_def
263283
end
264284
end

spec/system/ai_helper/ai_composer_helper_spec.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
RSpec.describe "AI Composer helper", type: :system, js: true do
44
fab!(:user) { Fabricate(:admin, refresh_auto_groups: true) }
55
fab!(:non_member_group) { Fabricate(:group) }
6+
fab!(:embedding_definition)
67

78
before do
89
Group.find_by(id: Group::AUTO_GROUPS[:admins]).add(user)
@@ -243,7 +244,10 @@ def trigger_composer_helper(content)
243244
end
244245

245246
context "when suggesting the category with AI category suggester" do
246-
before { SiteSetting.ai_embeddings_enabled = true }
247+
before do
248+
SiteSetting.ai_embeddings_selected_model = embedding_definition.id
249+
SiteSetting.ai_embeddings_enabled = true
250+
end
247251

248252
it "updates the category with the suggested category" do
249253
response =
@@ -274,7 +278,10 @@ def trigger_composer_helper(content)
274278
end
275279

276280
context "when suggesting the tags with AI tag suggester" do
277-
before { SiteSetting.ai_embeddings_enabled = true }
281+
before do
282+
SiteSetting.ai_embeddings_selected_model = embedding_definition.id
283+
SiteSetting.ai_embeddings_enabled = true
284+
end
278285

279286
it "updates the tag with the suggested tag" do
280287
response =

spec/system/ai_helper/ai_split_topic_suggestion_spec.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
fab!(:cloud) { Fabricate(:tag) }
3636
fab!(:feedback) { Fabricate(:tag) }
3737
fab!(:review) { Fabricate(:tag) }
38+
fab!(:embedding_definition)
3839

3940
before do
4041
Group.find_by(id: Group::AUTO_GROUPS[:admins]).add(user)
@@ -80,7 +81,10 @@ def open_move_topic_modal
8081
end
8182

8283
context "when suggesting categories with AI category suggester" do
83-
before { SiteSetting.ai_embeddings_enabled = true }
84+
before do
85+
SiteSetting.ai_embeddings_selected_model = embedding_definition.id
86+
SiteSetting.ai_embeddings_enabled = true
87+
end
8488

8589
skip "TODO: Category suggester only loading one category in test" do
8690
it "updates the category with the suggested category" do
@@ -108,7 +112,10 @@ def open_move_topic_modal
108112
end
109113

110114
context "when suggesting tags with AI tag suggester" do
111-
before { SiteSetting.ai_embeddings_enabled = true }
115+
before do
116+
SiteSetting.ai_embeddings_selected_model = embedding_definition.id
117+
SiteSetting.ai_embeddings_enabled = true
118+
end
112119

113120
it "update the tag with the suggested tag" do
114121
response =

0 commit comments

Comments
 (0)