-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathanswer.rb
More file actions
208 lines (176 loc) · 7.99 KB
/
answer.rb
File metadata and controls
208 lines (176 loc) · 7.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
class Answer < ApplicationRecord
include LlmCallsRecordable
module CannedResponses
NO_CONTENT_FOUND_RESPONSE = "I’m having difficulty finding an answer on GOV.UK. If you rephrase your question, I’ll search again. Or you can ask about something else.".freeze
ANSWER_SERVICE_ERROR_RESPONSE = "Something went wrong while trying to answer your question. Please try again.".freeze
TIMED_OUT_RESPONSE = "Something went wrong and I could not find an answer in time. Please try again.".freeze
UNSUCCESSFUL_REQUEST_MESSAGE = "Something went wrong while trying to answer your question. Please try again.".freeze
ANSWER_GUARDRAILS_FAILED_MESSAGE = <<~MESSAGE.freeze
I generated an answer to your question, but it does not meet the GOV.UK Chat content guidelines. This might be because it contains unclear or misleading information, or offers advice about money or your personal circumstances.
Please try asking about something else or rephrasing your question.
MESSAGE
JAILBREAK_GUARDRAILS_FAILED_MESSAGE = "I cannot answer that. Please try asking something else.".freeze
QUESTION_ROUTING_GUARDRAILS_FAILED_MESSAGE = <<~MESSAGE.freeze
I generated an answer to your question, but it does not meet the GOV.UK Chat content guidelines.
This could be because it contains misleading or inappropriate information, or offers advice about money or your personal circumstances.
Please try asking something else.
MESSAGE
LLM_CANNOT_ANSWER_MESSAGE = "I’m having difficulty finding an answer on GOV.UK. If you rephrase your question, I’ll search again. Or you can ask about something else.".freeze
FORBIDDEN_TERMS_MESSAGE = ANSWER_GUARDRAILS_FAILED_MESSAGE
def self.response_for_question_routing_label(label)
canned_responses = Rails.configuration.question_routing_labels.dig(label, :canned_responses)
raise "No canned responses for #{label}" unless canned_responses.respond_to?(:sample)
canned_responses.sample
end
end
GUARDRAIL_STATUSES = { pass: "pass", fail: "fail", error: "error" }.freeze
STATUSES_EXCLUDED_FROM_REPHRASING = %w[
guardrails_answer
guardrails_forbidden_terms
guardrails_jailbreak
guardrails_question_routing
].freeze
STATUSES_EXCLUDED_FROM_TOPIC_ANALYSIS = %w[
error_answer_guardrails
error_answer_service_error
error_jailbreak_guardrails
error_non_specific
error_question_routing_guardrails
error_timeout
guardrails_jailbreak
].freeze
scope :aggregate_status, ->(status) { where("SPLIT_PART(status::TEXT, '_', 1) = ?", status) }
belongs_to :question
has_many :sources, -> { order(relevancy: :asc) }, class_name: "AnswerSource"
has_one :feedback, class_name: "AnswerFeedback"
has_one :topics, class_name: "AnswerAnalysis::Topics"
has_one :answer_relevancy_aggregate, class_name: "AnswerAnalysis::AnswerRelevancyAggregate"
enum :status,
{
answered: "answered",
clarification: "clarification",
error_answer_guardrails: "error_answer_guardrails",
error_answer_service_error: "error_answer_service_error",
error_jailbreak_guardrails: "error_jailbreak_guardrails",
error_non_specific: "error_non_specific",
error_question_routing_guardrails: "error_question_routing_guardrails",
error_timeout: "error_timeout",
guardrails_answer: "guardrails_answer",
guardrails_forbidden_terms: "guardrails_forbidden_terms",
guardrails_jailbreak: "guardrails_jailbreak",
guardrails_question_routing: "guardrails_question_routing",
unanswerable_llm_cannot_answer: "unanswerable_llm_cannot_answer",
unanswerable_no_govuk_content: "unanswerable_no_govuk_content",
unanswerable_question_routing: "unanswerable_question_routing",
},
prefix: true
enum :question_routing_label,
{
about_chat: "about_chat",
about_mps: "about_mps",
advice_opinions_predictions: "advice_opinions_predictions",
character_fun: "character_fun",
genuine_rag: "genuine_rag",
gov_transparency: "gov_transparency",
greetings: "greetings",
harmful_vulgar_controversy: "harmful_vulgar_controversy",
multi_questions: "multi_questions",
negative_acknowledgement: "negative_acknowledgement",
non_english: "non_english",
personal_info: "personal_info",
requires_account_data: "requires_account_data",
positive_acknowledgement: "positive_acknowledgement",
vague_acronym_grammar: "vague_acronym_grammar",
unclear_intent: "unclear_intent",
},
prefix: true
enum :answer_guardrails_status, GUARDRAIL_STATUSES, prefix: true
enum :question_routing_guardrails_status, GUARDRAIL_STATUSES, prefix: true
enum :jailbreak_guardrails_status, GUARDRAIL_STATUSES, prefix: true
enum :completeness,
{
complete: "complete",
partial: "partial",
no_information: "no_information",
},
prefix: true
# guardrail failures are stored as an array so they are more challenging
# to produce aggregate counts of occurrences
def self.count_guardrails_failures(attribute)
unless attribute.in?(%i[answer_guardrails_failures question_routing_guardrails_failures])
raise ArgumentError, "Unexpected attribute: #{attribute}"
end
all_query_groups = current_scope&.group_values || []
guardrail_group_position = all_query_groups.index { |group| group.to_sym == attribute.to_sym }
raise "must have grouped by #{attribute}" unless guardrail_group_position
count_result = current_scope.count
count_result.each_with_object({}) do |(group, count), memo|
if all_query_groups.length == 1
# if we have only a single "group" in the query then we know the group
# value will only comprise of triggered guardrails
triggered_guardrails = group
triggered_guardrails.each do |guardrail|
memo[guardrail] ||= 0
memo[guardrail] += count
end
else
# if there are multiple "groups" in the query then some could come
# before the answer_guardrails_failures with others after
before_groupings = group.take(guardrail_group_position)
triggered_guardrails = group[guardrail_group_position]
after_groupings = group.drop(guardrail_group_position + 1)
triggered_guardrails.each do |guardrail|
new_group = before_groupings + [guardrail] + after_groupings
memo[new_group] ||= 0
memo[new_group] += count
end
end
end
end
def build_sources_from_search_results(search_results)
self.sources = search_results.map.with_index do |result, relevancy|
chunk = AnswerSourceChunk.find_or_create_from_search_result(result)
sources.build(
relevancy:,
chunk:,
search_score: result.score,
weighted_score: result.weighted_score,
)
end
end
def serialize_for_export
as_json(except: :llm_responses).merge(
"sources" => sources.map(&:serialize_for_export),
"llm_responses" => llm_responses.to_json,
)
end
alias_method :serialize_for_evaluation, :serialize_for_export
def use_in_rephrasing?
STATUSES_EXCLUDED_FROM_REPHRASING.exclude?(status)
end
def eligible_for_topic_analysis?
STATUSES_EXCLUDED_FROM_TOPIC_ANALYSIS.exclude?(status)
end
def set_sources_as_unused
sources.each { |source| source.used = false }
end
def group_used_answer_sources_by_base_path
sources_by_base_path = sources.used.group_by(&:base_path)
sources_by_base_path.map do |base_path, group|
result = group.first
path = group.count == 1 ? result.exact_path : base_path
title = result.title
title += ": #{result.heading}" if group.count == 1 && result.heading.present?
{
href: "#{Plek.website_root}#{path}",
title:,
}
end
end
def has_analysis?
topics.present? || answer_relevancy_aggregate.present?
end
def question_used
rephrased_question || question.message
end
end