diff --git a/eventstore/tasks.py b/eventstore/tasks.py index 142562e5..47eda84c 100644 --- a/eventstore/tasks.py +++ b/eventstore/tasks.py @@ -12,6 +12,7 @@ from celery.exceptions import SoftTimeLimitExceeded from django.conf import settings from django.utils import dateparse, timezone +from requests.auth import HTTPBasicAuth from requests.exceptions import RequestException from temba_client.exceptions import TembaHttpError @@ -778,3 +779,46 @@ def process_whatsapp_template_send_status(): status.status = WhatsAppTemplateSendStatus.Status.ACTION_COMPLETED status.action_completed_at = timezone.now() status.save() + + +@app.task( + autoretry_for=(RequestException, SoftTimeLimitExceeded), + retry_backoff=True, + max_retries=15, + acks_late=True, + soft_time_limit=10, + time_limit=15, +) +def get_inbound_intent(text): + params = {"question": text} + response = requests.get( + urljoin(settings.INTENT_CLASSIFIER_URL, "/nlu/"), + params=params, + auth=HTTPBasicAuth( + settings.INTENT_CLASSIFIER_USER, settings.INTENT_CLASSIFIER_PASS + ), + ) + response.raise_for_status() + return response.json()["intent"] + + +@app.task( + autoretry_for=(RequestException, SoftTimeLimitExceeded), + retry_backoff=True, + max_retries=15, + acks_late=True, + soft_time_limit=10, + time_limit=15, +) +def label_whatsapp_message(label, message_id): + headers = { + "Authorization": "Bearer {}".format(settings.TURN_TOKEN), + "content-type": "application/json", + "Accept": "application/vnd.v1+json", + } + response = requests.post( + urljoin(settings.TURN_URL, f"/v1/messages/{message_id}/labels"), + json={"labels": [label]}, + headers=headers, + ) + response.raise_for_status() diff --git a/eventstore/tests/test_whatsapp_actions.py b/eventstore/tests/test_whatsapp_actions.py index 46e130c7..7d9a05a8 100644 --- a/eventstore/tests/test_whatsapp_actions.py +++ b/eventstore/tests/test_whatsapp_actions.py @@ -1,3 +1,4 @@ +import json from datetime import timedelta from unittest.mock import Mock, patch @@ -203,6 +204,76 @@ def test_handle_edd_label_disabled(self): handle_inbound(message) handle.assert_not_called + @responses.activate + def test_intent_classification(self): + inbound_text = "The test inbound message body" + responses.add( + responses.GET, + "http://intent-classifier/nlu/", + json={ + "question": "The test inbound message body", + "intent": "Test Label", + "confidence": 90, + }, + ) + + responses.add( + responses.POST, "http://turn/v1/messages/msg-id-1/labels", json={} + ) + + message = Mock() + message.has_label.return_value = False + message.id = "msg-id-1" + message.type = "text" + message.data = {"text": {"body": inbound_text}} + + handle_inbound(message) + + [intent_call, label_call] = responses.calls + + self.assertEqual(intent_call.request.params, {"question": inbound_text}) + self.assertEqual( + intent_call.request.headers["Authorization"], + "Basic bmx1X3VzZXI6bmx1X3Bhc3M=", + ) + + self.assertEqual( + json.loads(label_call.request.body), {"labels": ["Test Label"]} + ) + self.assertEqual( + label_call.request.headers["Authorization"], + "Bearer turn-token", + ) + + @responses.activate + @override_settings(INTENT_CLASSIFIER_URL=None) + def test_intent_classification_disabled(self): + inbound_text = "The test inbound message body" + + message = Mock() + message.has_label.return_value = False + message.id = "msg-id-1" + message.type = "text" + message.data = {"text": {"body": inbound_text}} + + handle_inbound(message) + + self.assertEqual(len(responses.calls), 0) + + @responses.activate + def test_intent_classification_yes(self): + inbound_text = "YES" + + message = Mock() + message.has_label.return_value = False + message.id = "msg-id-1" + message.type = "text" + message.data = {"text": {"body": inbound_text}} + + handle_inbound(message) + + self.assertEqual(len(responses.calls), 0) + class UpdateRapidproAlertOptoutTests(DjangoTestCase): def test_contact_update_is_called(self): diff --git a/eventstore/whatsapp_actions.py b/eventstore/whatsapp_actions.py index 83e7c7f1..30d148fe 100644 --- a/eventstore/whatsapp_actions.py +++ b/eventstore/whatsapp_actions.py @@ -4,7 +4,9 @@ from eventstore.models import SMS_CHANNELTYPE, DeliveryFailure, Event, OptOut from eventstore.tasks import ( async_create_flow_start, + get_inbound_intent, get_rapidpro_contact_by_msisdn, + label_whatsapp_message, send_helpdesk_response_to_dhis2, update_rapidpro_contact, ) @@ -57,6 +59,14 @@ def handle_inbound(message): if not settings.DISABLE_EDD_LABEL_FLOW and message.has_label("EDD ISSUE"): handle_edd_message(message) + if settings.INTENT_CLASSIFIER_URL and message.type == "text": + text = message.data["text"]["body"] + if text.lower() != "yes": + chain( + get_inbound_intent.s(), + label_whatsapp_message.s(message.id), + ).delay(text) + def update_rapidpro_alert_optout(message): update_rapidpro_contact.delay( diff --git a/ndoh_hub/settings.py b/ndoh_hub/settings.py index e7acf7ee..13e69149 100644 --- a/ndoh_hub/settings.py +++ b/ndoh_hub/settings.py @@ -408,3 +408,7 @@ WHATSAPP_TEMPLATE_SEND_TIMEOUT_HOURS = env.int( "WHATSAPP_TEMPLATE_SEND_TIMEOUT_HOURS", 3 ) + +INTENT_CLASSIFIER_URL = env.str("INTENT_CLASSIFIER_URL", None) +INTENT_CLASSIFIER_USER = env.str("INTENT_CLASSIFIER_USER", None) +INTENT_CLASSIFIER_PASS = env.str("INTENT_CLASSIFIER_PASS", None) diff --git a/ndoh_hub/testsettings.py b/ndoh_hub/testsettings.py index e9fa8f9c..07bd42cb 100644 --- a/ndoh_hub/testsettings.py +++ b/ndoh_hub/testsettings.py @@ -26,3 +26,7 @@ AAQ_CORE_API_URL = "http://aaqcore" AAQ_UD_API_URL = "http://aaqud" AAQ_V2_API_URL = "http://aaq_v2" + +INTENT_CLASSIFIER_URL = "http://intent-classifier" +INTENT_CLASSIFIER_USER = "nlu_user" +INTENT_CLASSIFIER_PASS = "nlu_pass"