Madras1 commited on
Commit
7ba80b9
·
verified ·
1 Parent(s): 1250f45

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -42
app.py CHANGED
@@ -1,7 +1,6 @@
1
  # ==============================================================================
2
- # API do AetherMap — VERSÃO 6.5 GOLD (THE COMMAND KILLER + NLTK CLEANUP)
3
- # Backend com RAG Híbrido (Bi-Encoder + Cross-Encoder), Citações Nativas
4
- # e Stopwords Dinâmicas (PT/EN).
5
  # ==============================================================================
6
 
7
  import numpy as np
@@ -63,37 +62,53 @@ except Exception as e:
63
 
64
 
65
  # ==============================================================================
66
- # GERENCIAMENTO INTELIGENTE DE STOP WORDS (NLTK)
67
  # ==============================================================================
68
  def carregar_stopwords():
69
  """
70
- Carrega stop words em Português e Inglês usando NLTK.
71
- Remove a necessidade de listas hardcoded gigantes.
72
  """
73
- logging.info("Verificando dicionários de Stop Words (NLTK)...")
 
 
74
  try:
75
  nltk.data.find('corpora/stopwords')
76
  except LookupError:
77
  logging.info("Baixando corpus de stopwords...")
78
  nltk.download('stopwords')
79
 
80
- # Carrega listas oficiais
81
- pt_stops = set(stopwords.words('portuguese'))
82
- en_stops = set(stopwords.words('english'))
83
 
84
- # Palavras customizadas do domínio AetherMap/Web
85
- custom_stops = {
86
- 'dá', 'pergunta', 'resposta', 'aethermap', 'documento',
87
- 'id', 'sobre', 'texto', 'análise', 'dados', 'cluster',
88
- 'http', 'https', 'www', 'com', 'br', 'html', 'org'
89
- }
90
 
91
- # União de todos os conjuntos
92
- final_stops = list(pt_stops | en_stops | custom_stops)
93
- logging.info(f"Total de Stop Words carregadas: {len(final_stops)}")
94
- return final_stops
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- # Variável global para ser usada nos Vectorizers
97
  STOP_WORDS_MULTILINGUAL = carregar_stopwords()
98
 
99
 
@@ -117,7 +132,6 @@ def load_reranker():
117
  # PIPELINE DE PROCESSAMENTO DE DADOS
118
  # ==============================================================================
119
  def preparar_textos(file_bytes: bytes, n_samples: int) -> List[str]:
120
- # Decodifica e limpa linhas vazias ou muito curtas
121
  linhas = file_bytes.decode("utf-8", errors="ignore").splitlines()
122
  textos = [s for line in linhas if (s := line.strip()) and len(s.split()) > 3]
123
  return textos[:n_samples]
@@ -126,23 +140,23 @@ def processar_pipeline(textos: List[str]) -> (pd.DataFrame, np.ndarray):
126
  logging.info(f"Iniciando pipeline para {len(textos)} textos...")
127
  model = load_retriever()
128
 
129
- # 1. Gerar Embeddings
130
  embeddings = model.encode(textos, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
131
 
132
- # 2. Redução Dimensional (UMAP)
133
  reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric="cosine", random_state=42)
134
  emb_3d = reducer.fit_transform(embeddings)
135
  emb_3d = StandardScaler().fit_transform(emb_3d)
136
 
137
- # 3. Clustering (HDBSCAN Dinâmico)
138
  num_textos = len(textos)
139
  min_size = max(10, int(num_textos * 0.02))
140
- logging.info(f"HDBSCAN min_cluster_size definido para: {min_size}")
141
 
142
  clusterer = hdbscan.HDBSCAN(min_cluster_size=min_size)
143
  clusters = clusterer.fit_predict(emb_3d)
144
 
145
- # 4. Criar DataFrame
146
  df = pd.DataFrame({
147
  "x": emb_3d[:, 0], "y": emb_3d[:, 1], "z": emb_3d[:, 2],
148
  "full_text": textos, "cluster": clusters.astype(str)
@@ -152,10 +166,10 @@ def processar_pipeline(textos: List[str]) -> (pd.DataFrame, np.ndarray):
152
  return df, embeddings
153
 
154
  def calcular_metricas(textos: List[str]) -> Dict[str, Any]:
155
- logging.info("Calculando métricas globais com Stopwords NLTK...")
156
  if not textos: return {}
157
 
158
- # Usando a nova lista STOP_WORDS_MULTILINGUAL
159
  vectorizer_count = CountVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
160
  vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
161
 
@@ -202,14 +216,14 @@ def encontrar_duplicados(df: pd.DataFrame, embeddings: np.ndarray) -> Dict[str,
202
  return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos}
203
 
204
  def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
205
- logging.info("Analisando clusters (TF-IDF NLTK)...")
206
  analise = {}
207
  ids_clusters_validos = sorted([c for c in df["cluster"].unique() if c != "-1"], key=int)
208
  for cid in ids_clusters_validos:
209
  textos_cluster = df[df["cluster"] == cid]["full_text"].tolist()
210
  if len(textos_cluster) < 2: continue
211
  try:
212
- # Usando a nova lista aqui também
213
  vectorizer = TfidfVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
214
  tfidf_matrix = vectorizer.fit_transform(textos_cluster)
215
  vocab = vectorizer.get_feature_names_out()
@@ -225,16 +239,15 @@ def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
225
  # ==============================================================================
226
  # API FASTAPI
227
  # ==============================================================================
228
- app = FastAPI(title="AetherMap API 6.5", version="6.5.0", description="Backend Semantic Search with Reranking & Citations")
229
 
230
- # Rota Raiz para evitar o "Not Found" feio
231
  @app.get("/")
232
  async def root():
233
- return {"status": "online", "message": "Aether Map API 6.5 está operante. Use /docs para testar."}
234
 
235
  @app.post("/process/")
236
  async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)):
237
- logging.info(f"Requisição recebida para {file.filename}.")
238
  try:
239
  file_bytes = await file.read()
240
  textos = preparar_textos(file_bytes, n_samples)
@@ -274,10 +287,10 @@ async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)
274
  @app.post("/search/")
275
  async def search_api(query: str = Form(...), job_id: str = Form(...)):
276
  """
277
- ENDPOINT DE BUSCA (RAG Híbrido com Citações)
278
  1. Retrieval (Bi-Encoder) -> Top 50
279
  2. Reranking (Cross-Encoder) -> Top 5
280
- 3. Generation (Kimi K2) -> Resposta citada
281
  """
282
  logging.info(f"Busca: '{query}' [Job: {job_id}]")
283
  if job_id not in cache:
@@ -295,15 +308,16 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
295
  query_embedding = model.encode([query], convert_to_numpy=True)
296
  similarities = cosine_similarity(query_embedding, corpus_embeddings)[0]
297
 
298
- # Pega Top 50 candidatos (com filtro mínimo de relevância)
299
  top_k_retrieval = 50
300
  top_indices = np.argsort(similarities)[-top_k_retrieval:][::-1]
301
 
302
  candidate_docs = []
303
  candidate_indices = []
304
 
 
305
  for idx in top_indices:
306
- if similarities[idx] > 0.15: # Filtro de ruído básico
307
  doc_text = df.iloc[int(idx)]["full_text"]
308
  candidate_docs.append([query, doc_text])
309
  candidate_indices.append(int(idx))
@@ -311,7 +325,7 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
311
  if not candidate_docs:
312
  return {"summary": "Não foram encontrados documentos relevantes.", "results": []}
313
 
314
- # FASE 2: Reranking (O Juiz)
315
  logging.info(f"Reranking {len(candidate_docs)} documentos...")
316
  rerank_scores = reranker.predict(candidate_docs)
317
 
@@ -328,7 +342,7 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
328
 
329
  for rank, (idx, score) in enumerate(rerank_results[:final_top_k]):
330
  doc_text = df.iloc[idx]["full_text"]
331
- # Montagem do Contexto com ID para Citação
332
  context_parts.append(f"[ID: {rank+1}] DOCUMENTO:\n{doc_text}\n---------------------")
333
 
334
  final_results.append({
@@ -338,7 +352,7 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
338
  "citation_id": rank + 1
339
  })
340
 
341
- # FASE 3: Geração com Citações (Kimi K2)
342
  summary = ""
343
  if groq_client:
344
  context_str = "\n".join(context_parts)
 
1
  # ==============================================================================
2
+ # API do AetherMap — VERSÃO 7.0 (THE CONFIGURABLE COMMAND KILLER)
3
+ # Backend com RAG Híbrido, Citações Nativas e Stopwords via Arquivo Externo.
 
4
  # ==============================================================================
5
 
6
  import numpy as np
 
62
 
63
 
64
  # ==============================================================================
65
+ # GERENCIAMENTO HÍBRIDO DE STOP WORDS (NLTK + ARQUIVO TXT)
66
  # ==============================================================================
67
  def carregar_stopwords():
68
  """
69
+ Carrega stop words do NLTK e combina com um arquivo externo 'stopwords.txt'.
70
+ Isso permite editar a lista de palavras ignoradas sem tocar no código.
71
  """
72
+ logging.info("Iniciando carregamento de Stop Words...")
73
+
74
+ # 1. Base Gramatical (NLTK - Inglês e Português)
75
  try:
76
  nltk.data.find('corpora/stopwords')
77
  except LookupError:
78
  logging.info("Baixando corpus de stopwords...")
79
  nltk.download('stopwords')
80
 
81
+ # Cria um conjunto único com PT e EN
82
+ final_stops = set(stopwords.words('portuguese')) | set(stopwords.words('english'))
83
+ logging.info(f"Stopwords base (NLTK) carregadas: {len(final_stops)}")
84
 
85
+ # 2. Base Customizada (Lendo do arquivo stopwords.txt se existir)
86
+ arquivo_custom = "stopwords.txt"
 
 
 
 
87
 
88
+ if os.path.exists(arquivo_custom):
89
+ logging.info(f"Arquivo '{arquivo_custom}' encontrado. Lendo palavras customizadas...")
90
+ try:
91
+ count_custom = 0
92
+ with open(arquivo_custom, "r", encoding="utf-8") as f:
93
+ for linha in f:
94
+ # Remove comentários (#) e espaços em branco
95
+ palavra = linha.split('#')[0].strip().lower()
96
+ # Só adiciona se não for vazia e tiver mais de 1 letra
97
+ if palavra and len(palavra) > 1:
98
+ final_stops.add(palavra)
99
+ count_custom += 1
100
+ logging.info(f"{count_custom} stop words customizadas importadas do arquivo.")
101
+ except Exception as e:
102
+ logging.error(f"Erro ao ler '{arquivo_custom}': {e}")
103
+ else:
104
+ logging.warning(f"Arquivo '{arquivo_custom}' não encontrado no diretório. Usando apenas NLTK.")
105
+
106
+ # Converte para lista para compatibilidade com Scikit-Learn
107
+ lista_final = list(final_stops)
108
+ logging.info(f"Total final de Stop Words ativas: {len(lista_final)}")
109
+ return lista_final
110
 
111
+ # Variável global carregada na inicialização
112
  STOP_WORDS_MULTILINGUAL = carregar_stopwords()
113
 
114
 
 
132
  # PIPELINE DE PROCESSAMENTO DE DADOS
133
  # ==============================================================================
134
  def preparar_textos(file_bytes: bytes, n_samples: int) -> List[str]:
 
135
  linhas = file_bytes.decode("utf-8", errors="ignore").splitlines()
136
  textos = [s for line in linhas if (s := line.strip()) and len(s.split()) > 3]
137
  return textos[:n_samples]
 
140
  logging.info(f"Iniciando pipeline para {len(textos)} textos...")
141
  model = load_retriever()
142
 
143
+ # 1. Embeddings
144
  embeddings = model.encode(textos, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
145
 
146
+ # 2. UMAP
147
  reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric="cosine", random_state=42)
148
  emb_3d = reducer.fit_transform(embeddings)
149
  emb_3d = StandardScaler().fit_transform(emb_3d)
150
 
151
+ # 3. HDBSCAN
152
  num_textos = len(textos)
153
  min_size = max(10, int(num_textos * 0.02))
154
+ logging.info(f"HDBSCAN min_cluster_size: {min_size}")
155
 
156
  clusterer = hdbscan.HDBSCAN(min_cluster_size=min_size)
157
  clusters = clusterer.fit_predict(emb_3d)
158
 
159
+ # 4. DataFrame
160
  df = pd.DataFrame({
161
  "x": emb_3d[:, 0], "y": emb_3d[:, 1], "z": emb_3d[:, 2],
162
  "full_text": textos, "cluster": clusters.astype(str)
 
166
  return df, embeddings
167
 
168
  def calcular_metricas(textos: List[str]) -> Dict[str, Any]:
169
+ logging.info("Calculando métricas globais...")
170
  if not textos: return {}
171
 
172
+ # Usando a lista global que combinou NLTK + Arquivo TXT
173
  vectorizer_count = CountVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
174
  vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
175
 
 
216
  return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos}
217
 
218
  def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
219
+ logging.info("Analisando clusters...")
220
  analise = {}
221
  ids_clusters_validos = sorted([c for c in df["cluster"].unique() if c != "-1"], key=int)
222
  for cid in ids_clusters_validos:
223
  textos_cluster = df[df["cluster"] == cid]["full_text"].tolist()
224
  if len(textos_cluster) < 2: continue
225
  try:
226
+ # Usando a lista global aqui também
227
  vectorizer = TfidfVectorizer(stop_words=STOP_WORDS_MULTILINGUAL, max_features=1000)
228
  tfidf_matrix = vectorizer.fit_transform(textos_cluster)
229
  vocab = vectorizer.get_feature_names_out()
 
239
  # ==============================================================================
240
  # API FASTAPI
241
  # ==============================================================================
242
+ app = FastAPI(title="AetherMap API 7.0", version="7.0.0", description="Backend Semantic Search with Reranking & Configurable Stopwords")
243
 
 
244
  @app.get("/")
245
  async def root():
246
+ return {"status": "online", "message": "Aether Map API 7.0 Operacional. Use /docs para interagir."}
247
 
248
  @app.post("/process/")
249
  async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)):
250
+ logging.info(f"Processando arquivo: {file.filename}")
251
  try:
252
  file_bytes = await file.read()
253
  textos = preparar_textos(file_bytes, n_samples)
 
287
  @app.post("/search/")
288
  async def search_api(query: str = Form(...), job_id: str = Form(...)):
289
  """
290
+ ENDPOINT DE BUSCA (RAG Híbrido)
291
  1. Retrieval (Bi-Encoder) -> Top 50
292
  2. Reranking (Cross-Encoder) -> Top 5
293
+ 3. Generation (Kimi K2) -> Resposta citada [ID: X]
294
  """
295
  logging.info(f"Busca: '{query}' [Job: {job_id}]")
296
  if job_id not in cache:
 
308
  query_embedding = model.encode([query], convert_to_numpy=True)
309
  similarities = cosine_similarity(query_embedding, corpus_embeddings)[0]
310
 
311
+ # Pega Top 50 candidatos
312
  top_k_retrieval = 50
313
  top_indices = np.argsort(similarities)[-top_k_retrieval:][::-1]
314
 
315
  candidate_docs = []
316
  candidate_indices = []
317
 
318
+ # Filtro de ruído (Cosseno > 0.15)
319
  for idx in top_indices:
320
+ if similarities[idx] > 0.15:
321
  doc_text = df.iloc[int(idx)]["full_text"]
322
  candidate_docs.append([query, doc_text])
323
  candidate_indices.append(int(idx))
 
325
  if not candidate_docs:
326
  return {"summary": "Não foram encontrados documentos relevantes.", "results": []}
327
 
328
+ # FASE 2: Reranking (Cross-Encoder)
329
  logging.info(f"Reranking {len(candidate_docs)} documentos...")
330
  rerank_scores = reranker.predict(candidate_docs)
331
 
 
342
 
343
  for rank, (idx, score) in enumerate(rerank_results[:final_top_k]):
344
  doc_text = df.iloc[idx]["full_text"]
345
+ # Montagem do Contexto para Citação
346
  context_parts.append(f"[ID: {rank+1}] DOCUMENTO:\n{doc_text}\n---------------------")
347
 
348
  final_results.append({
 
352
  "citation_id": rank + 1
353
  })
354
 
355
+ # FASE 3: Geração (Kimi K2)
356
  summary = ""
357
  if groq_client:
358
  context_str = "\n".join(context_parts)