diff --git a/docs/openapi.json b/docs/openapi.json index d45ff5c6..45686231 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -386,6 +386,46 @@ } } } + }, + "put": { + "tags": [ + "feedback" + ], + "summary": "Update Feedback Status", + "description": "Handle feedback status update requests.\n\nTakes a request with the desired state of the feedback status.\nReturns the updated state of the feedback status based on the request's value.\nThese changes are for the life of the service and are on a per-worker basis.\n\nReturns:\n StatusResponse: Indicates whether feedback is enabled.", + "operationId": "update_feedback_status_v1_feedback_status_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackStatusUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackStatusUpdateResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } } }, "/v1/conversations": { @@ -712,6 +752,7 @@ "title": "Actions" } }, + "additionalProperties": false, "type": "object", "required": [ "role", @@ -843,6 +884,7 @@ ] } }, + "additionalProperties": false, "type": "object", "title": "AuthenticationConfiguration", "description": "Authentication configuration." @@ -857,6 +899,7 @@ "title": "Access Rules" } }, + "additionalProperties": false, "type": "object", "title": "AuthorizationConfiguration", "description": "Authorization configuration." @@ -933,6 +976,7 @@ ] } }, + "additionalProperties": false, "type": "object", "title": "CORSConfiguration", "description": "CORS configuration." @@ -1000,6 +1044,7 @@ "default": {} } }, + "additionalProperties": false, "type": "object", "required": [ "name", @@ -1223,6 +1268,7 @@ "title": "System Prompt" } }, + "additionalProperties": false, "type": "object", "title": "Customization", "description": "Service customization." @@ -1250,6 +1296,7 @@ ] } }, + "additionalProperties": false, "type": "object", "title": "DatabaseConfiguration", "description": "Database configuration." @@ -1446,6 +1493,47 @@ } ] }, + "FeedbackStatusUpdateRequest": { + "properties": { + "status": { + "type": "boolean", + "title": "Status", + "description": "Desired state of feedback enablement, must be False or True", + "default": false, + "examples": [ + true, + false + ] + } + }, + "type": "object", + "title": "FeedbackStatusUpdateRequest", + "description": "Model representing a feedback status update request.\n\nAttributes:\n status: Value of the desired feedback enabled state.\n\nExample:\n ```python\n feedback_request = FeedbackRequest(\n status=false\n )\n ```" + }, + "FeedbackStatusUpdateResponse": { + "properties": { + "status": { + "additionalProperties": true, + "type": "object", + "title": "Status" + } + }, + "type": "object", + "required": [ + "status" + ], + "title": "FeedbackStatusUpdateResponse", + "description": "Model representing a response to a feedback status update request.\n\nAttributes:\n status: The previous and current status of the service and who updated it.\n\nExample:\n ```python\n status_response = StatusResponse(\n status={\n \"previous_status\": true,\n \"updated_status\": false,\n \"updated_by\": \"user/test\"\n },\n )\n ```", + "examples": [ + { + "status": { + "previous_status": true, + "updated_by": "user/test", + "updated_status": false + } + } + ] + }, "ForbiddenResponse": { "properties": { "detail": { @@ -1503,6 +1591,7 @@ "title": "Default Provider" } }, + "additionalProperties": false, "type": "object", "title": "InferenceConfiguration", "description": "Inference configuration." @@ -1569,6 +1658,7 @@ } } }, + "additionalProperties": false, "type": "object", "required": [ "url" @@ -1596,6 +1686,7 @@ "title": "Role Rules" } }, + "additionalProperties": false, "type": "object", "title": "JwtConfiguration", "description": "JWT configuration." @@ -1625,6 +1716,7 @@ "title": "Roles" } }, + "additionalProperties": false, "type": "object", "required": [ "jsonpath", @@ -1701,6 +1793,7 @@ "title": "Library Client Config Path" } }, + "additionalProperties": false, "type": "object", "title": "LlamaStackConfiguration", "description": "Llama stack configuration." @@ -1721,6 +1814,7 @@ "title": "Url" } }, + "additionalProperties": false, "type": "object", "required": [ "name", @@ -1828,6 +1922,7 @@ "title": "Ca Cert Path" } }, + "additionalProperties": false, "type": "object", "required": [ "db", @@ -2131,6 +2226,7 @@ "title": "Db Path" } }, + "additionalProperties": false, "type": "object", "required": [ "db_path" @@ -2192,6 +2288,7 @@ } } }, + "additionalProperties": false, "type": "object", "title": "ServiceConfiguration", "description": "Service configuration." @@ -2263,6 +2360,7 @@ "title": "Tls Key Password" } }, + "additionalProperties": false, "type": "object", "title": "TLSConfiguration", "description": "TLS configuration." @@ -2321,6 +2419,7 @@ "title": "Transcripts Storage" } }, + "additionalProperties": false, "type": "object", "title": "UserDataCollection", "description": "User data collection configuration." diff --git a/src/app/endpoints/feedback.py b/src/app/endpoints/feedback.py index 39d2a269..c8e81877 100644 --- a/src/app/endpoints/feedback.py +++ b/src/app/endpoints/feedback.py @@ -1,6 +1,7 @@ """Handler for REST API endpoint for user feedback.""" import logging +import threading from typing import Annotated, Any from pathlib import Path import json @@ -12,10 +13,11 @@ from authorization.middleware import authorize from configuration import configuration from models.config import Action -from models.requests import FeedbackRequest +from models.requests import FeedbackRequest, FeedbackStatusUpdateRequest from models.responses import ( ErrorResponse, FeedbackResponse, + FeedbackStatusUpdateResponse, StatusResponse, UnauthorizedResponse, ForbiddenResponse, @@ -25,6 +27,7 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/feedback", tags=["feedback"]) auth_dependency = get_auth_dependency() +feedback_status_lock = threading.Lock() # Response for the feedback endpoint feedback_response: dict[int | str, dict[str, Any]] = { @@ -174,3 +177,42 @@ def feedback_status() -> StatusResponse: return StatusResponse( functionality="feedback", status={"enabled": feedback_status_enabled} ) + + +@router.put("/status") +@authorize(Action.ADMIN) +async def update_feedback_status( + feedback_update_request: FeedbackStatusUpdateRequest, + auth: Annotated[AuthTuple, Depends(auth_dependency)], +) -> FeedbackStatusUpdateResponse: + """ + Handle feedback status update requests. + + Takes a request with the desired state of the feedback status. + Returns the updated state of the feedback status based on the request's value. + These changes are for the life of the service and are on a per-worker basis. + + Returns: + StatusResponse: Indicates whether feedback is enabled. + """ + user_id, _, _ = auth + requested_status = feedback_update_request.get_value() + + with feedback_status_lock: + previous_status = ( + configuration.user_data_collection_configuration.feedback_enabled + ) + configuration.user_data_collection_configuration.feedback_enabled = ( + requested_status + ) + updated_status = ( + configuration.user_data_collection_configuration.feedback_enabled + ) + + return FeedbackStatusUpdateResponse( + status={ + "previous_status": previous_status, + "updated_status": updated_status, + "updated_by": user_id, + } + ) diff --git a/src/models/requests.py b/src/models/requests.py index 14755fbf..aef2778c 100644 --- a/src/models/requests.py +++ b/src/models/requests.py @@ -380,3 +380,28 @@ def check_feedback_provided(self) -> Self: "'sentiment', 'user_feedback', or 'categories'" ) return self + + +class FeedbackStatusUpdateRequest(BaseModel): + """Model representing a feedback status update request. + + Attributes: + status: Value of the desired feedback enabled state. + + Example: + ```python + feedback_request = FeedbackRequest( + status=false + ) + ``` + """ + + status: bool = Field( + False, + description="Desired state of feedback enablement, must be False or True", + examples=[True, False], + ) + + def get_value(self) -> bool: + """Return the value of the status attribute.""" + return self.status diff --git a/src/models/responses.py b/src/models/responses.py index d55dd65f..dbdd17e6 100644 --- a/src/models/responses.py +++ b/src/models/responses.py @@ -570,3 +570,40 @@ class ErrorResponse(BaseModel): ] } } + + +class FeedbackStatusUpdateResponse(BaseModel): + """ + Model representing a response to a feedback status update request. + + Attributes: + status: The previous and current status of the service and who updated it. + + Example: + ```python + status_response = StatusResponse( + status={ + "previous_status": true, + "updated_status": false, + "updated_by": "user/test" + }, + ) + ``` + """ + + status: dict + + # provides examples for /docs endpoint + model_config = { + "json_schema_extra": { + "examples": [ + { + "status": { + "previous_status": True, + "updated_status": False, + "updated_by": "user/test", + }, + } + ] + } + } diff --git a/tests/unit/app/endpoints/test_feedback.py b/tests/unit/app/endpoints/test_feedback.py index 4a155ef6..486818cc 100644 --- a/tests/unit/app/endpoints/test_feedback.py +++ b/tests/unit/app/endpoints/test_feedback.py @@ -9,8 +9,9 @@ assert_feedback_enabled, feedback_endpoint_handler, store_feedback, - feedback_status, + update_feedback_status, ) +from models.requests import FeedbackStatusUpdateRequest from tests.unit.utils.auth_helpers import mock_authorization_resolvers @@ -196,10 +197,33 @@ def test_store_feedback_on_io_error(mocker, feedback_request_data): store_feedback(user_id, feedback_request_data) -def test_feedback_status(): - """Test that feedback_status returns the correct status response.""" +async def test_update_feedback_status_different(): + """Test that update_feedback_status returns the correct status with an update.""" configuration.user_data_collection_configuration.feedback_enabled = True - response = feedback_status() - assert response.functionality == "feedback" - assert response.status == {"enabled": True} + req = FeedbackStatusUpdateRequest(status=False) + resp = await update_feedback_status( + req, + auth=("test_user_id", "test_username", "test_token"), + ) + assert resp.status == { + "previous_status": True, + "updated_status": False, + "updated_by": "test_user_id", + } + + +async def test_update_feedback_status_no_change(): + """Test that update_feedback_status returns the correct status with no update.""" + configuration.user_data_collection_configuration.feedback_enabled = True + + req = FeedbackStatusUpdateRequest(status=True) + resp = await update_feedback_status( + req, + auth=("test_user_id", "test_username", "test_token"), + ) + assert resp.status == { + "previous_status": True, + "updated_status": True, + "updated_by": "test_user_id", + }