# evaluator.py — G-Eval style LLM Evaluation Engine """ Implementa avaliação LLM-as-Judge para 5 dimensões críticas de produção: 1. Faithfulness — resposta é fiel ao contexto? Sem alucinações? 2. Answer Relevance — resposta realmente responde a pergunta? 3. Completeness — cobre todos os aspectos relevantes? 4. Conciseness — sem verbosidade ou padding? 5. Hallucination — detecta afirmações não suportadas pelo contexto Baseado na metodologia G-Eval (Liu et al., 2023) com Chain-of-Thought scoring. """ import json import re from dataclasses import dataclass, field from typing import Optional from openai import OpenAI # ── DIMENSÕES ───────────────────────────────────────────────── @dataclass class EvalDimension: name: str key: str description: str prompt: str weight: float = 1.0 DIMENSIONS = [ EvalDimension( name="Faithfulness", key="faithfulness", description="A resposta é fiel ao contexto fornecido? Sem informações inventadas?", weight=0.30, prompt="""Você é um avaliador especialista em qualidade de LLMs. Avalie a FAITHFULNESS (fidelidade) da resposta ao contexto fornecido. Critério: A resposta contém apenas informações suportadas pelo contexto? Penalize alucinações, afirmações sem base e extrapolações não justificadas. Contexto: {context} Pergunta: {question} Resposta do LLM: {answer} Raciocine passo a passo, identifique cada afirmação e verifique se tem suporte no contexto. Responda APENAS com JSON: {{"score": <1-10>, "reasoning": "análise em 2-3 frases", "issues": ["lista de problemas encontrados, vazia se nenhum"]}}""" ), EvalDimension( name="Answer Relevance", key="relevance", description="A resposta realmente responde o que foi perguntado?", weight=0.25, prompt="""Você é um avaliador especialista em qualidade de LLMs. Avalie a RELEVANCE (relevância) da resposta à pergunta. Critério: A resposta aborda diretamente a pergunta? Sem desvios ou respostas tangenciais? Pergunta: {question} Resposta do LLM: {answer} Raciocine: a resposta responde exatamente o que foi perguntado? Responda APENAS com JSON: {{"score": <1-10>, "reasoning": "análise em 2-3 frases", "issues": ["lista de problemas, vazia se nenhum"]}}""" ), EvalDimension( name="Completeness", key="completeness", description="A resposta cobre todos os aspectos relevantes da pergunta?", weight=0.20, prompt="""Você é um avaliador especialista em qualidade de LLMs. Avalie a COMPLETENESS (completude) da resposta. Critério: A resposta cobre todos os aspectos importantes da pergunta usando o contexto disponível? Contexto: {context} Pergunta: {question} Resposta do LLM: {answer} Raciocine: quais aspectos importantes estão faltando? Responda APENAS com JSON: {{"score": <1-10>, "reasoning": "análise em 2-3 frases", "issues": ["aspectos faltantes, vazio se completo"]}}""" ), EvalDimension( name="Conciseness", key="conciseness", description="A resposta é direta, sem verbosidade ou padding desnecessário?", weight=0.10, prompt="""Você é um avaliador especialista em qualidade de LLMs. Avalie a CONCISENESS (concisão) da resposta. Critério: A resposta é direta ao ponto? Sem repetições, padding ou verbosidade excessiva? Pergunta: {question} Resposta do LLM: {answer} Raciocine: há partes que poderiam ser removidas sem perda de informação? Responda APENAS com JSON: {{"score": <1-10>, "reasoning": "análise em 2-3 frases", "issues": ["redundâncias encontradas, vazio se conciso"]}}""" ), EvalDimension( name="Hallucination Detection", key="hallucination", description="Detecta afirmações específicas não suportadas pelo contexto.", weight=0.15, prompt="""Você é um detector especialista de alucinações em LLMs. Analise a resposta e identifique ALUCINAÇÕES — afirmações específicas, números, nomes ou fatos que NÃO estão no contexto e que o modelo parece ter inventado. Contexto: {context} Pergunta: {question} Resposta do LLM: {answer} Score: 10 = zero alucinações, 1 = resposta completamente alucinada. Responda APENAS com JSON: {{"score": <1-10>, "reasoning": "análise em 2-3 frases", "hallucinations": ["lista de alucinações específicas encontradas, vazia se nenhuma"]}}""" ), ] # ── RESULTADO ───────────────────────────────────────────────── @dataclass class DimensionResult: key: str name: str score: float reasoning: str issues: list = field(default_factory=list) weight: float = 1.0 @dataclass class EvalResult: question: str context: str answer: str dimensions: list # List[DimensionResult] overall_score: float verdict: str # EXCELLENT / GOOD / FAIR / POOR summary: str model_label: str = "LLM" @property def verdict_color(self): if self.overall_score >= 8.5: return "excellent" if self.overall_score >= 7.0: return "good" if self.overall_score >= 5.0: return "fair" return "poor" # ── ENGINE ──────────────────────────────────────────────────── class EvaluationEngine: def __init__(self, openai_api_key: str): self.client = OpenAI(api_key=openai_api_key) self.model = "gpt-4o-mini" def _call(self, prompt: str) -> dict: resp = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.0, max_tokens=400, ) raw = resp.choices[0].message.content.strip() raw = re.sub(r'```json|```', '', raw).strip() return json.loads(raw) def _eval_dimension(self, dim: EvalDimension, question: str, context: str, answer: str) -> DimensionResult: prompt = dim.prompt.format( question=question, context=context or "(nenhum contexto fornecido)", answer=answer, ) try: data = self._call(prompt) score = float(data.get("score", 5)) score = max(1.0, min(10.0, score)) issues = data.get("issues", data.get("hallucinations", [])) if isinstance(issues, str): issues = [issues] if issues else [] except Exception as e: score = 5.0 issues = [f"Erro na avaliação: {e}"] data = {"reasoning": "Erro ao processar"} return DimensionResult( key=dim.key, name=dim.name, score=score, reasoning=data.get("reasoning", ""), issues=[i for i in issues if i], weight=dim.weight, ) def _generate_summary(self, question: str, answer: str, dims: list, overall: float) -> str: weak = [d for d in dims if d.score < 7.0] strong = [d for d in dims if d.score >= 8.5] parts = [] if strong: parts.append(f"Pontos fortes: {', '.join(d.name for d in strong)}.") if weak: parts.append(f"Melhorar: {', '.join(d.name for d in weak)}.") if overall >= 8.5: parts.append("Resposta de alta qualidade para produção.") elif overall >= 7.0: parts.append("Resposta adequada com espaço para melhoria.") elif overall >= 5.0: parts.append("Qualidade abaixo do esperado para produção.") else: parts.append("Resposta inadequada. Revisar o pipeline.") return " ".join(parts) def evaluate(self, question: str, context: str, answer: str, model_label: str = "LLM") -> EvalResult: """Avalia uma resposta em todas as dimensões.""" results = [] for dim in DIMENSIONS: r = self._eval_dimension(dim, question, context, answer) results.append(r) # Weighted average total_w = sum(d.weight for d in results) overall = sum(r.score * r.weight for r in results) / total_w overall = round(overall, 2) verdict = ( "EXCELLENT" if overall >= 8.5 else "GOOD" if overall >= 7.0 else "FAIR" if overall >= 5.0 else "POOR" ) summary = self._generate_summary(question, answer, results, overall) return EvalResult( question=question, context=context, answer=answer, dimensions=results, overall_score=overall, verdict=verdict, summary=summary, model_label=model_label, )