Skip to content

Commit 933e104

Browse files
authored
Merge pull request #1079 from alphagov/523-always-rephrase-question
[CHAT-523] Always rephrase question for Sonnet 4.5
2 parents f5b4f38 + bdc3493 commit 933e104

5 files changed

Lines changed: 155 additions & 87 deletions

File tree

lib/answer_composition/pipeline/question_rephraser.rb

Lines changed: 20 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,25 @@ 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+
if message_records.present?
95+
config[:user_prompt_with_history].sub("{question}", question_message)
96+
.sub("{message_history}", message_history)
97+
else
98+
config[:user_prompt_without_history].sub("{question}", question_message)
99+
end
85100
end
86101

87102
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)