Skip to content

Commit 6459aac

Browse files
authored
Merge pull request #273 from zszabo-rh/feedback_category
MGMT-21204: add categories to feedback API
2 parents a15f073 + 4b256f3 commit 6459aac

File tree

5 files changed

+334
-36
lines changed

5 files changed

+334
-36
lines changed

docs/openapi.json

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,27 @@
957957
"title": "DataCollectorConfiguration",
958958
"description": "Data collector configuration for sending data to ingress server."
959959
},
960+
"FeedbackCategory": {
961+
"type": "string",
962+
"enum": [
963+
"incorrect",
964+
"not_relevant",
965+
"incomplete",
966+
"outdated_information",
967+
"unsafe",
968+
"other"
969+
],
970+
"x-enum-varnames": [
971+
"INCORRECT",
972+
"NOT_RELEVANT",
973+
"INCOMPLETE",
974+
"OUTDATED_INFORMATION",
975+
"UNSAFE",
976+
"OTHER"
977+
],
978+
"title": "FeedbackCategory",
979+
"description": "Enum representing predefined feedback categories for AI responses.\n\nThese categories help provide structured feedback about AI inference quality when users provide negative feedback (thumbs down). Multiple categories can be selected to provide comprehensive feedback about response issues."
980+
},
960981
"FeedbackRequest": {
961982
"properties": {
962983
"conversation_id": {
@@ -997,6 +1018,27 @@
9971018
"examples": [
9981019
"I'm not satisfied with the response because it is too vague."
9991020
]
1021+
},
1022+
"categories": {
1023+
"anyOf": [
1024+
{
1025+
"items": {
1026+
"$ref": "#/components/schemas/FeedbackCategory"
1027+
},
1028+
"type": "array"
1029+
},
1030+
{
1031+
"type": "null"
1032+
}
1033+
],
1034+
"title": "Categories",
1035+
"description": "List of feedback categories that apply to the LLM response.",
1036+
"examples": [
1037+
[
1038+
"incorrect",
1039+
"incomplete"
1040+
]
1041+
]
10001042
}
10011043
},
10021044
"type": "object",
@@ -1006,14 +1048,25 @@
10061048
"llm_response"
10071049
],
10081050
"title": "FeedbackRequest",
1009-
"description": "Model representing a feedback request.\n\nAttributes:\n conversation_id: The required conversation ID (UUID).\n user_question: The required user question.\n llm_response: The required LLM response.\n sentiment: The optional sentiment.\n user_feedback: The optional user feedback.\n\nExample:\n ```python\n feedback_request = FeedbackRequest(\n conversation_id=\"12345678-abcd-0000-0123-456789abcdef\",\n user_question=\"what are you doing?\",\n user_feedback=\"Great service!\",\n llm_response=\"I don't know\",\n sentiment=-1,\n )\n ```",
1051+
"description": "Model representing a feedback request.\n\nAttributes:\n conversation_id: The required conversation ID (UUID).\n user_question: The required user question.\n llm_response: The required LLM response.\n sentiment: The optional sentiment.\n user_feedback: The optional user feedback.\n categories: The optional list of feedback categories (multi-select).\n\nExample:\n ```python\n feedback_request = FeedbackRequest(\n conversation_id=\"12345678-abcd-0000-0123-456789abcdef\",\n user_question=\"what are you doing?\",\n user_feedback=\"Great service!\",\n llm_response=\"I don't know\",\n sentiment=-1,\n categories=[FeedbackCategory.INCOMPLETE, FeedbackCategory.UNSAFE]\n )\n ```",
10101052
"examples": [
10111053
{
10121054
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
10131055
"llm_response": "bar",
10141056
"sentiment": 1,
10151057
"user_feedback": "Great service!",
10161058
"user_question": "foo"
1059+
},
1060+
{
1061+
"categories": [
1062+
"incomplete",
1063+
"not_relevant"
1064+
],
1065+
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
1066+
"llm_response": "You need to use Docker and Kubernetes for everything.",
1067+
"sentiment": -1,
1068+
"user_feedback": "This response is too general and doesn't provide specific steps.",
1069+
"user_question": "How do I deploy a web app?"
10171070
}
10181071
]
10191072
},

docs/openapi.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,24 @@ Data collector configuration for sending data to ingress server.
457457
| connection_timeout | integer | |
458458

459459

460+
## FeedbackCategory
461+
462+
463+
Enum representing predefined feedback categories for AI responses.
464+
465+
These categories help provide structured feedback about AI inference quality.
466+
Multiple categories can be selected to provide comprehensive feedback.
467+
468+
| Value | Description |
469+
|-------|-------------|
470+
| incorrect | The answer provided is completely wrong |
471+
| not_relevant | This answer doesn't address my question at all |
472+
| incomplete | The answer only covers part of what I asked about |
473+
| outdated_information | This information is from several years ago and no longer accurate |
474+
| unsafe | This response could be harmful or dangerous if followed |
475+
| other | The response has issues not covered by other categories |
476+
477+
460478
## FeedbackRequest
461479

462480

@@ -468,15 +486,27 @@ Attributes:
468486
llm_response: The required LLM response.
469487
sentiment: The optional sentiment.
470488
user_feedback: The optional user feedback.
489+
categories: The optional list of feedback categories (multi-select).
471490

472-
Example:
491+
Examples:
473492
```python
493+
# Basic feedback
474494
feedback_request = FeedbackRequest(
475495
conversation_id="12345678-abcd-0000-0123-456789abcdef",
476496
user_question="what are you doing?",
477497
user_feedback="Great service!",
478498
llm_response="I don't know",
499+
sentiment=1
500+
)
501+
502+
# Feedback with categories
503+
feedback_request = FeedbackRequest(
504+
conversation_id="12345678-abcd-0000-0123-456789abcdef",
505+
user_question="How do I deploy a web app?",
506+
llm_response="You need to use Docker and Kubernetes for everything.",
507+
user_feedback="This response is too general and doesn't provide specific steps.",
479508
sentiment=-1,
509+
categories=["incomplete", "not_relevant"]
480510
)
481511
```
482512

@@ -488,6 +518,7 @@ Example:
488518
| llm_response | string | |
489519
| sentiment | | |
490520
| user_feedback | | Feedback on the LLM response. |
521+
| categories | array[FeedbackCategory] | List of feedback categories that apply to the LLM response. |
491522

492523

493524
## FeedbackResponse

src/models/requests.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Model for service requests."""
22

33
from typing import Optional, Self
4+
from enum import Enum
45

56
from pydantic import BaseModel, model_validator, field_validator, Field
67
from llama_stack_client.types.agents.turn_create_params import Document
@@ -146,6 +147,22 @@ def validate_media_type(self) -> Self:
146147
return self
147148

148149

150+
class FeedbackCategory(str, Enum):
151+
"""Enum representing predefined feedback categories for AI responses.
152+
153+
These categories help provide structured feedback about AI inference quality
154+
when users provide negative feedback (thumbs down). Multiple categories can
155+
be selected to provide comprehensive feedback about response issues.
156+
"""
157+
158+
INCORRECT = "incorrect" # "The answer provided is completely wrong"
159+
NOT_RELEVANT = "not_relevant" # "This answer doesn't address my question at all"
160+
INCOMPLETE = "incomplete" # "The answer only covers part of what I asked about"
161+
OUTDATED_INFORMATION = "outdated_information" # "This information is from several years ago and no longer accurate" # pylint: disable=line-too-long
162+
UNSAFE = "unsafe" # "This response could be harmful or dangerous if followed"
163+
OTHER = "other" # "The response has issues not covered by other categories"
164+
165+
149166
class FeedbackRequest(BaseModel):
150167
"""Model representing a feedback request.
151168
@@ -155,15 +172,17 @@ class FeedbackRequest(BaseModel):
155172
llm_response: The required LLM response.
156173
sentiment: The optional sentiment.
157174
user_feedback: The optional user feedback.
175+
categories: The optional list of feedback categories (multi-select for negative feedback).
158176
159177
Example:
160178
```python
161179
feedback_request = FeedbackRequest(
162180
conversation_id="12345678-abcd-0000-0123-456789abcdef",
163181
user_question="what are you doing?",
164-
user_feedback="Great service!",
182+
user_feedback="This response is not helpful",
165183
llm_response="I don't know",
166184
sentiment=-1,
185+
categories=[FeedbackCategory.INCORRECT, FeedbackCategory.INCOMPLETE]
167186
)
168187
```
169188
"""
@@ -179,6 +198,15 @@ class FeedbackRequest(BaseModel):
179198
description="Feedback on the LLM response.",
180199
examples=["I'm not satisfied with the response because it is too vague."],
181200
)
201+
# Optional list of predefined feedback categories for negative feedback
202+
categories: Optional[list[FeedbackCategory]] = Field(
203+
default=None,
204+
description=(
205+
"List of feedback categories that describe issues with the LLM response "
206+
"(for negative feedback)."
207+
),
208+
examples=[["incorrect", "incomplete"]],
209+
)
182210

183211
# provides examples for /docs endpoint
184212
model_config = {
@@ -188,9 +216,26 @@ class FeedbackRequest(BaseModel):
188216
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
189217
"user_question": "foo",
190218
"llm_response": "bar",
191-
"user_feedback": "Great service!",
192-
"sentiment": 1,
193-
}
219+
"user_feedback": "Not satisfied with the response quality.",
220+
"sentiment": -1,
221+
},
222+
{
223+
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
224+
"user_question": "What is the capital of France?",
225+
"llm_response": "The capital of France is Berlin.",
226+
"sentiment": -1,
227+
"categories": ["incorrect"],
228+
},
229+
{
230+
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
231+
"user_question": "How do I deploy a web app?",
232+
"llm_response": "Use Docker.",
233+
"user_feedback": (
234+
"This response is too general and doesn't provide specific steps."
235+
),
236+
"sentiment": -1,
237+
"categories": ["incomplete", "not_relevant"],
238+
},
194239
]
195240
}
196241
}
@@ -213,9 +258,34 @@ def check_sentiment(cls, value: Optional[int]) -> Optional[int]:
213258
)
214259
return value
215260

261+
@field_validator("categories")
262+
@classmethod
263+
def validate_categories(
264+
cls, value: Optional[list[FeedbackCategory]]
265+
) -> Optional[list[FeedbackCategory]]:
266+
"""Validate feedback categories list."""
267+
if value is None:
268+
return value
269+
270+
if not isinstance(value, list):
271+
raise ValueError("Categories must be a list")
272+
273+
if len(value) == 0:
274+
return None # Convert empty list to None for consistency
275+
276+
unique_categories = list(set(value))
277+
return unique_categories
278+
216279
@model_validator(mode="after")
217-
def check_sentiment_or_user_feedback_set(self) -> Self:
218-
"""Ensure that either 'sentiment' or 'user_feedback' is set."""
219-
if self.sentiment is None and self.user_feedback is None:
220-
raise ValueError("Either 'sentiment' or 'user_feedback' must be set")
280+
def check_feedback_provided(self) -> Self:
281+
"""Ensure that at least one form of feedback is provided."""
282+
if (
283+
self.sentiment is None
284+
and self.user_feedback is None
285+
and self.categories is None
286+
):
287+
raise ValueError(
288+
"At least one form of feedback must be provided: "
289+
"'sentiment', 'user_feedback', or 'categories'"
290+
)
221291
return self

tests/unit/app/endpoints/test_feedback.py

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,31 @@ async def test_assert_feedback_enabled(mocker):
4848
await assert_feedback_enabled(mocker.Mock())
4949

5050

51-
def test_feedback_endpoint_handler(mocker):
52-
"""Test that feedback_endpoint_handler processes feedback correctly."""
51+
@pytest.mark.parametrize(
52+
"feedback_request_data",
53+
[
54+
{},
55+
{
56+
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
57+
"user_question": "What is Kubernetes?",
58+
"llm_response": "It's some computer thing.",
59+
"sentiment": -1,
60+
"categories": ["incorrect", "incomplete"],
61+
},
62+
],
63+
ids=["no_categories", "with_negative_categories"],
64+
)
65+
def test_feedback_endpoint_handler(mocker, feedback_request_data):
66+
"""Test that feedback_endpoint_handler processes feedback for different payloads."""
67+
5368
# Mock the dependencies
5469
mocker.patch("app.endpoints.feedback.assert_feedback_enabled", return_value=None)
5570
mocker.patch("utils.common.retrieve_user_id", return_value="test_user_id")
5671
mocker.patch("app.endpoints.feedback.store_feedback", return_value=None)
5772

58-
# Mock the feedback request
73+
# Prepare the feedback request mock
5974
feedback_request = mocker.Mock()
75+
feedback_request.model_dump.return_value = feedback_request_data
6076

6177
# Call the endpoint handler
6278
result = feedback_endpoint_handler(
@@ -65,7 +81,7 @@ def test_feedback_endpoint_handler(mocker):
6581
_ensure_feedback_enabled=assert_feedback_enabled,
6682
)
6783

68-
# Assert that the response is as expected
84+
# Assert that the expected response is returned
6985
assert result.response == "feedback received"
7086

7187

@@ -94,35 +110,50 @@ def test_feedback_endpoint_handler_error(mocker):
94110
assert exc_info.value.detail["response"] == "Error storing user feedback"
95111

96112

97-
def test_store_feedback(mocker):
98-
"""Test that store_feedback calls the correct storage function."""
113+
@pytest.mark.parametrize(
114+
"feedback_request_data",
115+
[
116+
{
117+
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
118+
"user_question": "What is OpenStack?",
119+
"llm_response": "It's some cloud thing.",
120+
"user_feedback": "This response is not helpful!",
121+
"sentiment": -1,
122+
},
123+
{
124+
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
125+
"user_question": "What is Kubernetes?",
126+
"llm_response": "K8s.",
127+
"sentiment": -1,
128+
"categories": ["incorrect", "not_relevant", "incomplete"],
129+
},
130+
],
131+
ids=["negative_text_feedback", "negative_feedback_with_categories"],
132+
)
133+
def test_store_feedback(mocker, feedback_request_data):
134+
"""Test that store_feedback correctly stores various feedback payloads."""
135+
99136
configuration.user_data_collection_configuration.feedback_storage = "fake-path"
100137

138+
# Patch filesystem and helpers
101139
mocker.patch("builtins.open", mocker.mock_open())
102140
mocker.patch("app.endpoints.feedback.Path", return_value=mocker.MagicMock())
103141
mocker.patch("app.endpoints.feedback.get_suid", return_value="fake-uuid")
104142

105-
# Mock the JSON to assert the data is stored correctly
143+
# Patch json to inspect stored data
106144
mock_json = mocker.patch("app.endpoints.feedback.json")
107145

108146
user_id = "test_user_id"
109-
feedback_request = {
110-
"conversation_id": "12345678-abcd-0000-0123-456789abcdef",
111-
"user_question": "What is OpenStack?",
112-
"llm_response": "OpenStack is a cloud computing platform.",
113-
"user_feedback": "Great service!",
114-
"sentiment": 1,
147+
148+
store_feedback(user_id, feedback_request_data)
149+
150+
expected_data = {
151+
"user_id": user_id,
152+
"timestamp": mocker.ANY,
153+
**feedback_request_data,
115154
}
116155

117-
store_feedback(user_id, feedback_request)
118-
mock_json.dump.assert_called_once_with(
119-
{
120-
"user_id": user_id,
121-
"timestamp": mocker.ANY,
122-
**feedback_request,
123-
},
124-
mocker.ANY,
125-
)
156+
mock_json.dump.assert_called_once_with(expected_data, mocker.ANY)
126157

127158

128159
def test_feedback_status():

0 commit comments

Comments
 (0)