Skip to content

Commit

Permalink
Merge pull request #4588 from akolson/embed-topics-and-content-logic
Browse files Browse the repository at this point in the history
Embed topics and content logic
  • Loading branch information
akolson authored Jun 14, 2024
2 parents c77b320 + 34bbe9e commit b96d249
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 27 deletions.
19 changes: 10 additions & 9 deletions contentcuration/automation/utils/appnexus/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import time
import logging
import requests
import time
from abc import ABC
from abc import abstractmethod
from builtins import NotImplementedError

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

Expand All @@ -15,7 +15,7 @@ class SessionWithMaxConnectionAge(requests.Session):
Session with a maximum connection age. If the connection is older than the specified age, it will be closed and a new one will be created.
The age is specified in seconds.
"""
def __init__(self, age = 100):
def __init__(self, age=100):
super().__init__()
self.age = age
self.last_used = time.time()
Expand Down Expand Up @@ -56,7 +56,8 @@ def __init__(

class BackendResponse(object):
""" Class that should be inherited by specific backend for its responses"""
def __init__(self, **kwargs):
def __init__(self, error=None, **kwargs):
self.error = error
for key, value in kwargs.items():
setattr(self, key, value)

Expand All @@ -67,8 +68,8 @@ class Backend(ABC):
session = None
base_url = None
connect_endpoint = None
max_retries=1
backoff_factor=0.3
max_retries = 1
backoff_factor = 0.3

def __new__(cls, *args, **kwargs):
if not isinstance(cls._instance, cls):
Expand Down Expand Up @@ -156,14 +157,14 @@ def _make_request(self, request):
) as e:
logging.exception(e)
raise errors.InvalidResponse(f"Invalid response from {url}")

def connect(self, **kwargs):
""" Establishes a connection to the backend service. """
try:
request = BackendRequest(method="GET", path=self.connect_endpoint, **kwargs)
self._make_request(request)
return True
except Exception as e:
except Exception:
return False

def make_request(self, request):
Expand Down
15 changes: 9 additions & 6 deletions contentcuration/automation/utils/appnexus/errors.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@

class ConnectionError(Exception):
pass
pass


class TimeoutError(Exception):
pass
pass


class HttpError(Exception):
pass
pass


class InvalidRequest(Exception):
pass
pass


class InvalidResponse(Exception):
pass
pass
Original file line number Diff line number Diff line change
@@ -1,10 +1,93 @@
from automation.utils.appnexus import errors
from django.test import TestCase
from mock import MagicMock
from mock import patch

from contentcuration.models import ContentNode
from contentcuration.utils.recommendations import EmbeddingsResponse
from contentcuration.utils.recommendations import Recommendations
from contentcuration.utils.recommendations import RecommendationsAdapter


class RecommendationsTestCase(TestCase):
def test_backend_initialization(self):
recomendations = Recommendations()
self.assertIsNotNone(recomendations)
self.assertIsInstance(recomendations, Recommendations)


class RecommendationsAdapterTestCase(TestCase):
def setUp(self):
self.adapter = RecommendationsAdapter(MagicMock())
self.topic = {
'id': 'topic_id',
'title': 'topic_title',
'description': 'topic_description',
'language': 'en',
'ancestors': [
{
'id': 'ancestor_id',
'title': 'ancestor_title',
'description': 'ancestor_description',
}
]
}
self.resources = [
MagicMock(spec=ContentNode),
]

def test_adapter_initialization(self):
self.assertIsNotNone(self.adapter)
self.assertIsInstance(self.adapter, RecommendationsAdapter)

@patch('contentcuration.utils.recommendations.EmbedTopicsRequest')
def test_embed_topics_backend_connect_success(self, embed_topics_request_mock):
self.adapter.backend.connect.return_value = True
self.adapter.backend.make_request.return_value = MagicMock(spec=EmbeddingsResponse)
response = self.adapter.embed_topics(self.topic)
self.adapter.backend.connect.assert_called_once()
self.adapter.backend.make_request.assert_called_once()
self.assertIsInstance(response, EmbeddingsResponse)

def test_embed_topics_backend_connect_failure(self):
self.adapter.backend.connect.return_value = False
with self.assertRaises(errors.ConnectionError):
self.adapter.embed_topics(self.topic)
self.adapter.backend.connect.assert_called_once()
self.adapter.backend.make_request.assert_not_called()

@patch('contentcuration.utils.recommendations.EmbedTopicsRequest')
def test_embed_topics_make_request_exception(self, embed_topics_request_mock):
self.adapter.backend.connect.return_value = True
self.adapter.backend.make_request.side_effect = Exception("Mocked exception")
response = self.adapter.embed_topics(self.topic)
self.adapter.backend.connect.assert_called_once()
self.adapter.backend.make_request.assert_called_once()
self.assertIsInstance(response, EmbeddingsResponse)
self.assertEqual(str(response.error), "Mocked exception")

@patch('contentcuration.utils.recommendations.EmbedContentRequest')
def test_embed_content_backend_connect_success(self, embed_content_request_mock):
self.adapter.backend.connect.return_value = True
self.adapter.backend.make_request.return_value = MagicMock(spec=EmbeddingsResponse)
response = self.adapter.embed_content(self.resources)
self.adapter.backend.connect.assert_called_once()
self.adapter.backend.make_request.assert_called_once()
self.assertIsInstance(response, EmbeddingsResponse)

def test_embed_content_backend_connect_failure(self):
self.adapter.backend.connect.return_value = False
with self.assertRaises(errors.ConnectionError):
self.adapter.embed_content(self.resources)
self.adapter.backend.connect.assert_called_once()
self.adapter.backend.make_request.assert_not_called()

@patch('contentcuration.utils.recommendations.EmbedContentRequest')
def test_embed_content_make_request_exception(self, embed_content_request_mock):
self.adapter.backend.connect.return_value = True
self.adapter.backend.make_request.side_effect = Exception("Mocked exception")
response = self.adapter.embed_content(self.resources)
self.adapter.backend.connect.assert_called_once()
self.adapter.backend.make_request.assert_called_once()
self.assertIsInstance(response, EmbeddingsResponse)
self.assertEqual(str(response.error), "Mocked exception")
72 changes: 60 additions & 12 deletions contentcuration/contentcuration/utils/recommendations.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,56 @@
from typing import Any
from typing import Dict
from typing import List
from typing import Union

from automation.utils.appnexus import errors
from automation.utils.appnexus.base import Adapter
from automation.utils.appnexus.base import Backend
from automation.utils.appnexus.base import BackendFactory
from automation.utils.appnexus.base import BackendRequest
from automation.utils.appnexus.base import BackendResponse

from contentcuration.models import ContentNode


class RecommendationsBackendRequest(BackendRequest):
pass
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class RecommedationsRequest(RecommendationsBackendRequest):
def __init__(self) -> None:
super().__init__()
class RecommendationsRequest(RecommendationsBackendRequest):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class EmbeddingsRequest(RecommendationsBackendRequest):
def __init__(self) -> None:
super().__init__()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class RecommendationsBackendResponse(BackendResponse):
pass
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class RecommendationsResponse(RecommendationsBackendResponse):
def __init__(self) -> None:
pass
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class EmbedTopicsRequest(RecommendationsBackendRequest):
path = '/embed-topics'
method = 'POST'


class EmbedContentRequest(RecommendationsBackendRequest):
path = '/embed-content'
method = 'POST'


class EmbeddingsResponse(RecommendationsBackendResponse):
def __init__(self) -> None:
pass
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class RecommendationsBackendFactory(BackendFactory):
Expand Down Expand Up @@ -67,9 +85,39 @@ def cache_embeddings(self, embeddings_list) -> bool:
return True

def get_recommendations(self, embedding) -> RecommendationsResponse:
request = RecommedationsRequest(embedding)
request = RecommendationsRequest(embedding)
return self.backend.make_request(request)

def embed_topics(self, topics: Dict[str, Any]) -> EmbeddingsResponse:

if not self.backend.connect():
raise errors.ConnectionError("Connection to the backend failed")

try:
embed_topics_request = EmbedTopicsRequest(json=topics)
return self.backend.make_request(embed_topics_request)
except Exception as e:
return EmbeddingsResponse(error=e)

def embed_content(self, nodes: List[ContentNode]) -> EmbeddingsResponse:

if not self.backend.connect():
raise errors.ConnectionError("Connection to the backend failed")

try:
resources = [self.extract_content(node) for node in nodes]
json = {
'resources': resources,
'metadata': {}
}
embed_content_request = EmbedContentRequest(json=json)
return self.backend.make_request(embed_content_request)
except Exception as e:
return EmbeddingsResponse(error=e)

def extract_content(self, node: ContentNode) -> Dict[str, Any]:
return {}


class Recommendations(Backend):

Expand Down

0 comments on commit b96d249

Please sign in to comment.