Skip to content

Commit f8ebf07

Browse files
feat(feedback): heuristics for when to not include an associated label (#97735)
1 parent af3d5d4 commit f8ebf07

File tree

2 files changed

+260
-51
lines changed

2 files changed

+260
-51
lines changed

src/sentry/feedback/endpoints/organization_feedback_categories.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,18 @@
3535

3636
MAX_RETURN_CATEGORIES = 4
3737

38-
MAX_ASSOCIATED_LABELS = 12
38+
# Max labels in a label group (including the primary label)
39+
MAX_GROUP_LABELS = 12
3940

4041
# Number of top labels to pass to Seer to ask for similar labels
4142
NUM_TOP_LABELS = 6
4243

4344
# Two days because the largest granularity we cache at is the day
4445
CATEGORIES_CACHE_TIMEOUT = 172800
4546

47+
# If the number of feedbacks is less than this, we don't ask for associated labels
48+
THRESHOLD_TO_GET_ASSOCIATED_LABELS = 50
49+
4650

4751
class LabelGroupFeedbacksContext(TypedDict):
4852
"""Corresponds to LabelGroupFeedbacksContext in Seer."""
@@ -192,9 +196,16 @@ def get(self, request: Request, organization: Organization) -> Response:
192196
feedbacks_context=context_feedbacks,
193197
)
194198

195-
label_groups: list[FeedbackLabelGroup] = json.loads(
196-
make_seer_request(seer_request).decode("utf-8")
197-
)["data"]
199+
if len(context_feedbacks) >= THRESHOLD_TO_GET_ASSOCIATED_LABELS:
200+
label_groups: list[FeedbackLabelGroup] = json.loads(
201+
make_seer_request(seer_request).decode("utf-8")
202+
)["data"]
203+
else:
204+
# If there are less than THRESHOLD_TO_GET_ASSOCIATED_LABELS feedbacks, we don't ask for associated labels
205+
# The more feedbacks there are, the LLM does a better job of generating associated labels since it has more context
206+
label_groups = [
207+
FeedbackLabelGroup(primaryLabel=label, associatedLabels=[]) for label in top_labels
208+
]
198209

199210
# If the LLM just forgets or adds extra primary labels, log it but still generate categories
200211
if len(label_groups) != len(top_labels):
@@ -214,11 +225,48 @@ def get(self, request: Request, organization: Organization) -> Response:
214225
extra={"label_group": label_group},
215226
)
216227

217-
# Converts label_groups (which maps primary label to associated labels) to a list of lists, where the first element is the primary label and the rest are the associated labels
218-
label_groups_lists: list[list[str]] = [
219-
[label_group["primaryLabel"]] + label_group["associatedLabels"][:MAX_ASSOCIATED_LABELS]
220-
for label_group in label_groups
221-
]
228+
# Sometimes, the LLM will give us associated labels that, to put it bluntly, are not associated labels.
229+
# For example, if the primary label is "Navigation", the LLM might give us "Usability" or "User Interface" as associated labels.
230+
# In a case like that, "Usability" and "User Interface" are obviously more general, so will most likely have more feedbacks associated with them than "Navigation".
231+
# One way to filter these out is to check the counts of each associated label, and compare that to the counts of the primary label.
232+
# If the count of the associated label is >3/4 of the count of the primary label, we can assume that the associated label is not a valid associated label.
233+
# Even if it is valid, we don't really care, it matters more that we get rid of it in the situations that it is invalid (which is pretty often).
234+
235+
# Stores each label as an individual label group (so a list of lists, each inside list containing a single label)
236+
# This is done to get the counts of each label individually, so we can filter out invalid associated labels
237+
flattened_label_groups: list[list[str]] = []
238+
for label_group in label_groups:
239+
flattened_label_groups.append([label_group["primaryLabel"]])
240+
flattened_label_groups.extend([[label] for label in label_group["associatedLabels"]])
241+
242+
individual_label_counts = query_label_group_counts(
243+
organization_id=organization.id,
244+
project_ids=numeric_project_ids,
245+
start=start,
246+
end=end,
247+
labels_groups=flattened_label_groups,
248+
)
249+
250+
label_to_count = {}
251+
for label_lst, count in zip(flattened_label_groups, individual_label_counts):
252+
label_to_count[label_lst[0]] = count
253+
254+
label_groups_lists: list[list[str]] = []
255+
for i, label_group in enumerate(label_groups):
256+
primary_label = label_group["primaryLabel"]
257+
associated_labels = label_group["associatedLabels"]
258+
label_groups_lists.append([primary_label])
259+
for associated_label in associated_labels:
260+
# Once we have MAX_GROUP_LABELS total labels, stop adding more
261+
if len(label_groups_lists[i]) >= MAX_GROUP_LABELS:
262+
break
263+
# Ensure the associated label has feedbacks associated with it, and it doesn't have *too many* feedbacks associated with it
264+
# Worst case, if the associated label is wrong, <= 3/4 of the feedbacks associated with it are wrong
265+
if (
266+
label_to_count[associated_label] * 4 <= label_to_count[primary_label] * 3
267+
and label_to_count[associated_label] != 0
268+
):
269+
label_groups_lists[i].append(associated_label)
222270

223271
# label_groups_lists might be empty if the LLM just decides not to give us any primary labels (leading to ValueError, then 500)
224272
# This will be logged since top_labels is guaranteed to be non-empty, but label_groups_lists will be empty

0 commit comments

Comments
 (0)