import json import logging from typing import cast from anydi import singleton from snowflake.cortex import ( # type: ignore[reportMissingTypeStubs] CompleteOptions, ConversationMessage, complete, ) from ows_text_campaigns.adapters.db import DB from ows_text_campaigns.utils.text import strip_html_tags from .exceptions import ContentScannerCompleteError, ContentScannerParsingError from .prompts import SCAN_CONTENT_PROMPT from .types import ContentRecommendations logger = logging.getLogger(__name__) @singleton class ContentScanner: def __init__( self, db: DB, llm_model: str, llm_options: CompleteOptions, llm_timeout: int = 15, ) -> None: self.db = db self.llm_model = llm_model self.llm_options = llm_options self.llm_timeout = llm_timeout def scan(self, content: str, artist_name: str) -> ContentRecommendations: # Transactions are owned by the use-case layer (the calling handler). # The scanner is an inner component and is agnostic of the transactional # context - it only uses the ambient session for Snowpark access. scan_result = self._get_llm_response(content) exists = self._check_artist_name_presence(content, artist_name) return ContentRecommendations( sensitiveWords=scan_result, includedArtistName=exists ) def _get_llm_response(self, content: str) -> list[str]: cleaned = strip_html_tags(content) prompt = self._inject_content(cleaned) try: llm_response = complete( model=self.llm_model, prompt=prompt, session=self.db.create_snowpark_session(), options=self.llm_options, timeout=self.llm_timeout, ) except Exception as exc: logger.error( "Snowpark complete error.", exc_info=exc, extra={ "content": content, "prompt": prompt, "model": self.llm_model, "error": str(exc), }, ) raise ContentScannerCompleteError from exc try: response_text = cast(str, llm_response) data = json.loads(response_text) violations = [item["phrase"] for item in data["phrases"]] except Exception as exc: logger.error( "Parsing LLM response from content scanning.", extra={ "content": content, "prompt": prompt, "model": self.llm_model, "error": str(exc), }, ) raise ContentScannerParsingError from exc return violations @staticmethod def _check_artist_name_presence(content: str, artist_name: str) -> bool: if artist_name.lower() in content.lower(): return True return False @staticmethod def _inject_content(content: str) -> list[ConversationMessage]: prompt = SCAN_CONTENT_PROMPT.copy() for i, item in enumerate(prompt): if item.get("role") == "user": new_item = item.copy() new_item["content"] = content prompt[i] = new_item return prompt