Spaces:
Sleeping
Sleeping
| # 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 ───────────────────────────────────────────────── | |
| 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 ───────────────────────────────────────────────── | |
| class DimensionResult: | |
| key: str | |
| name: str | |
| score: float | |
| reasoning: str | |
| issues: list = field(default_factory=list) | |
| weight: float = 1.0 | |
| 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" | |
| 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, | |
| ) |