Madras1 commited on
Commit
68111fb
·
verified ·
1 Parent(s): 0441477

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +45 -24
app.py CHANGED
@@ -1,6 +1,6 @@
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
@@ -11,6 +11,7 @@ import uuid
11
  import os
12
  import json
13
  import logging
 
14
  import nltk
15
  from nltk.corpus import stopwords
16
 
@@ -28,6 +29,10 @@ from sklearn.metrics.pairwise import cosine_similarity
28
  from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
29
  from scipy.stats import entropy
30
 
 
 
 
 
31
  # A Conexão com o Oráculo
32
  from groq import Groq
33
 
@@ -47,6 +52,14 @@ UMAP_N_NEIGHBORS = 30
47
  # Cache de Sessão (Na memória RAM)
48
  cache: Dict[str, Any] = {}
49
 
 
 
 
 
 
 
 
 
50
  # Inicialização do Cliente Groq
51
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
52
  try:
@@ -67,7 +80,6 @@ except Exception as e:
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
 
@@ -82,7 +94,7 @@ def carregar_stopwords():
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):
@@ -91,9 +103,7 @@ def carregar_stopwords():
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
@@ -101,9 +111,8 @@ def carregar_stopwords():
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
@@ -169,7 +178,6 @@ 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
 
@@ -223,7 +231,6 @@ def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
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()
@@ -237,13 +244,18 @@ def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
237
 
238
 
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(...)):
@@ -287,10 +299,7 @@ async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)
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:
@@ -304,18 +313,16 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
304
  df = cached_data["df"]
305
  corpus_embeddings = cached_data["embeddings"]
306
 
307
- # FASE 1: Varredura Ampla (Cosseno)
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"]
@@ -325,7 +332,7 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
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
 
@@ -335,14 +342,12 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
335
  reverse=True
336
  )
337
 
338
- # Seleciona Top 5 Campeões
339
  final_top_k = 5
340
  final_results = []
341
  context_parts = []
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,7 +357,7 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
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)
@@ -370,12 +375,21 @@ async def search_api(query: str = Form(...), job_id: str = Form(...)):
370
  )
371
 
372
  try:
 
 
 
373
  chat_completion = groq_client.chat.completions.create(
374
  messages=[{"role": "user", "content": rag_prompt}],
375
  model="moonshotai/kimi-k2-instruct-0905",
376
  temperature=0.1,
377
  max_tokens=1024
378
  )
 
 
 
 
 
 
379
  summary = chat_completion.choices[0].message.content.strip()
380
  except Exception as e:
381
  logging.warning(f"Erro na geração do LLM: {e}")
@@ -425,6 +439,9 @@ async def describe_clusters_api(job_id: str = Form(...)):
425
  "Responda APENAS o JSON válido.\n\n" + "\n\n".join(prompt_sections)
426
  )
427
 
 
 
 
428
  chat_completion = groq_client.chat.completions.create(
429
  messages=[
430
  {"role": "system", "content": "JSON Output Only."},
@@ -432,6 +449,10 @@ async def describe_clusters_api(job_id: str = Form(...)):
432
  ], model="meta-llama/llama-4-maverick-17b-128e-instruct", temperature=0.2,
433
  )
434
 
 
 
 
 
435
  response_content = chat_completion.choices[0].message.content
436
  insights = json.loads(response_content.strip().replace("```json", "").replace("```", ""))
437
  return {"insights": insights}
 
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
 
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
 
 
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
 
 
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:
 
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
 
 
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):
 
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
 
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
 
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
 
 
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()
 
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(...)):
 
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:
 
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"]
 
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
 
 
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({
 
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)
 
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}")
 
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."},
 
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}