Skip to content

Commit

Permalink
feat: Add nutripatrol integration (#1326)
Browse files Browse the repository at this point in the history
* feat: switch to nutripatrol moderation service

* refactor: rename robotoff.slack into robotoff.notifier

* fix: add confidence score to image_flag prediction type

The confidence score was only stored in data->likelihood field

* fix: fix failing unit test
  • Loading branch information
raphael0202 authored Apr 11, 2024
1 parent 9d339d0 commit fa3dc0c
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 55 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/container-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
echo "ROBOTOFF_TLD=net" >> $GITHUB_ENV
echo "MONGO_URI=mongodb://10.1.0.200:27017" >> $GITHUB_ENV
echo "INFLUXDB_HOST=10.1.0.200" >> $GITHUB_ENV
echo "IMAGE_MODERATION_SERVICE_URL=https://nutripatrol.openfoodfacts.net/api/v1/flags"
- name: Set various variable for production deployment
if: matrix.env == 'robotoff-org'
run: |
Expand All @@ -39,6 +40,7 @@ jobs:
# use prod mongodb through stunnel
echo "MONGO_URI=mongodb://10.1.0.113:27017" >> $GITHUB_ENV
echo "INFLUXDB_HOST=10.1.0.201" >> $GITHUB_ENV
echo "IMAGE_MODERATION_SERVICE_URL=https://nutripatrol.openfoodfacts.org/api/v1/flags"
- name: Wait for container build workflow
uses: tomchv/[email protected]
id: wait-build
Expand Down Expand Up @@ -134,8 +136,7 @@ jobs:
echo "SLACK_TOKEN=${{ secrets.SLACK_TOKEN }}" >> .env
echo "GUNICORN_NUM_WORKERS=8"
echo "EVENTS_API_URL=https://event.openfoodfacts.${{ env.ROBOTOFF_TLD }}" >> .env
# TODO: remove this url when we have a proper server running for this purpose
echo "IMAGE_MODERATION_SERVICE_URL=https://amathjourney.com/api/off-annotation/flag-image"
echo "IMAGE_MODERATION_SERVICE_URL=${{ env.IMAGE_MODERATION_SERVICE_URL }}" >> .env
- name: Create Docker volumes
Expand Down
2 changes: 1 addition & 1 deletion robotoff/logos.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
)
from robotoff.models import Prediction as PredictionModel
from robotoff.models import ProductInsight, db
from robotoff.notifier import NotifierFactory
from robotoff.off import OFFAuthentication
from robotoff.slack import NotifierFactory
from robotoff.types import (
ElasticSearchIndex,
InsightImportResult,
Expand Down
18 changes: 15 additions & 3 deletions robotoff/slack.py → robotoff/notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,18 +173,30 @@ def notify_image_flag(
it"""
if not predictions:
return

prediction = predictions[0]
image_url = settings.BaseURLProvider.image_url(
product_id.server_type, source_image
)
image_id = int(source_image.rsplit("/", 1)[-1].split(".", 1)[0])
params = {"imgid": image_id, "url": image_url}
data = {
"barcode": product_id.barcode,
"type": "image",
"url": image_url,
"user_id": "roboto-app",
"source": "robotoff",
"confidence": prediction.confidence,
"image_id": image_id,
"flavor": product_id.server_type.value,
"comment": json.dumps(prediction.data),
}
try:
http_session.put(f"{self.service_url}/{product_id.barcode}", data=params)
http_session.post(self.service_url, json=data)
except Exception:
logger.exception(
"Error while notifying image to moderation service",
extra={
"params": params,
"params": data,
"url": image_url,
"barcode": product_id.barcode,
},
Expand Down
1 change: 1 addition & 0 deletions robotoff/prediction/ocr/image_flag.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def flag_image(content: Union[OCRResult, str]) -> list[Prediction]:
"likelihood": label_annotation.score,
},
predictor_version=PREDICTOR_VERSION,
confidence=label_annotation.score,
)
)
break
Expand Down
4 changes: 2 additions & 2 deletions robotoff/scheduler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from playhouse.postgres_ext import ServerSide
from sentry_sdk import capture_exception

from robotoff import settings, slack
from robotoff import notifier, settings
from robotoff.insights.annotate import UPDATED_ANNOTATION_RESULT, annotate
from robotoff.insights.importer import BrandInsightImporter, is_valid_insight_image
from robotoff.metrics import (
Expand Down Expand Up @@ -60,7 +60,7 @@ def process_insights() -> None:
if annotation_result == UPDATED_ANNOTATION_RESULT and insight.data.get(
"notify", False
):
slack.NotifierFactory.get_notifier().notify_automatic_processing(
notifier.NotifierFactory.get_notifier().notify_automatic_processing(
insight
)
except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion robotoff/workers/tasks/import_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@
db,
with_db,
)
from robotoff.notifier import NotifierFactory
from robotoff.off import generate_image_url, get_source_from_url, parse_ingredients
from robotoff.prediction import ingredient_list
from robotoff.prediction.upc_image import UPCImageType, find_image_is_upc
from robotoff.products import get_product_store
from robotoff.slack import NotifierFactory
from robotoff.taxonomy import get_taxonomy
from robotoff.triton import (
GRPCInferenceServiceStub,
Expand Down
104 changes: 58 additions & 46 deletions tests/unit/test_slack.py → tests/unit/test_notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@

import pytest

from robotoff import settings, slack
from robotoff import settings
from robotoff.models import ImageModel, ImagePrediction, LogoAnnotation, ProductInsight
from robotoff.notifier import (
ImageModerationNotifier,
MultiNotifier,
NoopSlackNotifier,
NotifierFactory,
SlackNotifier,
)
from robotoff.types import Prediction, PredictionType, ProductIdentifier, ServerType

DEFAULT_BARCODE = "123"
Expand Down Expand Up @@ -49,24 +56,22 @@ def __eq__(self, actual):
@pytest.mark.parametrize(
"token_value, moderation_url, want_type",
[
("T", "", slack.SlackNotifier),
("T", "http://test.org/", slack.MultiNotifier),
("", "", slack.NoopSlackNotifier),
("T", "", SlackNotifier),
("T", "http://test.org/", MultiNotifier),
("", "", NoopSlackNotifier),
],
)
def test_notifier_factory(monkeypatch, token_value, moderation_url, want_type):
monkeypatch.setattr(settings, "slack_token", lambda: token_value)
monkeypatch.setattr(settings, "IMAGE_MODERATION_SERVICE_URL", moderation_url)
notifier = slack.NotifierFactory.get_notifier()
notifier = NotifierFactory.get_notifier()
assert type(notifier) is want_type


def test_notify_image_flag_no_prediction(mocker):
mock = mocker.patch("robotoff.slack.http_session.post")
mock = mocker.patch("robotoff.notifier.http_session.post")

notifier = slack.MultiNotifier(
[slack.SlackNotifier(""), slack.ImageModerationNotifier("")]
)
notifier = MultiNotifier([SlackNotifier(""), ImageModerationNotifier("")])
# no predictions associated to image
notifier.notify_image_flag(
[],
Expand All @@ -79,15 +84,12 @@ def test_notify_image_flag_no_prediction(mocker):

def test_notify_image_flag_public(mocker, monkeypatch):
"""Test notifying a potentially sensitive public image"""
mock_slack = mocker.patch(
"robotoff.slack.http_session.post", return_value=MockSlackResponse()
mock_http = mocker.patch(
"robotoff.notifier.http_session.post", return_value=MockSlackResponse()
)
mock_image_moderation = mocker.patch(
"robotoff.slack.http_session.put", return_value=MockSlackResponse()
)
slack_notifier = slack.SlackNotifier("")
notifier = slack.MultiNotifier(
[slack_notifier, slack.ImageModerationNotifier("http://images.org/")]
slack_notifier = SlackNotifier("")
notifier = MultiNotifier(
[slack_notifier, ImageModerationNotifier("https://images.org")]
)

notifier.notify_image_flag(
Expand All @@ -101,7 +103,8 @@ def test_notify_image_flag_public(mocker, monkeypatch):
DEFAULT_PRODUCT_ID,
)

mock_slack.assert_called_once_with(
assert len(mock_http.mock_calls) == 2
mock_http.assert_any_call(
slack_notifier.POST_MESSAGE_URL,
data=PartialRequestMatcher(
f"type: SENSITIVE\nlabel: *flagged*, match: bad_word\n\n <{settings.BaseURLProvider.image_url(DEFAULT_SERVER_TYPE, '/source_image/2.jpg')}|Image> -- <{settings.BaseURLProvider.world(DEFAULT_SERVER_TYPE)}/cgi/product.pl?type=edit&code=123|*Edit*>",
Expand All @@ -111,42 +114,46 @@ def test_notify_image_flag_public(mocker, monkeypatch):
),
),
)
mock_image_moderation.assert_called_once_with(
"http://images.org/123",
data={
"imgid": 2,
"url": settings.BaseURLProvider.image_url(
DEFAULT_SERVER_TYPE, "/source_image/2.jpg"
),
mock_http.assert_any_call(
"https://images.org",
json={
"barcode": "123",
"type": "image",
"url": "https://images.openfoodfacts.net/images/products/source_image/2.jpg",
"user_id": "roboto-app",
"source": "robotoff",
"confidence": None,
"image_id": 2,
"flavor": "off",
"comment": '{"text": "bad_word", "type": "SENSITIVE", "label": "flagged"}',
},
)


def test_notify_image_flag_private(mocker, monkeypatch):
"""Test notifying a potentially sensitive private image"""
mock_slack = mocker.patch(
"robotoff.slack.http_session.post", return_value=MockSlackResponse()
)
mock_image_moderation = mocker.patch(
"robotoff.slack.http_session.put", return_value=MockSlackResponse()
mock_http = mocker.patch(
"robotoff.notifier.http_session.post", return_value=MockSlackResponse()
)
slack_notifier = slack.SlackNotifier("")
notifier = slack.MultiNotifier(
[slack_notifier, slack.ImageModerationNotifier("http://images.org/")]
slack_notifier = SlackNotifier("")
notifier = MultiNotifier(
[slack_notifier, ImageModerationNotifier("https://images.org")]
)

notifier.notify_image_flag(
[
Prediction(
type=PredictionType.image_flag,
data={"type": "label_annotation", "label": "face", "likelihood": 0.8},
confidence=0.8,
)
],
"/source_image/2.jpg",
DEFAULT_PRODUCT_ID,
)

mock_slack.assert_called_once_with(
assert len(mock_http.mock_calls) == 2
mock_http.assert_any_call(
slack_notifier.POST_MESSAGE_URL,
data=PartialRequestMatcher(
f"type: label_annotation\nlabel: *face*, score: 0.8\n\n <{settings.BaseURLProvider.image_url(DEFAULT_SERVER_TYPE, '/source_image/2.jpg')}|Image> -- <{settings.BaseURLProvider.world(DEFAULT_SERVER_TYPE)}/cgi/product.pl?type=edit&code=123|*Edit*>",
Expand All @@ -156,22 +163,27 @@ def test_notify_image_flag_private(mocker, monkeypatch):
),
),
)
mock_image_moderation.assert_called_once_with(
"http://images.org/123",
data={
"imgid": 2,
"url": settings.BaseURLProvider.image_url(
DEFAULT_SERVER_TYPE, "/source_image/2.jpg"
),
mock_http.assert_any_call(
"https://images.org",
json={
"barcode": "123",
"type": "image",
"url": "https://images.openfoodfacts.net/images/products/source_image/2.jpg",
"user_id": "roboto-app",
"source": "robotoff",
"image_id": 2,
"flavor": "off",
"comment": '{"type": "label_annotation", "label": "face", "likelihood": 0.8}',
"confidence": 0.8,
},
)


def test_notify_automatic_processing_weight(mocker, monkeypatch):
mock = mocker.patch(
"robotoff.slack.http_session.post", return_value=MockSlackResponse()
"robotoff.notifier.http_session.post", return_value=MockSlackResponse()
)
notifier = slack.SlackNotifier("")
notifier = SlackNotifier("")

print(settings.BaseURLProvider.image_url(DEFAULT_SERVER_TYPE, "/image/1"))
notifier.notify_automatic_processing(
Expand All @@ -196,9 +208,9 @@ def test_notify_automatic_processing_weight(mocker, monkeypatch):

def test_notify_automatic_processing_label(mocker, monkeypatch):
mock = mocker.patch(
"robotoff.slack.http_session.post", return_value=MockSlackResponse()
"robotoff.notifier.http_session.post", return_value=MockSlackResponse()
)
notifier = slack.SlackNotifier("")
notifier = SlackNotifier("")

notifier.notify_automatic_processing(
ProductInsight(
Expand All @@ -221,7 +233,7 @@ def test_notify_automatic_processing_label(mocker, monkeypatch):

def test_noop_slack_notifier_logging(caplog):
caplog.set_level(logging.INFO)
notifier = slack.NoopSlackNotifier()
notifier = NoopSlackNotifier()

notifier.send_logo_notification(
LogoAnnotation(
Expand Down

0 comments on commit fa3dc0c

Please sign in to comment.