""" CXInsights - Compression Module Tests Tests for transcript compression and semantic extraction. """ import pytest from src.compression.compressor import ( TranscriptCompressor, compress_for_prompt, compress_transcript, ) from src.compression.models import ( AgentOffer, CompressionConfig, CompressedTranscript, CustomerIntent, CustomerObjection, IntentType, KeyMoment, ObjectionType, ResolutionStatement, ResolutionType, ) from src.transcription.models import SpeakerTurn, Transcript, TranscriptMetadata class TestCustomerIntent: """Tests for CustomerIntent model.""" def test_to_prompt_text(self): """Test prompt text generation.""" intent = CustomerIntent( intent_type=IntentType.CANCEL, description="Customer wants to cancel service", confidence=0.9, verbatim_quotes=["quiero cancelar mi servicio"], ) text = intent.to_prompt_text() assert "CANCEL" in text assert "quiero cancelar" in text def test_to_prompt_text_no_quotes(self): """Test prompt text without quotes.""" intent = CustomerIntent( intent_type=IntentType.INQUIRY, description="Customer asking about prices", confidence=0.8, ) text = intent.to_prompt_text() assert "INQUIRY" in text assert "Evidence:" not in text class TestCustomerObjection: """Tests for CustomerObjection model.""" def test_addressed_status(self): """Test addressed status in prompt text.""" addressed = CustomerObjection( objection_type=ObjectionType.PRICE, description="Too expensive", turn_index=5, verbatim="Es muy caro", addressed=True, ) unaddressed = CustomerObjection( objection_type=ObjectionType.PRICE, description="Too expensive", turn_index=5, verbatim="Es muy caro", addressed=False, ) assert "[ADDRESSED]" in addressed.to_prompt_text() assert "[UNADDRESSED]" in unaddressed.to_prompt_text() class TestAgentOffer: """Tests for AgentOffer model.""" def test_acceptance_status(self): """Test acceptance status in prompt text.""" accepted = AgentOffer( offer_type="discount", description="10% discount", turn_index=10, verbatim="Le ofrezco un 10% de descuento", accepted=True, ) rejected = AgentOffer( offer_type="discount", description="10% discount", turn_index=10, verbatim="Le ofrezco un 10% de descuento", accepted=False, ) pending = AgentOffer( offer_type="discount", description="10% discount", turn_index=10, verbatim="Le ofrezco un 10% de descuento", accepted=None, ) assert "[ACCEPTED]" in accepted.to_prompt_text() assert "[REJECTED]" in rejected.to_prompt_text() assert "[ACCEPTED]" not in pending.to_prompt_text() assert "[REJECTED]" not in pending.to_prompt_text() class TestCompressedTranscript: """Tests for CompressedTranscript model.""" def test_to_prompt_text_basic(self): """Test basic prompt text generation.""" compressed = CompressedTranscript( call_id="TEST001", customer_intents=[ CustomerIntent( intent_type=IntentType.CANCEL, description="Wants to cancel", confidence=0.9, ) ], objections=[ CustomerObjection( objection_type=ObjectionType.PRICE, description="Too expensive", turn_index=5, verbatim="Es caro", ) ], ) text = compressed.to_prompt_text() assert "CUSTOMER INTENT" in text assert "CUSTOMER OBJECTIONS" in text assert "CANCEL" in text assert "price" in text.lower() def test_to_prompt_text_empty(self): """Test prompt text with no elements.""" compressed = CompressedTranscript(call_id="EMPTY001") text = compressed.to_prompt_text() # Should be mostly empty but not fail assert len(text) >= 0 def test_to_prompt_text_truncation(self): """Test prompt text truncation.""" compressed = CompressedTranscript( call_id="LONG001", key_moments=[ KeyMoment( moment_type="test", description="x" * 500, turn_index=i, start_time=float(i), verbatim="y" * 200, speaker="customer", ) for i in range(50) ], ) text = compressed.to_prompt_text(max_chars=1000) assert len(text) <= 1000 assert "truncated" in text def test_get_stats(self): """Test statistics generation.""" compressed = CompressedTranscript( call_id="STATS001", original_turn_count=50, original_char_count=10000, compressed_char_count=2000, compression_ratio=0.8, customer_intents=[ CustomerIntent(IntentType.CANCEL, "test", 0.9) ], objections=[ CustomerObjection(ObjectionType.PRICE, "test", 0, "test") ], ) stats = compressed.get_stats() assert stats["original_turns"] == 50 assert stats["original_chars"] == 10000 assert stats["compressed_chars"] == 2000 assert stats["compression_ratio"] == 0.8 assert stats["intents_extracted"] == 1 assert stats["objections_extracted"] == 1 class TestTranscriptCompressor: """Tests for TranscriptCompressor.""" @pytest.fixture def sample_transcript(self): """Create a sample transcript for testing.""" return Transcript( call_id="COMP001", turns=[ SpeakerTurn( speaker="agent", text="Hola, buenos días, gracias por llamar.", start_time=0.0, end_time=2.0, ), SpeakerTurn( speaker="customer", text="Hola, quiero cancelar mi servicio porque es muy caro.", start_time=2.5, end_time=5.0, ), SpeakerTurn( speaker="agent", text="Entiendo. Le puedo ofrecer un 20% de descuento.", start_time=5.5, end_time=8.0, ), SpeakerTurn( speaker="customer", text="No gracias, ya tomé la decisión.", start_time=8.5, end_time=10.0, ), SpeakerTurn( speaker="agent", text="Entiendo, si cambia de opinión estamos para ayudarle.", start_time=10.5, end_time=13.0, ), ], metadata=TranscriptMetadata( audio_duration_sec=60.0, language="es", ), ) def test_compress_extracts_intent(self, sample_transcript): """Test that cancel intent is extracted.""" compressor = TranscriptCompressor() compressed = compressor.compress(sample_transcript) assert len(compressed.customer_intents) > 0 assert any( i.intent_type == IntentType.CANCEL for i in compressed.customer_intents ) def test_compress_extracts_price_objection(self, sample_transcript): """Test that price objection is extracted.""" compressor = TranscriptCompressor() compressed = compressor.compress(sample_transcript) assert len(compressed.objections) > 0 assert any( o.objection_type == ObjectionType.PRICE for o in compressed.objections ) def test_compress_extracts_offer(self, sample_transcript): """Test that agent offer is extracted.""" compressor = TranscriptCompressor() compressed = compressor.compress(sample_transcript) assert len(compressed.agent_offers) > 0 def test_compress_extracts_key_moments(self, sample_transcript): """Test that key moments are extracted.""" compressor = TranscriptCompressor() compressed = compressor.compress(sample_transcript) # Should find rejection and firm_decision moment_types = [m.moment_type for m in compressed.key_moments] assert len(moment_types) > 0 def test_compression_ratio(self, sample_transcript): """Test that compression ratio is calculated.""" compressor = TranscriptCompressor() compressed = compressor.compress(sample_transcript) assert compressed.compression_ratio > 0 assert compressed.original_char_count > compressed.compressed_char_count def test_compression_respects_max_limits(self, sample_transcript): """Test that max limits are respected.""" config = CompressionConfig( max_intents=1, max_offers=1, max_objections=1, max_key_moments=2, ) compressor = TranscriptCompressor(config=config) compressed = compressor.compress(sample_transcript) assert len(compressed.customer_intents) <= 1 assert len(compressed.agent_offers) <= 1 assert len(compressed.objections) <= 1 assert len(compressed.key_moments) <= 2 def test_generates_summary(self, sample_transcript): """Test that summary is generated.""" compressor = TranscriptCompressor() compressed = compressor.compress(sample_transcript) assert len(compressed.call_summary) > 0 assert "cancel" in compressed.call_summary.lower() class TestIntentExtraction: """Tests for specific intent patterns.""" def make_transcript(self, customer_text: str) -> Transcript: """Helper to create transcript with customer turn.""" return Transcript( call_id="INT001", turns=[ SpeakerTurn(speaker="agent", text="Hola", start_time=0, end_time=1), SpeakerTurn(speaker="customer", text=customer_text, start_time=1, end_time=3), ], ) def test_cancel_intent_patterns(self): """Test various cancel intent patterns.""" patterns = [ "Quiero cancelar mi servicio", "Quiero dar de baja mi cuenta", "No quiero continuar con el servicio", ] compressor = TranscriptCompressor() for pattern in patterns: transcript = self.make_transcript(pattern) compressed = compressor.compress(transcript) assert any( i.intent_type == IntentType.CANCEL for i in compressed.customer_intents ), f"Failed for: {pattern}" def test_purchase_intent_patterns(self): """Test purchase intent patterns.""" patterns = [ "Quiero contratar el plan premium", "Me interesa comprar el servicio", ] compressor = TranscriptCompressor() for pattern in patterns: transcript = self.make_transcript(pattern) compressed = compressor.compress(transcript) assert any( i.intent_type == IntentType.PURCHASE for i in compressed.customer_intents ), f"Failed for: {pattern}" def test_complaint_intent_patterns(self): """Test complaint intent patterns.""" patterns = [ "Tengo un problema con mi factura", "Estoy muy molesto con el servicio", "Quiero poner una queja", ] compressor = TranscriptCompressor() for pattern in patterns: transcript = self.make_transcript(pattern) compressed = compressor.compress(transcript) assert any( i.intent_type == IntentType.COMPLAINT for i in compressed.customer_intents ), f"Failed for: {pattern}" class TestObjectionExtraction: """Tests for objection pattern extraction.""" def make_transcript(self, customer_text: str) -> Transcript: """Helper to create transcript with customer turn.""" return Transcript( call_id="OBJ001", turns=[ SpeakerTurn(speaker="agent", text="Hola", start_time=0, end_time=1), SpeakerTurn(speaker="customer", text=customer_text, start_time=1, end_time=3), ], ) def test_price_objection_patterns(self): """Test price objection patterns.""" patterns = [ "Es muy caro para mí", "Es demasiado costoso", "No tengo el dinero ahora", "Está fuera de mi presupuesto", ] compressor = TranscriptCompressor() for pattern in patterns: transcript = self.make_transcript(pattern) compressed = compressor.compress(transcript) assert any( o.objection_type == ObjectionType.PRICE for o in compressed.objections ), f"Failed for: {pattern}" def test_timing_objection_patterns(self): """Test timing objection patterns.""" patterns = [ "No es buen momento", "Déjame pensarlo", "Lo voy a pensar", ] compressor = TranscriptCompressor() for pattern in patterns: transcript = self.make_transcript(pattern) compressed = compressor.compress(transcript) assert any( o.objection_type == ObjectionType.TIMING for o in compressed.objections ), f"Failed for: {pattern}" class TestConvenienceFunctions: """Tests for convenience functions.""" @pytest.fixture def sample_transcript(self): """Create sample transcript.""" return Transcript( call_id="CONV001", turns=[ SpeakerTurn(speaker="agent", text="Hola", start_time=0, end_time=1), SpeakerTurn( speaker="customer", text="Quiero cancelar, es muy caro", start_time=1, end_time=3, ), ], ) def test_compress_transcript(self, sample_transcript): """Test compress_transcript function.""" compressed = compress_transcript(sample_transcript) assert isinstance(compressed, CompressedTranscript) assert compressed.call_id == "CONV001" def test_compress_for_prompt(self, sample_transcript): """Test compress_for_prompt function.""" text = compress_for_prompt(sample_transcript) assert isinstance(text, str) assert len(text) > 0 def test_compress_for_prompt_max_chars(self, sample_transcript): """Test max_chars parameter.""" text = compress_for_prompt(sample_transcript, max_chars=100) assert len(text) <= 100