File size: 11,441 Bytes
971cb75 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | # ==============================================================================
# AetherMap RAG Evaluation Script
# Avalia qualidade do sistema RAG usando RAGAS (sem ground-truth)
# ==============================================================================
"""
Uso:
python evaluate_rag.py --api-url https://madras1-aethermap.hf.space --dataset data.csv --text-column texto
Métricas avaliadas (sem precisar de ground-truth):
- Faithfulness: A resposta é fiel ao contexto recuperado?
- Answer Relevancy: A resposta é relevante à pergunta?
"""
import os
import json
import argparse
import requests
from typing import List, Dict, Any
from dataclasses import dataclass
from datetime import datetime
# Avaliação
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy
from datasets import Dataset
# LLM para avaliação (Groq via LangChain)
from langchain_groq import ChatGroq
@dataclass
class RAGSample:
"""Representa uma amostra para avaliação RAG."""
question: str
answer: str
contexts: List[str]
class AetherMapEvaluator:
"""Avaliador de RAG para o AetherMap."""
def __init__(self, api_url: str, groq_api_key: str = None):
self.api_url = api_url.rstrip("/")
self.groq_api_key = groq_api_key or os.environ.get("GROQ_API_KEY")
self.job_id = None
if not self.groq_api_key:
raise ValueError("GROQ_API_KEY não encontrada. Defina via argumento ou variável de ambiente.")
# LLM avaliador (diferente do modelo de geração)
self.eval_llm = ChatGroq(
model="llama-3.3-70b-versatile", # Modelo forte para avaliação
api_key=self.groq_api_key,
temperature=0
)
def upload_dataset(self, file_path: str, text_column: str = None, n_samples: int = 1000) -> bool:
"""Faz upload de um dataset para o AetherMap."""
print(f"📤 Uploading dataset: {file_path}")
with open(file_path, "rb") as f:
files = {"file": (os.path.basename(file_path), f)}
data = {"n_samples": n_samples}
if text_column and file_path.lower().endswith(".csv"):
data["text_column"] = text_column
response = requests.post(f"{self.api_url}/process/", files=files, data=data)
if response.status_code != 200:
print(f"❌ Erro no upload: {response.text}")
return False
result = response.json()
self.job_id = result["job_id"]
print(f"✅ Dataset carregado! Job ID: {self.job_id}")
print(f" 📊 {result['metadata']['num_documents_processed']} documentos")
print(f" 🎯 {result['metadata']['num_clusters_found']} clusters")
return True
def search(self, query: str) -> RAGSample:
"""Executa uma busca e retorna os componentes RAG."""
if not self.job_id:
raise ValueError("Nenhum dataset carregado. Use upload_dataset primeiro.")
response = requests.post(
f"{self.api_url}/search/",
data={"query": query, "job_id": self.job_id}
)
if response.status_code != 200:
raise Exception(f"Erro na busca: {response.text}")
result = response.json()
# Extrair contextos (precisamos buscar os textos completos)
contexts = []
for res in result.get("results", []):
# O resultado inclui o índice, precisamos do texto
# Por enquanto, vamos usar o summary que inclui as citações
pass
# Como não temos acesso direto aos textos via API,
# vamos extrair do summary as partes entre [ID: x]
# Ou podemos modificar a API para retornar os contextos
return RAGSample(
question=query,
answer=result.get("summary", ""),
contexts=contexts if contexts else [result.get("summary", "")] # Fallback
)
def search_with_contexts(self, query: str) -> RAGSample:
"""
Executa busca e extrai contextos completos.
Requer endpoint modificado ou acesso ao cache.
"""
if not self.job_id:
raise ValueError("Nenhum dataset carregado.")
# Primeira chamada: buscar documentos relevantes
response = requests.post(
f"{self.api_url}/search/",
data={"query": query, "job_id": self.job_id}
)
if response.status_code != 200:
raise Exception(f"Erro: {response.text}")
result = response.json()
answer = result.get("summary", "")
# Extrair contextos dos resultados
# Nota: Idealmente o endpoint deveria retornar os textos completos
# Por agora, usamos os índices para referência
contexts = []
for res in result.get("results", []):
# Placeholder - em produção, você buscaria o texto real
contexts.append(f"Documento #{res['index']} (score: {res['score']:.3f})")
# Se não conseguiu contextos, usa o summary como fallback
if not contexts:
contexts = [answer]
return RAGSample(
question=query,
answer=answer,
contexts=contexts
)
def evaluate_queries(self, queries: List[str], verbose: bool = True) -> Dict[str, Any]:
"""
Avalia uma lista de queries usando RAGAS.
Métricas:
- faithfulness: Resposta é fiel ao contexto?
- answer_relevancy: Resposta é relevante à pergunta?
"""
print(f"\n🔍 Avaliando {len(queries)} queries...")
samples = []
for i, query in enumerate(queries):
if verbose:
print(f" [{i+1}/{len(queries)}] {query[:50]}...")
try:
sample = self.search_with_contexts(query)
samples.append(sample)
except Exception as e:
print(f" ⚠️ Erro na query {i+1}: {e}")
if not samples:
return {"error": "Nenhuma amostra válida coletada."}
# Preparar dataset para RAGAS
eval_data = {
"question": [s.question for s in samples],
"answer": [s.answer for s in samples],
"contexts": [s.contexts for s in samples],
}
dataset = Dataset.from_dict(eval_data)
print("\n📊 Executando avaliação RAGAS...")
# Avaliar com RAGAS
try:
result = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy],
llm=self.eval_llm,
)
return {
"metrics": {
"faithfulness": float(result["faithfulness"]),
"answer_relevancy": float(result["answer_relevancy"]),
},
"num_samples": len(samples),
"timestamp": datetime.now().isoformat(),
}
except Exception as e:
return {"error": str(e)}
def generate_test_queries(self, n: int = 10) -> List[str]:
"""
Gera queries de teste baseadas no dataset carregado.
Usa o LLM para criar perguntas relevantes.
"""
# Queries genéricas de exemplo
# Em produção, você geraria baseado no conteúdo real
return [
"Qual é o tema principal dos documentos?",
"Quais são as entidades mais mencionadas?",
"Existe alguma tendência nos dados?",
"Quais são os pontos mais controversos?",
"Qual a conclusão principal?",
"Existem contradições entre os documentos?",
"Qual documento é mais relevante para o tema central?",
"Quais termos técnicos são mais usados?",
"Há alguma relação temporal entre os eventos?",
"Qual é o sentimento geral dos textos?",
][:n]
def main():
parser = argparse.ArgumentParser(description="Avaliador RAG para AetherMap")
parser.add_argument("--api-url", default="https://madras1-aethermap.hf.space",
help="URL da API do AetherMap")
parser.add_argument("--dataset", help="Arquivo de dataset para upload (CSV ou TXT)")
parser.add_argument("--text-column", help="Coluna de texto para CSV")
parser.add_argument("--job-id", help="Job ID existente (pula upload)")
parser.add_argument("--queries", nargs="+", help="Queries para testar")
parser.add_argument("--queries-file", help="Arquivo com queries (uma por linha)")
parser.add_argument("--n-samples", type=int, default=1000, help="Número de amostras do dataset")
parser.add_argument("--output", help="Arquivo de saída para resultados (JSON)")
args = parser.parse_args()
# Inicializar avaliador
evaluator = AetherMapEvaluator(api_url=args.api_url)
# Upload dataset ou usar job existente
if args.job_id:
evaluator.job_id = args.job_id
print(f"📎 Usando Job ID existente: {args.job_id}")
elif args.dataset:
if not evaluator.upload_dataset(args.dataset, args.text_column, args.n_samples):
return
else:
print("❌ Forneça --dataset ou --job-id")
return
# Carregar queries
queries = []
if args.queries:
queries = args.queries
elif args.queries_file:
with open(args.queries_file, "r", encoding="utf-8") as f:
queries = [line.strip() for line in f if line.strip()]
else:
print("⚡ Gerando queries de teste...")
queries = evaluator.generate_test_queries(10)
print(f"📝 {len(queries)} queries para avaliar")
# Executar avaliação
results = evaluator.evaluate_queries(queries)
# Mostrar resultados
print("\n" + "="*50)
print("📊 RESULTADOS DA AVALIAÇÃO RAGAS")
print("="*50)
if "error" in results:
print(f"❌ Erro: {results['error']}")
else:
metrics = results["metrics"]
print(f"\n🎯 Faithfulness: {metrics['faithfulness']:.3f}")
print(f"🎯 Answer Relevancy: {metrics['answer_relevancy']:.3f}")
print(f"\n📈 Amostras avaliadas: {results['num_samples']}")
# Interpretação
avg_score = (metrics['faithfulness'] + metrics['answer_relevancy']) / 2
if avg_score >= 0.8:
print("\n✅ Excelente! O RAG está performando muito bem.")
elif avg_score >= 0.6:
print("\n🟡 Bom, mas há espaço para melhorias.")
else:
print("\n🔴 Precisa de melhorias significativas.")
# Salvar resultados
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\n💾 Resultados salvos em: {args.output}")
if __name__ == "__main__":
main()
|