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

Commit 3b66fb3

Browse files
authored
FIX: Restore the accidentally deleted query prefix. (#1079)
Additionally, we add a prefix for embedding generation. Both are stored in the definitions table.
1 parent f5cf101 commit 3b66fb3

File tree

11 files changed

+119
-34
lines changed

11 files changed

+119
-34
lines changed

app/controllers/discourse_ai/admin/ai_embeddings_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ def ai_embeddings_params
111111
:url,
112112
:api_key,
113113
:tokenizer_class,
114+
:embed_prompt,
115+
:search_prompt,
114116
)
115117

116118
extra_field_names = EmbeddingDefinition.provider_params.dig(permitted[:provider]&.to_sym)

app/models/embedding_definition.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def presets
4242
pg_function: "<#>",
4343
tokenizer_class: "DiscourseAi::Tokenizer::BgeLargeEnTokenizer",
4444
provider: HUGGING_FACE,
45+
search_prompt: "Represent this sentence for searching relevant passages:",
4546
},
4647
{
4748
preset_id: "bge-m3",
@@ -228,4 +229,6 @@ def gemini_client
228229
# provider_params :jsonb
229230
# created_at :datetime not null
230231
# updated_at :datetime not null
232+
# embed_prompt :string default(""), not null
233+
# search_prompt :string default(""), not null
231234
#

app/serializers/ai_embedding_definition_serializer.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class AiEmbeddingDefinitionSerializer < ApplicationSerializer
1313
:api_key,
1414
:seeded,
1515
:tokenizer_class,
16+
:embed_prompt,
17+
:search_prompt,
1618
:provider_params
1719

1820
def api_key

assets/javascripts/discourse/admin/models/ai-embedding.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export default class AiEmbedding extends RestModel {
1414
"api_key",
1515
"max_sequence_length",
1616
"provider_params",
17-
"pg_function"
17+
"pg_function",
18+
"embed_prompt",
19+
"search_prompt"
1820
);
1921
}
2022

assets/javascripts/discourse/components/ai-embedding-editor.gjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,24 @@ export default class AiEmbeddingEditor extends Component {
290290
{{/if}}
291291
</div>
292292

293+
<div class="control-group">
294+
<label>{{i18n "discourse_ai.embeddings.embed_prompt"}}</label>
295+
<Input
296+
@type="text"
297+
class="ai-embedding-editor-input ai-embedding-editor__embed_prompt"
298+
@value={{this.editingModel.embed_prompt}}
299+
/>
300+
</div>
301+
302+
<div class="control-group">
303+
<label>{{i18n "discourse_ai.embeddings.search_prompt"}}</label>
304+
<Input
305+
@type="text"
306+
class="ai-embedding-editor-input ai-embedding-editor__search_prompt"
307+
@value={{this.editingModel.search_prompt}}
308+
/>
309+
</div>
310+
293311
<div class="control-group">
294312
<label>{{i18n "discourse_ai.embeddings.max_sequence_length"}}</label>
295313
<Input

config/locales/client.en.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,9 @@ en:
530530
tokenizer: "Tokenizer"
531531
dimensions: "Embedding dimensions"
532532
max_sequence_length: "Sequence length"
533-
533+
embed_prompt: "Embed prompt"
534+
search_prompt: "Search prompt"
535+
534536
distance_function: "Distance function"
535537
distance_functions:
536538
<#>: "Negative inner product (<#>)"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
class ConfigurableEmbeddingsPrefixes < ActiveRecord::Migration[7.2]
3+
def up
4+
add_column :embedding_definitions, :embed_prompt, :string, null: false, default: ""
5+
add_column :embedding_definitions, :search_prompt, :string, null: false, default: ""
6+
7+
# 4 is bge-large-en. Default model and the only one using this so far.
8+
execute <<~SQL
9+
UPDATE embedding_definitions
10+
SET search_prompt='Represent this sentence for searching relevant passages:'
11+
WHERE id = 4
12+
SQL
13+
end
14+
15+
def down
16+
raise ActiveRecord::IrreversibleMigration
17+
end
18+
end

lib/embeddings/strategies/truncation.rb

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,28 @@ def version
1515
def prepare_target_text(target, vdef)
1616
max_length = vdef.max_sequence_length - 2
1717

18-
case target
19-
when Topic
20-
topic_truncation(target, vdef.tokenizer, max_length)
21-
when Post
22-
post_truncation(target, vdef.tokenizer, max_length)
23-
when RagDocumentFragment
24-
vdef.tokenizer.truncate(target.fragment, max_length)
25-
else
26-
raise ArgumentError, "Invalid target type"
27-
end
18+
prepared_text =
19+
case target
20+
when Topic
21+
topic_truncation(target, vdef.tokenizer, max_length)
22+
when Post
23+
post_truncation(target, vdef.tokenizer, max_length)
24+
when RagDocumentFragment
25+
vdef.tokenizer.truncate(target.fragment, max_length)
26+
else
27+
raise ArgumentError, "Invalid target type"
28+
end
29+
30+
return prepared_text if vdef.embed_prompt.blank?
31+
32+
[vdef.embed_prompt, prepared_text].join(" ")
2833
end
2934

3035
def prepare_query_text(text, vdef, asymetric: false)
31-
qtext = asymetric ? "#{vdef.asymmetric_query_prefix} #{text}" : text
36+
qtext = asymetric ? "#{vdef.search_prompt} #{text}" : text
3237
max_length = vdef.max_sequence_length - 2
3338

34-
vdef.tokenizer.truncate(text, max_length)
39+
vdef.tokenizer.truncate(qtext, max_length)
3540
end
3641

3742
private

spec/lib/modules/embeddings/strategies/truncation_spec.rb

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,51 @@
33
RSpec.describe DiscourseAi::Embeddings::Strategies::Truncation do
44
subject(:truncation) { described_class.new }
55

6-
describe "#prepare_query_text" do
7-
context "when using vector def from OpenAI" do
8-
before { SiteSetting.max_post_length = 100_000 }
6+
fab!(:open_ai_embedding_def)
7+
let(:prefix) { "I come first:" }
98

10-
fab!(:topic)
11-
fab!(:post) do
12-
Fabricate(:post, topic: topic, raw: "Baby, bird, bird, bird\nBird is the word\n" * 500)
13-
end
14-
fab!(:post) do
15-
Fabricate(
16-
:post,
17-
topic: topic,
18-
raw: "Don't you know about the bird?\nEverybody knows that the bird is a word\n" * 400,
19-
)
20-
end
21-
fab!(:post) { Fabricate(:post, topic: topic, raw: "Surfin' bird\n" * 800) }
22-
fab!(:open_ai_embedding_def)
9+
describe "#prepare_target_text" do
10+
before { SiteSetting.max_post_length = 100_000 }
11+
12+
fab!(:topic)
13+
fab!(:post) do
14+
Fabricate(:post, topic: topic, raw: "Baby, bird, bird, bird\nBird is the word\n" * 500)
15+
end
16+
fab!(:post) do
17+
Fabricate(
18+
:post,
19+
topic: topic,
20+
raw: "Don't you know about the bird?\nEverybody knows that the bird is a word\n" * 400,
21+
)
22+
end
23+
fab!(:post) { Fabricate(:post, topic: topic, raw: "Surfin' bird\n" * 800) }
24+
fab!(:open_ai_embedding_def)
25+
26+
it "truncates a topic" do
27+
prepared_text = truncation.prepare_target_text(topic, open_ai_embedding_def)
28+
29+
expect(open_ai_embedding_def.tokenizer.size(prepared_text)).to be <=
30+
open_ai_embedding_def.max_sequence_length
31+
end
32+
33+
it "includes embed prefix" do
34+
open_ai_embedding_def.update!(embed_prompt: prefix)
35+
36+
prepared_text = truncation.prepare_target_text(topic, open_ai_embedding_def)
37+
38+
expect(prepared_text.starts_with?(prefix)).to eq(true)
39+
end
40+
end
41+
42+
describe "#prepare_query_text" do
43+
context "when search is asymetric" do
44+
it "includes search prefix" do
45+
open_ai_embedding_def.update!(search_prompt: prefix)
2346

24-
it "truncates a topic" do
25-
prepared_text = truncation.prepare_target_text(topic, open_ai_embedding_def)
47+
prepared_query_text =
48+
truncation.prepare_query_text("searching", open_ai_embedding_def, asymetric: true)
2649

27-
expect(open_ai_embedding_def.tokenizer.size(prepared_text)).to be <=
28-
open_ai_embedding_def.max_sequence_length
50+
expect(prepared_query_text.starts_with?(prefix)).to eq(true)
2951
end
3052
end
3153
end

spec/requests/admin/ai_embeddings_controller_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
url: "https://test.com/api/v1/embeddings",
1616
api_key: "test",
1717
tokenizer_class: "DiscourseAi::Tokenizer::BgeM3Tokenizer",
18+
embed_prompt: "I come first:",
19+
search_prompt: "prefix for search",
1820
}
1921
end
2022

@@ -27,6 +29,8 @@
2729

2830
expect(response.status).to eq(201)
2931
expect(created_def.display_name).to eq(valid_attrs[:display_name])
32+
expect(created_def.embed_prompt).to eq(valid_attrs[:embed_prompt])
33+
expect(created_def.search_prompt).to eq(valid_attrs[:search_prompt])
3034
end
3135

3236
it "stores provider-specific config params" do

0 commit comments

Comments
 (0)