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()