Skip to content

Commit e48cff4

Browse files
committed
Update QuestionRephraser to always rephrase post Sonnet 4.0
We want to update the QuestionRephraser to always rephrase the question, even if it is the users first question. This updates the QuestionRephraser to do this for all models except Sonnet 4.0 as we don't have updated prompts for that model so it should continue to use the existing implementation. We've also got a slight change due to always rephrasing, in that we need two user prompts. One for when there is history, and one for when there isn't. I've added some basic tests to check that the correct system prompt is being used in both situations.
1 parent 0f48dc5 commit e48cff4

5 files changed

Lines changed: 157 additions & 87 deletions

File tree

lib/answer_composition/pipeline/question_rephraser.rb

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ def initialize(context)
1818
end
1919

2020
def call
21-
return if message_records.blank?
21+
return if message_records.blank? && model_name == :claude_sonnet_4_0
2222

2323
start_time = Clock.monotonic_time
2424
response = anthropic_bedrock_client.messages.create(
25-
system: [{ type: "text", text: config[:system_prompt] }],
25+
system: [{ type: "text", text: system_prompt }],
2626
model: model_id,
2727
messages:,
2828
**inference_config,
@@ -78,10 +78,27 @@ def inference_config
7878
}
7979
end
8080

81+
def system_prompt
82+
return config[:system_prompt] if model_name == :claude_sonnet_4_0
83+
84+
config[:system_prompt_always_rephrase]
85+
end
86+
8187
def user_prompt
82-
config[:user_prompt]
83-
.sub("{question}", question_message)
84-
.sub("{message_history}", message_history)
88+
if model_name == :claude_sonnet_4_0
89+
return config[:user_prompt].sub("{question}", question_message)
90+
.sub("{message_history}", message_history)
91+
92+
end
93+
94+
config_key = message_records.present? ? :user_prompt_with_history : :user_prompt_without_history
95+
96+
if config_key == :user_prompt_with_history
97+
config[config_key].sub("{question}", question_message)
98+
.sub("{message_history}", message_history)
99+
else
100+
config[config_key].sub("{question}", question_message)
101+
end
85102
end
86103

87104
def messages

spec/lib/answer_composition/pipeline/question_rephraser_spec.rb

Lines changed: 132 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -30,96 +30,100 @@
3030
end
3131
end
3232

33-
context "when the question is the beginning of the conversation" do
34-
let(:context) { build(:answer_pipeline_context) }
35-
36-
it "returns nil" do
37-
expect(described_class.call(context)).to be_nil
38-
end
33+
it "includes the current question in the user prompt" do
34+
described_class.call(context)
35+
expect(stub).to have_been_requested
3936
end
4037

41-
context "when all other recent answers have statuses in Answer::STATUSES_EXCLUDED_FROM_REPHRASING" do
42-
it "returns nil" do
43-
conversation = create(:conversation)
44-
create(:question, conversation:)
45-
Answer::STATUSES_EXCLUDED_FROM_REPHRASING.sample(4) do |status|
46-
question = create(:question, conversation:)
47-
create(:answer, question:, status:)
48-
end
49-
latest_question = create(:question, conversation:)
50-
context = build(:answer_pipeline_context, question: latest_question)
38+
it "includes the message_history in the user prompt" do
39+
message_history = <<~HISTORY.strip
40+
user:
41+
"""
42+
How do I pay my tax
43+
"""
44+
assistant:
45+
"""
46+
What type of tax
47+
"""
48+
user:
49+
"""
50+
What types are there
51+
"""
52+
assistant:
53+
"""
54+
Self-assessment, PAYE, Corporation tax
55+
"""
56+
HISTORY
5157

52-
expect(described_class.call(context)).to be_nil
53-
end
58+
anthropic_request = stub_claude_question_rephrasing(
59+
Regexp.new(message_history),
60+
rephrased,
61+
)
62+
63+
described_class.call(context)
64+
65+
expect(anthropic_request).to have_been_made
5466
end
5567

56-
context "when the question is part of an ongoing chat" do
57-
it "includes the current question in the user prompt" do
58-
described_class.call(context)
59-
expect(stub).to have_been_requested
60-
end
68+
it "updates the context's question_message with the rephrased question" do
69+
described_class.call(context)
70+
expect(context.question_message).to eq(rephrased)
71+
end
6172

62-
it "includes the message_history in the user prompt" do
63-
message_history = <<~HISTORY.strip
64-
user:
65-
"""
66-
How do I pay my tax
67-
"""
68-
assistant:
69-
"""
70-
What type of tax
71-
"""
72-
user:
73-
"""
74-
What types are there
75-
"""
76-
assistant:
77-
"""
78-
Self-assessment, PAYE, Corporation tax
79-
"""
80-
HISTORY
73+
it "assigns metrics to the answer" do
74+
allow(Clock).to receive(:monotonic_time).and_return(100.0, 101.5)
8175

82-
anthropic_request = stub_claude_question_rephrasing(
83-
Regexp.new(message_history),
84-
rephrased,
85-
)
76+
described_class.call(context)
8677

87-
described_class.call(context)
78+
expect(context.answer.metrics["question_rephrasing"])
79+
.to eq({
80+
duration: 1.5,
81+
llm_prompt_tokens: 10,
82+
llm_completion_tokens: 20,
83+
llm_cached_tokens: nil,
84+
model: BedrockModels.model_id(described_class::DEFAULT_MODEL),
85+
})
86+
end
8887

89-
expect(anthropic_request).to have_been_made
90-
end
88+
it "assigns the llm response to the answer" do
89+
described_class.call(context)
9190

92-
it "updates the context's question_message with the rephrased question" do
93-
described_class.call(context)
94-
expect(context.question_message).to eq(rephrased)
95-
end
91+
expected_llm_response = claude_messages_response(
92+
content: [claude_messages_text_block(rephrased)],
93+
usage: claude_messages_usage_block(input_tokens: 10, output_tokens: 20),
94+
bedrock_model: described_class::DEFAULT_MODEL,
95+
).to_h
9696

97-
it "assigns metrics to the answer" do
98-
allow(Clock).to receive(:monotonic_time).and_return(100.0, 101.5)
97+
expect(context.answer.llm_responses["question_rephrasing"])
98+
.to eq(expected_llm_response)
99+
end
99100

101+
context "when the question is the first in the conversation" do
102+
let(:question) { create(:question) }
103+
let(:context) { build(:answer_pipeline_context, question:) }
104+
let!(:stub) { stub_claude_question_rephrasing(question.message, rephrased) }
105+
106+
it "calls the llm and rephrases the question" do
100107
described_class.call(context)
101108

102-
expect(context.answer.metrics["question_rephrasing"])
103-
.to eq({
104-
duration: 1.5,
105-
llm_prompt_tokens: 10,
106-
llm_completion_tokens: 20,
107-
llm_cached_tokens: nil,
108-
model: BedrockModels.model_id(described_class::DEFAULT_MODEL),
109-
})
109+
expect(stub).to have_been_requested
110+
expect(context.question_message).to eq(rephrased)
110111
end
111112

112-
it "assigns the llm response to the answer" do
113-
described_class.call(context)
113+
it "uses the user_prompt_without_history prompt" do
114+
expected_prompt = AnswerComposition::Pipeline::Prompts.config(
115+
:question_rephraser, described_class::DEFAULT_MODEL
116+
)[:user_prompt_without_history]
117+
.sub("{question}", question.message)
118+
119+
anthropic_request = stub_claude_question_rephrasing(
120+
expected_prompt,
121+
rephrased,
122+
)
114123

115-
expected_llm_response = claude_messages_response(
116-
content: [claude_messages_text_block(rephrased)],
117-
usage: claude_messages_usage_block(input_tokens: 10, output_tokens: 20),
118-
bedrock_model: described_class::DEFAULT_MODEL,
119-
).to_h
124+
described_class.call(context)
120125

121-
expect(context.answer.llm_responses["question_rephrasing"])
122-
.to eq(expected_llm_response)
126+
expect(anthropic_request).to have_been_made
123127
end
124128
end
125129

@@ -225,4 +229,60 @@
225229
expect(anthropic_request).to have_been_made
226230
end
227231
end
232+
233+
context "when the model is claude_sonnet_4_0" do
234+
let!(:stub) do
235+
stub_claude_question_rephrasing(
236+
question.message, rephrased, chat_options: { bedrock_model: :claude_sonnet_4_0 }
237+
)
238+
end
239+
240+
before { stub_const("#{described_class}::DEFAULT_MODEL", :claude_sonnet_4_0) }
241+
242+
it "uses the system prompt configured for claude_sonnet_4_0" do
243+
allow(AnswerComposition::Pipeline::Prompts.config(:question_rephraser, :claude_sonnet_4_0))
244+
.to receive(:[]).and_call_original
245+
246+
described_class.call(context)
247+
248+
expect(AnswerComposition::Pipeline::Prompts.config(:question_rephraser, :claude_sonnet_4_0))
249+
.to have_received(:[]).with(:system_prompt)
250+
end
251+
252+
it "calls the llm when there is message history" do
253+
described_class.call(context)
254+
255+
expect(stub).to have_been_requested
256+
expect(context.question_message).to eq(rephrased)
257+
end
258+
259+
context "and all other recent answers have statuses in Answer::STATUSES_EXCLUDED_FROM_REPHRASING" do
260+
it "returns nil" do
261+
conversation = create(:conversation)
262+
create(:question, conversation:)
263+
Answer::STATUSES_EXCLUDED_FROM_REPHRASING.sample(4) do |status|
264+
question = create(:question, conversation:)
265+
create(:answer, question:, status:)
266+
end
267+
latest_question = create(:question, conversation:)
268+
context = build(:answer_pipeline_context, question: latest_question)
269+
270+
expect(described_class.call(context)).to be_nil
271+
expect(stub).not_to have_been_requested
272+
end
273+
end
274+
275+
context "and there is no message history" do
276+
let(:conversation) { create(:conversation) }
277+
let(:question) { create(:question, conversation:) }
278+
let(:context) { build(:answer_pipeline_context, question:) }
279+
280+
it "returns nil" do
281+
result = described_class.call(context)
282+
283+
expect(stub).not_to have_been_requested
284+
expect(result).to be_nil
285+
end
286+
end
287+
end
228288
end

spec/support/system_spec_helpers.rb

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,13 @@ def given_i_am_an_admin_with_the_settings_permission
3535

3636
def stubs_for_mock_answer(question,
3737
answer,
38-
rephrase_question: false,
3938
sources_used: [],
4039
create_content_chunk: true)
4140
stub_claude_jailbreak_guardrails(question)
41+
rephrased_question = "Rephrased #{question}"
42+
stub_claude_question_rephrasing(question, rephrased_question)
4243

43-
if rephrase_question
44-
rephrased_question = "Rephrased #{question}"
45-
46-
stub_claude_question_rephrasing(question, rephrased_question)
47-
48-
question = rephrased_question
49-
end
44+
question = rephrased_question
5045

5146
stub_bedrock_titan_embedding(question)
5247

spec/system/conversation_js_features_spec.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ def when_the_second_answer_is_generated
182182

183183
stubs_for_mock_answer(@second_question,
184184
@second_answer,
185-
rephrase_question: true,
186185
sources_used: %w[link_1],
187186
create_content_chunk: false)
188187

spec/system/conversation_with_claude_structured_answer_spec.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ def when_the_second_answer_is_generated
6666

6767
stubs_for_mock_answer(@second_question,
6868
@second_answer,
69-
rephrase_question: true,
7069
sources_used: %w[link_1],
7170
create_content_chunk: false)
7271

0 commit comments

Comments
 (0)