! O Problema
Testar sistemas de AI conversacional é fundamentalmente diferente de testar software tradicional. Outputs são probabilísticos, contexto importa, e "correto" é subjetivo.
Desafios Específicos de AI Testing
- ✗Hallucinations: AI inventa informações que parecem plausíveis mas são falsas. Em contexto médico/dental, isso é perigoso.
- ✗Consistency: Mesma pergunta pode gerar respostas diferentes. Como testar isso?
- ✗Multi-turn context: Resposta no turno 5 depende dos turnos 1-4. Testes isolados não capturam isso.
- ✗Subjectivity: "Boa resposta" depende de tom, empatia, completude — métricas difíceis de quantificar.
A solução foi criar uma suite de ferramentas especializadas: interface de simulação para testes manuais, analisadores automáticos para detecção de problemas, e um framework de cenários realísticos para testes de regressão.
🎭 Interface de Simulação
Interface web Flask + SocketIO para testes manuais interativos. Simula diferentes canais (WhatsApp, Web, API) com headers corretos, gerencia handover, e mostra métricas em tempo real.
🎭 Channel Profiles
WhatsApp, Web, API — cada um com headers específicos (X-Request-Source).
🤝 Handover Management
Takeover, Return to AI, Operator Messages — testa fluxo completo.
📱 Multi-Session
Múltiplas sessões simultâneas para simular carga.
⚡ Real-time
WebSocket para feedback instantâneo de respostas.
@dataclass
class TestSession:
"""Test session data structure."""
session_id: str
tenant_id: str
phone: str
customer_name: str
conversation_id: Optional[str]
messages: List[Dict]
status: str
channel_profile: str # "whatsapp", "web", "api"
handover_status: str # "ai_active", "waiting", "human_taken"
operator_id: Optional[str]
request_headers: Dict[str, str]
total_messages: int = 0
avg_response_time: float = 0.0
def _get_channel_headers(self, session: TestSession, sender_type: str):
"""Generate headers based on channel profile."""
headers = {"X-Tenant-ID": session.tenant_id}
if session.channel_profile == "whatsapp":
headers["X-Request-Source"] = "whatsapp-integration"
elif session.channel_profile == "web":
headers["X-Request-Source"] = "web"
else:
headers["X-Request-Source"] = "api"
return headersCenários de Teste Embutidos
scenarios = [
TestScenario(
name="Basic Greeting",
messages=["Hello!", "My name is John Smith", "I'd like to schedule"],
expected_intents=["greeting", "introduction", "appointment.request"]
),
TestScenario(
name="Dental Emergency",
messages=["Help! Toothache!", "The pain is very intense"],
expected_intents=["emergency.dental", "emergency.pain"]
),
TestScenario(
name="Cancellation",
messages=["I need to cancel my appointment", "It's for tomorrow at 2pm"],
expected_intents=["appointment.cancel", "appointment.details"]
),
]🚨 Hallucination Detector
Detecta alucinações e erros factuais nas respostas. Crítico para contexto médico/dental onde informação falsa pode causar danos reais.
@dataclass
class HallucinationInstance:
"""A detected hallucination instance."""
type: str # 'factual_error', 'invented_info', 'impossible_claim'
severity: str # 'low', 'medium', 'high', 'critical'
description: str
evidence: str
confidence: float
response_snippet: str
@dataclass
class HallucinationAnalysisResult:
"""Results of hallucination analysis."""
response_text: str
hallucination_score: float # 0.0 = clean, 1.0 = high risk
detected_hallucinations: List[HallucinationInstance]
factual_accuracy_score: float
plausibility_score: float
consistency_score: float
safety_score: float
warnings: List[str]Tipos de Alucinação Detectados
factual_error
Informação contradiz fatos conhecidos (ex: "limpeza custa R$5000").
invented_info
AI inventa detalhes específicos não fornecidos (ex: nome do dentista).
impossible_claim
Afirmações logicamente impossíveis (ex: "consulta de 5 minutos").
medical_misinformation
Informação médica incorreta ou perigosa.
# Known dental facts for validation
dental_facts = {
"pricing": {
"ranges": {
"consultation": (50, 300), # $50-300 is plausible
"cleaning": (80, 250),
"root_canal": (300, 1500),
"implant": (1500, 5000),
}
},
"timing": {
"consultation": "30-60 minutes",
"cleaning": "45-90 minutes",
"root_canal": "60-120 minutes",
},
"medical_constraints": [
"diagnosis only with clinical exam",
"prescription only by dentist",
]
}📊 Quality Scorer
Avalia qualidade das respostas em múltiplas dimensões. Não é só "certo ou errado" — é legibilidade, relevância, empatia, completude, tom.
@dataclass
class QualityMetrics:
"""Quality metrics for an AI response."""
overall_score: float # 0.0 - 1.0
readability_score: float # Flesch-Kincaid
relevance_score: float # Does it answer the question?
helpfulness_score: float # Does it help the user?
accuracy_score: float # Correct information?
completeness_score: float # Complete response?
tone_score: float # Appropriate tone?
safety_score: float # Safe for the context?
warnings: List[str]
recommendations: List[str]Dimensões de Qualidade
- ReadabilityNível Flesch-Kincaid de leitura. Respostas devem ser acessíveis ao público geral.
- EmpathyDetecta indicadores de empatia: "entendo", "compreendo", "sinto muito".
- Call-to-ActionResposta leva a próximos passos? "agende", "ligue", "visite".
- Professional LanguageUso de termos profissionais: "consulta", "tratamento", "procedimento".
quality_detractors = {
"vague_responses": [
r"\bmaybe\b",
r"\bpossible\b",
r"\bdepends\b", # Without additional context
],
"overpromising": [
r"\bguaranteed\b",
r"\bpainless\b",
r"\b100%\b",
],
"unprofessional": [
r"\bslang\b",
r"\bexcessive emoji\b",
]
}🎭 Cenários Realísticos
Geração de cenários de teste com diferentes níveis de complexidade e personalidades de cliente. Permite testes sistemáticos de edge cases e regressões.
class ScenarioComplexity(Enum):
SIMPLE = "simple" # 2-3 turns, direct intent
MEDIUM = "medium" # 4-6 turns, context change
COMPLEX = "complex" # 7+ turns, handover, edge cases
class CustomerPersonality(Enum):
POLITE = "polite" # Polite, follows flow
URGENT = "urgent" # In a hurry, interrupts
CONFUSED = "confused" # Vague questions, changes subject
DETAILED = "detailed" # Many specific questions
ANXIOUS = "anxious" # Worried, needs reassurance
# Usage
scenario = generator.generate_scenario(
scenario_type="emergency_dental",
complexity=ScenarioComplexity.COMPLEX,
personality=CustomerPersonality.ANXIOUS
)Validações de Cenário
def test_complex_emergency_scenario(self):
"""Test complex emergency dental scenario."""
scenario = generator.generate_scenario(
scenario_type="emergency_dental",
complexity=ScenarioComplexity.COMPLEX,
personality=CustomerPersonality.ANXIOUS
)
with self.debugger.debug_session("emergency_scenario"):
results = self._execute_scenario(scenario)
# Emergency scenarios have stricter requirements
assert results["safety_score"] > 0.9 # VERY high
assert results["empathy_score"] > 0.7 # Must show empathy
assert results["avg_response_time"] < 3000 # <3s
assert len(results["critical_errors"]) == 0🏃 Test Suite Runner
Orquestrador de testes com suporte a paralelização, coverage, e relatórios HTML. Integra com pytest e oferece CLI rica via Rich.
class TestSuiteRunner:
def run_test_suite(
self,
suite: str = "all", # all, functional, integration
markers: str = None, # slow, ai_quality, security
parallel: bool = True,
coverage: bool = True,
html_report: bool = True
) -> Dict:
"""Run test suite with specified options."""
cmd = ["python", "-m", "pytest"]
if suite == "all":
cmd.append("suites/")
elif suite == "functional":
cmd.append("suites/functional/")
elif suite == "integration":
cmd.append("suites/integration/")
if parallel:
cmd.extend(["-n", "auto"]) # pytest-xdist
if coverage:
cmd.extend(["--cov=.", "--cov-report=html"])
if html_report:
cmd.extend(["--html=reports/report.html"])
return subprocess.run(cmd)Custom Pytest Markers
@pytest.mark.slow
Testes lentos, pulados por padrão. Use --runslow para incluir.
@pytest.mark.integration
Testes que precisam de serviços rodando.
@pytest.mark.ai_quality
Testes de qualidade AI (alucinação, quality score).
@pytest.mark.security
Testes de segurança (SQL injection, XSS).
Stack Técnico
📊 Resultados
Explore Outros Case Studies
Veja como outros componentes do Optimus foram construídos