Skip to content

Commit

Permalink
feat: Add spellcheck annotate (#1434)
Browse files Browse the repository at this point in the history
* feat: 🔥 Add annotation spellcheck

* docs: 📝 Add API doc for spellcheck - additional data

* refactor: 🎨 Black

* test: 🧪 Add unittest and fix code

* docs: 📝 Update OpenAPI insights/annotate
  • Loading branch information
jeremyarancio authored Oct 21, 2024
1 parent a8d3b7d commit f9a60dc
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 3 deletions.
9 changes: 7 additions & 2 deletions doc/references/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ paths:
The annotation is an integer that can take 4 values: `0`, `1`, `2`, `-1`. `0` means the insight is incorrect
(so it won't be applied), `1` means it is correct (so it will be applied) and `-1` means the insight
won't be returned to the user (_skip_). `2` is used when user submit some data to the annotate endpoint
(for example in some cases of category annotation).
(for example in some cases of category annotation or ingredients spellcheck).
We use the voting mecanism system to remember which insight to skip for a user (authenticated or not).
requestBody:
Expand All @@ -344,18 +344,23 @@ paths:
description: ID of the insight
annotation:
type: integer
description: "Annotation of the prediction: 1 to accept the prediction, 0 to refuse it, and -1 for _skip_"
description: "Annotation of the prediction: 1 to accept the prediction, 0 to refuse it, and -1 for _skip_, 2 to accept and add data"
enum:
- 0
- 1
- -1
- 2
update:
type: integer
description: "Send the update to Openfoodfacts if `update=1`, don't send the update otherwise. This parameter is useful if the update is performed client-side"
default: 1
enum:
- 0
- 1
data:
type: object
description: "Additional data provided by the user as key-value pairs"

required:
- "insight_id"
- "annotation"
Expand Down
40 changes: 40 additions & 0 deletions robotoff/insights/annotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
add_label_tag,
add_packaging,
add_store,
save_ingredients,
select_rotate_image,
unselect_image,
update_emb_codes,
Expand Down Expand Up @@ -100,6 +101,12 @@ class AnnotationStatus(Enum):
description="Open Food Facts update failed",
)

INVALID_DATA = AnnotationResult(
status_code=AnnotationStatus.error_invalid_data.value,
status=AnnotationStatus.error_invalid_data.name,
description="The data schema is invalid.",
)


class InsightAnnotator(metaclass=abc.ABCMeta):
@classmethod
Expand Down Expand Up @@ -671,6 +678,38 @@ def is_data_required(cls) -> bool:
return True


class IngredientSpellcheckAnnotator(InsightAnnotator):
@classmethod
def process_annotation(
cls,
insight: ProductInsight,
data: Optional[dict] = None,
auth: Optional[OFFAuthentication] = None,
is_vote: bool = False,
) -> AnnotationResult:
# Possibility for the annotator to change the spellcheck correction if data is provided
if data is not None:
annotation = data.get("annotation")
if not annotation or len(data) > 1:
return INVALID_DATA
# We add the new annotation to the Insight.
json_data = insight.data
json_data["annotation"] = annotation
insight.data = json_data
insight.save()

ingredient_text = data.get("annotation") if data else insight.data["correction"]
save_ingredients(
product_id=insight.get_product_id(),
ingredient_text=ingredient_text,
lang=insight.value_tag,
insight_id=insight.id,
auth=auth,
is_vote=is_vote,
)
return UPDATED_ANNOTATION_RESULT


ANNOTATOR_MAPPING: dict[str, Type] = {
InsightType.packager_code.name: PackagerCodeAnnotator,
InsightType.label.name: LabelAnnotator,
Expand All @@ -683,6 +722,7 @@ def is_data_required(cls) -> bool:
InsightType.nutrition_image.name: NutritionImageAnnotator,
InsightType.nutrition_table_structure.name: NutritionTableStructureAnnotator,
InsightType.is_upc_image.name: UPCImageAnnotator,
InsightType.ingredient_spellcheck.name: IngredientSpellcheckAnnotator,
}


Expand Down
70 changes: 69 additions & 1 deletion tests/integration/insights/test_annotate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from unittest.mock import Mock

import pytest

from robotoff.insights.annotate import AnnotationResult, CategoryAnnotator
from robotoff.insights.annotate import (
UPDATED_ANNOTATION_RESULT,
INVALID_DATA,
AnnotationResult,
CategoryAnnotator,
IngredientSpellcheckAnnotator,
)
from robotoff.models import ProductInsight

from ..models_utils import ProductInsightFactory, clean_db
Expand Down Expand Up @@ -92,3 +100,63 @@ def test_process_annotation_with_invalid_user_input_data(self, user_data, mocker
status="error_invalid_data",
description="`data` is invalid, expected a single `value_tag` string field with the category tag",
)


class TestIngredientSpellcheckAnnotator:

@pytest.fixture
def mock_save_ingredients(self, mocker) -> Mock:
return mocker.patch("robotoff.insights.annotate.save_ingredients")

@pytest.fixture
def spellcheck_insight(self):
return ProductInsightFactory(
type="ingredient_spellcheck",
data={
"original": "List of ingredient",
"correction": "List fo ingredients",
},
)

def test_process_annotation(
self,
mock_save_ingredients: Mock,
spellcheck_insight: ProductInsightFactory,
):
user_data = {"annotation": "List of ingredients"}
annotation_result = IngredientSpellcheckAnnotator.process_annotation(
insight=spellcheck_insight,
data=user_data,
)
assert annotation_result == UPDATED_ANNOTATION_RESULT
assert "annotation" in spellcheck_insight.data
mock_save_ingredients.assert_called()

@pytest.mark.parametrize(
"user_data",
[{}, {"annotation": "List of ingredients", "wrong_key": "wrong_item"}],
)
def test_process_annotation_invalid_data(
self,
user_data: dict,
mock_save_ingredients: Mock,
spellcheck_insight: ProductInsightFactory,
):
annotation_result = IngredientSpellcheckAnnotator.process_annotation(
insight=spellcheck_insight,
data=user_data,
)
assert annotation_result == INVALID_DATA
mock_save_ingredients.assert_not_called()

def test_process_annotate_no_user_data(
self,
mock_save_ingredients: Mock,
spellcheck_insight: ProductInsightFactory,
):
annotation_result = IngredientSpellcheckAnnotator.process_annotation(
insight=spellcheck_insight,
)
assert annotation_result == UPDATED_ANNOTATION_RESULT
assert "annotation" not in spellcheck_insight.data
mock_save_ingredients.assert_called()

0 comments on commit f9a60dc

Please sign in to comment.