Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add nutripatrol integration #1326

Merged
merged 4 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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(

Check warning on line 63 in robotoff/scheduler/__init__.py

View check run for this annotation

Codecov / codecov/patch

robotoff/scheduler/__init__.py#L63

Added line #L63 was not covered by tests
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
2 changes: 1 addition & 1 deletion tests/unit/insights/test_question.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def test_category_question_formatter(
"Bio européen",
"en:eu-organic",
"Le produit a-t-il ce label ?",
"https://static.openfoodfacts.net/images/lang/en/labels/eu-organic.135x90.svg",
"https://static.openfoodfacts.org/images/lang/en/labels/eu-organic.135x90.svg",
),
(
"en",
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
Loading