Skip to content

Commit 42fffe5

Browse files
feat(feedback): reduce labels passed to LLM, cap associated label count (#97702)
1 parent c2e93b6 commit 42fffe5

File tree

2 files changed

+88
-10
lines changed

2 files changed

+88
-10
lines changed

src/sentry/feedback/endpoints/organization_feedback_categories.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535

3636
MAX_RETURN_CATEGORIES = 4
3737

38+
MAX_ASSOCIATED_LABELS = 12
39+
40+
# Number of top labels to pass to Seer to ask for similar labels
41+
NUM_TOP_LABELS = 6
42+
3843
# Two days because the largest granularity we cache at is the day
3944
CATEGORIES_CACHE_TIMEOUT = 172800
4045

@@ -169,21 +174,21 @@ def get(self, request: Request, organization: Organization) -> Response:
169174
LabelGroupFeedbacksContext(feedback=feedback["feedback"], labels=feedback["labels"])
170175
)
171176

172-
# Gets the top 10 labels by feedbacks to augment the context that the LLM has, instead of just asking it to generate categories without knowing the most common labels
173-
top_10_labels_result = query_top_ai_labels_by_feedback_count(
177+
# Gets the top labels by feedbacks to augment the context that the LLM has, instead of just asking it to generate categories without knowing the most common labels
178+
top_labels_result = query_top_ai_labels_by_feedback_count(
174179
organization_id=organization.id,
175180
project_ids=numeric_project_ids,
176181
start=start,
177182
end=end,
178-
limit=10,
183+
limit=NUM_TOP_LABELS,
179184
)
180185

181186
# Guaranteed to be non-empty since recent_feedbacks is non-empty
182-
top_10_labels = [result["label"] for result in top_10_labels_result]
187+
top_labels = [result["label"] for result in top_labels_result]
183188

184189
seer_request = LabelGroupsRequest(
185190
organization_id=organization.id,
186-
labels=top_10_labels,
191+
labels=top_labels,
187192
feedbacks_context=context_feedbacks,
188193
)
189194

@@ -192,31 +197,31 @@ def get(self, request: Request, organization: Organization) -> Response:
192197
)["data"]
193198

194199
# If the LLM just forgets or adds extra primary labels, log it but still generate categories
195-
if len(label_groups) != len(top_10_labels):
200+
if len(label_groups) != len(top_labels):
196201
logger.warning(
197202
"Number of label groups does not match number of primary labels passed in Seer",
198203
extra={
199204
"label_groups": label_groups,
200-
"top_10_labels": top_10_labels,
205+
"top_labels": top_labels,
201206
},
202207
)
203208

204209
# If the LLM hallucinates primary label(s), log it but still generate categories
205210
for label_group in label_groups:
206-
if label_group["primaryLabel"] not in top_10_labels:
211+
if label_group["primaryLabel"] not in top_labels:
207212
logger.warning(
208213
"LLM hallucinated primary label",
209214
extra={"label_group": label_group},
210215
)
211216

212217
# 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
213218
label_groups_lists: list[list[str]] = [
214-
[label_group["primaryLabel"]] + label_group["associatedLabels"]
219+
[label_group["primaryLabel"]] + label_group["associatedLabels"][:MAX_ASSOCIATED_LABELS]
215220
for label_group in label_groups
216221
]
217222

218223
# label_groups_lists might be empty if the LLM just decides not to give us any primary labels (leading to ValueError, then 500)
219-
# This will be logged since top_10_labels is guaranteed to be non-empty, but label_groups_lists will be empty
224+
# This will be logged since top_labels is guaranteed to be non-empty, but label_groups_lists will be empty
220225
label_feedback_counts = query_label_group_counts(
221226
organization_id=organization.id,
222227
project_ids=numeric_project_ids,

tests/sentry/feedback/endpoints/test_organization_feedback_categories.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,79 @@ def test_get_feedback_categories_with_project_filter(self) -> None:
253253
elif category["primaryLabel"] == "Authentication":
254254
assert category["feedbackCount"] == 3
255255

256+
@django_db_all
257+
@responses.activate
258+
def test_max_associated_labels_limit(self) -> None:
259+
"""Test that MAX_ASSOCIATED_LABELS constant is respected when processing label groups."""
260+
# Mock Seer to return a label group with more than MAX_ASSOCIATED_LABELS associated labels
261+
mock_seer_category_response(
262+
status=200,
263+
json={
264+
"data": [
265+
{
266+
"primaryLabel": "User Interface",
267+
"associatedLabels": [
268+
"Usability",
269+
"Design",
270+
"Layout",
271+
"Colors",
272+
"Typography",
273+
"Spacing",
274+
"Alignment",
275+
"Responsiveness",
276+
"Accessibility",
277+
"Navigation",
278+
"Buttons",
279+
"Forms",
280+
"Extra Label 1", # This should be truncated
281+
"Extra Label 2", # This should be truncated
282+
"Extra Label 3", # This should be truncated
283+
],
284+
}
285+
]
286+
},
287+
)
288+
289+
with self.feature(self.features):
290+
response = self.get_success_response(self.org.slug)
291+
292+
assert response.data["success"] is True
293+
assert "categories" in response.data
294+
295+
categories = response.data["categories"]
296+
assert len(categories) == 1
297+
298+
user_interface_category = categories[0]
299+
assert user_interface_category["primaryLabel"] == "User Interface"
300+
301+
# Verify that associatedLabels is truncated to MAX_ASSOCIATED_LABELS (12)
302+
associated_labels = user_interface_category["associatedLabels"]
303+
assert (
304+
len(associated_labels) == 12
305+
), f"Expected 12 associated labels, got {len(associated_labels)}"
306+
307+
# Verify the first 12 labels are preserved
308+
expected_labels = [
309+
"Usability",
310+
"Design",
311+
"Layout",
312+
"Colors",
313+
"Typography",
314+
"Spacing",
315+
"Alignment",
316+
"Responsiveness",
317+
"Accessibility",
318+
"Navigation",
319+
"Buttons",
320+
"Forms",
321+
]
322+
assert associated_labels == expected_labels
323+
324+
# Verify that the extra labels beyond MAX_ASSOCIATED_LABELS are not included
325+
assert "Extra Label 1" not in associated_labels
326+
assert "Extra Label 2" not in associated_labels
327+
assert "Extra Label 3" not in associated_labels
328+
256329
@django_db_all
257330
@responses.activate
258331
def test_seer_timeout(self) -> None:

0 commit comments

Comments
 (0)