Repository: deepset-ai/haystack
Pull request: Add `DELETE /feedback` for testing and make the label's id generate server-side
Closes #1825 and adds a `DELETE /feedback` endpoint to the REST API for testing purposes.



Problem statement:
Create ID of the feedback labels server side
In the current REST API, the `POST /feedback` endpoint expects the request to contain the ID of the Label to create.

It should be instead the server responsibility to create these labels, and indeed the `Label` constructor does not require the ID at construction time. However, immediate solutions like adding an optional `id` parameter to `LabelSerialized` did not work right away.

We should make the ID optional and make sure that the ID it later created by the server when not provided.

Hints from issue discussion:

Commit message: Update Documentation & Code Style

Patch to review:
diff --git a/docs/_src/api/openapi/openapi.json b/docs/_src/api/openapi/openapi.json
index b8ab396b15..b093e3e545 100644
--- a/docs/_src/api/openapi/openapi.json
+++ b/docs/_src/api/openapi/openapi.json
@@ -1 +1 @@
-{"openapi": "3.0.2", "info": {"title": "Haystack-API", "version": "1.0.0"}, "paths": {"/initialized": {"get": {"tags": ["search"], "summary": "Check Status", "description": "This endpoint can be used during startup to understand if the\nserver is ready to take any requests, or is still loading.\n\nThe recommended approach is to call this endpoint with a short timeout,\nlike 500ms, and in case of no reply, consider the server busy.", "operationId": "check_status_initialized_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}}}, "/hs_version": {"get": {"tags": ["search"], "summary": "Haystack Version", "description": "Get the running Haystack version.", "operationId": "haystack_version_hs_version_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}}}, "/query": {"post": {"tags": ["search"], "summary": "Query", "description": "This endpoint receives the question as a string and allows the requester to set\nadditional parameters that will be passed on to the Haystack pipeline.", "operationId": "query_query_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QueryRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QueryResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/feedback": {"get": {"tags": ["feedback"], "summary": "Get Feedback", "description": "This endpoint allows the API user to retrieve all the\nfeedback that has been sumbitted through the\n`POST /feedback` endpoint", "operationId": "get_feedback_feedback_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}}, "post": {"tags": ["feedback"], "summary": "Post Feedback", "description": "This endpoint allows the API user to submit feedback on\nan answer for a particular query. For example, the user\ncan send feedback on whether the answer was correct and\nwhether the right snippet was identified as the answer.\nInformation submitted through this endpoint is used to\ntrain the underlying QA model.", "operationId": "post_feedback_feedback_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/LabelSerialized"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/eval-feedback": {"post": {"tags": ["feedback"], "summary": "Get Feedback Metrics", "description": "This endpoint returns basic accuracy metrics based on user feedback,\ne.g., the ratio of correct answers or correctly identified documents.\nYou can filter the output by document or label.\n\nExample:\n`curl --location --request POST 'http://127.0.0.1:8000/eval-doc-qa-feedback'      --header 'Content-Type: application/json'      --data-raw '{ \"filters\": {\"document_id\": [\"XRR3xnEBCYVTkbTystOB\"]} }'`", "operationId": "get_feedback_metrics_eval_feedback_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FilterRequest"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/export-feedback": {"get": {"tags": ["feedback"], "summary": "Export Feedback", "description": "This endpoint returns JSON output in the SQuAD format for question/answer pairs\nthat were marked as \"relevant\" by user feedback through the `POST /feedback` endpoint.\n\nThe context_size param can be used to limit response size for large documents.", "operationId": "export_feedback_export_feedback_get", "parameters": [{"required": false, "schema": {"title": "Context Size", "type": "integer", "default": 100000}, "name": "context_size", "in": "query"}, {"required": false, "schema": {"title": "Full Document Context", "type": "boolean", "default": true}, "name": "full_document_context", "in": "query"}, {"required": false, "schema": {"title": "Only Positive Labels", "type": "boolean", "default": false}, "name": "only_positive_labels", "in": "query"}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/file-upload": {"post": {"tags": ["file-upload"], "summary": "Upload File", "description": "You can use this endpoint to upload a file for indexing\n(see [http://localhost:3000/guides/rest-api#indexing-documents-in-the-haystack-rest-api-document-store]).", "operationId": "upload_file_file_upload_post", "requestBody": {"content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Body_upload_file_file_upload_post"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/documents/get_by_filters": {"post": {"tags": ["document"], "summary": "Get Documents", "description": "This endpoint allows you to retrieve documents contained in your document store.\nYou can filter the documents to delete by metadata (like the document's name),\nor provide an empty JSON object to clear the document store.\n\nExample of filters:\n`'{\"filters\": {{\"name\": [\"some\", \"more\"], \"category\": [\"only_one\"]}}'`\n\nTo get all documents you should provide an empty dict, like:\n`'{\"filters\": {}}'`", "operationId": "get_documents_documents_get_by_filters_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FilterRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Get Documents Documents Get By Filters Post", "type": "array", "items": {"$ref": "#/components/schemas/DocumentSerialized"}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/documents/delete_by_filters": {"post": {"tags": ["document"], "summary": "Delete Documents", "description": "This endpoint allows you to delete documents contained in your document store.\nYou can filter the documents to delete by metadata (like the document's name),\nor provide an empty JSON object to clear the document store.\n\nExample of filters:\n`'{\"filters\": {{\"name\": [\"some\", \"more\"], \"category\": [\"only_one\"]}}'`\n\nTo get all documents you should provide an empty dict, like:\n`'{\"filters\": {}}'`", "operationId": "delete_documents_documents_delete_by_filters_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FilterRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Delete Documents Documents Delete By Filters Post", "type": "boolean"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"AnswerSerialized": {"title": "AnswerSerialized", "required": ["answer"], "type": "object", "properties": {"answer": {"title": "Answer", "type": "string"}, "type": {"title": "Type", "enum": ["generative", "extractive", "other"], "type": "string", "default": "extractive"}, "score": {"title": "Score", "type": "number"}, "context": {"title": "Context", "type": "string"}, "offsets_in_document": {"title": "Offsets In Document", "type": "array", "items": {"$ref": "#/components/schemas/Span"}}, "offsets_in_context": {"title": "Offsets In Context", "type": "array", "items": {"$ref": "#/components/schemas/Span"}}, "document_id": {"title": "Document Id", "type": "string"}, "meta": {"title": "Meta", "type": "object"}}}, "Body_upload_file_file_upload_post": {"title": "Body_upload_file_file_upload_post", "required": ["files"], "type": "object", "properties": {"files": {"title": "Files", "type": "array", "items": {"type": "string", "format": "binary"}}, "meta": {"title": "Meta", "type": "string", "default": "null"}, "remove_numeric_tables": {"title": "Remove Numeric Tables"}, "valid_languages": {"title": "Valid Languages"}, "clean_whitespace": {"title": "Clean Whitespace"}, "clean_empty_lines": {"title": "Clean Empty Lines"}, "clean_header_footer": {"title": "Clean Header Footer"}, "split_by": {"title": "Split By"}, "split_length": {"title": "Split Length"}, "split_overlap": {"title": "Split Overlap"}, "split_respect_sentence_boundary": {"title": "Split Respect Sentence Boundary"}}}, "DocumentSerialized": {"title": "DocumentSerialized", "required": ["content", "content_type", "id", "meta"], "type": "object", "properties": {"content": {"title": "Content", "type": "string"}, "content_type": {"title": "Content Type", "enum": ["text", "table", "image"], "type": "string"}, "id": {"title": "Id", "type": "string"}, "meta": {"title": "Meta", "type": "object"}, "score": {"title": "Score", "type": "number"}, "embedding": {"title": "Embedding", "type": "array", "items": {"type": "number"}}, "id_hash_keys": {"title": "Id Hash Keys", "type": "array", "items": {"type": "string"}}}}, "FilterRequest": {"title": "FilterRequest", "type": "object", "properties": {"filters": {"title": "Filters", "type": "object", "additionalProperties": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]}}}}, "HTTPValidationError": {"title": "HTTPValidationError", "type": "object", "properties": {"detail": {"title": "Detail", "type": "array", "items": {"$ref": "#/components/schemas/ValidationError"}}}}, "LabelSerialized": {"title": "LabelSerialized", "required": ["id", "query", "document", "is_correct_answer", "is_correct_document", "origin"], "type": "object", "properties": {"id": {"title": "Id", "type": "string"}, "query": {"title": "Query", "type": "string"}, "document": {"$ref": "#/components/schemas/DocumentSerialized"}, "is_correct_answer": {"title": "Is Correct Answer", "type": "boolean"}, "is_correct_document": {"title": "Is Correct Document", "type": "boolean"}, "origin": {"title": "Origin", "enum": ["user-feedback", "gold-label"], "type": "string"}, "answer": {"$ref": "#/components/schemas/AnswerSerialized"}, "no_answer": {"title": "No Answer", "type": "boolean"}, "pipeline_id": {"title": "Pipeline Id", "type": "string"}, "created_at": {"title": "Created At", "type": "string"}, "updated_at": {"title": "Updated At", "type": "string"}, "meta": {"title": "Meta", "type": "object"}, "filters": {"title": "Filters", "type": "object"}}}, "QueryRequest": {"title": "QueryRequest", "required": ["query"], "type": "object", "properties": {"query": {"title": "Query", "type": "string"}, "params": {"title": "Params", "type": "object"}, "debug": {"title": "Debug", "type": "boolean", "default": false}}, "additionalProperties": false}, "QueryResponse": {"title": "QueryResponse", "required": ["query", "answers"], "type": "object", "properties": {"query": {"title": "Query", "type": "string"}, "answers": {"title": "Answers", "type": "array", "items": {"$ref": "#/components/schemas/AnswerSerialized"}}, "documents": {"title": "Documents", "type": "array", "items": {"$ref": "#/components/schemas/DocumentSerialized"}}, "_debug": {"title": " Debug", "type": "object"}}}, "Span": {"title": "Span", "required": ["start", "end"], "type": "object", "properties": {"start": {"title": "Start", "type": "integer"}, "end": {"title": "End", "type": "integer"}}}, "ValidationError": {"title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", "properties": {"loc": {"title": "Location", "type": "array", "items": {"type": "string"}}, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}}}}}}
\ No newline at end of file
+{"openapi": "3.0.2", "info": {"title": "Haystack-API", "version": "1.0.0"}, "paths": {"/initialized": {"get": {"tags": ["search"], "summary": "Check Status", "description": "This endpoint can be used during startup to understand if the\nserver is ready to take any requests, or is still loading.\n\nThe recommended approach is to call this endpoint with a short timeout,\nlike 500ms, and in case of no reply, consider the server busy.", "operationId": "check_status_initialized_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}}}, "/hs_version": {"get": {"tags": ["search"], "summary": "Haystack Version", "description": "Get the running Haystack version.", "operationId": "haystack_version_hs_version_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}}}, "/query": {"post": {"tags": ["search"], "summary": "Query", "description": "This endpoint receives the question as a string and allows the requester to set\nadditional parameters that will be passed on to the Haystack pipeline.", "operationId": "query_query_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QueryRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QueryResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/feedback": {"get": {"tags": ["feedback"], "summary": "Get Feedback", "description": "This endpoint allows the API user to retrieve all the\nfeedback that has been sumbitted through the\n`POST /feedback` endpoint", "operationId": "get_feedback_feedback_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}}, "post": {"tags": ["feedback"], "summary": "Post Feedback", "description": "This endpoint allows the API user to submit feedback on\nan answer for a particular query. For example, the user\ncan send feedback on whether the answer was correct and\nwhether the right snippet was identified as the answer.\nInformation submitted through this endpoint is used to\ntrain the underlying QA model.", "operationId": "post_feedback_feedback_post", "requestBody": {"content": {"application/json": {"schema": {"title": "Feedback", "anyOf": [{"$ref": "#/components/schemas/LabelSerialized"}, {"$ref": "#/components/schemas/CreateLabelSerialized"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["feedback"], "summary": "Delete Feedback", "description": "This endpoint allows the API user to delete all the\nfeedback that has been sumbitted through the\n`POST /feedback` endpoint", "operationId": "delete_feedback_feedback_delete", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}}}, "/eval-feedback": {"post": {"tags": ["feedback"], "summary": "Get Feedback Metrics", "description": "This endpoint returns basic accuracy metrics based on user feedback,\ne.g., the ratio of correct answers or correctly identified documents.\nYou can filter the output by document or label.\n\nExample:\n`curl --location --request POST 'http://127.0.0.1:8000/eval-doc-qa-feedback'      --header 'Content-Type: application/json'      --data-raw '{ \"filters\": {\"document_id\": [\"XRR3xnEBCYVTkbTystOB\"]} }'`", "operationId": "get_feedback_metrics_eval_feedback_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FilterRequest"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/export-feedback": {"get": {"tags": ["feedback"], "summary": "Export Feedback", "description": "This endpoint returns JSON output in the SQuAD format for question/answer pairs\nthat were marked as \"relevant\" by user feedback through the `POST /feedback` endpoint.\n\nThe context_size param can be used to limit response size for large documents.", "operationId": "export_feedback_export_feedback_get", "parameters": [{"required": false, "schema": {"title": "Context Size", "type": "integer", "default": 100000}, "name": "context_size", "in": "query"}, {"required": false, "schema": {"title": "Full Document Context", "type": "boolean", "default": true}, "name": "full_document_context", "in": "query"}, {"required": false, "schema": {"title": "Only Positive Labels", "type": "boolean", "default": false}, "name": "only_positive_labels", "in": "query"}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/file-upload": {"post": {"tags": ["file-upload"], "summary": "Upload File", "description": "You can use this endpoint to upload a file for indexing\n(see [http://localhost:3000/guides/rest-api#indexing-documents-in-the-haystack-rest-api-document-store]).", "operationId": "upload_file_file_upload_post", "requestBody": {"content": {"multipart/form-data": {"schema": {"$ref": "#/components/schemas/Body_upload_file_file_upload_post"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/documents/get_by_filters": {"post": {"tags": ["document"], "summary": "Get Documents", "description": "This endpoint allows you to retrieve documents contained in your document store.\nYou can filter the documents to delete by metadata (like the document's name),\nor provide an empty JSON object to clear the document store.\n\nExample of filters:\n`'{\"filters\": {{\"name\": [\"some\", \"more\"], \"category\": [\"only_one\"]}}'`\n\nTo get all documents you should provide an empty dict, like:\n`'{\"filters\": {}}'`", "operationId": "get_documents_documents_get_by_filters_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FilterRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Get Documents Documents Get By Filters Post", "type": "array", "items": {"$ref": "#/components/schemas/DocumentSerialized"}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/documents/delete_by_filters": {"post": {"tags": ["document"], "summary": "Delete Documents", "description": "This endpoint allows you to delete documents contained in your document store.\nYou can filter the documents to delete by metadata (like the document's name),\nor provide an empty JSON object to clear the document store.\n\nExample of filters:\n`'{\"filters\": {{\"name\": [\"some\", \"more\"], \"category\": [\"only_one\"]}}'`\n\nTo get all documents you should provide an empty dict, like:\n`'{\"filters\": {}}'`", "operationId": "delete_documents_documents_delete_by_filters_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/FilterRequest"}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Delete Documents Documents Delete By Filters Post", "type": "boolean"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"AnswerSerialized": {"title": "AnswerSerialized", "required": ["answer"], "type": "object", "properties": {"answer": {"title": "Answer", "type": "string"}, "type": {"title": "Type", "enum": ["generative", "extractive", "other"], "type": "string", "default": "extractive"}, "score": {"title": "Score", "type": "number"}, "context": {"title": "Context", "type": "string"}, "offsets_in_document": {"title": "Offsets In Document", "type": "array", "items": {"$ref": "#/components/schemas/Span"}}, "offsets_in_context": {"title": "Offsets In Context", "type": "array", "items": {"$ref": "#/components/schemas/Span"}}, "document_id": {"title": "Document Id", "type": "string"}, "meta": {"title": "Meta", "type": "object"}}}, "Body_upload_file_file_upload_post": {"title": "Body_upload_file_file_upload_post", "required": ["files"], "type": "object", "properties": {"files": {"title": "Files", "type": "array", "items": {"type": "string", "format": "binary"}}, "meta": {"title": "Meta", "type": "string", "default": "null"}, "remove_numeric_tables": {"title": "Remove Numeric Tables"}, "valid_languages": {"title": "Valid Languages"}, "clean_whitespace": {"title": "Clean Whitespace"}, "clean_empty_lines": {"title": "Clean Empty Lines"}, "clean_header_footer": {"title": "Clean Header Footer"}, "split_by": {"title": "Split By"}, "split_length": {"title": "Split Length"}, "split_overlap": {"title": "Split Overlap"}, "split_respect_sentence_boundary": {"title": "Split Respect Sentence Boundary"}}}, "CreateLabelSerialized": {"title": "CreateLabelSerialized", "required": ["query", "document", "is_correct_answer", "is_correct_document", "origin"], "type": "object", "properties": {"id": {"title": "Id", "type": "string"}, "query": {"title": "Query", "type": "string"}, "document": {"$ref": "#/components/schemas/DocumentSerialized"}, "is_correct_answer": {"title": "Is Correct Answer", "type": "boolean"}, "is_correct_document": {"title": "Is Correct Document", "type": "boolean"}, "origin": {"title": "Origin", "enum": ["user-feedback", "gold-label"], "type": "string"}, "answer": {"$ref": "#/components/schemas/AnswerSerialized"}, "no_answer": {"title": "No Answer", "type": "boolean"}, "pipeline_id": {"title": "Pipeline Id", "type": "string"}, "created_at": {"title": "Created At", "type": "string"}, "updated_at": {"title": "Updated At", "type": "string"}, "meta": {"title": "Meta", "type": "object"}, "filters": {"title": "Filters", "type": "object"}}, "additionalProperties": false}, "DocumentSerialized": {"title": "DocumentSerialized", "required": ["content", "content_type", "id", "meta"], "type": "object", "properties": {"content": {"title": "Content", "type": "string"}, "content_type": {"title": "Content Type", "enum": ["text", "table", "image"], "type": "string"}, "id": {"title": "Id", "type": "string"}, "meta": {"title": "Meta", "type": "object"}, "score": {"title": "Score", "type": "number"}, "embedding": {"title": "Embedding", "type": "array", "items": {"type": "number"}}, "id_hash_keys": {"title": "Id Hash Keys", "type": "array", "items": {"type": "string"}}}}, "FilterRequest": {"title": "FilterRequest", "type": "object", "properties": {"filters": {"title": "Filters", "type": "object", "additionalProperties": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]}}}}, "HTTPValidationError": {"title": "HTTPValidationError", "type": "object", "properties": {"detail": {"title": "Detail", "type": "array", "items": {"$ref": "#/components/schemas/ValidationError"}}}}, "LabelSerialized": {"title": "LabelSerialized", "required": ["id", "query", "document", "is_correct_answer", "is_correct_document", "origin"], "type": "object", "properties": {"id": {"title": "Id", "type": "string"}, "query": {"title": "Query", "type": "string"}, "document": {"$ref": "#/components/schemas/DocumentSerialized"}, "is_correct_answer": {"title": "Is Correct Answer", "type": "boolean"}, "is_correct_document": {"title": "Is Correct Document", "type": "boolean"}, "origin": {"title": "Origin", "enum": ["user-feedback", "gold-label"], "type": "string"}, "answer": {"$ref": "#/components/schemas/AnswerSerialized"}, "no_answer": {"title": "No Answer", "type": "boolean"}, "pipeline_id": {"title": "Pipeline Id", "type": "string"}, "created_at": {"title": "Created At", "type": "string"}, "updated_at": {"title": "Updated At", "type": "string"}, "meta": {"title": "Meta", "type": "object"}, "filters": {"title": "Filters", "type": "object"}}}, "QueryRequest": {"title": "QueryRequest", "required": ["query"], "type": "object", "properties": {"query": {"title": "Query", "type": "string"}, "params": {"title": "Params", "type": "object"}, "debug": {"title": "Debug", "type": "boolean", "default": false}}, "additionalProperties": false}, "QueryResponse": {"title": "QueryResponse", "required": ["query", "answers"], "type": "object", "properties": {"query": {"title": "Query", "type": "string"}, "answers": {"title": "Answers", "type": "array", "items": {"$ref": "#/components/schemas/AnswerSerialized"}}, "documents": {"title": "Documents", "type": "array", "items": {"$ref": "#/components/schemas/DocumentSerialized"}}, "_debug": {"title": " Debug", "type": "object"}}}, "Span": {"title": "Span", "required": ["start", "end"], "type": "object", "properties": {"start": {"title": "Start", "type": "integer"}, "end": {"title": "End", "type": "integer"}}}, "ValidationError": {"title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", "properties": {"loc": {"title": "Location", "type": "array", "items": {"type": "string"}}, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}}}}}}
\ No newline at end of file
diff --git a/rest_api/controller/feedback.py b/rest_api/controller/feedback.py
index b2654b02bc..89d4f3f23b 100644
--- a/rest_api/controller/feedback.py
+++ b/rest_api/controller/feedback.py
@@ -4,7 +4,8 @@
 import logging
 
 from fastapi import APIRouter
-from rest_api.schema import FilterRequest, LabelSerialized
+from haystack.schema import Label
+from rest_api.schema import FilterRequest, LabelSerialized, CreateLabelSerialized
 from rest_api.controller.search import DOCUMENT_STORE
 
 
@@ -14,7 +15,7 @@
 
 
 @router.post("/feedback")
-def post_feedback(feedback: LabelSerialized):
+def post_feedback(feedback: Union[LabelSerialized, CreateLabelSerialized]):
     """
     This endpoint allows the API user to submit feedback on
     an answer for a particular query. For example, the user
@@ -25,7 +26,9 @@ def post_feedback(feedback: LabelSerialized):
     """
     if feedback.origin is None:
         feedback.origin = "user-feedback"
-    DOCUMENT_STORE.write_labels([feedback])
+
+    label = Label(**feedback.dict())
+    DOCUMENT_STORE.write_labels([label])
 
 
 @router.get("/feedback")
@@ -39,6 +42,16 @@ def get_feedback():
     return labels
 
 
+@router.delete("/feedback")
+def delete_feedback():
+    """
+    This endpoint allows the API user to delete all the
+    feedback that has been sumbitted through the
+    `POST /feedback` endpoint
+    """
+    DOCUMENT_STORE.delete_labels()
+
+
 @router.post("/eval-feedback")
 def get_feedback_metrics(filters: FilterRequest = None):
     """
diff --git a/rest_api/schema.py b/rest_api/schema.py
index 92e3ff878b..27c63aa604 100644
--- a/rest_api/schema.py
+++ b/rest_api/schema.py
@@ -39,11 +39,31 @@ class DocumentSerialized(Document):
 
 
 @pydantic_dataclass
-class LabelSerialized(Label):
+class LabelSerialized(Label, BaseModel):
     document: DocumentSerialized
     answer: Optional[AnswerSerialized] = None
 
 
+class CreateLabelSerialized(BaseModel):
+    id: Optional[str] = None
+    query: str
+    document: DocumentSerialized
+    is_correct_answer: bool
+    is_correct_document: bool
+    origin: Literal["user-feedback", "gold-label"]
+    answer: Optional[AnswerSerialized] = None
+    no_answer: Optional[bool] = None
+    pipeline_id: Optional[str] = None
+    created_at: Optional[str] = None
+    updated_at: Optional[str] = None
+    meta: Optional[dict] = None
+    filters: Optional[dict] = None
+
+    class Config:
+        # Forbid any extra fields in the request to avoid silent failures
+        extra = Extra.forbid
+
+
 class QueryResponse(BaseModel):
     query: str
     answers: List[AnswerSerialized]
diff --git a/rest_api/test/test_rest_api.py b/rest_api/test/test_rest_api.py
index f0a66a6a85..45860d6f02 100644
--- a/rest_api/test/test_rest_api.py
+++ b/rest_api/test/test_rest_api.py
@@ -4,7 +4,7 @@
 import pytest
 from fastapi.testclient import TestClient
 
-
+from rest_api.schema import LabelSerialized, CreateLabelSerialized
 from rest_api.application import app
 
 
@@ -49,14 +49,24 @@ def client() -> TestClient:
     )
     os.environ["INDEXING_PIPELINE_NAME"] = "indexing_text_pipeline"
     client = TestClient(app)
+
+    # Clean up
+    client.post(url="/documents/delete_by_filters", data='{"filters": {}}')
+    client.delete(url="/feedback")
+
     yield client
+
     # Clean up
     client.post(url="/documents/delete_by_filters", data='{"filters": {}}')
+    client.delete(url="/feedback")
 
 
 @pytest.fixture(scope="session")
 def populated_client(client: TestClient) -> TestClient:
+    # Clean up
     client.post(url="/documents/delete_by_filters", data='{"filters": {}}')
+    client.delete(url="/feedback")
+
     files_to_upload = [
         {"files": (Path(__file__).parent / "samples" / "pdf" / "sample_pdf_1.pdf").open("rb")},
         {"files": (Path(__file__).parent / "samples" / "pdf" / "sample_pdf_2.pdf").open("rb")},
@@ -66,8 +76,12 @@ def populated_client(client: TestClient) -> TestClient:
             url="/file-upload", files=fi, data={"meta": f'{{"meta_key": "meta_value", "meta_index": "{index}"}}'}
         )
         assert 200 == response.status_code
+
     yield client
+
+    # Clean up
     client.post(url="/documents/delete_by_filters", data='{"filters": {}}')
+    client.delete(url="/feedback")
 
 
 def test_get_documents():
@@ -220,7 +234,23 @@ def test_write_feedback(populated_client: TestClient):
     assert 200 == response.status_code
 
 
-def test_get_feedback(client: TestClient):
+def test_write_feedback_without_id(populated_client: TestClient):
+    feedback = {key: value for key, value in FEEDBACK.items() if key != "id"}
+    response = populated_client.post(url="/feedback", json=feedback)
+    assert 200 == response.status_code
+
+
+def test_get_feedback():
+    os.environ["PIPELINE_YAML_PATH"] = str(
+        (Path(__file__).parent / "samples" / "pipeline" / "test_pipeline.yaml").absolute()
+    )
+    os.environ["INDEXING_PIPELINE_NAME"] = "indexing_text_pipeline"
+    client = TestClient(app)
+
+    # Clean up to make sure the docstore is empty
+    client.post(url="/documents/delete_by_filters", data='{"filters": {}}')
+    client.delete(url="/feedback")
+
     response = client.post(url="/feedback", json=FEEDBACK)
     assert response.status_code == 200
     response = client.get(url="/feedback")
@@ -230,6 +260,30 @@ def test_get_feedback(client: TestClient):
         assert response_item == expected_item
 
 
+def test_delete_feedback():
+    os.environ["PIPELINE_YAML_PATH"] = str(
+        (Path(__file__).parent / "samples" / "pipeline" / "test_pipeline.yaml").absolute()
+    )
+    os.environ["INDEXING_PIPELINE_NAME"] = "indexing_text_pipeline"
+    client = TestClient(app)
+
+    # Clean up to make sure the docstore is empty
+    client.post(url="/documents/delete_by_filters", data='{"filters": {}}')
+    client.delete(url="/feedback")
+
+    client.post(url="/feedback", json=FEEDBACK)
+    response = client.get(url="/feedback")
+    json_response = response.json()
+    assert len(json_response) == 1
+
+    response = client.delete(url="/feedback")
+    assert 200 == response.status_code
+
+    response = client.get(url="/feedback")
+    json_response = response.json()
+    assert len(json_response) == 0
+
+
 def test_export_feedback(client: TestClient):
     response = client.post(url="/feedback", json=FEEDBACK)
     assert 200 == response.status_code
@@ -249,7 +303,7 @@ def test_export_feedback(client: TestClient):
 
 
 def test_get_feedback_malformed_query(client: TestClient):
-    feedback = FEEDBACK.copy()
+    feedback = {key: value for key, value in FEEDBACK.items()}
     feedback["unexpected_field"] = "misplaced-value"
     response = client.post(url="/feedback", json=feedback)
     assert response.status_code == 422
diff --git a/ui/utils.py b/ui/utils.py
index f1bb72c056..d430973e81 100644
--- a/ui/utils.py
+++ b/ui/utils.py
@@ -92,7 +92,6 @@ def send_feedback(query, answer_obj, is_correct_answer, is_correct_document, doc
     """
     url = f"{API_ENDPOINT}/{DOC_FEEDBACK}"
     req = {
-        "id": str(uuid4()),
         "query": query,
         "document": document,
         "is_correct_answer": is_correct_answer,


Code review comments:
diff_hunk: @@ -39,6 +42,16 @@ def get_feedback():
     return labels
 
 
+@router.delete("/feedback")
+def delete_feedback():
+    """
+    This endpoint allows the API user to delete all the
+    feedback that has been sumbitted through the
+    `POST /feedback` endpoint
+    """
+    DOCUMENT_STORE.delete_labels()
path: rest_api/controller/feedback.py
text: @user1:
I wonder whether `delete_feedback()`should only delete those labels that were created from user feedback. So I'd say we only delete Labels where `origin` is `"user-feedback"` and not `"gold-label"`. What do you think?

diff_hunk: @@ -230,6 +260,30 @@ def test_get_feedback(client: TestClient):
         assert response_item == expected_item
 
 
+def test_delete_feedback():
+    os.environ["PIPELINE_YAML_PATH"] = str(
path: rest_api/test/test_rest_api.py
text: @user1:
I'd say we change the test in a way that checks whether only labels with origin "feedback" have been deleted.

diff_hunk: @@ -249,7 +303,7 @@ def test_export_feedback(client: TestClient):
 
 
 def test_get_feedback_malformed_query(client: TestClient):
-    feedback = FEEDBACK.copy()
+    feedback = {key: value for key, value in FEEDBACK.items()}
path: rest_api/test/test_rest_api.py
text: @user1:
If we want to create an exact copy of FEEDBACK, we can use `deepcopy()` here, I think.

@author:
I think it's equivalent but I'll replace it. deepcopy is slightly clearer now that I look at it.

diff_hunk: @@ -4,7 +4,7 @@
 import pytest
 from fastapi.testclient import TestClient
 
-
+from rest_api.schema import LabelSerialized, CreateLabelSerialized
path: rest_api/test/test_rest_api.py
text: @user1:
I can't see where these two imports are used, so let's remove them.

@author:
Right, a leftover :+1:

diff_hunk: @@ -220,7 +234,23 @@ def test_write_feedback(populated_client: TestClient):
     assert 200 == response.status_code
 
 
-def test_get_feedback(client: TestClient):
+def test_write_feedback_without_id(populated_client: TestClient):
+    feedback = {key: value for key, value in FEEDBACK.items() if key != "id"}
+    response = populated_client.post(url="/feedback", json=feedback)
+    assert 200 == response.status_code
+
+
+def test_get_feedback():
+    os.environ["PIPELINE_YAML_PATH"] = str(
+        (Path(__file__).parent / "samples" / "pipeline" / "test_pipeline.yaml").absolute()
+    )
+    os.environ["INDEXING_PIPELINE_NAME"] = "indexing_text_pipeline"
+    client = TestClient(app)
path: rest_api/test/test_rest_api.py
text: @user1:
Could you please explain why we are not using `client: TestClient` as a parameter of the test here? Maybe I'm missing something? You would like to reuse the same client throughout the different tests? Then maybe use `populated_client` here?

@author:
Yes that is confusing. For some reason, I can't get the client to clean up properly and unless I instantiate it again in the test, it just carries over all the documents and labels. I'll try to work out what's happening though, and fix this