import re from typing import Dict, List, Tuple, Any from dataclasses import dataclass import numpy as np try: from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity TRANSFORMERS_AVAILABLE = True print("✅ NLP libraries loaded successfully") except ImportError as e: print(f"⚠️ Warning: Some NLP libraries not available: {e}") print(" Falling back to keyword-only matching") TRANSFORMERS_AVAILABLE = False @dataclass class MatchResult: rule_category: str rule_type: str confidence: float matched_phrases: List[str] reasoning: str class NLPMatcher: def __init__(self): print("🔄 Initializing NLP matcher...") if TRANSFORMERS_AVAILABLE: try: print(" Loading sentence transformer model...") self.model = SentenceTransformer('all-MiniLM-L6-v2') print(" ✅ Sentence transformer loaded") except Exception as e: print(f" ⚠️ Failed to load transformer: {e}") print(" Falling back to pattern matching only") self.model = None else: self.model = None # Key phrase patterns for quick matching print(" Loading pattern matching rules...") self.positive_patterns = { 'music_first': [ r'\bmusic[- ]first\b', r'\bmusic company\b', r'\bartist empowerment\b', r'\bcreativity\b', r'\bimagination\b', r'\bmusic[- ]making\b' ], 'inclusive_tone': [ r'\binclusive\b', r'\baccessible\b', r'\bcommunity\b', r'\bconnection\b', r'\bbelonging\b', r'\bwelcoming\b' ], 'human_centered': [ r'\bhuman value\b', r'\bself[- ]expression\b', r'\bmeaningful creativity\b', r'\bartist\b(?!\s+replacement)', r'\bcollaborat\w+\b' ], 'authentic_voice': [ r'\bauthentic\b', r'\bgenuine\b', r'\bpoetic\b', r'\blyrical\b', r'\bplayful\b', r'\bexperimental\b' ] } self.negative_patterns = { 'ai_first': [ r'\bAI[- ]first\b', r'\bAI music company\b', r'\btechnology over\b', r'\btech[- ]heavy\b', r'\bAI does everything\b' ], 'replacement_language': [ r'\breplace\w* (?:human )?artist\w*\b', r'\bmusic is broken\b', r'\bmake music great again\b', r'\bdemocratize music\b' ], 'tech_bro': [ r'\bdisrupt\w*\b', r'\bscale\b', r'\bleverage\b', r'\bsynerg\w+\b', r'\boptimize\b', r'\bmonetize\b' ], 'trivializing': [ r'\beasy\b.*\bmusic\b', r'\beffortless\b.*\bcreation\b', r'\bcheap\b.*\bslop\b', r'\bshortcut\b' ], 'salesy_tone': [ r'\bAMAZING\b', r'\bREVOLUTIONARY\b', r'\bGAME[- ]CHANGER\b', r'\bUNLIMITED\b', r'\bFREE FOREVER\b' ] } print(" ✅ Pattern matching rules loaded") def analyze_content(self, content: str, approval_rules: List, rejection_rules: List) -> Dict[str, Any]: print(f"🔍 Analyzing content: '{content[:50]}{'...' if len(content) > 50 else ''}'") content_lower = content.lower() # Quick pattern matching print(" Checking pattern matches...") pattern_matches = self._match_patterns(content_lower) print(f" Found {len(pattern_matches)} pattern matches") # Semantic similarity matching if self.model is not None: print(" Performing semantic analysis...") semantic_matches = self._semantic_matching(content, approval_rules, rejection_rules) print(f" Found {len(semantic_matches)} semantic matches") else: print(" Using keyword fallback matching...") semantic_matches = self._keyword_fallback_matching(content, approval_rules, rejection_rules) print(f" Found {len(semantic_matches)} keyword matches") # Combine results all_matches = pattern_matches + semantic_matches print(f" Total matches: {len(all_matches)}") # Calculate overall score approval_score = sum(m.confidence for m in all_matches if m.rule_type == 'approve') rejection_score = sum(m.confidence for m in all_matches if m.rule_type == 'reject') net_score = approval_score - rejection_score approved = net_score > 0 and rejection_score < 0.7 print(f" 📊 Scores - Approval: {approval_score:.2f}, Rejection: {rejection_score:.2f}, Net: {net_score:.2f}") print(f" 🎯 Decision: {'APPROVED' if approved else 'REJECTED'}") # Generate reasoning reasoning = self._generate_reasoning(all_matches, approved, net_score) return { "approved": approved, "approval_score": approval_score, "rejection_score": rejection_score, "net_score": net_score, "matches": all_matches, "reasoning": reasoning } def _match_patterns(self, content: str) -> List[MatchResult]: matches = [] # Check positive patterns for category, patterns in self.positive_patterns.items(): for pattern in patterns: if re.search(pattern, content, re.IGNORECASE): matches.append(MatchResult( rule_category=category, rule_type='approve', confidence=0.8, matched_phrases=[pattern], reasoning=f"Contains positive pattern: {pattern}" )) # Check negative patterns for category, patterns in self.negative_patterns.items(): for pattern in patterns: if re.search(pattern, content, re.IGNORECASE): matches.append(MatchResult( rule_category=category, rule_type='reject', confidence=0.9, matched_phrases=[pattern], reasoning=f"Contains problematic pattern: {pattern}" )) return matches def _semantic_matching(self, content: str, approval_rules: List, rejection_rules: List) -> List[MatchResult]: matches = [] if not content.strip() or self.model is None: return matches try: # Encode content content_embedding = self.model.encode([content]) # Check approval rules for rule in approval_rules: rule_phrases = rule.phrases + rule.keywords if rule_phrases: phrase_embeddings = self.model.encode(rule_phrases) similarities = cosine_similarity(content_embedding, phrase_embeddings)[0] max_similarity = np.max(similarities) if max_similarity > 0.4: # Threshold for semantic similarity best_match_idx = np.argmax(similarities) matches.append(MatchResult( rule_category=rule.category, rule_type='approve', confidence=min(max_similarity, 0.9), matched_phrases=[rule_phrases[best_match_idx]], reasoning=f"Semantically similar to approved concept: {rule_phrases[best_match_idx]}" )) # Check rejection rules for rule in rejection_rules: rule_phrases = rule.phrases + rule.keywords if rule_phrases: phrase_embeddings = self.model.encode(rule_phrases) similarities = cosine_similarity(content_embedding, phrase_embeddings)[0] max_similarity = np.max(similarities) if max_similarity > 0.5: # Higher threshold for rejection best_match_idx = np.argmax(similarities) matches.append(MatchResult( rule_category=rule.category, rule_type='reject', confidence=min(max_similarity, 0.95), matched_phrases=[rule_phrases[best_match_idx]], reasoning=f"Semantically similar to rejected concept: {rule_phrases[best_match_idx]}" )) except Exception as e: print(f" ⚠️ Semantic matching failed: {e}") # Fallback to keyword matching return self._keyword_fallback_matching(content, approval_rules, rejection_rules) return matches def _keyword_fallback_matching(self, content: str, approval_rules: List, rejection_rules: List) -> List[MatchResult]: matches = [] content_lower = content.lower() # Simple keyword matching as fallback for rule in approval_rules: for keyword in rule.keywords: if keyword.lower() in content_lower: matches.append(MatchResult( rule_category=rule.category, rule_type='approve', confidence=0.6, matched_phrases=[keyword], reasoning=f"Contains approved keyword: {keyword}" )) for rule in rejection_rules: for keyword in rule.keywords: if keyword.lower() in content_lower: matches.append(MatchResult( rule_category=rule.category, rule_type='reject', confidence=0.7, matched_phrases=[keyword], reasoning=f"Contains problematic keyword: {keyword}" )) return matches def _generate_reasoning(self, matches: List[MatchResult], approved: bool, net_score: float) -> str: if not matches: return "No significant matches found against brand guidelines." approve_matches = [m for m in matches if m.rule_type == 'approve'] reject_matches = [m for m in matches if m.rule_type == 'reject'] reasoning_parts = [] if approved: reasoning_parts.append("✅ APPROVED:") if approve_matches: top_approve = max(approve_matches, key=lambda x: x.confidence) reasoning_parts.append(f"Strong alignment with {top_approve.rule_category}") if reject_matches: reasoning_parts.append(f"Minor concerns noted but overall positive (score: {net_score:.2f})") else: reasoning_parts.append("❌ REJECTED:") if reject_matches: top_reject = max(reject_matches, key=lambda x: x.confidence) reasoning_parts.append(f"Violates {top_reject.rule_category} guidelines") if approve_matches: reasoning_parts.append("Some positive elements found but insufficient to approve") # Add specific match details if len(matches) <= 3: for match in matches[:3]: reasoning_parts.append(f"• {match.reasoning}") return " ".join(reasoning_parts)