Madras1 commited on
Commit
7613840
·
verified ·
1 Parent(s): 5a2a4d1

Upload 6 files

Browse files
Files changed (6) hide show
  1. .gitattributes +35 -35
  2. Dockerfile +33 -33
  3. README.md +11 -11
  4. app.py +608 -461
  5. requirements.txt +22 -21
  6. stopwords.txt +545 -545
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,33 +1,33 @@
1
- # ==============================================================================
2
- # Dockerfile — AetherMap API (versão profissional)
3
- # ==============================================================================
4
-
5
- # Imagem Python robusta (não slim → evita erros de build)
6
- FROM python:3.10
7
-
8
- # Define diretório da aplicação
9
- WORKDIR /app
10
-
11
- # --- INSTALAR TORCH CPU ANTES (CRÍTICO!) ---
12
- # Isso garante que a versão certa (CPU) seja instalada
13
- RUN pip install --no-cache-dir \
14
- torch \
15
- torchvision \
16
- torchaudio \
17
- --index-url https://download.pytorch.org/whl/cpu
18
-
19
-
20
- # Copiar requirements
21
- COPY requirements.txt .
22
-
23
- # Instalar dependências restantes
24
- RUN pip install --no-cache-dir -r requirements.txt
25
-
26
- # Copiar código da aplicação
27
- COPY . .
28
-
29
- # Expor porta usada pelo Hugging Face Spaces
30
- EXPOSE 7860
31
-
32
- # Comando padrão para executar FastAPI
33
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # ==============================================================================
2
+ # Dockerfile — AetherMap API (versão profissional)
3
+ # ==============================================================================
4
+
5
+ # Imagem Python robusta (não slim → evita erros de build)
6
+ FROM python:3.10
7
+
8
+ # Define diretório da aplicação
9
+ WORKDIR /app
10
+
11
+ # --- INSTALAR TORCH CPU ANTES (CRÍTICO!) ---
12
+ # Isso garante que a versão certa (CPU) seja instalada
13
+ RUN pip install --no-cache-dir \
14
+ torch \
15
+ torchvision \
16
+ torchaudio \
17
+ --index-url https://download.pytorch.org/whl/cpu
18
+
19
+
20
+ # Copiar requirements
21
+ COPY requirements.txt .
22
+
23
+ # Instalar dependências restantes
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copiar código da aplicação
27
+ COPY . .
28
+
29
+ # Expor porta usada pelo Hugging Face Spaces
30
+ EXPOSE 7860
31
+
32
+ # Comando padrão para executar FastAPI
33
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,11 @@
1
- ---
2
- title: AetherMap
3
- emoji: 🦀
4
- colorFrom: indigo
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: AetherMap
3
+ emoji: 🦀
4
+ colorFrom: indigo
5
+ colorTo: pink
6
+ sdk: docker
7
+ pinned: false
8
+ license: apache-2.0
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -1,462 +1,609 @@
1
- # ==============================================================================
2
- # API do AetherMap — VERSÃO 7.1 (OBSERVABILITY EDITION)
3
- # Backend com RAG Híbrido, Citações Nativas e Monitoramento Prometheus
4
- # ==============================================================================
5
-
6
- import numpy as np
7
- import pandas as pd
8
- import torch
9
- import gc
10
- import uuid
11
- import os
12
- import json
13
- import logging
14
- import time # Adicionado para medir tempo
15
- import nltk
16
- from nltk.corpus import stopwords
17
-
18
- from fastapi import FastAPI, UploadFile, File, Form, HTTPException
19
- from fastapi.responses import JSONResponse
20
- from typing import List, Dict, Any
21
- from functools import lru_cache
22
-
23
- # Ferramentas de Alquimia (ML & NLP)
24
- from sentence_transformers import SentenceTransformer, CrossEncoder
25
- import umap
26
- import hdbscan
27
- from sklearn.preprocessing import StandardScaler
28
- from sklearn.metrics.pairwise import cosine_similarity
29
- from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
30
- from scipy.stats import entropy
31
-
32
- # Monitoramento (O Toque da Berta)
33
- from prometheus_fastapi_instrumentator import Instrumentator
34
- from prometheus_client import Histogram
35
-
36
- # A Conexão com o Oráculo
37
- from groq import Groq
38
-
39
- # ==============================================================================
40
- # CONFIGURAÇÕES GERAIS E LOGGING
41
- # ==============================================================================
42
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
43
-
44
- # Modelos de IA
45
- RETRIEVAL_MODEL = "all-MiniLM-L6-v2" # Rápido para varredura inicial
46
- RERANKER_MODEL = "cross-encoder/ms-marco-MiniLM-L-6-v2" # Preciso para reordenação
47
-
48
- # Parâmetros de Processamento
49
- BATCH_SIZE = 256
50
- UMAP_N_NEIGHBORS = 30
51
-
52
- # Cache de Sessão (Na memória RAM)
53
- cache: Dict[str, Any] = {}
54
-
55
- # Definição de Métricas Customizadas do Prometheus
56
- # Isso permite separar a latência da sua lógica vs a latência da API externa
57
- GROQ_LATENCY = Histogram(
58
- "groq_api_latency_seconds",
59
- "Tempo de resposta da API externa Groq (LLM Generation)",
60
- buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0]
61
- )
62
-
63
- # Inicialização do Cliente Groq
64
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
65
- try:
66
- if not GROQ_API_KEY:
67
- logging.warning("GROQ_API_KEY não encontrada. Funcionalidades de LLM estarão indisponíveis.")
68
- groq_client = None
69
- else:
70
- groq_client = Groq(api_key=GROQ_API_KEY)
71
- logging.info("Cliente Groq inicializado com sucesso.")
72
- except Exception as e:
73
- logging.error(f"FALHA AO INICIALIZAR GROQ: {e}")
74
- groq_client = None
75
-
76
-
77
- # ==============================================================================
78
- # GERENCIAMENTO HÍBRIDO DE STOP WORDS (NLTK + ARQUIVO TXT)
79
- # ==============================================================================
80
- def carregar_stopwords():
81
- """
82
- Carrega stop words do NLTK e combina com um arquivo externo 'stopwords.txt'.
83
- """
84
- logging.info("Iniciando carregamento de Stop Words...")
85
-
86
- # 1. Base Gramatical (NLTK - Inglês e Português)
87
- try:
88
- nltk.data.find('corpora/stopwords')
89
- except LookupError:
90
- logging.info("Baixando corpus de stopwords...")
91
- nltk.download('stopwords')
92
-
93
- # Cria um conjunto único com PT e EN
94
- final_stops = set(stopwords.words('portuguese')) | set(stopwords.words('english'))
95
- logging.info(f"Stopwords base (NLTK) carregadas: {len(final_stops)}")
96
-
97
- # 2. Base Customizada
98
- arquivo_custom = "stopwords.txt"
99
-
100
- if os.path.exists(arquivo_custom):
101
- logging.info(f"Arquivo '{arquivo_custom}' encontrado. Lendo palavras customizadas...")
102
- try:
103
- count_custom = 0
104
- with open(arquivo_custom, "r", encoding="utf-8") as f:
105
- for linha in f:
106
- palavra = linha.split('#')[0].strip().lower()
107
- if palavra and len(palavra) > 1:
108
- final_stops.add(palavra)
109
- count_custom += 1
110
- logging.info(f"{count_custom} stop words customizadas importadas do arquivo.")
111
- except Exception as e:
112
- logging.error(f"Erro ao ler '{arquivo_custom}': {e}")
113
- else:
114
- logging.warning(f"Arquivo '{arquivo_custom}' não encontrado. Usando apenas NLTK.")
115
-
116
- lista_final = list(final_stops)
117
- logging.info(f"Total final de Stop Words ativas: {len(lista_final)}")
118
- return lista_final
119
-
120
- # Variável global carregada na inicialização
121
- STOP_WORDS_MULTILINGUAL = carregar_stopwords()
122
-
123
-
124
- # ==============================================================================
125
- # CARREGAMENTO DE MODELOS (COM CACHE)
126
- # ==============================================================================
127
- @lru_cache(maxsize=1)
128
- def load_retriever():
129
- device = "cuda" if torch.cuda.is_available() else "cpu"
130
- logging.info(f"Carregando Retriever '{RETRIEVAL_MODEL}' em: {device}")
131
- return SentenceTransformer(RETRIEVAL_MODEL, device=device)
132
-
133
- @lru_cache(maxsize=1)
134
- def load_reranker():
135
- device = "cuda" if torch.cuda.is_available() else "cpu"
136
- logging.info(f"Carregando Reranker '{RERANKER_MODEL}' em: {device}")
137
- return CrossEncoder(RERANKER_MODEL, device=device)
138
-
139
-
140
- # ==============================================================================
141
- # PIPELINE DE PROCESSAMENTO DE DADOS
142
- # ==============================================================================
143
- def preparar_textos(file_bytes: bytes, n_samples: int) -> List[str]:
144
- linhas = file_bytes.decode("utf-8", errors="ignore").splitlines()
145
- textos = [s for line in linhas if (s := line.strip()) and len(s.split()) > 3]
146
- return textos[:n_samples]
147
-
148
- def processar_pipeline(textos: List[str]) -> (pd.DataFrame, np.ndarray):
149
- logging.info(f"Iniciando pipeline para {len(textos)} textos...")
150
- model = load_retriever()
151
-
152
- # 1. Embeddings
153
- embeddings = model.encode(textos, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
154
-
155
- # 2. UMAP
156
- reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric="cosine", random_state=42)
157
- emb_3d = reducer.fit_transform(embeddings)
158
- emb_3d = StandardScaler().fit_transform(emb_3d)
159
-
160
- # 3. HDBSCAN
161
- num_textos = len(textos)
162
- min_size = max(10, int(num_textos * 0.02))
163
- logging.info(f"HDBSCAN min_cluster_size: {min_size}")
164
-
165
- clusterer = hdbscan.HDBSCAN(min_cluster_size=min_size)
166
- clusters = clusterer.fit_predict(emb_3d)
167
-
168
- # 4. DataFrame
169
- df = pd.DataFrame({
170
- "x": emb_3d[:, 0], "y": emb_3d[:, 1], "z": emb_3d[:, 2],
171
- "full_text": textos, "cluster": clusters.astype(str)
172
- })
173
-
174
- del reducer, clusterer, emb_3d; gc.collect()
175
- return df, embeddings
176
-
177
- def calcular_metricas(textos: List[str]) -> Dict[str, Any]:
178
- logging.info("Calculando métricas globais...")
179
- if not textos: return {}
180
-
181
- vectorizer_count = CountVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
182
- vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
183
-
184
- try:
185
- counts_matrix = vectorizer_count.fit_transform(textos)
186
- tfidf_matrix = vectorizer_tfidf.fit_transform(textos)
187
- except ValueError:
188
- return {"riqueza_lexical": 0, "top_tfidf_palavras": [], "entropia": 0.0}
189
-
190
- vocab_count = vectorizer_count.get_feature_names_out()
191
- contagens = counts_matrix.sum(axis=0).A1
192
-
193
- vocab_tfidf = vectorizer_tfidf.get_feature_names_out()
194
- soma_tfidf = tfidf_matrix.sum(axis=0).A1
195
- top_idx_tfidf = np.argsort(soma_tfidf)[-10:][::-1]
196
- top_tfidf = [{"palavra": vocab_tfidf[i], "score": round(float(soma_tfidf[i]), 4)} for i in top_idx_tfidf]
197
-
198
- return {
199
- "riqueza_lexical": len(vocab_count),
200
- "top_tfidf_palavras": top_tfidf,
201
- "entropia": float(entropy(contagens / contagens.sum(), base=2)) if contagens.sum() > 0 else 0.0
202
- }
203
-
204
- def encontrar_duplicados(df: pd.DataFrame, embeddings: np.ndarray) -> Dict[str, Any]:
205
- logging.info("Detectando duplicados...")
206
- mask = df["full_text"].duplicated(keep=False)
207
- grupos_exatos = {t: [int(i) for i in idxs] for t, idxs in df[mask].groupby("full_text").groups.items()}
208
- pares_semanticos = []
209
-
210
- if 2 < len(embeddings) < 5000:
211
- sim = cosine_similarity(embeddings)
212
- triu_indices = np.triu_indices_from(sim, k=1)
213
- sim_vetor = sim[triu_indices]
214
- pares_idx = np.where(sim_vetor > 0.98)[0]
215
- top_pares_idx = pares_idx[np.argsort(sim_vetor[pares_idx])[-5:][::-1]]
216
- for i in top_pares_idx:
217
- idx1, idx2 = triu_indices[0][i], triu_indices[1][i]
218
- if df["full_text"].iloc[idx1] != df["full_text"].iloc[idx2]:
219
- pares_semanticos.append({
220
- "similaridade": float(sim[idx1, idx2]),
221
- "texto1": df["full_text"].iloc[idx1],
222
- "texto2": df["full_text"].iloc[idx2]
223
- })
224
- return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos}
225
-
226
- def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
227
- logging.info("Analisando clusters...")
228
- analise = {}
229
- ids_clusters_validos = sorted([c for c in df["cluster"].unique() if c != "-1"], key=int)
230
- for cid in ids_clusters_validos:
231
- textos_cluster = df[df["cluster"] == cid]["full_text"].tolist()
232
- if len(textos_cluster) < 2: continue
233
- try:
234
- vectorizer = TfidfVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
235
- tfidf_matrix = vectorizer.fit_transform(textos_cluster)
236
- vocab = vectorizer.get_feature_names_out()
237
- soma = tfidf_matrix.sum(axis=0).A1
238
- top_idx = np.argsort(soma)[-5:][::-1]
239
- top_palavras = [{"palavra": vocab[i], "score": round(float(soma[i]), 4)} for i in top_idx]
240
- except ValueError:
241
- top_palavras = []
242
- analise[cid] = {"num_documentos": len(textos_cluster), "top_palavras": top_palavras}
243
- return analise
244
-
245
-
246
- # ==============================================================================
247
- # API FASTAPI & INSTRUMENTAÇÃO
248
- # ==============================================================================
249
- app = FastAPI(title="AetherMap API 7.1", version="7.1.0", description="Backend Semantic Search + Prometheus Metrics")
250
-
251
- # --- A MÁGICA ACONTECE AQUI ---
252
- # Isso expõe automaticamente o endpoint /metrics para o Prometheus/Grafana
253
- Instrumentator().instrument(app).expose(app)
254
- # ------------------------------
255
-
256
- @app.get("/")
257
- async def root():
258
- return {"status": "online", "message": "Aether Map API 7.1 (Observability Ready)."}
259
-
260
- @app.post("/process/")
261
- async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)):
262
- logging.info(f"Processando arquivo: {file.filename}")
263
- try:
264
- file_bytes = await file.read()
265
- textos = preparar_textos(file_bytes, n_samples)
266
- if not textos: raise HTTPException(status_code=400, detail="Nenhum texto válido encontrado.")
267
-
268
- df, embeddings = processar_pipeline(textos)
269
-
270
- job_id = str(uuid.uuid4())
271
- cache[job_id] = {"embeddings": embeddings, "df": df}
272
- logging.info(f"Job criado: {job_id}")
273
-
274
- metricas_globais = calcular_metricas(df["full_text"].tolist())
275
- analise_de_duplicados = encontrar_duplicados(df, embeddings)
276
- analise_por_cluster_tfidf = analisar_clusters(df)
277
-
278
- n_clusters = len(df["cluster"].unique()) - (1 if "-1" in df["cluster"].unique() else 0)
279
- n_ruido = int((df["cluster"] == "-1").sum())
280
-
281
- return {
282
- "job_id": job_id,
283
- "metadata": {
284
- "filename": file.filename,
285
- "num_documents_processed": len(df),
286
- "num_clusters_found": n_clusters,
287
- "num_noise_points": n_ruido
288
- },
289
- "metrics": metricas_globais,
290
- "duplicates": analise_de_duplicados,
291
- "cluster_analysis": analise_por_cluster_tfidf,
292
- "plot_data": df[["x", "y", "z", "cluster", "full_text"]].to_dict("records"),
293
- }
294
- except Exception as e:
295
- logging.error(f"ERRO EM /process/: {e}", exc_info=True)
296
- raise HTTPException(status_code=500, detail=str(e))
297
-
298
-
299
- @app.post("/search/")
300
- async def search_api(query: str = Form(...), job_id: str = Form(...)):
301
- """
302
- ENDPOINT DE BUSCA (RAG Híbrido) com Monitoramento de Latência
303
- """
304
- logging.info(f"Busca: '{query}' [Job: {job_id}]")
305
- if job_id not in cache:
306
- raise HTTPException(status_code=404, detail="Job ID não encontrado.")
307
-
308
- try:
309
- model = load_retriever()
310
- reranker = load_reranker()
311
-
312
- cached_data = cache[job_id]
313
- df = cached_data["df"]
314
- corpus_embeddings = cached_data["embeddings"]
315
-
316
- # FASE 1: Varredura Ampla
317
- query_embedding = model.encode([query], convert_to_numpy=True)
318
- similarities = cosine_similarity(query_embedding, corpus_embeddings)[0]
319
-
320
- top_k_retrieval = 50
321
- top_indices = np.argsort(similarities)[-top_k_retrieval:][::-1]
322
-
323
- candidate_docs = []
324
- candidate_indices = []
325
-
326
- for idx in top_indices:
327
- if similarities[idx] > 0.15:
328
- doc_text = df.iloc[int(idx)]["full_text"]
329
- candidate_docs.append([query, doc_text])
330
- candidate_indices.append(int(idx))
331
-
332
- if not candidate_docs:
333
- return {"summary": "Não foram encontrados documentos relevantes.", "results": []}
334
-
335
- # FASE 2: Reranking
336
- logging.info(f"Reranking {len(candidate_docs)} documentos...")
337
- rerank_scores = reranker.predict(candidate_docs)
338
-
339
- rerank_results = sorted(
340
- zip(candidate_indices, rerank_scores),
341
- key=lambda x: x[1],
342
- reverse=True
343
- )
344
-
345
- final_top_k = 5
346
- final_results = []
347
- context_parts = []
348
-
349
- for rank, (idx, score) in enumerate(rerank_results[:final_top_k]):
350
- doc_text = df.iloc[idx]["full_text"]
351
- context_parts.append(f"[ID: {rank+1}] DOCUMENTO:\n{doc_text}\n---------------------")
352
-
353
- final_results.append({
354
- "index": idx,
355
- "score": float(score),
356
- "cosine_score": float(similarities[idx]),
357
- "citation_id": rank + 1
358
- })
359
-
360
- # FASE 3: Geração (Groq) com TELEMETRIA
361
- summary = ""
362
- if groq_client:
363
- context_str = "\n".join(context_parts)
364
- rag_prompt = (
365
- "INSTRUÇÃO DE SISTEMA:\n"
366
- "Você é o Aetherius, um motor de busca semântica de alta precisão.\n"
367
- "Sua missão é responder à pergunta do usuário baseando-se ESTRITAMENTE nos documentos fornecidos.\n\n"
368
- "REGRAS OBRIGATÓRIAS:\n"
369
- "1. CITAÇÕES: Toda afirmação deve ter fonte [ID: x]. Ex: 'O lucro subiu [ID: 1].'\n"
370
- "2. HONESTIDADE: Se não estiver no texto, diga que não encontrou.\n"
371
- "3. IDIOMA: Português do Brasil.\n\n"
372
- f"CONTEXTO RECUPERADO:\n{context_str}\n\n"
373
- f"PERGUNTA DO USUÁRIO: \"{query}\"\n\n"
374
- "RESPOSTA:"
375
- )
376
-
377
- try:
378
- # --- INÍCIO DA MEDIÇÃO DA API EXTERNA ---
379
- start_time_groq = time.time()
380
-
381
- chat_completion = groq_client.chat.completions.create(
382
- messages=[{"role": "user", "content": rag_prompt}],
383
- model="moonshotai/kimi-k2-instruct-0905",
384
- temperature=0.1,
385
- max_tokens=1024
386
- )
387
-
388
- # Registra o tempo gasto apenas na chamada da API
389
- duration = time.time() - start_time_groq
390
- GROQ_LATENCY.observe(duration)
391
- # --- FIM DA MEDIÇÃO ---
392
-
393
- summary = chat_completion.choices[0].message.content.strip()
394
- except Exception as e:
395
- logging.warning(f"Erro na geração do LLM: {e}")
396
- summary = "Não foi possível gerar o resumo automático, mas os documentos estão listados abaixo."
397
-
398
- return {"summary": summary, "results": final_results}
399
-
400
- except Exception as e:
401
- logging.error(f"ERRO EM /search/: {e}", exc_info=True)
402
- raise HTTPException(status_code=500, detail=str(e))
403
-
404
-
405
- @app.post("/describe_clusters/")
406
- async def describe_clusters_api(job_id: str = Form(...)):
407
- logging.info(f"Descrevendo clusters para Job: {job_id}")
408
- if not groq_client: raise HTTPException(status_code=503, detail="Groq indisponível.")
409
- if job_id not in cache: raise HTTPException(status_code=404, detail="Job não encontrado.")
410
-
411
- try:
412
- cached_data = cache[job_id]
413
- df = cached_data["df"]
414
- embeddings = cached_data["embeddings"]
415
-
416
- champion_docs_by_cluster = {}
417
- cluster_ids = sorted([c for c in df["cluster"].unique() if c != "-1"], key=int)
418
-
419
- for cid in cluster_ids:
420
- mask = df["cluster"] == cid
421
- cluster_embeddings = embeddings[mask]
422
- cluster_texts = df[mask]["full_text"].tolist()
423
- if len(cluster_texts) < 3: continue
424
-
425
- centroid = np.mean(cluster_embeddings, axis=0)
426
- similarities = cosine_similarity([centroid], cluster_embeddings)[0]
427
- top_indices = np.argsort(similarities)[-3:][::-1]
428
- champion_docs_by_cluster[cid] = [cluster_texts[i] for i in top_indices]
429
-
430
- if not champion_docs_by_cluster: return {"insights": {}}
431
-
432
- prompt_sections = []
433
- for cid, docs in champion_docs_by_cluster.items():
434
- doc_list = "\n".join([f"- {doc[:300]}..." for doc in docs])
435
- prompt_sections.append(f"Grupo {cid}:\n{doc_list}")
436
-
437
- master_prompt = (
438
- "Analise os grupos de texto abaixo. Para cada grupo, retorne um JSON com 'topic_name' e 'core_insight'.\n"
439
- "Responda APENAS o JSON válido.\n\n" + "\n\n".join(prompt_sections)
440
- )
441
-
442
- # --- INÍCIO DA MEDIÇÃO DA API EXTERNA ---
443
- start_time_groq = time.time()
444
-
445
- chat_completion = groq_client.chat.completions.create(
446
- messages=[
447
- {"role": "system", "content": "JSON Output Only."},
448
- {"role": "user", "content": master_prompt},
449
- ], model="meta-llama/llama-4-maverick-17b-128e-instruct", temperature=0.2,
450
- )
451
-
452
- duration = time.time() - start_time_groq
453
- GROQ_LATENCY.observe(duration)
454
- # --- FIM DA MEDIÇÃO ---
455
-
456
- response_content = chat_completion.choices[0].message.content
457
- insights = json.loads(response_content.strip().replace("```json", "").replace("```", ""))
458
- return {"insights": insights}
459
-
460
- except Exception as e:
461
- logging.error(f"ERRO EM /describe_clusters/: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  raise HTTPException(status_code=500, detail=str(e))
 
1
+ # ==============================================================================
2
+ # API do AetherMap — VERSÃO 7.2 (CSV + TAVILY EDITION)
3
+ # Backend com RAG Híbrido, CSV Support, Web Search via Tavily
4
+ # ==============================================================================
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import torch
9
+ import gc
10
+ import uuid
11
+ import os
12
+ import io
13
+ import json
14
+ import logging
15
+ import time
16
+ import nltk
17
+ from nltk.corpus import stopwords
18
+
19
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
20
+ from fastapi.responses import JSONResponse
21
+ from typing import List, Dict, Any
22
+ from functools import lru_cache
23
+
24
+ # Ferramentas de Alquimia (ML & NLP)
25
+ from sentence_transformers import SentenceTransformer, CrossEncoder
26
+ import umap
27
+ import hdbscan
28
+ from sklearn.preprocessing import StandardScaler
29
+ from sklearn.metrics.pairwise import cosine_similarity
30
+ from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
31
+ from scipy.stats import entropy
32
+
33
+ # Monitoramento (O Toque da Berta)
34
+ from prometheus_fastapi_instrumentator import Instrumentator
35
+ from prometheus_client import Histogram
36
+
37
+ # A Conexão com o Oráculo
38
+ from groq import Groq
39
+
40
+ # ==============================================================================
41
+ # CONFIGURAÇÕES GERAIS E LOGGING
42
+ # ==============================================================================
43
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
44
+
45
+ # Modelos de IA
46
+ RETRIEVAL_MODEL = "all-MiniLM-L6-v2" # Rápido para varredura inicial
47
+ RERANKER_MODEL = "cross-encoder/ms-marco-MiniLM-L-6-v2" # Preciso para reordenação
48
+
49
+ # Parâmetros de Processamento
50
+ BATCH_SIZE = 256
51
+ UMAP_N_NEIGHBORS = 30
52
+
53
+ # Cache de Sessão (Na memória RAM)
54
+ cache: Dict[str, Any] = {}
55
+
56
+ # Definição de Métricas Customizadas do Prometheus
57
+ # Isso permite separar a latência da sua lógica vs a latência da API externa
58
+ GROQ_LATENCY = Histogram(
59
+ "groq_api_latency_seconds",
60
+ "Tempo de resposta da API externa Groq (LLM Generation)",
61
+ buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0]
62
+ )
63
+
64
+ # Inicialização do Cliente Groq
65
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
66
+ try:
67
+ if not GROQ_API_KEY:
68
+ logging.warning("GROQ_API_KEY não encontrada. Funcionalidades de LLM estarão indisponíveis.")
69
+ groq_client = None
70
+ else:
71
+ groq_client = Groq(api_key=GROQ_API_KEY)
72
+ logging.info("Cliente Groq inicializado com sucesso.")
73
+ except Exception as e:
74
+ logging.error(f"FALHA AO INICIALIZAR GROQ: {e}")
75
+ groq_client = None
76
+
77
+ # Inicialização do Cliente Tavily (Web Search)
78
+ TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY")
79
+ tavily_client = None
80
+ try:
81
+ if TAVILY_API_KEY:
82
+ from tavily import TavilyClient
83
+ tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
84
+ logging.info("Cliente Tavily inicializado com sucesso.")
85
+ else:
86
+ logging.warning("TAVILY_API_KEY não encontrada. Busca web estará indisponível.")
87
+ except Exception as e:
88
+ logging.error(f"FALHA AO INICIALIZAR TAVILY: {e}")
89
+ tavily_client = None
90
+
91
+
92
+ # ==============================================================================
93
+ # GERENCIAMENTO HÍBRIDO DE STOP WORDS (NLTK + ARQUIVO TXT)
94
+ # ==============================================================================
95
+ def carregar_stopwords():
96
+ """
97
+ Carrega stop words do NLTK e combina com um arquivo externo 'stopwords.txt'.
98
+ """
99
+ logging.info("Iniciando carregamento de Stop Words...")
100
+
101
+ # 1. Base Gramatical (NLTK - Inglês e Português)
102
+ try:
103
+ nltk.data.find('corpora/stopwords')
104
+ except LookupError:
105
+ logging.info("Baixando corpus de stopwords...")
106
+ nltk.download('stopwords')
107
+
108
+ # Cria um conjunto único com PT e EN
109
+ final_stops = set(stopwords.words('portuguese')) | set(stopwords.words('english'))
110
+ logging.info(f"Stopwords base (NLTK) carregadas: {len(final_stops)}")
111
+
112
+ # 2. Base Customizada
113
+ arquivo_custom = "stopwords.txt"
114
+
115
+ if os.path.exists(arquivo_custom):
116
+ logging.info(f"Arquivo '{arquivo_custom}' encontrado. Lendo palavras customizadas...")
117
+ try:
118
+ count_custom = 0
119
+ with open(arquivo_custom, "r", encoding="utf-8") as f:
120
+ for linha in f:
121
+ palavra = linha.split('#')[0].strip().lower()
122
+ if palavra and len(palavra) > 1:
123
+ final_stops.add(palavra)
124
+ count_custom += 1
125
+ logging.info(f"{count_custom} stop words customizadas importadas do arquivo.")
126
+ except Exception as e:
127
+ logging.error(f"Erro ao ler '{arquivo_custom}': {e}")
128
+ else:
129
+ logging.warning(f"Arquivo '{arquivo_custom}' não encontrado. Usando apenas NLTK.")
130
+
131
+ lista_final = list(final_stops)
132
+ logging.info(f"Total final de Stop Words ativas: {len(lista_final)}")
133
+ return lista_final
134
+
135
+ # Variável global carregada na inicialização
136
+ STOP_WORDS_MULTILINGUAL = carregar_stopwords()
137
+
138
+
139
+ # ==============================================================================
140
+ # CARREGAMENTO DE MODELOS (COM CACHE)
141
+ # ==============================================================================
142
+ @lru_cache(maxsize=1)
143
+ def load_retriever():
144
+ device = "cuda" if torch.cuda.is_available() else "cpu"
145
+ logging.info(f"Carregando Retriever '{RETRIEVAL_MODEL}' em: {device}")
146
+ return SentenceTransformer(RETRIEVAL_MODEL, device=device)
147
+
148
+ @lru_cache(maxsize=1)
149
+ def load_reranker():
150
+ device = "cuda" if torch.cuda.is_available() else "cpu"
151
+ logging.info(f"Carregando Reranker '{RERANKER_MODEL}' em: {device}")
152
+ return CrossEncoder(RERANKER_MODEL, device=device)
153
+
154
+
155
+ # ==============================================================================
156
+ # PIPELINE DE PROCESSAMENTO DE DADOS
157
+ # ==============================================================================
158
+ def preparar_textos(file_bytes: bytes, n_samples: int) -> List[str]:
159
+ """Prepara textos de arquivo TXT (uma linha por documento)."""
160
+ linhas = file_bytes.decode("utf-8", errors="ignore").splitlines()
161
+ textos = [s for line in linhas if (s := line.strip()) and len(s.split()) > 3]
162
+ return textos[:n_samples]
163
+
164
+ def preparar_textos_csv(file_bytes: bytes, text_column: str, n_samples: int) -> List[str]:
165
+ """Prepara textos de arquivo CSV extraindo coluna especificada."""
166
+ try:
167
+ df = pd.read_csv(io.BytesIO(file_bytes), encoding="utf-8")
168
+ except UnicodeDecodeError:
169
+ df = pd.read_csv(io.BytesIO(file_bytes), encoding="latin-1")
170
+
171
+ if text_column not in df.columns:
172
+ available = ", ".join(df.columns.tolist()[:10])
173
+ raise ValueError(f"Coluna '{text_column}' não encontrada. Colunas disponíveis: {available}")
174
+
175
+ textos = df[text_column].dropna().astype(str).tolist()
176
+ # Filtrar textos muito curtos
177
+ textos = [t.strip() for t in textos if len(t.strip().split()) > 3]
178
+ return textos[:n_samples]
179
+
180
+ def get_csv_columns(file_bytes: bytes) -> List[str]:
181
+ """Retorna lista de colunas de um arquivo CSV."""
182
+ try:
183
+ df = pd.read_csv(io.BytesIO(file_bytes), nrows=0, encoding="utf-8")
184
+ except UnicodeDecodeError:
185
+ df = pd.read_csv(io.BytesIO(file_bytes), nrows=0, encoding="latin-1")
186
+ return df.columns.tolist()
187
+
188
+
189
+ def processar_pipeline(textos: List[str]) -> (pd.DataFrame, np.ndarray):
190
+ logging.info(f"Iniciando pipeline para {len(textos)} textos...")
191
+ model = load_retriever()
192
+
193
+ # 1. Embeddings
194
+ embeddings = model.encode(textos, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
195
+
196
+ # 2. UMAP
197
+ reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric="cosine", random_state=42)
198
+ emb_3d = reducer.fit_transform(embeddings)
199
+ emb_3d = StandardScaler().fit_transform(emb_3d)
200
+
201
+ # 3. HDBSCAN
202
+ num_textos = len(textos)
203
+ min_size = max(10, int(num_textos * 0.02))
204
+ logging.info(f"HDBSCAN min_cluster_size: {min_size}")
205
+
206
+ clusterer = hdbscan.HDBSCAN(min_cluster_size=min_size)
207
+ clusters = clusterer.fit_predict(emb_3d)
208
+
209
+ # 4. DataFrame
210
+ df = pd.DataFrame({
211
+ "x": emb_3d[:, 0], "y": emb_3d[:, 1], "z": emb_3d[:, 2],
212
+ "full_text": textos, "cluster": clusters.astype(str)
213
+ })
214
+
215
+ del reducer, clusterer, emb_3d; gc.collect()
216
+ return df, embeddings
217
+
218
+ def calcular_metricas(textos: List[str]) -> Dict[str, Any]:
219
+ logging.info("Calculando métricas globais...")
220
+ if not textos: return {}
221
+
222
+ vectorizer_count = CountVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
223
+ vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
224
+
225
+ try:
226
+ counts_matrix = vectorizer_count.fit_transform(textos)
227
+ tfidf_matrix = vectorizer_tfidf.fit_transform(textos)
228
+ except ValueError:
229
+ return {"riqueza_lexical": 0, "top_tfidf_palavras": [], "entropia": 0.0}
230
+
231
+ vocab_count = vectorizer_count.get_feature_names_out()
232
+ contagens = counts_matrix.sum(axis=0).A1
233
+
234
+ vocab_tfidf = vectorizer_tfidf.get_feature_names_out()
235
+ soma_tfidf = tfidf_matrix.sum(axis=0).A1
236
+ top_idx_tfidf = np.argsort(soma_tfidf)[-10:][::-1]
237
+ top_tfidf = [{"palavra": vocab_tfidf[i], "score": round(float(soma_tfidf[i]), 4)} for i in top_idx_tfidf]
238
+
239
+ return {
240
+ "riqueza_lexical": len(vocab_count),
241
+ "top_tfidf_palavras": top_tfidf,
242
+ "entropia": float(entropy(contagens / contagens.sum(), base=2)) if contagens.sum() > 0 else 0.0
243
+ }
244
+
245
+ def encontrar_duplicados(df: pd.DataFrame, embeddings: np.ndarray) -> Dict[str, Any]:
246
+ logging.info("Detectando duplicados...")
247
+ mask = df["full_text"].duplicated(keep=False)
248
+ grupos_exatos = {t: [int(i) for i in idxs] for t, idxs in df[mask].groupby("full_text").groups.items()}
249
+ pares_semanticos = []
250
+
251
+ if 2 < len(embeddings) < 5000:
252
+ sim = cosine_similarity(embeddings)
253
+ triu_indices = np.triu_indices_from(sim, k=1)
254
+ sim_vetor = sim[triu_indices]
255
+ pares_idx = np.where(sim_vetor > 0.98)[0]
256
+ top_pares_idx = pares_idx[np.argsort(sim_vetor[pares_idx])[-5:][::-1]]
257
+ for i in top_pares_idx:
258
+ idx1, idx2 = triu_indices[0][i], triu_indices[1][i]
259
+ if df["full_text"].iloc[idx1] != df["full_text"].iloc[idx2]:
260
+ pares_semanticos.append({
261
+ "similaridade": float(sim[idx1, idx2]),
262
+ "texto1": df["full_text"].iloc[idx1],
263
+ "texto2": df["full_text"].iloc[idx2]
264
+ })
265
+ return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos}
266
+
267
+ def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
268
+ logging.info("Analisando clusters...")
269
+ analise = {}
270
+ ids_clusters_validos = sorted([c for c in df["cluster"].unique() if c != "-1"], key=int)
271
+ for cid in ids_clusters_validos:
272
+ textos_cluster = df[df["cluster"] == cid]["full_text"].tolist()
273
+ if len(textos_cluster) < 2: continue
274
+ try:
275
+ vectorizer = TfidfVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
276
+ tfidf_matrix = vectorizer.fit_transform(textos_cluster)
277
+ vocab = vectorizer.get_feature_names_out()
278
+ soma = tfidf_matrix.sum(axis=0).A1
279
+ top_idx = np.argsort(soma)[-5:][::-1]
280
+ top_palavras = [{"palavra": vocab[i], "score": round(float(soma[i]), 4)} for i in top_idx]
281
+ except ValueError:
282
+ top_palavras = []
283
+ analise[cid] = {"num_documentos": len(textos_cluster), "top_palavras": top_palavras}
284
+ return analise
285
+
286
+
287
+ # ==============================================================================
288
+ # API FASTAPI & INSTRUMENTAÇÃO
289
+ # ==============================================================================
290
+ app = FastAPI(title="AetherMap API 7.2", version="7.2.0", description="Backend Semantic Search + CSV + Tavily Web Search")
291
+
292
+ # --- A MÁGICA ACONTECE AQUI ---
293
+ # Isso expõe automaticamente o endpoint /metrics para o Prometheus/Grafana
294
+ Instrumentator().instrument(app).expose(app)
295
+ # ------------------------------
296
+
297
+ @app.get("/")
298
+ async def root():
299
+ return {"status": "online", "message": "Aether Map API 7.2 (CSV + Tavily Ready)."}
300
+
301
+ @app.post("/csv_columns/")
302
+ async def get_columns_api(file: UploadFile = File(...)):
303
+ """Retorna as colunas de um arquivo CSV para preview."""
304
+ if not file.filename.lower().endswith('.csv'):
305
+ raise HTTPException(status_code=400, detail="Arquivo deve ser CSV.")
306
+ try:
307
+ file_bytes = await file.read()
308
+ columns = get_csv_columns(file_bytes)
309
+ return {"columns": columns, "filename": file.filename}
310
+ except Exception as e:
311
+ raise HTTPException(status_code=400, detail=f"Erro ao ler CSV: {str(e)}")
312
+
313
+ @app.post("/process/")
314
+ async def process_api(
315
+ n_samples: int = Form(10000),
316
+ file: UploadFile = File(...),
317
+ text_column: str = Form(None) # Coluna de texto para CSV
318
+ ):
319
+ logging.info(f"Processando arquivo: {file.filename}")
320
+ try:
321
+ file_bytes = await file.read()
322
+
323
+ # Detectar tipo de arquivo e processar
324
+ if file.filename.lower().endswith('.csv'):
325
+ if not text_column:
326
+ raise HTTPException(status_code=400, detail="Para CSV, informe 'text_column'.")
327
+ textos = preparar_textos_csv(file_bytes, text_column, n_samples)
328
+ else:
329
+ textos = preparar_textos(file_bytes, n_samples)
330
+
331
+ if not textos: raise HTTPException(status_code=400, detail="Nenhum texto válido encontrado.")
332
+
333
+ df, embeddings = processar_pipeline(textos)
334
+
335
+ job_id = str(uuid.uuid4())
336
+ cache[job_id] = {"embeddings": embeddings, "df": df}
337
+ logging.info(f"Job criado: {job_id}")
338
+
339
+ metricas_globais = calcular_metricas(df["full_text"].tolist())
340
+ analise_de_duplicados = encontrar_duplicados(df, embeddings)
341
+ analise_por_cluster_tfidf = analisar_clusters(df)
342
+
343
+ n_clusters = len(df["cluster"].unique()) - (1 if "-1" in df["cluster"].unique() else 0)
344
+ n_ruido = int((df["cluster"] == "-1").sum())
345
+
346
+ return {
347
+ "job_id": job_id,
348
+ "metadata": {
349
+ "filename": file.filename,
350
+ "num_documents_processed": len(df),
351
+ "num_clusters_found": n_clusters,
352
+ "num_noise_points": n_ruido
353
+ },
354
+ "metrics": metricas_globais,
355
+ "duplicates": analise_de_duplicados,
356
+ "cluster_analysis": analise_por_cluster_tfidf,
357
+ "plot_data": df[["x", "y", "z", "cluster", "full_text"]].to_dict("records"),
358
+ }
359
+ except Exception as e:
360
+ logging.error(f"ERRO EM /process/: {e}", exc_info=True)
361
+ raise HTTPException(status_code=500, detail=str(e))
362
+
363
+
364
+ @app.post("/search/")
365
+ async def search_api(query: str = Form(...), job_id: str = Form(...)):
366
+ """
367
+ ENDPOINT DE BUSCA (RAG Híbrido) com Monitoramento de Latência
368
+ """
369
+ logging.info(f"Busca: '{query}' [Job: {job_id}]")
370
+ if job_id not in cache:
371
+ raise HTTPException(status_code=404, detail="Job ID não encontrado.")
372
+
373
+ try:
374
+ model = load_retriever()
375
+ reranker = load_reranker()
376
+
377
+ cached_data = cache[job_id]
378
+ df = cached_data["df"]
379
+ corpus_embeddings = cached_data["embeddings"]
380
+
381
+ # FASE 1: Varredura Ampla
382
+ query_embedding = model.encode([query], convert_to_numpy=True)
383
+ similarities = cosine_similarity(query_embedding, corpus_embeddings)[0]
384
+
385
+ top_k_retrieval = 50
386
+ top_indices = np.argsort(similarities)[-top_k_retrieval:][::-1]
387
+
388
+ candidate_docs = []
389
+ candidate_indices = []
390
+
391
+ for idx in top_indices:
392
+ if similarities[idx] > 0.15:
393
+ doc_text = df.iloc[int(idx)]["full_text"]
394
+ candidate_docs.append([query, doc_text])
395
+ candidate_indices.append(int(idx))
396
+
397
+ if not candidate_docs:
398
+ return {"summary": "Não foram encontrados documentos relevantes.", "results": []}
399
+
400
+ # FASE 2: Reranking
401
+ logging.info(f"Reranking {len(candidate_docs)} documentos...")
402
+ rerank_scores = reranker.predict(candidate_docs)
403
+
404
+ rerank_results = sorted(
405
+ zip(candidate_indices, rerank_scores),
406
+ key=lambda x: x[1],
407
+ reverse=True
408
+ )
409
+
410
+ final_top_k = 5
411
+ final_results = []
412
+ context_parts = []
413
+
414
+ for rank, (idx, score) in enumerate(rerank_results[:final_top_k]):
415
+ doc_text = df.iloc[idx]["full_text"]
416
+ context_parts.append(f"[ID: {rank+1}] DOCUMENTO:\n{doc_text}\n---------------------")
417
+
418
+ final_results.append({
419
+ "index": idx,
420
+ "score": float(score),
421
+ "cosine_score": float(similarities[idx]),
422
+ "citation_id": rank + 1
423
+ })
424
+
425
+ # FASE 3: Geração (Groq) com TELEMETRIA
426
+ summary = ""
427
+ if groq_client:
428
+ context_str = "\n".join(context_parts)
429
+ rag_prompt = (
430
+ "INSTRUÇÃO DE SISTEMA:\n"
431
+ "Você é o Aetherius, um motor de busca semântica de alta precisão.\n"
432
+ "Sua missão é responder à pergunta do usuário baseando-se ESTRITAMENTE nos documentos fornecidos.\n\n"
433
+ "REGRAS OBRIGATÓRIAS:\n"
434
+ "1. CITAÇÕES: Toda afirmação deve ter fonte [ID: x]. Ex: 'O lucro subiu [ID: 1].'\n"
435
+ "2. HONESTIDADE: Se não estiver no texto, diga que não encontrou.\n"
436
+ "3. IDIOMA: Português do Brasil.\n\n"
437
+ f"CONTEXTO RECUPERADO:\n{context_str}\n\n"
438
+ f"PERGUNTA DO USUÁRIO: \"{query}\"\n\n"
439
+ "RESPOSTA:"
440
+ )
441
+
442
+ try:
443
+ # --- INÍCIO DA MEDIÇÃO DA API EXTERNA ---
444
+ start_time_groq = time.time()
445
+
446
+ chat_completion = groq_client.chat.completions.create(
447
+ messages=[{"role": "user", "content": rag_prompt}],
448
+ model="moonshotai/kimi-k2-instruct-0905",
449
+ temperature=0.1,
450
+ max_tokens=1024
451
+ )
452
+
453
+ # Registra o tempo gasto apenas na chamada da API
454
+ duration = time.time() - start_time_groq
455
+ GROQ_LATENCY.observe(duration)
456
+ # --- FIM DA MEDIÇÃO ---
457
+
458
+ summary = chat_completion.choices[0].message.content.strip()
459
+ except Exception as e:
460
+ logging.warning(f"Erro na geração do LLM: {e}")
461
+ summary = "Não foi possível gerar o resumo automático, mas os documentos estão listados abaixo."
462
+
463
+ return {"summary": summary, "results": final_results}
464
+
465
+ except Exception as e:
466
+ logging.error(f"ERRO EM /search/: {e}", exc_info=True)
467
+ raise HTTPException(status_code=500, detail=str(e))
468
+
469
+
470
+ @app.post("/describe_clusters/")
471
+ async def describe_clusters_api(job_id: str = Form(...)):
472
+ logging.info(f"Descrevendo clusters para Job: {job_id}")
473
+ if not groq_client: raise HTTPException(status_code=503, detail="Groq indisponível.")
474
+ if job_id not in cache: raise HTTPException(status_code=404, detail="Job não encontrado.")
475
+
476
+ try:
477
+ cached_data = cache[job_id]
478
+ df = cached_data["df"]
479
+ embeddings = cached_data["embeddings"]
480
+
481
+ champion_docs_by_cluster = {}
482
+ cluster_ids = sorted([c for c in df["cluster"].unique() if c != "-1"], key=int)
483
+
484
+ for cid in cluster_ids:
485
+ mask = df["cluster"] == cid
486
+ cluster_embeddings = embeddings[mask]
487
+ cluster_texts = df[mask]["full_text"].tolist()
488
+ if len(cluster_texts) < 3: continue
489
+
490
+ centroid = np.mean(cluster_embeddings, axis=0)
491
+ similarities = cosine_similarity([centroid], cluster_embeddings)[0]
492
+ top_indices = np.argsort(similarities)[-3:][::-1]
493
+ champion_docs_by_cluster[cid] = [cluster_texts[i] for i in top_indices]
494
+
495
+ if not champion_docs_by_cluster: return {"insights": {}}
496
+
497
+ prompt_sections = []
498
+ for cid, docs in champion_docs_by_cluster.items():
499
+ doc_list = "\n".join([f"- {doc[:300]}..." for doc in docs])
500
+ prompt_sections.append(f"Grupo {cid}:\n{doc_list}")
501
+
502
+ master_prompt = (
503
+ "Analise os grupos de texto abaixo. Para cada grupo, retorne um JSON com 'topic_name' e 'core_insight'.\n"
504
+ "Responda APENAS o JSON válido.\n\n" + "\n\n".join(prompt_sections)
505
+ )
506
+
507
+ # --- INÍCIO DA MEDIÇÃO DA API EXTERNA ---
508
+ start_time_groq = time.time()
509
+
510
+ chat_completion = groq_client.chat.completions.create(
511
+ messages=[
512
+ {"role": "system", "content": "JSON Output Only."},
513
+ {"role": "user", "content": master_prompt},
514
+ ], model="meta-llama/llama-4-maverick-17b-128e-instruct", temperature=0.2,
515
+ )
516
+
517
+ duration = time.time() - start_time_groq
518
+ GROQ_LATENCY.observe(duration)
519
+ # --- FIM DA MEDIÇÃO ---
520
+
521
+ response_content = chat_completion.choices[0].message.content
522
+ insights = json.loads(response_content.strip().replace("```json", "").replace("```", ""))
523
+ return {"insights": insights}
524
+
525
+ except Exception as e:
526
+ logging.error(f"ERRO EM /describe_clusters/: {e}", exc_info=True)
527
+ raise HTTPException(status_code=500, detail=str(e))
528
+
529
+
530
+ # ==============================================================================
531
+ # ENDPOINT TAVILY WEB SEARCH
532
+ # ==============================================================================
533
+ @app.post("/search_web/")
534
+ async def search_web_api(
535
+ query: str = Form(...),
536
+ max_results: int = Form(20),
537
+ search_depth: str = Form("basic") # "basic" ou "advanced"
538
+ ):
539
+ """
540
+ Busca na web via Tavily e processa resultados para visualização.
541
+ """
542
+ if not tavily_client:
543
+ raise HTTPException(status_code=503, detail="Tavily não configurado. Defina TAVILY_API_KEY.")
544
+
545
+ logging.info(f"Tavily Search: '{query}' (max: {max_results})")
546
+
547
+ try:
548
+ # Buscar via Tavily
549
+ search_result = tavily_client.search(
550
+ query=query,
551
+ max_results=max_results,
552
+ search_depth=search_depth,
553
+ include_answer=False
554
+ )
555
+
556
+ results = search_result.get("results", [])
557
+ if not results:
558
+ return {"error": "Nenhum resultado encontrado.", "results_count": 0}
559
+
560
+ # Extrair textos dos resultados
561
+ textos = []
562
+ sources = []
563
+ for r in results:
564
+ title = r.get("title", "")
565
+ content = r.get("content", "")
566
+ url = r.get("url", "")
567
+
568
+ # Combinar título + conteúdo
569
+ full_text = f"{title}: {content}" if title else content
570
+ if len(full_text.strip().split()) > 5:
571
+ textos.append(full_text.strip())
572
+ sources.append({"title": title, "url": url})
573
+
574
+ if not textos:
575
+ return {"error": "Resultados sem conteúdo válido.", "results_count": 0}
576
+
577
+ # Processar através do pipeline existente
578
+ df, embeddings = processar_pipeline(textos)
579
+
580
+ # Criar job e cachear
581
+ job_id = str(uuid.uuid4())
582
+ cache[job_id] = {"embeddings": embeddings, "df": df, "sources": sources}
583
+ logging.info(f"Tavily Job criado: {job_id}")
584
+
585
+ # Calcular métricas e análises
586
+ metricas_globais = calcular_metricas(df["full_text"].tolist())
587
+ analise_por_cluster_tfidf = analisar_clusters(df)
588
+
589
+ n_clusters = len(df["cluster"].unique()) - (1 if "-1" in df["cluster"].unique() else 0)
590
+ n_ruido = int((df["cluster"] == "-1").sum())
591
+
592
+ return {
593
+ "job_id": job_id,
594
+ "metadata": {
595
+ "query": query,
596
+ "source": "tavily_web_search",
597
+ "num_documents_processed": len(df),
598
+ "num_clusters_found": n_clusters,
599
+ "num_noise_points": n_ruido
600
+ },
601
+ "metrics": metricas_globais,
602
+ "cluster_analysis": analise_por_cluster_tfidf,
603
+ "plot_data": df[["x", "y", "z", "cluster", "full_text"]].to_dict("records"),
604
+ "sources": sources # URLs originais
605
+ }
606
+
607
+ except Exception as e:
608
+ logging.error(f"ERRO EM /search_web/: {e}", exc_info=True)
609
  raise HTTPException(status_code=500, detail=str(e))
requirements.txt CHANGED
@@ -1,22 +1,23 @@
1
- # --- SERVIDOR E API ---
2
- fastapi
3
- uvicorn[standard]
4
- python-multipart
5
- groq
6
- prometheus-fastapi-instrumentator
7
- prometheus-client
8
-
9
- # --- MACHINE LEARNING E NLP ---
10
- sentence-transformers
11
- numpy
12
- pandas
13
- scikit-learn
14
- scipy
15
- umap-learn
16
- hdbscan
17
- nltk
18
-
19
- # --- TORCH CPU ---
20
- torch
21
- torchvision
 
22
  torchaudio
 
1
+ # --- SERVIDOR E API ---
2
+ fastapi
3
+ uvicorn[standard]
4
+ python-multipart
5
+ groq
6
+ prometheus-fastapi-instrumentator
7
+ prometheus-client
8
+ tavily-python
9
+
10
+ # --- MACHINE LEARNING E NLP ---
11
+ sentence-transformers
12
+ numpy
13
+ pandas
14
+ scikit-learn
15
+ scipy
16
+ umap-learn
17
+ hdbscan
18
+ nltk
19
+
20
+ # --- TORCH CPU ---
21
+ torch
22
+ torchvision
23
  torchaudio
stopwords.txt CHANGED
@@ -1,546 +1,546 @@
1
- # ==============================================================================
2
- # STOPWORDS DO AETHER MAP - LISTA MESTRA (PT + EN)
3
- # ==============================================================================
4
-
5
- # --- TERMOS DE SISTEMA E WEB ---
6
- http
7
- https
8
- www
9
- com
10
- br
11
- org
12
- net
13
- html
14
- php
15
- jsp
16
- asp
17
- pdf
18
- docx
19
- xlsx
20
- json
21
- api
22
- id
23
- url
24
- email
25
- site
26
- website
27
- page
28
- pagina
29
- link
30
- click
31
- login
32
-
33
- # --- TERMOS GENÉRICOS DE DOCUMENTOS ---
34
- document
35
- documento
36
- texto
37
- text
38
- file
39
- arquivo
40
- data
41
- dados
42
- database
43
- base
44
- dataset
45
- sample
46
- amostra
47
- example
48
- exemplo
49
- case
50
- caso
51
- study
52
- estudo
53
- analysis
54
- analise
55
- análise
56
- report
57
- relatorio
58
- relatório
59
- paper
60
- artigo
61
- results
62
- resultados
63
- conclusion
64
- conclusão
65
- introduction
66
- introdução
67
- abstract
68
- resumo
69
- chapter
70
- capitulo
71
- capítulo
72
- section
73
- seção
74
- part
75
- parte
76
- figure
77
- figura
78
- fig
79
- table
80
- tabela
81
- tab
82
- chart
83
- grafico
84
- gráfico
85
- image
86
- imagem
87
- source
88
- fonte
89
- reference
90
- referencia
91
- referência
92
- bibliography
93
- bibliografia
94
- et
95
- al
96
- citation
97
- citação
98
-
99
- # --- INGLÊS ACADÊMICO E "FILLER WORDS" ---
100
- the
101
- be
102
- to
103
- of
104
- and
105
- a
106
- in
107
- that
108
- have
109
- i
110
- it
111
- for
112
- not
113
- on
114
- with
115
- he
116
- as
117
- you
118
- do
119
- at
120
- this
121
- but
122
- his
123
- by
124
- from
125
- they
126
- we
127
- say
128
- her
129
- she
130
- or
131
- an
132
- will
133
- my
134
- one
135
- all
136
- would
137
- there
138
- their
139
- what
140
- so
141
- up
142
- out
143
- if
144
- about
145
- who
146
- get
147
- which
148
- go
149
- me
150
- when
151
- make
152
- can
153
- like
154
- time
155
- no
156
- just
157
- him
158
- know
159
- take
160
- people
161
- into
162
- year
163
- your
164
- good
165
- some
166
- could
167
- them
168
- see
169
- other
170
- than
171
- then
172
- now
173
- look
174
- only
175
- come
176
- its
177
- over
178
- think
179
- also
180
- back
181
- after
182
- use
183
- two
184
- how
185
- our
186
- work
187
- first
188
- well
189
- way
190
- even
191
- new
192
- want
193
- because
194
- any
195
- these
196
- give
197
- day
198
- most
199
- us
200
- is
201
- are
202
- was
203
- were
204
- been
205
- has
206
- had
207
- did
208
- does
209
- may
210
- might
211
- should
212
- must
213
- shall
214
- used
215
- using
216
- uses
217
- based
218
- found
219
- show
220
- shown
221
- shows
222
- suggest
223
- suggests
224
- however
225
- therefore
226
- thus
227
- hence
228
- although
229
- though
230
- whereas
231
- while
232
- meanwhile
233
- furthermore
234
- moreover
235
- additionally
236
- besides
237
- indeed
238
- fact
239
- overall
240
- general
241
- specific
242
- significantly
243
- associated
244
- related
245
- various
246
- several
247
- many
248
- much
249
- less
250
- more
251
- high
252
- low
253
- increase
254
- decrease
255
- positive
256
- negative
257
-
258
- # --- PORTUGUÊS ACADÊMICO E "PALAVRAS VAZIAS" ---
259
- de
260
- a
261
- o
262
- que
263
- e
264
- do
265
- da
266
- em
267
- um
268
- para
269
- é
270
- com
271
- não
272
- uma
273
- os
274
- no
275
- se
276
- na
277
- por
278
- mais
279
- as
280
- dos
281
- como
282
- mas
283
- foi
284
- ao
285
- ele
286
- das
287
- tem
288
- à
289
- seu
290
- sua
291
- ou
292
- ser
293
- quando
294
- muito
295
-
296
- nos
297
-
298
- está
299
- eu
300
- também
301
-
302
- pelo
303
- pela
304
- até
305
- isso
306
- ela
307
- entre
308
- era
309
- depois
310
- sem
311
- mesmo
312
- aos
313
- ter
314
- seus
315
- quem
316
- nas
317
- me
318
- esse
319
- eles
320
- estão
321
- você
322
- tinha
323
- foram
324
- essa
325
- num
326
- nem
327
- suas
328
- meu
329
- às
330
- minha
331
- numa
332
- pelos
333
- elas
334
- havia
335
- seja
336
- qual
337
- será
338
- nós
339
- tenho
340
- lhe
341
- deles
342
- essas
343
- esses
344
- pelas
345
- este
346
- fosse
347
- dele
348
- tu
349
- te
350
- vocês
351
- vos
352
- lhes
353
- meus
354
- minhas
355
- teu
356
- tua
357
- teus
358
- tuas
359
- nosso
360
- nossa
361
- nossos
362
- nossas
363
- dela
364
- delas
365
- esta
366
- estes
367
- estas
368
- aquele
369
- aquela
370
- aqueles
371
- aquelas
372
- isto
373
- aquilo
374
- estou
375
- está
376
- estamos
377
- estão
378
- estive
379
- esteve
380
- estivemos
381
- estiveram
382
- estava
383
- estávamos
384
- estavam
385
- estivera
386
- estivéramos
387
- esteja
388
- estejamos
389
- estejam
390
- estivesse
391
- estivéssemos
392
- estivessem
393
- estiver
394
- estivermos
395
- estiverem
396
- hei
397
-
398
- havemos
399
- hão
400
- houve
401
- houvemos
402
- houveram
403
- houvera
404
- houvéramos
405
- haja
406
- hajamos
407
- hajam
408
- houvesse
409
- houvéssemos
410
- houvessem
411
- houver
412
- houvermos
413
- houverem
414
- houverei
415
- houverá
416
- houveremos
417
- houverão
418
- houveria
419
- houveríamos
420
- houveriam
421
- sou
422
- somos
423
- são
424
- era
425
- éramos
426
- eram
427
- fui
428
- foi
429
- fomos
430
- foram
431
- fora
432
- fôramos
433
- seja
434
- sejamos
435
- sejam
436
- fosse
437
- fôssemos
438
- fossem
439
- for
440
- formos
441
- forem
442
- serei
443
- será
444
- seremos
445
- serão
446
- seria
447
- seríamos
448
- seriam
449
- tenho
450
- tem
451
- temos
452
- tém
453
- tinha
454
- tínhamos
455
- tinham
456
- tive
457
- teve
458
- tivemos
459
- tiveram
460
- tivera
461
- tivéramos
462
- tenha
463
- tenhamos
464
- tenham
465
- tivesse
466
- tivéssemos
467
- tivessem
468
- tiver
469
- tivermos
470
- tiverem
471
- terei
472
- terá
473
- teremos
474
- terão
475
- teria
476
- teríamos
477
- teriam
478
-
479
- pode
480
- poder
481
- podem
482
- poderia
483
- poderiam
484
- fazer
485
- feito
486
- faz
487
- fazem
488
- dizer
489
- diz
490
- disse
491
- dizem
492
- coisa
493
- coisas
494
- tudo
495
- todo
496
- toda
497
- todos
498
- todas
499
- algo
500
- alguém
501
- algum
502
- alguma
503
- alguns
504
- algumas
505
- nada
506
- ninguém
507
- nenhum
508
- nenhuma
509
- cada
510
- onde
511
- aonde
512
- qualquer
513
- vários
514
- várias
515
- apenas
516
- somente
517
- através
518
- mediante
519
- conforme
520
- segundo
521
- visto
522
- dado
523
- sendo
524
- tendo
525
- havendo
526
- ficando
527
- geral
528
- grande
529
- pequeno
530
- novo
531
- nova
532
- velho
533
- velha
534
- bom
535
- boa
536
- mau
537
-
538
- alto
539
- baixo
540
- primeiro
541
- segundo
542
- terceiro
543
- último
544
- próximo
545
- anterior
546
  seguinte
 
1
+ # ==============================================================================
2
+ # STOPWORDS DO AETHER MAP - LISTA MESTRA (PT + EN)
3
+ # ==============================================================================
4
+
5
+ # --- TERMOS DE SISTEMA E WEB ---
6
+ http
7
+ https
8
+ www
9
+ com
10
+ br
11
+ org
12
+ net
13
+ html
14
+ php
15
+ jsp
16
+ asp
17
+ pdf
18
+ docx
19
+ xlsx
20
+ json
21
+ api
22
+ id
23
+ url
24
+ email
25
+ site
26
+ website
27
+ page
28
+ pagina
29
+ link
30
+ click
31
+ login
32
+
33
+ # --- TERMOS GENÉRICOS DE DOCUMENTOS ---
34
+ document
35
+ documento
36
+ texto
37
+ text
38
+ file
39
+ arquivo
40
+ data
41
+ dados
42
+ database
43
+ base
44
+ dataset
45
+ sample
46
+ amostra
47
+ example
48
+ exemplo
49
+ case
50
+ caso
51
+ study
52
+ estudo
53
+ analysis
54
+ analise
55
+ análise
56
+ report
57
+ relatorio
58
+ relatório
59
+ paper
60
+ artigo
61
+ results
62
+ resultados
63
+ conclusion
64
+ conclusão
65
+ introduction
66
+ introdução
67
+ abstract
68
+ resumo
69
+ chapter
70
+ capitulo
71
+ capítulo
72
+ section
73
+ seção
74
+ part
75
+ parte
76
+ figure
77
+ figura
78
+ fig
79
+ table
80
+ tabela
81
+ tab
82
+ chart
83
+ grafico
84
+ gráfico
85
+ image
86
+ imagem
87
+ source
88
+ fonte
89
+ reference
90
+ referencia
91
+ referência
92
+ bibliography
93
+ bibliografia
94
+ et
95
+ al
96
+ citation
97
+ citação
98
+
99
+ # --- INGLÊS ACADÊMICO E "FILLER WORDS" ---
100
+ the
101
+ be
102
+ to
103
+ of
104
+ and
105
+ a
106
+ in
107
+ that
108
+ have
109
+ i
110
+ it
111
+ for
112
+ not
113
+ on
114
+ with
115
+ he
116
+ as
117
+ you
118
+ do
119
+ at
120
+ this
121
+ but
122
+ his
123
+ by
124
+ from
125
+ they
126
+ we
127
+ say
128
+ her
129
+ she
130
+ or
131
+ an
132
+ will
133
+ my
134
+ one
135
+ all
136
+ would
137
+ there
138
+ their
139
+ what
140
+ so
141
+ up
142
+ out
143
+ if
144
+ about
145
+ who
146
+ get
147
+ which
148
+ go
149
+ me
150
+ when
151
+ make
152
+ can
153
+ like
154
+ time
155
+ no
156
+ just
157
+ him
158
+ know
159
+ take
160
+ people
161
+ into
162
+ year
163
+ your
164
+ good
165
+ some
166
+ could
167
+ them
168
+ see
169
+ other
170
+ than
171
+ then
172
+ now
173
+ look
174
+ only
175
+ come
176
+ its
177
+ over
178
+ think
179
+ also
180
+ back
181
+ after
182
+ use
183
+ two
184
+ how
185
+ our
186
+ work
187
+ first
188
+ well
189
+ way
190
+ even
191
+ new
192
+ want
193
+ because
194
+ any
195
+ these
196
+ give
197
+ day
198
+ most
199
+ us
200
+ is
201
+ are
202
+ was
203
+ were
204
+ been
205
+ has
206
+ had
207
+ did
208
+ does
209
+ may
210
+ might
211
+ should
212
+ must
213
+ shall
214
+ used
215
+ using
216
+ uses
217
+ based
218
+ found
219
+ show
220
+ shown
221
+ shows
222
+ suggest
223
+ suggests
224
+ however
225
+ therefore
226
+ thus
227
+ hence
228
+ although
229
+ though
230
+ whereas
231
+ while
232
+ meanwhile
233
+ furthermore
234
+ moreover
235
+ additionally
236
+ besides
237
+ indeed
238
+ fact
239
+ overall
240
+ general
241
+ specific
242
+ significantly
243
+ associated
244
+ related
245
+ various
246
+ several
247
+ many
248
+ much
249
+ less
250
+ more
251
+ high
252
+ low
253
+ increase
254
+ decrease
255
+ positive
256
+ negative
257
+
258
+ # --- PORTUGUÊS ACADÊMICO E "PALAVRAS VAZIAS" ---
259
+ de
260
+ a
261
+ o
262
+ que
263
+ e
264
+ do
265
+ da
266
+ em
267
+ um
268
+ para
269
+ é
270
+ com
271
+ não
272
+ uma
273
+ os
274
+ no
275
+ se
276
+ na
277
+ por
278
+ mais
279
+ as
280
+ dos
281
+ como
282
+ mas
283
+ foi
284
+ ao
285
+ ele
286
+ das
287
+ tem
288
+ à
289
+ seu
290
+ sua
291
+ ou
292
+ ser
293
+ quando
294
+ muito
295
+
296
+ nos
297
+
298
+ está
299
+ eu
300
+ também
301
+
302
+ pelo
303
+ pela
304
+ até
305
+ isso
306
+ ela
307
+ entre
308
+ era
309
+ depois
310
+ sem
311
+ mesmo
312
+ aos
313
+ ter
314
+ seus
315
+ quem
316
+ nas
317
+ me
318
+ esse
319
+ eles
320
+ estão
321
+ você
322
+ tinha
323
+ foram
324
+ essa
325
+ num
326
+ nem
327
+ suas
328
+ meu
329
+ às
330
+ minha
331
+ numa
332
+ pelos
333
+ elas
334
+ havia
335
+ seja
336
+ qual
337
+ será
338
+ nós
339
+ tenho
340
+ lhe
341
+ deles
342
+ essas
343
+ esses
344
+ pelas
345
+ este
346
+ fosse
347
+ dele
348
+ tu
349
+ te
350
+ vocês
351
+ vos
352
+ lhes
353
+ meus
354
+ minhas
355
+ teu
356
+ tua
357
+ teus
358
+ tuas
359
+ nosso
360
+ nossa
361
+ nossos
362
+ nossas
363
+ dela
364
+ delas
365
+ esta
366
+ estes
367
+ estas
368
+ aquele
369
+ aquela
370
+ aqueles
371
+ aquelas
372
+ isto
373
+ aquilo
374
+ estou
375
+ está
376
+ estamos
377
+ estão
378
+ estive
379
+ esteve
380
+ estivemos
381
+ estiveram
382
+ estava
383
+ estávamos
384
+ estavam
385
+ estivera
386
+ estivéramos
387
+ esteja
388
+ estejamos
389
+ estejam
390
+ estivesse
391
+ estivéssemos
392
+ estivessem
393
+ estiver
394
+ estivermos
395
+ estiverem
396
+ hei
397
+
398
+ havemos
399
+ hão
400
+ houve
401
+ houvemos
402
+ houveram
403
+ houvera
404
+ houvéramos
405
+ haja
406
+ hajamos
407
+ hajam
408
+ houvesse
409
+ houvéssemos
410
+ houvessem
411
+ houver
412
+ houvermos
413
+ houverem
414
+ houverei
415
+ houverá
416
+ houveremos
417
+ houverão
418
+ houveria
419
+ houveríamos
420
+ houveriam
421
+ sou
422
+ somos
423
+ são
424
+ era
425
+ éramos
426
+ eram
427
+ fui
428
+ foi
429
+ fomos
430
+ foram
431
+ fora
432
+ fôramos
433
+ seja
434
+ sejamos
435
+ sejam
436
+ fosse
437
+ fôssemos
438
+ fossem
439
+ for
440
+ formos
441
+ forem
442
+ serei
443
+ será
444
+ seremos
445
+ serão
446
+ seria
447
+ seríamos
448
+ seriam
449
+ tenho
450
+ tem
451
+ temos
452
+ tém
453
+ tinha
454
+ tínhamos
455
+ tinham
456
+ tive
457
+ teve
458
+ tivemos
459
+ tiveram
460
+ tivera
461
+ tivéramos
462
+ tenha
463
+ tenhamos
464
+ tenham
465
+ tivesse
466
+ tivéssemos
467
+ tivessem
468
+ tiver
469
+ tivermos
470
+ tiverem
471
+ terei
472
+ terá
473
+ teremos
474
+ terão
475
+ teria
476
+ teríamos
477
+ teriam
478
+
479
+ pode
480
+ poder
481
+ podem
482
+ poderia
483
+ poderiam
484
+ fazer
485
+ feito
486
+ faz
487
+ fazem
488
+ dizer
489
+ diz
490
+ disse
491
+ dizem
492
+ coisa
493
+ coisas
494
+ tudo
495
+ todo
496
+ toda
497
+ todos
498
+ todas
499
+ algo
500
+ alguém
501
+ algum
502
+ alguma
503
+ alguns
504
+ algumas
505
+ nada
506
+ ninguém
507
+ nenhum
508
+ nenhuma
509
+ cada
510
+ onde
511
+ aonde
512
+ qualquer
513
+ vários
514
+ várias
515
+ apenas
516
+ somente
517
+ através
518
+ mediante
519
+ conforme
520
+ segundo
521
+ visto
522
+ dado
523
+ sendo
524
+ tendo
525
+ havendo
526
+ ficando
527
+ geral
528
+ grande
529
+ pequeno
530
+ novo
531
+ nova
532
+ velho
533
+ velha
534
+ bom
535
+ boa
536
+ mau
537
+
538
+ alto
539
+ baixo
540
+ primeiro
541
+ segundo
542
+ terceiro
543
+ último
544
+ próximo
545
+ anterior
546
  seguinte